From 1a38114f595b232e29aa22733ea4cc6b24c4b654 Mon Sep 17 00:00:00 2001 From: Cameron Reikes Date: Thu, 8 Jun 2023 02:48:59 -0700 Subject: [PATCH] NPCs can select who their actions and speech are directed at --- assets/drama.mdesk | 2 +- character_info.h | 3 +- main.c | 350 ++++++++++++++++++++++++++++++++------------- makeprompt.h | 119 ++++++++------- todo.txt | 7 +- tuning.h | 1 + 6 files changed, 328 insertions(+), 154 deletions(-) diff --git a/assets/drama.mdesk b/assets/drama.mdesk index 00de87c..6b2a57f 100644 --- a/assets/drama.mdesk +++ b/assets/drama.mdesk @@ -1,6 +1,6 @@ [ {can_hear: [NPC_Jester, NPC_Bill]}, - {enum: NPC_Jester , dialog: "Hehehe! Quit quant, a peasant from the mortal realm!"}, + {enum: NPC_Jester , dialog: "Hehehe! Quit quant, a peasant from the mortal realm!", to: Bill}, {enum: NPC_Bill , dialog: "No...Please! Stay away from me! Who the Hell are you!", thoughts: "I'm kind of mad, and this Jester guy is kind of an asshole"}, {enum: NPC_Jester , dialog: "Poor Bill, I'm sure your ex-wife says the same..."}, {enum: NPC_Bill , dialog: "You evil BASTARD!! What is this place???", thoughts: "I hate the Jester because he dissed my ex-wife. And, he's being all vague and condescending. I would punch him if I could"}, diff --git a/character_info.h b/character_info.h index b7c388d..54ebef8 100644 --- a/character_info.h +++ b/character_info.h @@ -5,8 +5,9 @@ // @TODO allow AI to prefix out of character statemetns with [ooc], this is a well khnown thing on role playing forums so gpt would pick up on it. const char *global_prompt = "You are a colorful and interesting personality in an RPG video game, who remembers important memories from the conversation history and stays in character.\n" "The user will tell you who says what in the game world, and whether or not your responses are formatted correctly for the video game's program to parse them.\n" -"Messages are json-like dictionaries that look like this: `{action: your_action, speech: \"Hey player!\", thoughts: \"Your thoughts\"}`. The required fields are `action` and `thoughts`\n" +"Messages are json-like dictionaries that look like this: `{who_i_am: who you're acting as, talking_to: who this action is directed at, could be nobody, action: your_action, speech: \"Hey player!\", thoughts: \"Your thoughts\"}`. The required fields are `action`, `thoughts`, and `who_i_am`\n" "Some actions take an argument, which you can provide with the field `action_arg`, e.g for the action `give_item` you would provide an item in your inventory, like {action: give_item, action_arg: Chalice}. The item must come from your inventory which is listed below\n" +"If was_heard_in_passing is true, then the action wasn't said directly to you, it was heard physically close to you. So, you usually respond with action: none and omit the speech field, as something you hear around the corner or whatever you don't normally respond to unless you feel the need to interrupt or something like that.\n" "Do NOT give away an item until the player gives you something you think is of equal value\n" ; diff --git a/main.c b/main.c index d841afa..68b0093 100644 --- a/main.c +++ b/main.c @@ -119,7 +119,6 @@ void web_arena_set_auto_align(WebArena *arena, size_t align) #include "md.c" #pragma warning(pop) -MD_Arena *persistent_arena = 0; // watch out, arenas have limited size. #include @@ -255,7 +254,7 @@ void do_parsing_tests() speech = MD_S8Lit("Better have a good reason for bothering me."); MD_String8 thoughts = MD_S8Lit("Man I'm tired today Whatever."); - MD_String8 to_parse = FmtWithLint(scratch.arena, "{action: none, speech: \"%.*s\", thoughts: \"%.*s\"}", MD_S8VArg(speech), MD_S8VArg(thoughts)); + MD_String8 to_parse = FmtWithLint(scratch.arena, "{action: none, speech: \"%.*s\", thoughts: \"%.*s\", who_i_am: TheBlacksmith, talking_to: nobody}", MD_S8VArg(speech), MD_S8VArg(thoughts)); error = parse_chatgpt_response(scratch.arena, &e, to_parse, &a); assert(error.size == 0); assert(a.kind == ACT_none); @@ -273,7 +272,7 @@ void do_parsing_tests() error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(Chalice \""), &a); assert(error.size > 0); - to_parse = MD_S8Lit("{action: give_item, action_arg: Chalice, speech: \"Here you go\", thoughts: \"Man I'm gonna miss that chalice\"}"); + to_parse = MD_S8Lit("{action: give_item, action_arg: Chalice, speech: \"Here you go\", thoughts: \"Man I'm gonna miss that chalice\", who_i_am: TheBlacksmith, talking_to: nobody}"); error = parse_chatgpt_response(scratch.arena, &e, to_parse, &a); assert(error.size == 0); assert(a.kind == ACT_give_item); @@ -423,6 +422,7 @@ Vec2 FloorV2(Vec2 v) } MD_Arena *frame_arena = 0; +MD_Arena *persistent_arena = 0; // watch out, arenas have limited size. #ifdef WINDOWS // uses frame arena @@ -962,6 +962,31 @@ void push_memory(Entity *e, MD_String8 speech, MD_String8 monologue, ActionKind } } +CanTalkTo get_can_talk_to(Entity *e) +{ + CanTalkTo to_return = {0}; + ENTITIES_ITER(gs.entities) + { + if(it != e && it->is_npc && LenV2(SubV2(it->pos, e->pos)) < PROPAGATE_ACTIONS_RADIUS) + { + BUFF_APPEND(&to_return, it->npc_kind); + } + } + return to_return; +} + +Entity *get_targeted(Entity *from, NpcKind targeted) +{ + ENTITIES_ITER(gs.entities) + { + if(it != from && it->is_npc && LenV2(SubV2(it->pos, from->pos)) < PROPAGATE_ACTIONS_RADIUS && it->npc_kind == targeted) + { + return it; + } + } + return 0; +} + void remember_error(Entity *to_modify, MD_String8 error_message) { assert(!to_modify->is_character); // this is a game logic bug if a player action is invalid @@ -979,20 +1004,87 @@ void remember_action(Entity *to_modify, Action a, MemoryContext context) } } -// from must not be null, to can be null if the action isn't directed at anybody +// returns reason why allocated on arena if invalid +// to might be null here, from can't be null +MD_String8 is_action_valid(MD_Arena *arena, Entity *from, Action a) +{ + assert(a.speech_length <= MAX_SENTENCE_LENGTH && a.speech_length >= 0); + assert(a.kind >= 0 && a.kind < ARRLEN(actions)); + assert(from); + + CanTalkTo talk = get_can_talk_to(from); + if(a.talking_to_somebody) + { + bool found = false; + BUFF_ITER(NpcKind, &talk) + { + if(*it == a.talking_to_kind) + { + found = true; + break; + } + } + if(!found) + { + return FmtWithLint(arena, "Character you're talking to, %s, isn't close enough to be talked to", characters[a.talking_to_kind].enum_name); + } + } + + if(a.kind == ACT_give_item) + { + assert(a.argument.item_to_give >= 0 && a.argument.item_to_give < ARRLEN(items)); + bool has_it = false; + BUFF_ITER(ItemKind, &from->held_items) + { + if(*it == a.argument.item_to_give) + { + has_it = true; + break; + } + } + + if(!has_it) + { + MD_StringJoin join = {.mid = MD_S8Lit(", ")}; + return FmtWithLint(arena, "Can't give item `ITEM_%s`, you only have [%.*s] in your inventory", items[a.argument.item_to_give].enum_name, MD_S8VArg(MD_S8ListJoin(arena, held_item_strings(arena, from), &join))); + } + + if(!a.talking_to_somebody) + { + return MD_S8Lit("You can't give an item to nobody, must target somebody to give an item"); + } + } + + if(a.kind == ACT_leaves_player && from->standing != STANDING_JOINED) + { + return MD_S8Lit("You can't leave the player unless you joined them."); + } + + return (MD_String8){0}; +} + +// from must not be null // the action must have been validated to be valid if you're calling this -void cause_action_side_effects(Entity *from, Entity *to, Action a) +void cause_action_side_effects(Entity *from, Action a) { assert(from); MD_ArenaTemp scratch = MD_GetScratch(0, 0); - MD_String8 failure_reason = is_action_valid(scratch.arena, from, to, a); + + MD_String8 failure_reason = is_action_valid(scratch.arena, from, a); if(failure_reason.size > 0) { Log("Failed to process action, invalid action: `%.*s`\n", MD_S8VArg(failure_reason)); assert(false); } + Entity *to = 0; + if(a.talking_to_somebody) + { + to = get_targeted(from, a.talking_to_kind); + assert(to); + } + if(a.kind == ACT_give_item) { assert(a.argument.item_to_give != ITEM_invalid); @@ -1037,6 +1129,56 @@ void cause_action_side_effects(Entity *from, Entity *to, Action a) MD_ReleaseScratch(scratch); } +typedef struct PropagatingAction +{ + struct PropagatingAction *next; + Action a; + MemoryContext context; + Vec2 from; + bool already_propagated_to[MAX_ENTITIES]; // tracks by index of entity + float progress; // if greater than or equal to 1.0, is freed +} PropagatingAction; + +PropagatingAction *propagating = 0; + +PropagatingAction ignore_entity(Entity *to_ignore, PropagatingAction p) +{ + PropagatingAction to_return = p; + to_return.already_propagated_to[frome(to_ignore).index] = true; + return to_return; +} + +void push_propagating(PropagatingAction to_push) +{ + to_push.context.heard_physically = true; + bool found = false; + for(PropagatingAction *cur = propagating; cur; cur = cur->next) + { + if(cur->progress >= 1.0f) + { + PropagatingAction *prev_next = cur->next; + *cur = to_push; + cur->next = prev_next; + found = true; + break; + } + } + + if(!found) + { + PropagatingAction *cur = MD_PushArray(persistent_arena, PropagatingAction, 1); + *cur = to_push; + MD_StackPush(propagating, cur); + } +} + +float propagating_radius(PropagatingAction *p) +{ + float t = powf(p->progress, 0.65f); + return Lerp(0.0f, t, PROPAGATE_ACTIONS_RADIUS ); +} + + // only called when the action is instantiated, correctly propagates the information // of the action physically and through the party // If the action is invalid, remembers the error if it's an NPC, and does nothing else @@ -1048,22 +1190,18 @@ bool perform_action(Entity *from, Action a) MemoryContext context = {0}; context.author_npc_kind = from->npc_kind; - Entity *action_target = 0; if(from == player && gete(from->talking_to)) { - action_target = gete(from->talking_to); - assert(action_target->is_npc); + context.was_talking_to_somebody = true; + context.talking_to_kind = gete(from->talking_to)->npc_kind; } - else if(gete(player->talking_to) == from) - action_target = player; - - if(action_target) + else { - context.was_directed_at_somebody = true; - context.directed_at_kind = action_target->npc_kind; + context.was_talking_to_somebody = a.talking_to_somebody; + context.talking_to_kind = a.talking_to_kind; } - MD_String8 is_valid = is_action_valid(scratch.arena, from, action_target, a); + MD_String8 is_valid = is_action_valid(scratch.arena, from, a); bool proceed_propagating = true; if(is_valid.size > 0) { @@ -1078,7 +1216,9 @@ bool perform_action(Entity *from, Action a) if(proceed_propagating) { - cause_action_side_effects(from, action_target, a); + Entity *targeted = get_targeted(from, a.talking_to_kind); + + cause_action_side_effects(from, a); // self memory if(!from->is_character) { @@ -1088,46 +1228,22 @@ bool perform_action(Entity *from, Action a) } // memory of target - if(action_target) - { - remember_action(action_target, a, context); - } - - bool propagate_to_party = from->is_character || (from->is_npc && from->standing == STANDING_JOINED); - - if(action_target == player) propagate_to_party = true; - - - if(context.eavesdropped_from_party) propagate_to_party = false; - - if(propagate_to_party) + if(targeted) { - ENTITIES_ITER(gs.entities) - { - if(it->is_npc && it->standing == STANDING_JOINED && it != from && it != action_target) - { - MemoryContext eavesdropped_context = context; - eavesdropped_context.eavesdropped_from_party = true; - remember_action(it, a, eavesdropped_context); - } - } + remember_action(targeted, a, context); } - - // npcs in party when they talk should have their speech heard by who the player is talking to - if(from->is_npc && from->standing == STANDING_JOINED) + // propagate physically + PropagatingAction to_propagate = {0}; + to_propagate.a = a; + to_propagate.context = context; + to_propagate.from = from->pos; + to_propagate = ignore_entity(from, to_propagate); + if(targeted) { - if(gete(player->talking_to) && gete(player->talking_to) != from) - { - assert(gete(player->talking_to)); - assert(gete(player->talking_to)->is_npc); - MemoryContext from_party_context = context; - from_party_context.directed_at_kind = gete(player->talking_to)->npc_kind; - remember_action(gete(player->talking_to), a, from_party_context); - } + to_propagate = ignore_entity(targeted, to_propagate); } - - // TODO Propagate physically + push_propagating(to_propagate); } MD_ReleaseScratch(scratch); @@ -1303,9 +1419,13 @@ void reset_level() assert(ARRLEN(to_load->initial_entities) == ARRLEN(gs.entities)); memcpy(gs.entities, to_load->initial_entities, sizeof(Entity) * MAX_ENTITIES); gs.version = CURRENT_VERSION; - ENTITIES_ITER(gs.entities) + + for (Entity *it = gs.entities; it < gs.entities + ARRLEN(gs.entities); it++) { - if (it->generation == 0) it->generation = 1; // zero value generation means doesn't exist + if(it->exists && it->generation == 0) + { + it->generation = 1; + } } } update_player_from_entities(); @@ -2563,6 +2683,7 @@ void draw_animated_sprite(DrawnAnimatedSprite d) draw_quad(drawn); } + // gets aabbs overlapping the input aabb, including gs.entities and tiles Overlapping get_overlapping(Level *l, AABB aabb) { @@ -2888,7 +3009,6 @@ typedef struct MD_u8 speech[MAX_SENTENCE_LENGTH]; int speech_length; DialogElementKind kind; - bool was_eavesdropped; NpcKind who_said_it; bool was_last_said; } DialogElement; @@ -2921,7 +3041,7 @@ MD_String8List last_said_without_unsaid_words(MD_Arena *arena, Entity *it) // and an argument. So worst case every perception has 2 dialog // elements right now is why it's *2 typedef BUFF(DialogElement, REMEMBERED_MEMORIES*2) Dialog; -Dialog produce_dialog(Entity *talking_to, bool character_names) +Dialog get_dialog_elems(Entity *talking_to, bool character_names) { MD_ArenaTemp scratch = MD_GetScratch(0, 0); assert(talking_to->is_npc); @@ -2932,7 +3052,7 @@ Dialog produce_dialog(Entity *talking_to, bool character_names) { if(it->speech_length > 0) { - DialogElement new_element = { .who_said_it = it->context.author_npc_kind, .was_eavesdropped = it->context.eavesdropped_from_party }; + DialogElement new_element = { .who_said_it = it->context.author_npc_kind }; MD_String8 my_speech = MD_S8(it->speech, it->speech_length); if(last_said_sentence(talking_to).str == it->speech) @@ -2941,7 +3061,17 @@ Dialog produce_dialog(Entity *talking_to, bool character_names) my_speech = MD_S8ListJoin(scratch.arena, last_said_without_unsaid_words(scratch.arena, talking_to), &(MD_StringJoin){.mid = MD_S8Lit(" ")}); } - MD_String8 dialog_speech = FmtWithLint(scratch.arena, "%s: %.*s", characters[it->context.author_npc_kind].name, MD_S8VArg(my_speech)); + MD_String8 name_string = {0}; + if(it->context.was_talking_to_somebody) + { + name_string = FmtWithLint(scratch.arena, "%s to %s", characters[it->context.author_npc_kind].name, characters[it->context.talking_to_kind].name); + } + else + { + name_string = FmtWithLint(scratch.arena, "%s", characters[it->context.author_npc_kind].name); + } + + MD_String8 dialog_speech = FmtWithLint(scratch.arena, "%.*s: %.*s", MD_S8VArg(name_string), MD_S8VArg(my_speech)); memcpy(new_element.speech, dialog_speech.str, dialog_speech.size); new_element.speech_length = (int)dialog_speech.size; @@ -3058,7 +3188,7 @@ void draw_dialog_panel(Entity *talking_to, float alpha) { float new_line_height = dialog_panel.lower_right.Y; - Dialog dialog = produce_dialog(talking_to, false); + Dialog dialog = get_dialog_elems(talking_to, false); if (dialog.cur_index > 0) { for (int i = dialog.cur_index - 1; i >= 0; i--) @@ -3068,28 +3198,21 @@ void draw_dialog_panel(Entity *talking_to, float alpha) Color color; // decide color { - if(it->was_eavesdropped) + if (it->kind == DELEM_PLAYER) { - color = colhex(0x9341a3); + color = BLACK; + } + else if (it->kind == DELEM_NPC) + { + color = colhex(0x345e22); + } + else if (it->kind == DELEM_ACTION_DESCRIPTION) + { + color = colhex(0xb5910e); } else { - if (it->kind == DELEM_PLAYER) - { - color = BLACK; - } - else if (it->kind == DELEM_NPC) - { - color = colhex(0x345e22); - } - else if (it->kind == DELEM_ACTION_DESCRIPTION) - { - color = colhex(0xb5910e); - } - else - { - assert(false); - } + assert(false); } } @@ -3408,6 +3531,29 @@ void frame(void) gs.tick += 1; + PROFILE_SCOPE("propagate actions") + { + for(PropagatingAction *cur = propagating; cur; cur = cur->next) + { + if(cur->progress < 1.0f) + { + cur->progress += dt; + float effective_radius = propagating_radius(cur); + ENTITIES_ITER(gs.entities) + { + if(it->is_npc && LenV2(SubV2(it->pos, cur->from)) < effective_radius) + { + if(!cur->already_propagated_to[frome(it).index]) + { + cur->already_propagated_to[frome(it).index] = true; + remember_action(it, cur->a, cur->context); + } + } + } + } + } + } + // process gs.entities player_in_combat = false; // in combat set by various enemies when they fight the player PROFILE_SCOPE("entity processing") @@ -3988,7 +4134,7 @@ void frame(void) it->perceptions_dirty = false; // needs to be in beginning because they might be redirtied by the new perception MD_String8 prompt_str = {0}; #ifdef DO_CHATGPT_PARSING - prompt_str = generate_chatgpt_prompt(frame_arena, it); + prompt_str = generate_chatgpt_prompt(frame_arena, it, get_can_talk_to(it)); #else generate_prompt(it, &prompt); #endif @@ -4026,10 +4172,6 @@ void frame(void) ActionKind act = ACT_none; it->times_talked_to++; - if(it->memories.data[it->memories.cur_index-1].context.eavesdropped_from_party) - { - PushWithLint(scratch.arena, &dialog_elems, "Responding to eavesdropped: "); - } if(it->npc_kind == NPC_TheBlacksmith && it->standing != STANDING_JOINED) { assert(it->times_talked_to == 1); @@ -4524,6 +4666,19 @@ void frame(void) } } + PROFILE_SCOPE("propagating") + { + for(PropagatingAction *cur = propagating; cur; cur = cur->next) + { + if(cur->progress < 1.0f) + { + float radius = propagating_radius(cur); + Quad to_draw = quad_centered(cur->from, V2(radius, radius)); + draw_quad((DrawParams){true, to_draw, IMG(image_hovering_circle), blendalpha(WHITE, 1.0f - cur->progress)}); + } + } + } + PROFILE_SCOPE("dialog menu") // big dialog panel draw big dialog panel { static float on_screen = 0.0f; @@ -4607,35 +4762,28 @@ void frame(void) if (talking_to && aabb_is_valid(dialog_panel)) { MD_ArenaTemp scratch = MD_GetScratch(0, 0); - Dialog dialog = produce_dialog(talking_to, true); + Dialog dialog = get_dialog_elems(talking_to, true); { for (int i = dialog.cur_index - 1; i >= 0; i--) { DialogElement *it = &dialog.data[i]; { Color color; - if(it->was_eavesdropped) + if (it->kind == DELEM_PLAYER) + { + color = WHITE; + } + else if (it->kind == DELEM_NPC) + { + color = colhex(0x34e05c); + } + else if (it->kind == DELEM_ACTION_DESCRIPTION) { - color = colhex(0xcb40e6); + color = colhex(0xebc334); } else { - if (it->kind == DELEM_PLAYER) - { - color = WHITE; - } - else if (it->kind == DELEM_NPC) - { - color = colhex(0x34e05c); - } - else if (it->kind == DELEM_ACTION_DESCRIPTION) - { - color = colhex(0xebc334); - } - else - { - assert(false); - } + assert(false); } color = blendalpha(color, alpha); diff --git a/makeprompt.h b/makeprompt.h index 36bb010..175912e 100644 --- a/makeprompt.h +++ b/makeprompt.h @@ -97,16 +97,20 @@ typedef struct Action MD_u8 speech[MAX_SENTENCE_LENGTH]; int speech_length; + bool talking_to_somebody; + NpcKind talking_to_kind; + MD_u8 internal_monologue[MAX_SENTENCE_LENGTH]; int internal_monologue_length; } Action; + typedef struct { - bool eavesdropped_from_party; bool i_said_this; // don't trigger npc action on own self memory modification NpcKind author_npc_kind; // only valid if author is AuthorNpc - bool was_directed_at_somebody; - NpcKind directed_at_kind; + bool was_talking_to_somebody; + NpcKind talking_to_kind; + bool heard_physically; // if not physically, the source was directly bool dont_show_to_player; // jester and past memories are hidden to the player when made into dialog } MemoryContext; @@ -275,6 +279,8 @@ typedef struct Entity float anim_change_timer; } Entity; +typedef BUFF(NpcKind, 32) CanTalkTo; + bool npc_is_knight_sprite(Entity *it) { return it->is_npc && (false @@ -360,7 +366,7 @@ typedef struct GameState { Entity entities[MAX_ENTITIES]; } GameState; -#define ENTITIES_ITER(ents) for (Entity *it = ents; it < ents + ARRLEN(ents); it++) if (it->exists) +#define ENTITIES_ITER(ents) for (Entity *it = ents; it < ents + ARRLEN(ents); it++) if (it->exists && !it->destroy && it->generation > 0) bool npc_does_dialog(Entity *it) { @@ -398,49 +404,9 @@ MD_String8List held_item_strings(MD_Arena *arena, Entity *e) return to_return; } -// returns reason why allocated on arena if invalid -// to might be null here, from can't be null -MD_String8 is_action_valid(MD_Arena *arena, Entity *from, Entity *to_might_be_null, Action a) -{ - assert(a.speech_length <= MAX_SENTENCE_LENGTH && a.speech_length >= 0); - assert(a.kind >= 0 && a.kind < ARRLEN(actions)); - assert(from); - - if(a.kind == ACT_give_item) - { - assert(a.argument.item_to_give >= 0 && a.argument.item_to_give < ARRLEN(items)); - bool has_it = false; - BUFF_ITER(ItemKind, &from->held_items) - { - if(*it == a.argument.item_to_give) - { - has_it = true; - break; - } - } - - if(!has_it) - { - MD_StringJoin join = {.mid = MD_S8Lit(", ")}; - return FmtWithLint(arena, "Can't give item `ITEM_%s`, you only have [%.*s] in your inventory", items[a.argument.item_to_give].enum_name, MD_S8VArg(MD_S8ListJoin(arena, held_item_strings(arena, from), &join))); - } - - if(!to_might_be_null) - { - return MD_S8Lit("You can't give an item to nobody, you're currently not in conversation or targeting somebody."); - } - } - - if(a.kind == ACT_leaves_player && from->standing != STANDING_JOINED) - { - return MD_S8Lit("You can't leave the player unless you joined them."); - } - - return (MD_String8){0}; -} // outputs json -MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e) +MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e, CanTalkTo can_talk_to) { assert(e->is_npc); assert(e->npc_kind < ARRLEN(characters)); @@ -461,7 +427,7 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e) { if(it->is_error) { - MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, FmtWithLint(scratch.arena, "ERROR, what you said is incorrect because: %.*s", it->speech_length, it->speech))); + MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_USER, FmtWithLint(scratch.arena, "ERROR, what you said is incorrect because: %.*s", it->speech_length, it->speech))); } else { @@ -472,10 +438,14 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e) PushWithLint(scratch.arena, &cur_list, "{"); if(!it->context.i_said_this) { - PushWithLint(scratch.arena, &cur_list, "character: %s, ", characters[it->context.author_npc_kind].name); + PushWithLint(scratch.arena, &cur_list, "who_i_am: %s, ", characters[it->context.author_npc_kind].name); } MD_String8 speech = MD_S8(it->speech, it->speech_length); + PushWithLint(scratch.arena, &cur_list, "was_heard_in_passing: %s, ", it->context.heard_physically ? "true" : "false"); + + PushWithLint(scratch.arena, &cur_list, "talking_to: %s, ", it->context.was_talking_to_somebody ? characters[it->context.talking_to_kind].enum_name : "nobody"); + // add speech { if(it->context.author_npc_kind == NPC_Player) @@ -595,6 +565,13 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e) } PushWithLint(scratch.arena, &latest_state, "]\n"); + PushWithLint(scratch.arena, &latest_state, "The characters close enough for you to talk to with `talking_to`: ["); + BUFF_ITER(NpcKind, &can_talk_to) + { + PushWithLint(scratch.arena, &latest_state, "%s, ", characters[*it].enum_name); + } + PushWithLint(scratch.arena, &latest_state, "]\n"); + // last thought explanation and re-prompt { MD_String8 last_thought_string = {0}; @@ -650,18 +627,33 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc MD_String8 speech_str = {0}; MD_String8 thoughts_str = {0}; MD_String8 action_arg_str = {0}; + MD_String8 who_i_am_str = {0}; + MD_String8 talking_to_str = {0}; if(error_message.size == 0) { action_str = get_field(message_obj, MD_S8Lit("action")); + who_i_am_str = get_field(message_obj, MD_S8Lit("who_i_am")); speech_str = get_field(message_obj, MD_S8Lit("speech")); thoughts_str = get_field(message_obj, MD_S8Lit("thoughts")); action_arg_str = get_field(message_obj, MD_S8Lit("action_arg")); + talking_to_str = get_field(message_obj, MD_S8Lit("talking_to")); + } + if(error_message.size == 0 && who_i_am_str.size == 0) + { + error_message = MD_S8Lit("Expected field named `who_i_am` in message"); } - if(error_message.size == 0 && action_str.size == 0) { error_message = MD_S8Lit("Expected field named `action` in message"); } + if(error_message.size == 0 && talking_to_str.size == 0) + { + error_message = MD_S8Lit("Expected field named `talking_to` in message"); + } + if(error_message.size == 0 && thoughts_str.size == 0) + { + error_message = MD_S8Lit("Expected field named `thoughts` in message, and to have nonzero size"); + } if(error_message.size == 0 && speech_str.size >= MAX_SENTENCE_LENGTH) { error_message = FmtWithLint(arena, "Speech string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH); @@ -671,6 +663,37 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc error_message = FmtWithLint(arena, "Thoughts string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH); } + assert(!e->is_character); // player can't perform AI actions? + MD_String8 my_name = MD_S8CString(characters[e->npc_kind].enum_name); + if(error_message.size == 0 && !MD_S8Match(who_i_am_str, my_name, 0)) + { + error_message = FmtWithLint(arena, "You are acting as %.*s, not what you said in who_i_am, `%.*s`", MD_S8VArg(my_name), MD_S8VArg(who_i_am_str)); + } + + if(error_message.size == 0) + { + if(MD_S8Match(talking_to_str, MD_S8Lit("nobody"), 0)) + { + out->talking_to_somebody = false; + } + else + { + bool found = false; + for(int i = 0; i < ARRLEN(characters); i++) + { + if(MD_S8Match(talking_to_str, MD_S8CString(characters[i].enum_name), 0)) + { + found = true; + out->talking_to_kind = i; + } + } + if(!found) + { + error_message = FmtWithLint(arena, "Unrecognized character provided in talking_to: `%.*s`", MD_S8VArg(talking_to_str)); + } + } + } + if(error_message.size == 0) { memcpy(out->speech, speech_str.str, speech_str.size); diff --git a/todo.txt b/todo.txt index d191f98..14f9df4 100644 --- a/todo.txt +++ b/todo.txt @@ -1,11 +1,12 @@ DONE - rewrite to have metadesk format for speech and actions - - action and item explanations in system message, along with available actions and items - - remove party eavesdropping, but make clear to AI when things are heard physically or told directly. Allow AI to choose people in vicinity to target with conversation and action. I.e a `talking_to` field. Also add a required character: field in chatgpt response, and make sure it matches the character it's supposed to act as. +DONE - action and item explanations in system message, along with available actions and items +DONE - remove party eavesdropping, but make clear to AI when things are heard physically or told directly. Allow AI to choose people in vicinity to target with conversation and action. I.e a `talking_to` field. Also add a required character: field in chatgpt response, and make sure it matches the character it's supposed to act as. + - The using of items and backpack inventory view. - delete peace tokens, replace with key items and scroll item that says word. - display actions, like giving an item, in memory history - door npc that refuses to open unless player says the word - The Mighty Sword that's conservative, when convinced to be pulled player gets it as an item - - The using of items and backpack inventory view. + - Change give_item to give_item_for_free, and add propose_trade that does trade workflow so NPCs can't easily be screwed over diff --git a/tuning.h b/tuning.h index 7dd2a9b..dc78821 100644 --- a/tuning.h +++ b/tuning.h @@ -10,6 +10,7 @@ #define PERCEPTION_HEARING_RAGE (TILE_SIZE*4.0f) #define CHARACTERS_PER_SEC 45.0f #define PEACE_TOKENS_NEEDED 5 +#define PROPAGATE_ACTIONS_RADIUS (TILE_SIZE*5.0f) #define ARENA_SIZE (1024*1024)