Complete refactor for web, fix parsing bug and add test

main
parent 6fba00cc17
commit 679322313f

@ -7,6 +7,7 @@ const char *global_prompt = "You are a wise dungeonmaster who carefully crafts i
"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" "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"
"You might see messages that look like this: `Within the player's party, while the player is talking to 'Davis', you hear: 'Davis: ACT_none \"This is some example text\"' . You should MOST of the time respond with `ACT_none \"\"` in these cases, as it's not normal to always respond to words you're eavesdropping\n" "You might see messages that look like this: `Within the player's party, while the player is talking to 'Davis', you hear: 'Davis: ACT_none \"This is some example text\"' . You should MOST of the time respond with `ACT_none \"\"` in these cases, as it's not normal to always respond to words you're eavesdropping\n"
"Do NOT make up details that don't exist in the game, this is a big mistake as it confuses the player. The game is simple and small, so prefer to tell the player in character that you don't know how to do something if you aren't explicitly told the information about the game the player requests. E.g, if the player asks how to get rare metals and you don't know how, DO NOT make up something plausible like 'Go to the frost mines in the north', instead say 'I have no idea, sorry.', unless the detail about the game they're asking for is included below.\n"
; ;
const char *top_of_header = "" const char *top_of_header = ""

@ -243,7 +243,16 @@ void do_parsing_tests()
e.npc_kind = NPC_TheBlacksmith; e.npc_kind = NPC_TheBlacksmith;
e.exists = true; e.exists = true;
Action a = {0}; Action a = {0};
MD_String8 error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &a); MD_String8 error;
MD_String8 speech;
speech = MD_S8Lit("Better have a good reason for bothering me.");
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Fmt(scratch.arena, " Within the player's party, while the player is talking to Meld, you hear: ACT_none \"%.*s\"", MD_S8VArg(speech)), &a);
assert(error.size == 0);
assert(a.kind == ACT_none);
assert(MD_S8Match(speech, MD_S8(a.speech, a.speech_length), 0));
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &a);
assert(error.size > 0); assert(error.size > 0);
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \""), &a); error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \""), &a);
assert(error.size > 0); assert(error.size > 0);
@ -257,7 +266,7 @@ void do_parsing_tests()
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &a); error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &a);
assert(error.size == 0); assert(error.size == 0);
assert(a.kind == ACT_give_item); assert(a.kind == ACT_give_item);
assert(a.item_to_give == ITEM_Chalice); assert(a.argument.item_to_give == ITEM_Chalice);
MD_ReleaseScratch(scratch); MD_ReleaseScratch(scratch);
} }
@ -757,12 +766,14 @@ Entity *gete(EntityRef ref)
} }
} }
void push_memory(Entity *e, MD_String8 speech, ActionKind a_kind, MemoryContext context) void push_memory(Entity *e, MD_String8 speech, ActionKind a_kind, ActionArgument a_argument, MemoryContext context, bool is_error)
{ {
Memory new_memory = {.action_taken = a_kind}; Memory new_memory = {.action_taken = a_kind};
assert(speech.size <= ARRLEN(new_memory.speech)); assert(speech.size <= ARRLEN(new_memory.speech));
new_memory.tick_happened = gs.tick; new_memory.tick_happened = gs.tick;
new_memory.context = context; new_memory.context = context;
new_memory.is_error = is_error;
new_memory.action_argument = a_argument;
memcpy(new_memory.speech, speech.str, speech.size); memcpy(new_memory.speech, speech.str, speech.size);
new_memory.speech_length = (int)speech.size; new_memory.speech_length = (int)speech.size;
if(!BUFF_HAS_SPACE(&e->memories)) if(!BUFF_HAS_SPACE(&e->memories))
@ -782,12 +793,12 @@ 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 assert(!to_modify->is_character); // this is a game logic bug if a player action is invalid
push_memory(to_modify, error_message, ACT_none, (MemoryContext){0}); push_memory(to_modify, error_message, ACT_none, (ActionArgument){0}, (MemoryContext){0}, true);
} }
void remember_action(Entity *to_modify, Action a, MemoryContext context) void remember_action(Entity *to_modify, Action a, MemoryContext context)
{ {
push_memory(to_modify, MD_S8(a.speech, a.speech_length), a.kind, context); push_memory(to_modify, MD_S8(a.speech, a.speech_length), a.kind, (ActionArgument){0}, context, false);
} }
// from must not be null, to can be null if the action isn't directed at anybody // from must not be null, to can be null if the action isn't directed at anybody
@ -806,14 +817,14 @@ void cause_action_side_effects(Entity *from, Entity *to, Action a)
if(a.kind == ACT_give_item) if(a.kind == ACT_give_item)
{ {
assert(a.item_to_give != ITEM_none); assert(a.argument.item_to_give != ITEM_none);
assert(to); assert(to);
int item_to_remove = -1; int item_to_remove = -1;
Entity *e = from; Entity *e = from;
BUFF_ITER_I(ItemKind, &e->held_items, i) BUFF_ITER_I(ItemKind, &e->held_items, i)
{ {
if (*it == a.item_to_give) if (*it == a.argument.item_to_give)
{ {
item_to_remove = i; item_to_remove = i;
break; break;
@ -821,13 +832,13 @@ void cause_action_side_effects(Entity *from, Entity *to, Action a)
} }
if (item_to_remove < 0) if (item_to_remove < 0)
{ {
Log("Can't find item %s to give from NPC %s to the player\n", items[a.item_to_give].name, characters[e->npc_kind].name); Log("Can't find item %s to give from NPC %s to the player\n", items[a.argument.item_to_give].name, characters[e->npc_kind].name);
assert(false); assert(false);
} }
else else
{ {
BUFF_REMOVE_AT_INDEX(&e->held_items, item_to_remove); BUFF_REMOVE_AT_INDEX(&e->held_items, item_to_remove);
BUFF_APPEND(&to->held_items, a.item_to_give); BUFF_APPEND(&to->held_items, a.argument.item_to_give);
} }
} }
@ -2886,28 +2897,31 @@ void frame(void)
if (status == 1) if (status == 1)
{ {
// done! we can get the string // done! we can get the string
char sentence_str[MAX_SENTENCE_LENGTH] = { 0 }; char sentence_cstr[MAX_SENTENCE_LENGTH] = { 0 };
EM_ASM( { EM_ASM( {
let generation = get_generation_request_content($0); let generation = get_generation_request_content($0);
stringToUTF8(generation, $1, $2); stringToUTF8(generation, $1, $2);
}, it->gen_request_id, sentence_str, ARRLEN(sentence_str)); }, it->gen_request_id, sentence_cstr, ARRLEN(sentence_cstr) - 1); // I think minus one for null terminator...
MD_String8 sentence_str = MD_S8CString(sentence_cstr);
// parse out from the sentence NPC action and dialog // parse out from the sentence NPC action and dialog
Perception out = { 0 }; Action out = {0};
ChatgptParse parse_response = parse_chatgpt_response(it, sentence_str, &out); MD_ArenaTemp scratch = MD_GetScratch(0, 0);
MD_String8 parse_response = parse_chatgpt_response(scratch.arena, it, sentence_str, &out);
if (parse_response.succeeded) if (parse_response.size == 0)
{ {
process_perception(it, out, player, &gs); perform_action(it, out);
} }
else else
{ {
process_perception(it, (Perception){.type = ErrorMessage, .error = parse_response.error_message}, player, &gs); remember_error(it, parse_response);
it->perceptions_dirty = true; // on poorly formatted AI, just retry request. Explain to it why it's wrong. Adapt, improve, overcome. Time stops for nothing!
} }
MD_ReleaseScratch(scratch);
EM_ASM( { EM_ASM( {
done_with_generation_request($0); done_with_generation_request($0);
}, it->gen_request_id); }, it->gen_request_id);
@ -2916,7 +2930,11 @@ void frame(void)
{ {
Log("Failed to generate dialog! Fuck!\n"); Log("Failed to generate dialog! Fuck!\n");
// need somethin better here. Maybe each sentence has to know if it's player or NPC, that way I can remove the player's dialog // need somethin better here. Maybe each sentence has to know if it's player or NPC, that way I can remove the player's dialog
process_perception(it, (Perception) { .type = NPCDialog, .npc_action_type = ACT_none, .npc_dialog = SENTENCE_CONST("I'm not sure...") }, player, &gs); Action to_perform = {0};
MD_String8 speech_mdstring = MD_S8Lit("I'm not sure...");
memcpy(to_perform.speech, speech_mdstring.str, speech_mdstring.size);
to_perform.speech_length = speech_mdstring.size;
perform_action(it, to_perform);
} }
else if (status == -1) else if (status == -1)
{ {
@ -3422,12 +3440,13 @@ void frame(void)
#ifdef WEB #ifdef WEB
// fire off generation request, save id // fire off generation request, save id
BUFF(char, 512) completion_server_url = { 0 }; MD_ArenaTemp scratch = MD_GetScratch(0, 0);
printf_buff(&completion_server_url, "%s/completion", SERVER_URL); MD_String8 terminated_completion_url = MD_S8Fmt(scratch.arena, "%s/completion\0", SERVER_URL);
int req_id = EM_ASM_INT( { int req_id = EM_ASM_INT( {
return make_generation_request(UTF8ToString($1, $2), UTF8ToString($0)); return make_generation_request(UTF8ToString($1, $2), UTF8ToString($0));
}, completion_server_url.data, prompt_str.str, prompt_str.size); }, terminated_completion_url.str, prompt_str.str, prompt_str.size);
it->gen_request_id = req_id; it->gen_request_id = req_id;
MD_ReleaseScratch(scratch);
#endif #endif
#ifdef DESKTOP #ifdef DESKTOP
@ -3455,7 +3474,7 @@ void frame(void)
MD_String8 mocked_ai_response = {0}; MD_String8 mocked_ai_response = {0};
if(true) if(false)
{ {
MD_StringJoin join = {0}; MD_StringJoin join = {0};
MD_String8 dialog = MD_S8ListJoin(scratch.arena, dialog_elems, &join); MD_String8 dialog = MD_S8ListJoin(scratch.arena, dialog_elems, &join);
@ -3468,6 +3487,10 @@ void frame(void)
mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s \"%.*s\"", actions[act].name, MD_S8VArg(dialog)); mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s \"%.*s\"", actions[act].name, MD_S8VArg(dialog));
} }
} }
else
{
mocked_ai_response = MD_S8Lit(" Within the player's party, while the player is talking to Meld, you hear: ACT_none \"Better have a good reason for bothering me.\"");
}
Action a = {0}; Action a = {0};
MD_String8 error_message = parse_chatgpt_response(scratch.arena, it, mocked_ai_response, &a); MD_String8 error_message = parse_chatgpt_response(scratch.arena, it, mocked_ai_response, &a);
@ -4099,7 +4122,7 @@ void frame(void)
ItemKind given_item_kind = player->held_items.data[to_give]; ItemKind given_item_kind = player->held_items.data[to_give];
BUFF_REMOVE_AT_INDEX(&player->held_items, to_give); BUFF_REMOVE_AT_INDEX(&player->held_items, to_give);
Action give_action = {.kind = ACT_give_item, .item_to_give = given_item_kind}; Action give_action = {.kind = ACT_give_item, .argument = { .item_to_give = given_item_kind }};
perform_action(player, give_action); perform_action(player, give_action);
} }

