Improved eavesdropping and error detection

main
Cameron Murphy Reikes 2 years ago
parent 9285bc5683
commit 52b0eab2e4

@ -4,9 +4,8 @@
if "%1" == "codegen" ( call run_codegen.bat || goto :error ) else ( echo NOT RUNNING CODEGEN ) 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 START /B zig cc -DDEVTOOLS -Igen -Ithirdparty -lDbghelp -lGdi32 -lD3D11 -lOle32 -gfull -gcodeview -o main_zig.exe main.c
zig cc -DDEVTOOLS -Igen -Ithirdparty -lDbghelp -lGdi32 -lD3D11 -lOle32 -gfull -gcodeview -o main.exe main.c || goto :error cl /diagnostics:caret /DDEVTOOLS /Igen /Ithirdparty /W3 /Zi /WX Dbghelp.lib main.c || goto :error
@REM cl /Igen /Ithirdparty /W3 /Zi /WX main.c || goto :error
goto :EOF goto :EOF

@ -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. // @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" 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 = "" const char *top_of_header = ""
"#pragma once\n" "#pragma once\n"
@ -101,8 +103,11 @@ typedef struct
CharacterGen characters[] = { 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 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 PLAYERSAY(stuff) "Player: \"" stuff "\"\n"
#define PLAYERDO_ARG(action, arg) "Player: " action "(" arg ")\n"
#define NPCSAY(stuff) NPC_NAME ": ACT_none \"" stuff "\"\n" #define NPCSAY(stuff) NPC_NAME ": ACT_none \"" stuff "\"\n"
#define NPCDOSAY_ARG(stuff, action, arg) NPC_NAME ": " action "(" arg ") \"" 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", .name = "Invalid",
.enum_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.", "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", .enum_name = "Edeline",
.prompt = "\n" .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" "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" "\n"
"Player: \"Hello\"\n" PLAYERSAY("What's up? Who are you?")
"Edeline: ACT_none \"I see great danger in your future.\"\n" NPCSAY("I am Edeline, master of the future")
"Player: \"Oh really?\"" PLAYERSAY("Oh really? What do you say about joinin my party?")
"The player is currently holding a tripod\n" NPCDOSAY("ACT_joins_player", "Absolutely!")
"Edeline: ACT_none \"That tripod will be the decisive factor in your victory\"\n" "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",
"\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.",
}, },
{ {
.name = "Death", .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") NPCSAY("I can clearly see you don't have it. Do not attempt to fool me if you value your head")
PLAYERSAY("Presents it") PLAYERSAY("Presents it")
NPCSAY("Did you just say 'presents it' out loud thinking I'd think that means you have the chalice?") 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" "\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 #undef NPC_NAME

@ -3387,6 +3387,10 @@ F cost: G + H
Action act = ACT_none; Action act = ACT_none;
it->times_talked_to++; 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) if(it->npc_kind == NPC_TheBlacksmith && it->standing != STANDING_JOINED)
{ {
assert(it->times_talked_to == 1); assert(it->times_talked_to == 1);
@ -3401,14 +3405,15 @@ F cost: G + H
BUFF(char, 1024) mocked_ai_response = { 0 }; BUFF(char, 1024) mocked_ai_response = { 0 };
if (argument) 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 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 }; 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); process_perception(it, p, player, &gs);
#undef SAY #undef SAY
#endif #endif

@ -83,6 +83,7 @@ typedef struct Perception
PerceptionType type; PerceptionType type;
bool was_eavesdropped; // when the npc is in a party they perceive player conversations, but in the third party. Formatted differently 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 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 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) if(!p.was_eavesdropped && p.type == NPCDialog)
p.who_said_it = happened_to_npc->npc_kind; 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)) if (!BUFF_HAS_SPACE(&happened_to_npc->remembered_perceptions))
BUFF_REMOVE_FRONT(&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; Perception eavesdropped = p;
eavesdropped.was_eavesdropped = true; eavesdropped.was_eavesdropped = true;
eavesdropped.talked_to_while_eavesdropped = happened_to_npc->npc_kind;
ENTITIES_ITER(gs->entities) ENTITIES_ITER(gs->entities)
{ {
if(it->is_npc && it->standing == STANDING_JOINED && it != happened_to_npc) 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) 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)); if (p.npc_action_type == ACT_allows_player_to_pass)
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 (*it == p.given_item) happened_to_npc->target_goto = AddV2(happened_to_npc->pos, V2(-50.0, 0.0));
{ happened_to_npc->moved = true;
item_to_remove = i;
break;
}
} }
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, happened_to_npc->standing = STANDING_FIGHTING;
characters[happened_to_npc->npc_kind].name); }
assert(false); 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 else
{ {
BUFF_REMOVE_AT_INDEX(&happened_to_npc->held_items, item_to_remove); // actions that take an argument have to have some kind of side effect based on that argument...
BUFF_APPEND(&player->held_items, p.given_item); 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) else if(p.type == ErrorMessage)
{ {
@ -546,7 +553,7 @@ bool printf_buff_impl(BuffRef into, const char *format, ...)
return succeeded; 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; typedef BUFF(char, 512) SmallTextChunk;
@ -683,7 +690,7 @@ void generate_chatgpt_prompt(Entity *it, PromptBuff *into)
{ {
assert(it->error.cur_index > 0); 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); 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) else if (it->type == PlayerAction)
{ {
@ -724,7 +731,9 @@ void generate_chatgpt_prompt(Entity *it, PromptBuff *into)
else if (it->type == NPCDialog) else if (it->type == NPCDialog)
{ {
assert(it->npc_action_type < ARRLEN(actions)); 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); percept_action_str(*it, it->npc_action_type).data, it->npc_dialog.data);
sent_type = MSG_ASSISTANT; sent_type = MSG_ASSISTANT;
} }
@ -752,7 +761,7 @@ void generate_chatgpt_prompt(Entity *it, PromptBuff *into)
if(it->was_eavesdropped) if(it->was_eavesdropped)
{ {
DialogNode eavesdropped = {0}; 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; cur_node = eavesdropped;
} }
dump_json_node(into,sent_type, cur_node.data); 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) 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 else
{ {
@ -969,6 +978,28 @@ ChatgptParse parse_chatgpt_response(Entity *it, char *sentence_str, Perception *
size_t sentence_length = strlen(sentence_str); 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 }; SmallTextChunk action_string = { 0 };
sentence_str += get_until(&action_string, sentence_str, "( "); 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 }; SmallTextChunk dialog_str = { 0 };
if (actions[out->npc_action_type].takes_argument) 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, '('); EXPECT(*sentence_str, '(');
sentence_str += 1; sentence_str += 1;
@ -1062,6 +1093,13 @@ ChatgptParse parse_chatgpt_response(Entity *it, char *sentence_str, Perception *
return to_return; 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); memcpy(out->npc_dialog.data, dialog_str.data, dialog_str.cur_index);
out->npc_dialog.cur_index = dialog_str.cur_index; out->npc_dialog.cur_index = dialog_str.cur_index;

@ -371,6 +371,8 @@ func completion(w http.ResponseWriter, req *http.Request) {
} else { } else {
// parse the json walter // parse the json walter
var parsed []ChatGPTElem var parsed []ChatGPTElem
log.Printf("----------------------------------------------------------")
defer log.Printf("----------------------------------------------------------")
log.Printf("Parsing prompt string `%s`\n", promptString) log.Printf("Parsing prompt string `%s`\n", promptString)
err = json.Unmarshal([]byte(promptString), &parsed) err = json.Unmarshal([]byte(promptString), &parsed)
if err != nil { 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) log.Printf("Full response: \n````\n%s\n````\n", resp)
response = resp.Choices[0].Message.Content response = resp.Choices[0].Message.Content
/*
with_action := strings.SplitAfter(response, "ACT_") with_action := strings.SplitAfter(response, "ACT_")
if len(with_action) != 2 { if len(with_action) != 2 {
log.Printf("Could not find action in response string `%s`\n", response) 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 return
} }
response = with_action[1] response = with_action[1]
// trim ending quotation mark. There might be text after the ending quotation mark because chatgpt sometimes // 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 // 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 // quotation mark needs to be ignored
between_quotes := strings.Split(response, "\"") between_quotes := strings.Split(response, "\"")
// [action] " [stuff] " [anything extra] // [action] " [stuff] " [anything extra]
if len(between_quotes) < 2 { if len(between_quotes) < 2 {
log.Printf("Could not find enough quotes in response string `%s`\n", response) log.Printf("Could not find enough quotes in response string `%s`\n", response)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
} }
response = between_quotes[0] + "\"" + between_quotes[1] + "\"" response = between_quotes[0] + "\"" + between_quotes[1] + "\""
*/
} }
if logResponses { if logResponses {
log.Println("Println response: `", response + "`") log.Println("Println response: `", response + "`")

Loading…
Cancel
Save