From 52b0eab2e49df9db5451591f7526643c4a8b032c Mon Sep 17 00:00:00 2001 From: Cameron Reikes Date: Wed, 10 May 2023 13:33:12 -0700 Subject: [PATCH] Improved eavesdropping and error detection --- build_desktop_debug.bat | 5 +- character_info.h | 27 +++++--- main.c | 11 +++- makeprompt.h | 142 +++++++++++++++++++++++++--------------- server/main.go | 22 ++++--- 5 files changed, 130 insertions(+), 77 deletions(-) diff --git a/build_desktop_debug.bat b/build_desktop_debug.bat index 27987cf..3842b4c 100644 --- a/build_desktop_debug.bat +++ b/build_desktop_debug.bat @@ -4,9 +4,8 @@ if "%1" == "codegen" ( call run_codegen.bat || goto :error ) else ( echo NOT RUNNING CODEGEN ) -@REM cl /diagnostics:caret /DDEVTOOLS /Igen /Ithirdparty /W3 /Zi /WX Dbghelp.lib main.c || goto :error -zig cc -DDEVTOOLS -Igen -Ithirdparty -lDbghelp -lGdi32 -lD3D11 -lOle32 -gfull -gcodeview -o main.exe main.c || goto :error -@REM cl /Igen /Ithirdparty /W3 /Zi /WX main.c || goto :error +START /B zig cc -DDEVTOOLS -Igen -Ithirdparty -lDbghelp -lGdi32 -lD3D11 -lOle32 -gfull -gcodeview -o main_zig.exe main.c +cl /diagnostics:caret /DDEVTOOLS /Igen /Ithirdparty /W3 /Zi /WX Dbghelp.lib main.c || goto :error goto :EOF diff --git a/character_info.h b/character_info.h index 7f663b3..4f91eef 100644 --- a/character_info.h +++ b/character_info.h @@ -4,7 +4,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 wise dungeonmaster who carefully crafts interesting dialog and actions for an NPC in an action-rpg video game. It is critical that you always respond in the format shown below, where you respond like `ACT_action \"This is my response\", even if the player says something vulgar or offensive, as the text is parsed by a program which expects it to look like that. Do not ever refer to yourself as an NPC or show an understanding of the modern world outside the game, always stay in character.\n" -"Actions which have () after them take an argument, which somes from some information in the prompt. For example, ACT_give_item() takes an argument, the item to give to the player from the NPC. So the output text looks something like `ACT_give_item(ITEM_sword) \"Here is my sword, young traveler\"`. This item must come from the NPC's inventory which is specified farther down.\n"; +"Actions which have () after them take an argument, which somes from some information in the prompt. For example, ACT_give_item() takes an argument, the item to give to the player from the NPC. So the output text looks something like `ACT_give_item(ITEM_sword) \"Here is my sword, young traveler\"`. This item must come from the NPC's inventory which is specified farther down.\n" +"From within the player's party, NPCs may hear eavesdropped conversations. Often they don't need to interject, so it's fine to say something like `ACT_none ""` to signify that the NPC doesn't need to interject.\n" +; const char *top_of_header = "" "#pragma once\n" @@ -101,8 +103,11 @@ typedef struct CharacterGen characters[] = { #define NUMEROLOGIST "They are a 'numberoligist' who believes in the sacred power of numbers, that if you have the number 8 in your birthday you are magic and destined for success. " #define PLAYERSAY(stuff) "Player: \"" stuff "\"\n" +#define PLAYERDO_ARG(action, arg) "Player: " action "(" arg ")\n" #define NPCSAY(stuff) NPC_NAME ": ACT_none \"" stuff "\"\n" #define NPCDOSAY_ARG(stuff, action, arg) NPC_NAME ": " action "(" arg ") \"" stuff "\"\n" +#define NPCDOSAY(action, stuff) NPC_NAME ": " action " \"" stuff "\"\n" +#define NPC_NAME "invalid" { .name = "Invalid", .enum_name = "Invalid", @@ -154,18 +159,18 @@ CharacterGen characters[] = { "The NPC you will be acting as is named TheGuard. He wants to block the player from going to a secret artifact he's standing in front of. He has no idea how long he's been alive for, his entire existence is just standing there doing nothing. He'll let the player pass if they bring him Tripod, as he's fascinated by it.", }, { - .name = "Edeline", +#undef NPC_NAME +#define NPC_NAME "Edeline" + .name = NPC_NAME, .enum_name = "Edeline", .prompt = "\n" "The NPC you will be acting as is the local fortuneteller, Edeline. Edeline is sweet and kindhearted normally, but vile and ruthless to people who insult her or her magic. She specializes in a new 'Purple Magic' that Meld despises. Meld, the local blacksmith, thinks Edeline's magic is silly. An example of an interaction between the player and the NPC, Edeline:\n" "\n" - "Player: \"Hello\"\n" - "Edeline: ACT_none \"I see great danger in your future.\"\n" - "Player: \"Oh really?\"" - "The player is currently holding a tripod\n" - "Edeline: ACT_none \"That tripod will be the decisive factor in your victory\"\n" - "\n" - "The NPC you will be acting as is named Edeline. She is the master of the future, the star reader. Both are self-given titles, but her ability to predict the future has garnered attention from many who live in Worchen. However, some have called her 'unreliable' at times and her predictions can at times be either cryptic or broadly interpreted.", + PLAYERSAY("What's up? Who are you?") + NPCSAY("I am Edeline, master of the future") + PLAYERSAY("Oh really? What do you say about joinin my party?") + NPCDOSAY("ACT_joins_player", "Absolutely!") + "Edeline is the master of the future, the star reader. Both are self-given titles, but her ability to predict the future has garnered attention from many who live in Worchen. However, some have called her 'unreliable' at times and her predictions can at times be either cryptic or broadly interpreted. She is eager to join the player's party if asked", }, { .name = "Death", @@ -213,8 +218,10 @@ CharacterGen characters[] = { NPCSAY("I can clearly see you don't have it. Do not attempt to fool me if you value your head") PLAYERSAY("Presents it") NPCSAY("Did you just say 'presents it' out loud thinking I'd think that means you have the chalice?") + PLAYERDO_ARG("ACT_gives_item", "ITEM_Chalice") + NPCDOSAY("ACT_knights_player", "How beautiful... You are clearly worth the title of knight!") "\n" - "If the player does indeed present the king with the chalice of gold, the king will be overwhelemd with respect and feel he has no choice but to knight the player, ending the game.", + "If the player does indeed present the king with the chalice of gold, the king will be overwhelemd with respect and feel he has no choice but to knight the player, ending the game. The Chalice of Gold ALWAYS makes the player a knight, if the player gives it to the king.", }, { #undef NPC_NAME diff --git a/main.c b/main.c index 2029305..d2988a2 100644 --- a/main.c +++ b/main.c @@ -3387,6 +3387,10 @@ F cost: G + H Action act = ACT_none; it->times_talked_to++; + if(it->remembered_perceptions.data[it->remembered_perceptions.cur_index-1].was_eavesdropped) + { + printf_buff(&dialog_string, "Responding to eavesdropped: "); + } if(it->npc_kind == NPC_TheBlacksmith && it->standing != STANDING_JOINED) { assert(it->times_talked_to == 1); @@ -3401,14 +3405,15 @@ F cost: G + H BUFF(char, 1024) mocked_ai_response = { 0 }; if (argument) { - printf_buff(&mocked_ai_response, "%s(%s) \"%s\"", actions[act].name, argument, dialog_string.data); + printf_buff(&mocked_ai_response, "ACT_%s(%s) \"%s\"", actions[act].name, argument, dialog_string.data); } else { - printf_buff(&mocked_ai_response, "%s \"%s\"", actions[act].name, dialog_string.data); + printf_buff(&mocked_ai_response, "ACT_%s \"%s\"", actions[act].name, dialog_string.data); } Perception p = { 0 }; - assert(parse_chatgpt_response(it, mocked_ai_response.data, &p)); + ChatgptParse parsed = parse_chatgpt_response(it, mocked_ai_response.data, &p); + assert(parsed.succeeded); process_perception(it, p, player, &gs); #undef SAY #endif diff --git a/makeprompt.h b/makeprompt.h index 4ddb85c..a0ad180 100644 --- a/makeprompt.h +++ b/makeprompt.h @@ -83,6 +83,7 @@ typedef struct Perception PerceptionType type; bool was_eavesdropped; // when the npc is in a party they perceive player conversations, but in the third party. Formatted differently + NpcKind talked_to_while_eavesdropped; // better chatpgpt messages when the NPCs know who the player is talking to when they eavesdrop a perception float damage_done; // Valid in player action and enemy action ItemKind given_item; // valid in player action and enemy action when the kind is such that there is an item to be given @@ -410,7 +411,8 @@ void process_perception(Entity *happened_to_npc, Perception p, Entity *player, G if(!p.was_eavesdropped && p.type == NPCDialog) p.who_said_it = happened_to_npc->npc_kind; - if (!p.was_eavesdropped && p.type != NPCDialog) happened_to_npc->perceptions_dirty = true; // NPCs perceive their own actions. Self is a perception + bool should_respond_to_this = !(!p.was_eavesdropped && p.type == NPCDialog); // NPCs shouldn't respond to what they said, what they said is self-perception. Would trigger endless NPC thought loop if possible + if (should_respond_to_this) happened_to_npc->perceptions_dirty = true; // NPCs perceive their own actions. Self is a perception if (!BUFF_HAS_SPACE(&happened_to_npc->remembered_perceptions)) BUFF_REMOVE_FRONT(&happened_to_npc->remembered_perceptions); @@ -420,6 +422,7 @@ void process_perception(Entity *happened_to_npc, Perception p, Entity *player, G { Perception eavesdropped = p; eavesdropped.was_eavesdropped = true; + eavesdropped.talked_to_while_eavesdropped = happened_to_npc->npc_kind; ENTITIES_ITER(gs->entities) { if(it->is_npc && it->standing == STANDING_JOINED && it != happened_to_npc) @@ -454,60 +457,64 @@ void process_perception(Entity *happened_to_npc, Perception p, Entity *player, G } else if (p.type == NPCDialog) { - if (p.npc_action_type == ACT_allows_player_to_pass) + // everything in this branch has an effect + if(!p.was_eavesdropped) { - happened_to_npc->target_goto = AddV2(happened_to_npc->pos, V2(-50.0, 0.0)); - happened_to_npc->moved = true; - } - else if (p.npc_action_type == ACT_fights_player) - { - happened_to_npc->standing = STANDING_FIGHTING; - } - else if(p.npc_action_type == ACT_knights_player) - { - player->knighted = true; - } - else if (p.npc_action_type == ACT_stops_fighting_player) - { - happened_to_npc->standing = STANDING_INDIFFERENT; - } - else if (p.npc_action_type == ACT_leaves_player) - { - happened_to_npc->standing = STANDING_INDIFFERENT; - } - else if (p.npc_action_type == ACT_joins_player) - { - happened_to_npc->standing = STANDING_JOINED; - } - else if (p.npc_action_type == ACT_give_item) - { - int item_to_remove = -1; - Entity *e = happened_to_npc; - BUFF_ITER_I(ItemKind, &e->held_items, i) + if (p.npc_action_type == ACT_allows_player_to_pass) { - if (*it == p.given_item) - { - item_to_remove = i; - break; - } + happened_to_npc->target_goto = AddV2(happened_to_npc->pos, V2(-50.0, 0.0)); + happened_to_npc->moved = true; } - if (item_to_remove < 0) + else if (p.npc_action_type == ACT_fights_player) { - Log("Can't find item %s to give from NPC %s to the player\n", items[p.given_item].name, - characters[happened_to_npc->npc_kind].name); - assert(false); + happened_to_npc->standing = STANDING_FIGHTING; + } + else if(p.npc_action_type == ACT_knights_player) + { + player->knighted = true; + } + else if (p.npc_action_type == ACT_stops_fighting_player) + { + happened_to_npc->standing = STANDING_INDIFFERENT; + } + else if (p.npc_action_type == ACT_leaves_player) + { + happened_to_npc->standing = STANDING_INDIFFERENT; + } + else if (p.npc_action_type == ACT_joins_player) + { + happened_to_npc->standing = STANDING_JOINED; + } + else if (p.npc_action_type == ACT_give_item) + { + int item_to_remove = -1; + Entity *e = happened_to_npc; + BUFF_ITER_I(ItemKind, &e->held_items, i) + { + if (*it == p.given_item) + { + item_to_remove = i; + break; + } + } + if (item_to_remove < 0) + { + Log("Can't find item %s to give from NPC %s to the player\n", items[p.given_item].name, + characters[happened_to_npc->npc_kind].name); + assert(false); + } + else + { + BUFF_REMOVE_AT_INDEX(&happened_to_npc->held_items, item_to_remove); + BUFF_APPEND(&player->held_items, p.given_item); + } } else { - BUFF_REMOVE_AT_INDEX(&happened_to_npc->held_items, item_to_remove); - BUFF_APPEND(&player->held_items, p.given_item); + // actions that take an argument have to have some kind of side effect based on that argument... + assert(!actions[p.npc_action_type].takes_argument); } } - else - { - // actions that take an argument have to have some kind of side effect based on that argument... - assert(!actions[p.npc_action_type].takes_argument); - } } else if(p.type == ErrorMessage) { @@ -546,7 +553,7 @@ bool printf_buff_impl(BuffRef into, const char *format, ...) return succeeded; } -#define printf_buff(buff_ptr, ...) printf_buff_impl(BUFF_MAKEREF(buff_ptr), __VA_ARGS__) +#define printf_buff(buff_ptr, ...) { printf_buff_impl(BUFF_MAKEREF(buff_ptr), __VA_ARGS__); if(false) printf(__VA_ARGS__); } typedef BUFF(char, 512) SmallTextChunk; @@ -683,7 +690,7 @@ void generate_chatgpt_prompt(Entity *it, PromptBuff *into) { assert(it->error.cur_index > 0); printf_buff(&cur_node, "ERROR, YOU SAID SOMETHING WRONG: The program can't parse what you said because: %s", it->error.data); - sent_type = MSG_USER; + sent_type = MSG_SYSTEM; } else if (it->type == PlayerAction) { @@ -724,7 +731,9 @@ void generate_chatgpt_prompt(Entity *it, PromptBuff *into) else if (it->type == NPCDialog) { assert(it->npc_action_type < ARRLEN(actions)); - printf_buff(&cur_node, "%s: %s \"%s\"", characters[e->npc_kind].name, + NpcKind who_said_it = e->npc_kind; + if(it->was_eavesdropped) who_said_it = it->talked_to_while_eavesdropped; + printf_buff(&cur_node, "%s: %s \"%s\"", characters[who_said_it].name, percept_action_str(*it, it->npc_action_type).data, it->npc_dialog.data); sent_type = MSG_ASSISTANT; } @@ -752,7 +761,7 @@ void generate_chatgpt_prompt(Entity *it, PromptBuff *into) if(it->was_eavesdropped) { DialogNode eavesdropped = {0}; - printf_buff(&eavesdropped , "From within the player's party, you hear: '%s'", cur_node); + printf_buff(&eavesdropped , "Within the player's party, while the player is talking to '%s', you hear: '%s'", characters[it->talked_to_while_eavesdropped].name, cur_node.data); cur_node = eavesdropped; } dump_json_node(into,sent_type, cur_node.data); @@ -781,7 +790,7 @@ void generate_chatgpt_prompt(Entity *it, PromptBuff *into) if(e->held_items.cur_index > 0) { - printf_buff(&latest_state_node, "\nThe NPC you're acting as, %s, has these items in their inventory: [%s]", characters[it->npc_kind].name, item_string(it).data); + printf_buff(&latest_state_node, "\nThe NPC you're acting as, %s, has these items in their inventory: [%s]\n", characters[it->npc_kind].name, item_string(it).data); } else { @@ -969,6 +978,28 @@ ChatgptParse parse_chatgpt_response(Entity *it, char *sentence_str, Perception * size_t sentence_length = strlen(sentence_str); + // dialog begins at ACT_ + const char *to_find = "ACT_"; + size_t to_find_len = strlen(to_find); + bool found = false; + while(true) + { + if(*to_find == '\0') break; + if(strncmp(sentence_str, to_find, to_find_len) == 0) + { + sentence_str += to_find_len; + found = true; + break; + } + sentence_str += 1; + } + + if(!found) + { + printf_buff(&to_return.error_message, "Couldn't find action beginning with 'ACT_'.\n"); + return to_return; + } + SmallTextChunk action_string = { 0 }; sentence_str += get_until(&action_string, sentence_str, "( "); @@ -995,7 +1026,7 @@ ChatgptParse parse_chatgpt_response(Entity *it, char *sentence_str, Perception * SmallTextChunk dialog_str = { 0 }; if (actions[out->npc_action_type].takes_argument) { -#define EXPECT(chr, val) if (chr != val) { printf_buff(&to_return.error_message, "Improperly formatted sentence, expected character '%c' but got '%c'\n", sentence_str, val, chr); return to_return; } +#define EXPECT(chr, val) if (chr != val) { printf_buff(&to_return.error_message, "Improperly formatted sentence, expected character '%c' but got '%c'\n", val, chr); return to_return; } EXPECT(*sentence_str, '('); sentence_str += 1; @@ -1062,6 +1093,13 @@ ChatgptParse parse_chatgpt_response(Entity *it, char *sentence_str, Perception * return to_return; } + char next_char = *(sentence_str + 1); + if(!(next_char == '\0' || next_char == '\n')) + { + printf_buff(&to_return.error_message, "Expected dialog to end after the last quote, but instead found character '%c'\n", next_char); + return to_return; + } + memcpy(out->npc_dialog.data, dialog_str.data, dialog_str.cur_index); out->npc_dialog.cur_index = dialog_str.cur_index; diff --git a/server/main.go b/server/main.go index d9b7937..089f696 100644 --- a/server/main.go +++ b/server/main.go @@ -371,6 +371,8 @@ func completion(w http.ResponseWriter, req *http.Request) { } else { // parse the json walter var parsed []ChatGPTElem + log.Printf("----------------------------------------------------------") + defer log.Printf("----------------------------------------------------------") log.Printf("Parsing prompt string `%s`\n", promptString) err = json.Unmarshal([]byte(promptString), &parsed) if err != nil { @@ -405,6 +407,7 @@ func completion(w http.ResponseWriter, req *http.Request) { log.Printf("Full response: \n````\n%s\n````\n", resp) response = resp.Choices[0].Message.Content + /* with_action := strings.SplitAfter(response, "ACT_") if len(with_action) != 2 { log.Printf("Could not find action in response string `%s`\n", response) @@ -412,18 +415,19 @@ func completion(w http.ResponseWriter, req *http.Request) { return } response = with_action[1] - + // trim ending quotation mark. There might be text after the ending quotation mark because chatgpt sometimes - // puts addendums in its responses, like `ACT_none "Hey" (I wanted the NPC to say hey here)`. The stuffafter the second - // quotation mark needs to be ignored - between_quotes := strings.Split(response, "\"") - // [action] " [stuff] " [anything extra] + // puts addendums in its responses, like `ACT_none "Hey" (I wanted the NPC to say hey here)`. The stuffafter the second + // quotation mark needs to be ignored + between_quotes := strings.Split(response, "\"") + // [action] " [stuff] " [anything extra] if len(between_quotes) < 2 { - log.Printf("Could not find enough quotes in response string `%s`\n", response) - w.WriteHeader(http.StatusInternalServerError) - return + log.Printf("Could not find enough quotes in response string `%s`\n", response) + w.WriteHeader(http.StatusInternalServerError) + return } - response = between_quotes[0] + "\"" + between_quotes[1] + "\"" + response = between_quotes[0] + "\"" + between_quotes[1] + "\"" + */ } if logResponses { log.Println("Println response: `", response + "`")