@ -82,13 +82,18 @@ MD_String8 escape_for_json(MD_Arena *arena, MD_String8 from)
return output; return output;
} }
typedef struct
{
ItemKind item_to_give;
} ActionArgument;
typedef struct Action typedef struct Action
{ {
ActionKind kind; ActionKind kind;
ActionArgument argument;
MD_u8 speech[MAX_SENTENCE_LENGTH]; MD_u8 speech[MAX_SENTENCE_LENGTH];
int speech_length; int speech_length;
ItemKind item_to_give; // only when giving items (duh)
} Action; } Action;
typedef struct typedef struct
{ {
@ -105,6 +110,7 @@ typedef struct Memory
uint64_t tick_happened; // can sort memories by time for some modes of display uint64_t tick_happened; // can sort memories by time for some modes of display
// if action_taken is none, there might still be speech. If speech_length == 0 and action_taken == none, it's an invalid memory and something has gone wrong // if action_taken is none, there might still be speech. If speech_length == 0 and action_taken == none, it's an invalid memory and something has gone wrong
ActionKind action_taken; ActionKind action_taken;
ActionArgument action_argument;
bool is_error; // if is an error message then no context is relevant bool is_error; // if is an error message then no context is relevant
@ -412,11 +418,11 @@ MD_String8 is_action_valid(MD_Arena *arena, Entity *from, Entity *to_might_be_nu
if(a.kind == ACT_give_item) if(a.kind == ACT_give_item)
{ {
assert(a.item_to_give >= 0 && a.item_to_give < ARRLEN(items)); assert(a.argument.item_to_give >= 0 && a.argument.item_to_give < ARRLEN(items));
bool has_it = false; bool has_it = false;
BUFF_ITER(ItemKind, &from->held_items) BUFF_ITER(ItemKind, &from->held_items)
{ {
if(*it == a.item_to_give) if(*it == a.argument.item_to_give)
{ {
has_it = true; has_it = true;
break; break;
@ -426,7 +432,7 @@ MD_String8 is_action_valid(MD_Arena *arena, Entity *from, Entity *to_might_be_nu
if(!has_it) if(!has_it)
{ {
MD_StringJoin join = {.mid = MD_S8Lit(", ")}; MD_StringJoin join = {.mid = MD_S8Lit(", ")};
return MD_S8Fmt(arena, "Can't give item `ITEM_%s`, you only have [%.*s] in your inventory", items[a.item_to_give].enum_name, MD_S8VArg(MD_S8ListJoin(arena, held_item_strings(arena, from), &join))); return MD_S8Fmt(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) if(!to_might_be_null)
@ -518,7 +524,21 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e)
sent_type = it->context.author_npc_kind == e->npc_kind ? MSG_ASSISTANT : MSG_USER; sent_type = it->context.author_npc_kind == e->npc_kind ? MSG_ASSISTANT : MSG_USER;
} }
current_string = MD_S8Fmt(scratch.arena, "%.*s ACT_%s %.*s", MD_S8VArg(context_string), actions[it->action_taken].name, it->speech_length, it->speech); if(actions[it->action_taken].takes_argument)
{
if(it->action_taken == ACT_give_item)
{
current_string = MD_S8Fmt(scratch.arena, "%.*s ACT_%s(ITEM_%s) \"%.*s\"", MD_S8VArg(context_string), actions[it->action_taken].name, items[it->action_argument.item_to_give].enum_name, it->speech_length, it->speech);
}
else
{
assert(false); // don't know how to serialize this action with argument into text
}
}
else
{
current_string = MD_S8Fmt(scratch.arena, "%.*s ACT_%s \"%.*s\"", MD_S8VArg(context_string), actions[it->action_taken].name, it->speech_length, it->speech);
}
} }
assert(sent_type != -1); assert(sent_type != -1);
@ -603,8 +623,8 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
MD_u64 beginning_of_action = act_pos + action_prefix.size; MD_u64 beginning_of_action = act_pos + action_prefix.size;
MD_u64 parenth = MD_S8FindSubstring(sentence, MD_S8Lit("("), 0, 0); MD_u64 parenth = MD_S8FindSubstring(sentence, MD_S8Lit("("), beginning_of_action, 0);
MD_u64 space = MD_S8FindSubstring(sentence, MD_S8Lit(" "), 0, 0); MD_u64 space = MD_S8FindSubstring(sentence, MD_S8Lit(" "), beginning_of_action, 0);
MD_u64 end_of_action = parenth < space ? parenth : space; MD_u64 end_of_action = parenth < space ? parenth : space;
if(end_of_action == sentence.size) if(end_of_action == sentence.size)
@ -686,7 +706,7 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
if(MD_S8Match(item_str, item_name, 0)) if(MD_S8Match(item_str, item_name, 0))
{ {
item_found = true; item_found = true;
out->item_to_give = *it; out->argument.item_to_give = *it;
} }
} }

Loading…
Cancel
Save