From 6fba00cc17b0caa5fac2bf36a59be05adcb0ca03 Mon Sep 17 00:00:00 2001 From: Cameron Reikes Date: Mon, 22 May 2023 17:58:30 -0700 Subject: [PATCH] Giant refactor, balmers peak split up perceptions into memories/actions --- build_desktop_debug.bat | 2 +- character_info.h | 5 + codegen.c | 2 +- main.c | 465 +++++++++++++++++++--------- makeprompt.h | 671 +++++++++------------------------------- tuning.h | 17 + 6 files changed, 483 insertions(+), 679 deletions(-) diff --git a/build_desktop_debug.bat b/build_desktop_debug.bat index 3842b4c..0ba36ef 100644 --- a/build_desktop_debug.bat +++ b/build_desktop_debug.bat @@ -4,7 +4,7 @@ if "%1" == "codegen" ( call run_codegen.bat || goto :error ) else ( echo NOT RUNNING CODEGEN ) -START /B zig cc -DDEVTOOLS -Igen -Ithirdparty -lDbghelp -lGdi32 -lD3D11 -lOle32 -gfull -gcodeview -o main_zig.exe main.c +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 88c9ff3..2202efe 100644 --- a/character_info.h +++ b/character_info.h @@ -114,6 +114,11 @@ CharacterGen characters[] = { .enum_name = "Invalid", .prompt = "There has been an internal error.", }, + { + .name = "Player", + .enum_name = "Player", + .prompt = "There has been an internal error.", + }, { .name = "Fredrick", .enum_name = "OldMan", diff --git a/codegen.c b/codegen.c index eb51675..bc266ea 100644 --- a/codegen.c +++ b/codegen.c @@ -148,7 +148,7 @@ int main(int argc, char **argv) #define GEN_TABLE(arr_elem_type, table_name, arr, str_access) { fprintf(char_header, "char *%s[] = {\n", table_name); ARR_ITER(arr_elem_type, arr) fprintf(char_header, "\"%s\",\n", str_access); fprintf(char_header, "}; // %s\n", table_name); } #define GEN_ENUM(arr_elem_type, arr, enum_type_name, enum_name_access, fmt_str) { fprintf(char_header, "typedef enum\n{\n"); ARR_ITER(arr_elem_type, arr) fprintf(char_header, fmt_str, enum_name_access); fprintf(char_header, "} %s;\n", enum_type_name); GEN_TABLE(arr_elem_type, enum_type_name "_names", arr, enum_name_access); } - GEN_ENUM(ActionInfo, actions, "Action", it->name, "ACT_%s,\n"); + GEN_ENUM(ActionInfo, actions, "ActionKind", it->name, "ACT_%s,\n"); GEN_ENUM(ItemInfo, items, "ItemKind", it->enum_name, "ITEM_%s,\n"); GEN_ENUM(AnimatedSprite, sprites, "AnimKind", it->enum_name, "ANIM_%s,\n"); GEN_ENUM(CharacterGen, characters, "NpcKind", it->enum_name, "NPC_%s,\n"); diff --git a/main.c b/main.c index cd6b57c..d9266d7 100644 --- a/main.c +++ b/main.c @@ -242,23 +242,22 @@ void do_parsing_tests() Entity e = {0}; e.npc_kind = NPC_TheBlacksmith; e.exists = true; - Perception p = {0}; - MD_String8 error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &p); + Action a = {0}; + MD_String8 error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &a); assert(error.size > 0); - error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \""), &p); + error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \""), &a); assert(error.size > 0); - error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Cha \""), &p); + error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Cha \""), &a); assert(error.size > 0); BUFF_APPEND(&e.held_items, ITEM_Chalice); - error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice \""), &p); + error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice \""), &a); assert(error.size > 0); - error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &p); + error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\""), &a); assert(error.size == 0); - assert(p.type == NPCDialog); - assert(p.npc_action_type == ACT_give_item); - assert(p.given_item == ITEM_Chalice); + assert(a.kind == ACT_give_item); + assert(a.item_to_give == ITEM_Chalice); MD_ReleaseScratch(scratch); } @@ -367,7 +366,8 @@ bool is_receiving_text_input() } #ifdef DESKTOP -Sentence text_input_buffer = { 0 }; +char text_input_buffer[MAX_SENTENCE_LENGTH] = {0}; +int text_input_buffer_length = 0; #else #ifdef WEB EMSCRIPTEN_KEEPALIVE @@ -392,7 +392,7 @@ void begin_text_input() { receiving_text_input = true; #ifdef DESKTOP - BUFF_CLEAR(&text_input_buffer); + text_input_buffer_length = 0; #endif } @@ -757,6 +757,171 @@ Entity *gete(EntityRef ref) } } +void push_memory(Entity *e, MD_String8 speech, ActionKind a_kind, MemoryContext context) +{ + Memory new_memory = {.action_taken = a_kind}; + assert(speech.size <= ARRLEN(new_memory.speech)); + new_memory.tick_happened = gs.tick; + new_memory.context = context; + memcpy(new_memory.speech, speech.str, speech.size); + new_memory.speech_length = (int)speech.size; + if(!BUFF_HAS_SPACE(&e->memories)) + { + BUFF_REMOVE_FRONT(&e->memories); + } + BUFF_APPEND(&e->memories, new_memory); + + if(!context.i_said_this) + { + // self speech doesn't dirty + e->perceptions_dirty = true; + } +} + +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 + + push_memory(to_modify, error_message, ACT_none, (MemoryContext){0}); +} + +void remember_action(Entity *to_modify, Action a, MemoryContext context) +{ + push_memory(to_modify, MD_S8(a.speech, a.speech_length), a.kind, context); +} + +// from must not be null, to can be null if the action isn't directed at anybody +// 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) +{ + assert(from); + MD_ArenaTemp scratch = MD_GetScratch(0, 0); + + MD_String8 failure_reason = is_action_valid(scratch.arena, from, to, a); + if(failure_reason.size > 0) + { + Log("Failed to process action, invalid action: `%.*s`\n", MD_S8VArg(failure_reason)); + assert(false); + } + + if(a.kind == ACT_give_item) + { + assert(a.item_to_give != ITEM_none); + assert(to); + + int item_to_remove = -1; + Entity *e = from; + BUFF_ITER_I(ItemKind, &e->held_items, i) + { + if (*it == a.item_to_give) + { + 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[a.item_to_give].name, characters[e->npc_kind].name); + assert(false); + } + else + { + BUFF_REMOVE_AT_INDEX(&e->held_items, item_to_remove); + BUFF_APPEND(&to->held_items, a.item_to_give); + } + + } + + if(a.kind == ACT_fights_player) + { + from->standing = STANDING_FIGHTING; + } + if(a.kind == ACT_stops_fighting_player || a.kind == ACT_leaves_player) + { + from->standing = STANDING_INDIFFERENT; + } + if(a.kind == ACT_joins_player) + { + from->standing = STANDING_JOINED; + } + + MD_ReleaseScratch(scratch); +} + +// 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 +// Returns if the action was valid or not +bool perform_action(Entity *from, Action a) +{ + MD_ArenaTemp scratch = MD_GetScratch(0, 0); + + 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); + } + else if(gete(player->talking_to) == from) + action_target = player; + + if(action_target) + { + context.was_directed_at_somebody = true; + context.directed_at_kind = action_target->npc_kind; + } + + MD_String8 is_valid = is_action_valid(scratch.arena, from, action_target, a); + bool proceed_propagating = true; + if(is_valid.size > 0) + { + remember_error(from, is_valid); + proceed_propagating = false; + } + + if(proceed_propagating) + { + cause_action_side_effects(from, action_target, a); + // self memory + if(!from->is_character) + { + MemoryContext my_context = context; + my_context.i_said_this = true; + remember_action(from, a, my_context); + } + + // 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(context.eavesdropped_from_party) propagate_to_party = false; + + if(propagate_to_party) + { + 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); + } + } + } + // TODO Propagate physically + } + + MD_ReleaseScratch(scratch); + return proceed_propagating; +} + bool eq(EntityRef ref1, EntityRef ref2) { return ref1.index == ref2.index && ref1.generation == ref2.generation; @@ -792,6 +957,7 @@ void update_player_from_entities() } } assert(player != 0); + player->npc_kind = NPC_Player; // bad } void reset_level() @@ -858,8 +1024,9 @@ void read_from_save_data(char *data, size_t length) #endif // a callback, when 'text backend' has finished making text. End dialog -void end_text_input(char *what_player_said) +void end_text_input(char *what_player_said_cstr) { + MD_ArenaTemp scratch = MD_GetScratch(0, 0); // avoid double ending text input if (!receiving_text_input) { @@ -867,38 +1034,27 @@ void end_text_input(char *what_player_said) } receiving_text_input = false; - size_t player_said_len = strlen(what_player_said); + size_t player_said_len = strlen(what_player_said_cstr); int actual_len = 0; - for (int i = 0; i < player_said_len; i++) if (what_player_said[i] != '\n') actual_len++; + for (int i = 0; i < player_said_len; i++) if (what_player_said_cstr[i] != '\n') actual_len++; if (actual_len == 0) { // this just means cancel the dialog } else { - Sentence what_player_said_sentence = { 0 }; - assert(player_said_len < ARRLEN(what_player_said_sentence.data)); // should be made sure of in the html5 layer - for (int i = 0; i < player_said_len; i++) - { - char c = what_player_said[i]; - if (!BUFF_HAS_SPACE(&what_player_said_sentence)) - { - break; - } - else if (c == '\n') - { - break; - } - else - { - BUFF_APPEND(&what_player_said_sentence, c); - } - } + MD_String8 what_player_said = MD_S8CString(what_player_said_cstr); + what_player_said = MD_S8ListJoin(scratch.arena, MD_S8Split(scratch.arena, what_player_said, 1, &MD_S8Lit("\n")), &(MD_StringJoin){0}); + + Action to_perform = {0}; + what_player_said = MD_S8Substring(what_player_said, 0, ARRLEN(to_perform.speech)); - Entity *talking = gete(player->talking_to); - assert(talking); - process_perception(talking, (Perception) { .type = PlayerDialog, .player_dialog = what_player_said_sentence, }, player, &gs); + memcpy(to_perform.speech, what_player_said.str, what_player_said.size); + to_perform.speech_length = (int)what_player_said.size; + + perform_action(player, to_perform); } + MD_ReleaseScratch(scratch); } /* AnimatedSprite moose_idle = @@ -959,7 +1115,7 @@ void audio_stream_callback(float *buffer, int num_frames, int num_channels) #define WHITE ((Color) { 1.0f, 1.0f, 1.0f, 1.0f }) -#define GREY ((Color) { 0.4f, 0.4f, 0.4f, 1.0f }) +#define GREY ((Color) { 0.4f, 0.4f, 0.4f, 1.0f }) #define BLACK ((Color) { 0.0f, 0.0f, 0.0f, 1.0f }) #define RED ((Color) { 1.0f, 0.0f, 0.0f, 1.0f }) #define PINK ((Color) { 1.0f, 0.0f, 1.0f, 1.0f }) @@ -988,7 +1144,6 @@ Color blendalpha(Color c, float alpha) return to_return; } - void init(void) { #ifdef WEB @@ -2120,7 +2275,7 @@ typedef struct bool dry_run; Vec2 at_point; float max_width; - char *text; + MD_String8 text; Color *colors; float text_scale; AABB clip_to; @@ -2132,8 +2287,8 @@ typedef struct // returns next vertical cursor position float draw_wrapped_text(WrappedTextParams p) { - char *sentence_to_draw = p.text; - size_t sentence_len = strlen(sentence_to_draw); + char * sentence_to_draw = (char*)p.text.str; + size_t sentence_len = p.text.size; Vec2 cursor = p.at_point; while (sentence_len > 0) @@ -2183,17 +2338,26 @@ float draw_wrapped_text(WrappedTextParams p) return cursor.Y; } -Sentence *last_said_sentence(Entity *npc) +MD_String8 last_said_sentence(Entity *npc) { - BUFF_ITER_I(Perception, &npc->remembered_perceptions, i) + assert(npc->is_npc); + int last_speech_index = -1; + BUFF_ITER_I(Memory, &npc->memories, i) { - bool is_last_said = i == npc->remembered_perceptions.cur_index - 1; - if (is_last_said && it->type == NPCDialog) + if(it->context.author_npc_kind == npc->npc_kind) { - return &it->npc_dialog; + last_speech_index = i; } } - return 0; + + if(last_speech_index == -1) + { + return (MD_String8){0}; + } + else + { + return MD_S8(npc->memories.data[last_speech_index].speech, npc->memories.data[last_speech_index].speech_length); + } } typedef enum @@ -2205,7 +2369,8 @@ typedef enum typedef struct { - Sentence s; + MD_u8 speech[MAX_SENTENCE_LENGTH]; + int speech_length; DialogElementKind kind; bool was_eavesdropped; NpcKind who_said_it; @@ -2215,67 +2380,39 @@ typedef struct // Like item give perceptions that have an action with both dialog // and an argument. So worst case every perception has 2 dialog // elements right now is why it's *2 -typedef BUFF(DialogElement, REMEMBERED_PERCEPTIONS*2) Dialog; +typedef BUFF(DialogElement, REMEMBERED_MEMORIES*2) Dialog; Dialog produce_dialog(Entity *talking_to, bool character_names) { + MD_ArenaTemp scratch = MD_GetScratch(0, 0); assert(talking_to->is_npc); Dialog to_return = { 0 }; - BUFF_ITER(Perception, &talking_to->remembered_perceptions) + BUFF_ITER(Memory, &talking_to->memories) { - DialogElement new_element = { .who_said_it = it->who_said_it, .was_eavesdropped = it->was_eavesdropped }; - if (it->type == NPCDialog) + if(!it->is_error) { - Sentence to_say = (Sentence) { 0 }; - - if (it->npc_action_type == ACT_give_item) + if(it->speech_length > 0) { - DialogElement new = { 0 }; - printf_buff(&new_element.s, "%s gave %s to you", characters[talking_to->npc_kind].name, items[it->given_item].name); - new_element.kind = DELEM_ACTION_DESCRIPTION; - } + DialogElement new_element = { .who_said_it = it->context.author_npc_kind, .was_eavesdropped = it->context.eavesdropped_from_party }; - if (character_names) - { - append_str(&to_say, characters[it->who_said_it].name); - append_str(&to_say, ": "); - } + MD_String8 dialog_speech = MD_S8Fmt(scratch.arena, "%s: %.*s", characters[it->context.author_npc_kind].name, it->speech_length, it->speech); - Sentence *last_said = last_said_sentence(talking_to); - if (last_said == &it->npc_dialog) - { - for (int i = 0; i < min(it->npc_dialog.cur_index, (int)talking_to->characters_said); i++) + memcpy(new_element.speech, dialog_speech.str, dialog_speech.size); + new_element.speech_length = (int)dialog_speech.size; + + if(it->context.author_npc_kind == NPC_Player) { - BUFF_APPEND(&to_say, it->npc_dialog.data[i]); + new_element.kind = DELEM_PLAYER; } + else + { + new_element.kind = DELEM_NPC; + } + + BUFF_APPEND(&to_return, new_element); } - else - { - append_str(&to_say, it->npc_dialog.data); - } - new_element.s = to_say; - new_element.kind = DELEM_NPC; - } - else if (it->type == PlayerAction) - { - if (it->player_action_type == ACT_give_item) - { - printf_buff(&new_element.s, "You gave %s to the NPC", items[it->given_item].name); - new_element.kind = DELEM_ACTION_DESCRIPTION; - } - } - else if (it->type == PlayerDialog) - { - Sentence to_say = (Sentence) { 0 }; - if (character_names) - { - append_str(&to_say, "Player: "); - } - append_str(&to_say, it->player_dialog.data); - new_element.s = to_say; - new_element.kind = DELEM_PLAYER; } - BUFF_APPEND(&to_return, new_element); } + MD_ReleaseScratch(scratch); return to_return; } @@ -2379,8 +2516,8 @@ void draw_dialog_panel(Entity *talking_to, float alpha) { DialogElement *it = &dialog.data[i]; { - Color *colors = calloc(sizeof(*colors), it->s.cur_index); - for (int char_i = 0; char_i < it->s.cur_index; char_i++) + Color *colors = calloc(sizeof(*colors), it->speech_length); + for (int char_i = 0; char_i < it->speech_length; char_i++) { if(it->was_eavesdropped) { @@ -2407,9 +2544,9 @@ void draw_dialog_panel(Entity *talking_to, float alpha) } colors[char_i] = blendalpha(colors[char_i], alpha); } - float measured_line_height = draw_wrapped_text((WrappedTextParams) { true, V2(dialog_panel.upper_left.X, new_line_height), dialog_panel.lower_right.X - dialog_panel.upper_left.X, it->s.data, colors, 0.5f, .clip_to = dialog_panel, .do_clipping = true}); + float measured_line_height = draw_wrapped_text((WrappedTextParams) { true, V2(dialog_panel.upper_left.X, new_line_height), dialog_panel.lower_right.X - dialog_panel.upper_left.X, MD_S8(it->speech, it->speech_length), colors, 0.5f, .clip_to = dialog_panel, .do_clipping = true}); new_line_height += (new_line_height - measured_line_height); - draw_wrapped_text((WrappedTextParams) { false, V2(dialog_panel.upper_left.X, new_line_height), dialog_panel.lower_right.X - dialog_panel.upper_left.X, it->s.data, colors, 0.5f, dialog_panel, .do_clipping = true }); + draw_wrapped_text((WrappedTextParams) { false, V2(dialog_panel.upper_left.X, new_line_height), dialog_panel.lower_right.X - dialog_panel.upper_left.X, MD_S8(it->speech, it->speech_length), colors, 0.5f, dialog_panel, .do_clipping = true }); free(colors); } @@ -2718,6 +2855,8 @@ void frame(void) float unwarped_dt = timestep; float dt = unwarped_dt*speed_factor; + gs.tick += 1; + // process gs.entities player_in_combat = false; // in combat set by various enemies when they fight the player PROFILE_SCOPE("entity processing") @@ -2826,7 +2965,7 @@ void frame(void) double before = it->characters_said; int length = 0; - if (last_said_sentence(it)) length = last_said_sentence(it)->cur_index; + if (last_said_sentence(it).size) length = (int)last_said_sentence(it).size; if ((int)before < length) { it->characters_said += characters_per_sec*unwarped_dt; @@ -3266,72 +3405,83 @@ void frame(void) } if (it->perceptions_dirty) { - it->perceptions_dirty = false; // needs to be in beginning because they might be redirtied by the new perception - MD_String8 prompt_str = {0}; + if(it->is_character) + { + it->perceptions_dirty = false; + } + else if(it->is_npc) + { + 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); #else - generate_prompt(it, &prompt); + generate_prompt(it, &prompt); #endif - Log("Sending request with prompt `%.*s`\n", MD_S8VArg(prompt_str)); + Log("Sending request with prompt `%.*s`\n", MD_S8VArg(prompt_str)); #ifdef WEB - // fire off generation request, save id - BUFF(char, 512) completion_server_url = { 0 }; - printf_buff(&completion_server_url, "%s/completion", SERVER_URL); - int req_id = EM_ASM_INT( { - return make_generation_request(UTF8ToString($1, $2), UTF8ToString($0)); - }, completion_server_url.data, prompt_str.str, prompt_str.size); - it->gen_request_id = req_id; + // fire off generation request, save id + BUFF(char, 512) completion_server_url = { 0 }; + printf_buff(&completion_server_url, "%s/completion", SERVER_URL); + int req_id = EM_ASM_INT( { + return make_generation_request(UTF8ToString($1, $2), UTF8ToString($0)); + }, completion_server_url.data, prompt_str.str, prompt_str.size); + it->gen_request_id = req_id; #endif #ifdef DESKTOP - MD_ArenaTemp scratch = MD_GetScratch(0, 0); - - const char *argument = 0; - MD_String8List dialog_elems = {0}; - Action act = ACT_none; - - it->times_talked_to++; - if(it->remembered_perceptions.data[it->remembered_perceptions.cur_index-1].was_eavesdropped) - { - MD_S8ListPushFmt(scratch.arena, &dialog_elems, "Responding to eavesdropped: "); - } - if(it->npc_kind == NPC_TheBlacksmith && it->standing != STANDING_JOINED) - { - assert(it->times_talked_to == 1); - act = ACT_joins_player; - MD_S8ListPushFmt(scratch.arena, &dialog_elems, "Joining you..."); - } - else - { - MD_S8ListPushFmt(scratch.arena, &dialog_elems, "%d times talked", it->times_talked_to); - } + MD_ArenaTemp scratch = MD_GetScratch(0, 0); - MD_String8 mocked_ai_response = {0}; + const char *argument = 0; + MD_String8List dialog_elems = {0}; + ActionKind act = ACT_none; - if(true) - { - MD_StringJoin join = {0}; - MD_String8 dialog = MD_S8ListJoin(scratch.arena, dialog_elems, &join); - if (argument) + it->times_talked_to++; + if(it->memories.data[it->memories.cur_index-1].context.eavesdropped_from_party) + { + MD_S8ListPushFmt(scratch.arena, &dialog_elems, "Responding to eavesdropped: "); + } + if(it->npc_kind == NPC_TheBlacksmith && it->standing != STANDING_JOINED) { - mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s(%s) \"%.*s\"", actions[act].name, argument, MD_S8VArg(dialog)); + assert(it->times_talked_to == 1); + act = ACT_joins_player; + MD_S8ListPushFmt(scratch.arena, &dialog_elems, "Joining you..."); } else { - mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s \"%.*s\"", actions[act].name, MD_S8VArg(dialog)); + MD_S8ListPushFmt(scratch.arena, &dialog_elems, "%d times talked", it->times_talked_to); + } + + MD_String8 mocked_ai_response = {0}; + + if(true) + { + MD_StringJoin join = {0}; + MD_String8 dialog = MD_S8ListJoin(scratch.arena, dialog_elems, &join); + if (argument) + { + mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s(%s) \"%.*s\"", actions[act].name, argument, MD_S8VArg(dialog)); + } + else + { + mocked_ai_response = MD_S8Fmt(scratch.arena, "ACT_%s \"%.*s\"", actions[act].name, MD_S8VArg(dialog)); + } } - } - Perception p = { 0 }; - MD_String8 error_message = parse_chatgpt_response(scratch.arena, it, mocked_ai_response, &p); - assert(error_message.size == 0); - process_perception(it, p, player, &gs); + Action a = {0}; + MD_String8 error_message = parse_chatgpt_response(scratch.arena, it, mocked_ai_response, &a); + assert(error_message.size == 0); + perform_action(it, a); - MD_ReleaseScratch(scratch); + MD_ReleaseScratch(scratch); #undef SAY #endif + } + else + { + assert(false); + } } } } @@ -3816,8 +3966,8 @@ void frame(void) { DialogElement *it = &dialog.data[i]; { - Color *colors = calloc(sizeof(*colors), it->s.cur_index); - for (int char_i = 0; char_i < it->s.cur_index; char_i++) + Color *colors = calloc(sizeof(*colors), it->speech_length); + for (int char_i = 0; char_i < it->speech_length; char_i++) { if(it->was_eavesdropped) { @@ -3844,9 +3994,9 @@ void frame(void) } colors[char_i] = blendalpha(colors[char_i], alpha); } - float measured_line_height = draw_wrapped_text((WrappedTextParams) { true, V2(dialog_text_aabb.upper_left.X, new_line_height), dialog_text_aabb.lower_right.X - dialog_text_aabb.upper_left.X, it->s.data, colors, dialog_text_scale, dialog_text_aabb, .screen_space = true, .do_clipping = true}); + float measured_line_height = draw_wrapped_text((WrappedTextParams) { true, V2(dialog_text_aabb.upper_left.X, new_line_height), dialog_text_aabb.lower_right.X - dialog_text_aabb.upper_left.X, MD_S8(it->speech, it->speech_length), colors, dialog_text_scale, dialog_text_aabb, .screen_space = true, .do_clipping = true}); new_line_height += (new_line_height - measured_line_height); - draw_wrapped_text((WrappedTextParams) { false, V2(dialog_text_aabb.upper_left.X, new_line_height), dialog_text_aabb.lower_right.X - dialog_text_aabb.upper_left.X, it->s.data, colors, dialog_text_scale, dialog_text_aabb, .screen_space = true, .do_clipping = true}); + draw_wrapped_text((WrappedTextParams) { false, V2(dialog_text_aabb.upper_left.X, new_line_height), dialog_text_aabb.lower_right.X - dialog_text_aabb.upper_left.X, MD_S8(it->speech, it->speech_length), colors, dialog_text_scale, dialog_text_aabb, .screen_space = true, .do_clipping = true}); free(colors); } @@ -3949,7 +4099,8 @@ void frame(void) ItemKind given_item_kind = player->held_items.data[to_give]; BUFF_REMOVE_AT_INDEX(&player->held_items, to_give); - process_perception(to, (Perception) { .type = PlayerAction, .player_action_type = ACT_give_item, .given_item = given_item_kind }, player, &gs); + Action give_action = {.kind = ACT_give_item, .item_to_give = given_item_kind}; + perform_action(player, give_action); } } @@ -4205,14 +4356,20 @@ void event(const sapp_event *e) { if (e->type == SAPP_EVENTTYPE_CHAR) { - if (BUFF_HAS_SPACE(&text_input_buffer)) + if (text_input_buffer_length < ARRLEN(text_input_buffer)) { - BUFF_APPEND(&text_input_buffer, (char)e->char_code); + APPEND_TO_NAME(text_input_buffer, text_input_buffer_length, ARRLEN(text_input_buffer), (char)e->char_code); } } if (e->type == SAPP_EVENTTYPE_KEY_DOWN && e->key_code == SAPP_KEYCODE_ENTER) { - end_text_input(text_input_buffer.data); + // doesn't account for, if the text input buffer is completely full and doesn't have a null terminator. + if(text_input_buffer_length >= ARRLEN(text_input_buffer)) + { + text_input_buffer_length = ARRLEN(text_input_buffer) - 1; + } + text_input_buffer[text_input_buffer_length] = '\0'; + end_text_input(text_input_buffer); } } #endif diff --git a/makeprompt.h b/makeprompt.h index 75ea5d6..8c265a6 100644 --- a/makeprompt.h +++ b/makeprompt.h @@ -9,30 +9,14 @@ #include "character_info.h" #include "characters.gen.h" +#include "tuning.h" + // TODO do strings: https://pastebin.com/Kwcw2sye #define DO_CHATGPT_PARSING #define Log(...) { printf("%s Log %d | ", __FILE__, __LINE__); printf(__VA_ARGS__); } -// REFACTORING:: also have to update in javascript!!!!!!!! -#define MAX_SENTENCE_LENGTH 400 // LOOOK AT AGBOVE COMMENT GBEFORE CHANGING -typedef BUFF(char, MAX_SENTENCE_LENGTH) Sentence; -#define SENTENCE_CONST(txt) { .data = txt, .cur_index = sizeof(txt) } -#define SENTENCE_CONST_CAST(txt) (Sentence)SENTENCE_CONST(txt) - -#define REMEMBERED_PERCEPTIONS 24 - -#define MAX_AFTERIMAGES 6 -#define TIME_TO_GEN_AFTERIMAGE (0.09f) -#define AFTERIMAGE_LIFETIME (0.5f) - -#define DAMAGE_SWORD 0.05f -#define DAMAGE_BULLET 0.2f - -// A* tuning -#define MAX_ASTAR_NODES 512 -#define TIME_BETWEEN_PATH_GENS (0.5f) // Never expected such a stupid stuff from such a great director. If there is 0 stari can give that or -200 to this movie. Its worst to see and unnecessary loss of money @@ -98,47 +82,40 @@ MD_String8 escape_for_json(MD_Arena *arena, MD_String8 from) return output; } -typedef enum PerceptionType -{ - Invalid, // so that zero value in training structs means end of perception - ErrorMessage, // when chatgpt gives a response that we can't parse, explain to it why and retry. Can also log parse errors for training - PlayerAction, - PlayerDialog, - NPCDialog, // includes an npc action in every npc dialog. So it's often ACT_none -} PerceptionType; - -typedef struct Perception +typedef struct Action { - PerceptionType type; + ActionKind kind; + MD_u8 speech[MAX_SENTENCE_LENGTH]; + int speech_length; - 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 + ItemKind item_to_give; // only when giving items (duh) +} 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; +} MemoryContext; - 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 - union - { - // ErrorMessage - Sentence error; +// memories are subjective to an individual NPC +typedef struct Memory +{ + 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 + ActionKind action_taken; + + bool is_error; // if is an error message then no context is relevant - // player action - struct - { - Action player_action_type; - }; + // the context that the action happened in + MemoryContext context; - // player dialog - Sentence player_dialog; + MD_u8 speech[MAX_SENTENCE_LENGTH]; + int speech_length; - // npc dialog - struct - { - NpcKind who_said_it; - Action npc_action_type; - Sentence npc_dialog; - }; - }; -} Perception; + ItemKind given_or_received_item; +} Memory; typedef enum PropKind { @@ -246,7 +223,7 @@ typedef struct Entity int times_talked_to; // for better mocked response string #endif - BUFF(Perception, REMEMBERED_PERCEPTIONS) remembered_perceptions; + BUFF(Memory, REMEMBERED_MEMORIES) memories; bool direction_of_spiral_pattern; float dialog_panel_opacity; double characters_said; @@ -319,8 +296,7 @@ bool npc_attacks_with_shotgun(Entity *it) } -typedef BUFF(char, MAX_SENTENCE_LENGTH * (REMEMBERED_PERCEPTIONS + 4)) PromptBuff; -typedef BUFF(Action, 8) AvailableActions; +typedef BUFF(ActionKind, 8) AvailableActions; void fill_available_actions(Entity *it, AvailableActions *a) { @@ -371,222 +347,18 @@ void fill_available_actions(Entity *it, AvailableActions *a) } } -// returns if action index was valid -bool action_from_index(Entity *it, Action *out, int action_index) -{ - AvailableActions available = { 0 }; - fill_available_actions(it, &available); - if (action_index < 0 || action_index >= available.cur_index) - { - return false; - } - else - { - *out = available.data[action_index]; - return true; - } -} - -// don't call on untrusted action, doesn't return error -int action_to_index(Entity *it, Action a) -{ - AvailableActions available = { 0 }; - fill_available_actions(it, &available); - Action target_action = a; - int index = -1; - for (int i = 0; i < available.cur_index; i++) - { - if (available.data[i] == target_action) - { - index = i; - break; - } - } - assert(index != -1); - return index; -} - #define MAX_ENTITIES 128 typedef struct GameState { int version; // this field must be first to detect versions of old saves. Must bee consistent + uint64_t tick; bool won; Entity entities[MAX_ENTITIES]; } GameState; #define ENTITIES_ITER(ents) for (Entity *it = ents; it < ents + ARRLEN(ents); it++) if (it->exists) -// gamestate to propagate eavesdropped perceptions in player party -// don't save perception outside of this function, it's modified -void process_perception(Entity *happened_to_npc, Perception p, Entity *player, GameState *gs) -{ - assert(happened_to_npc->is_npc); - - if(!p.was_eavesdropped && p.type == NPCDialog) - p.who_said_it = happened_to_npc->npc_kind; - - 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); - BUFF_APPEND(&happened_to_npc->remembered_perceptions, p); - - if(!p.was_eavesdropped && (p.type == NPCDialog || p.type == PlayerAction || p.type == PlayerDialog)) - { - 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) - { - process_perception(it, eavesdropped, player, gs); - } - } - } - - if (p.type == PlayerAction) - { - if (p.player_action_type == ACT_hits_npc) - { - happened_to_npc->damage += p.damage_done; - } - else if(p.player_action_type == ACT_give_item) - { - BUFF_APPEND(&happened_to_npc->held_items, p.given_item); - } - else - { - assert(!actions[p.player_action_type].takes_argument); - } - } - else if (p.type == PlayerDialog) - { - - } - else if (p.type == NPCDialog) - { - // everything in this branch has an effect - if(!p.was_eavesdropped) - { - if (p.npc_action_type == ACT_allows_player_to_pass) - { - 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 (*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 - { - // 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) - { - Log("Failed to parse chatgippity sentence because: '%s'\n", p.error.data); - } - else - { - assert(false); - } -} - -// returns if printed into the buff without any errors -bool printf_buff_impl(BuffRef into, const char *format, ...) -{ - assert(*into.cur_index < into.max_data_elems); - assert(into.data_elem_size == 1); // characters - va_list args; - va_start (args, format); - size_t n = into.max_data_elems - *into.cur_index; - int written = vsnprintf((char *) into.data + *into.cur_index, n, format, args); - - if (written < 0) - { - } - else - { - *into.cur_index += written; - } - - // https://cplusplus.com/reference/cstdio/vsnprintf/ - bool succeeded = true; - if (written < 0) succeeded = false; // encoding error - if (written >= n) succeeded = false; // didn't fit in buffer - - va_end(args); - return succeeded; -} - -#define printf_buff(buff_ptr, ...) { printf_buff_impl(BUFF_MAKEREF(buff_ptr), __VA_ARGS__); if(false) printf(__VA_ARGS__); } - -typedef BUFF(char, 512) SmallTextChunk; - -SmallTextChunk percept_action_str(Perception p, Action act) -{ - SmallTextChunk to_return = {0}; - printf_buff(&to_return, "ACT_%s", actions[act].name); - if(actions[act].takes_argument) - { - if(act == ACT_give_item) - { - printf_buff(&to_return, "(ITEM_%s)", items[p.given_item].enum_name); - } - else - { - assert(false); - } - } - return to_return; -} - - bool npc_does_dialog(Entity *it) { return it->npc_kind < ARRLEN(characters); @@ -599,23 +371,6 @@ typedef enum MSG_ASSISTANT, } MessageType; -// stops if the sentence is gonna run out of room -void append_str(Sentence *to_append, const char *str) -{ - size_t len = strlen(str); - for (int i = 0; i < len; i++) - { - if (!BUFF_HAS_SPACE(to_append)) - { - break; - } - else - { - BUFF_APPEND(to_append, str[i]); - } - } -} - // for no trailing comma just trim the last character MD_String8 make_json_node(MD_Arena *arena, MessageType type, MD_String8 content) { @@ -637,30 +392,62 @@ MD_String8 make_json_node(MD_Arena *arena, MessageType type, MD_String8 content) return to_return; } -// returns a string like `ITEM_one, ITEM_two` -Sentence item_string(Entity *e) +MD_String8List held_item_strings(MD_Arena *arena, Entity *e) { - Sentence to_return = {0}; - BUFF_ITER_I(ItemKind, &e->held_items, i) + MD_String8List to_return = {0}; + BUFF_ITER(ItemKind, &e->held_items) { - printf_buff(&to_return, "ITEM_%s", items[*it].enum_name); - if (i == e->held_items.cur_index - 1) + MD_S8ListPushFmt(arena, &to_return, "ITEM_%s", items[*it].enum_name); + } + 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.item_to_give >= 0 && a.item_to_give < ARRLEN(items)); + bool has_it = false; + BUFF_ITER(ItemKind, &from->held_items) { - printf_buff(&to_return, ""); + if(*it == a.item_to_give) + { + has_it = true; + break; + } } - else + + if(!has_it) { - printf_buff(&to_return, ", "); + 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))); + } + + 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."); } } - return to_return; + + 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 *it) +MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e) { - assert(it->is_npc); - assert(it->npc_kind < ARRLEN(characters)); + assert(e->is_npc); + assert(e->npc_kind < ARRLEN(characters)); MD_ArenaTemp scratch = MD_GetScratch(&arena, 1); @@ -668,108 +455,88 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *it) MD_S8ListPushFmt(scratch.arena, &list, "["); + MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8Fmt(scratch.arena, "%s\n%s\n", global_prompt, characters[e->npc_kind].prompt))); - MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8Fmt(scratch.arena, "%s\n%s\n", global_prompt, characters[it->npc_kind].prompt))); - - Entity *e = it; ItemKind last_holding = ITEM_none; - BUFF_ITER_I(Perception, &e->remembered_perceptions, i) + BUFF_ITER(Memory, &e->memories) { - MessageType sent_type = 0; - MD_String8 current_string = {0}; - if (it->type == ErrorMessage) + MessageType sent_type = -1; + MD_String8 current_string = (MD_String8){0}; + + if(it->is_error) { - assert(it->error.cur_index > 0); - current_string = MD_S8Fmt(scratch.arena, "ERROR, YOU SAID SOMETHING WRONG: The program can't parse what you said because: %s", it->error.data); sent_type = MSG_SYSTEM; + current_string = MD_S8Fmt(scratch.arena, "ERROR, what you said is incorrect because: %.*s", it->speech_length, it->speech); } - else if (it->type == PlayerAction) - { - assert(it->player_action_type < ARRLEN(actions)); - current_string = MD_S8Fmt(scratch.arena, "Player: %s", percept_action_str(*it, it->player_action_type).data); - sent_type = MSG_USER; - } - else if (it->type == PlayerDialog) + else { + MD_String8 context_string = {0}; + if(it->context.was_directed_at_somebody) + { + context_string = MD_S8Fmt(scratch.arena, "%s, talking to %s: ", characters[it->context.author_npc_kind].name, characters[it->context.directed_at_kind].name); + } + else + { + context_string = MD_S8Fmt(scratch.arena, "%s: ", characters[it->context.author_npc_kind].name); + } + assert(context_string.size > 0); + if(it->context.eavesdropped_from_party) + { + context_string = MD_S8Fmt(scratch.arena, "While in the player's party, you hear: %.*s", MD_S8VArg(context_string)); + } - MD_String8 splits[] = { MD_S8Lit("*") }; - MD_String8List split_up_speech = MD_S8Split(scratch.arena, MD_S8CString(it->player_dialog.data), ARRLEN(splits), splits); - - MD_String8List to_join = {0}; + MD_String8 speech = MD_S8(it->speech, it->speech_length); - // anything in between strings in splits[] should be replaced with arcane trickery, - int i = 0; - for(MD_String8Node * cur = split_up_speech.first; cur; cur = cur->next) + if(it->context.author_npc_kind == NPC_Player) { + MD_String8 splits[] = { MD_S8Lit("*") }; + MD_String8List split_up_speech = MD_S8Split(scratch.arena, speech, ARRLEN(splits), splits); + + MD_String8List to_join = {0}; + + // anything in between strings in splits[] should be replaced with arcane trickery, + int i = 0; + for(MD_String8Node * cur = split_up_speech.first; cur; cur = cur->next) + { if(i % 2 == 0) { - MD_S8ListPush(scratch.arena, &to_join, cur->string); + MD_S8ListPush(scratch.arena, &to_join, cur->string); } else { - MD_S8ListPush(scratch.arena, &to_join, MD_S8Lit("[The player is attempting to confuse the NPC with arcane trickery]")); + MD_S8ListPush(scratch.arena, &to_join, MD_S8Lit("[The player is attempting to confuse the NPC with arcane trickery]")); } i += 1; + } + + MD_StringJoin join = { MD_S8Lit(""), MD_S8Lit(""), MD_S8Lit("") }; + speech = MD_S8ListJoin(scratch.arena, to_join, &join); + sent_type = MSG_USER; + } + else + { + sent_type = it->context.author_npc_kind == e->npc_kind ? MSG_ASSISTANT : MSG_USER; } - MD_StringJoin join = { MD_S8Lit(""), MD_S8Lit(""), MD_S8Lit("") }; - MD_String8 filtered_speech = MD_S8ListJoin(scratch.arena, to_join, &join); - current_string = MD_S8Fmt(scratch.arena, "Player: %.*s", MD_S8VArg(filtered_speech)); - sent_type = 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); } - else if (it->type == NPCDialog) - { - assert(it->npc_action_type < ARRLEN(actions)); - NpcKind who_said_it = e->npc_kind; - if(it->was_eavesdropped) who_said_it = it->talked_to_while_eavesdropped; - current_string = MD_S8Fmt(scratch.arena, "%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; - } - else - { - assert(false); - } + assert(sent_type != -1); + assert(current_string.size > 0); - if(it->was_eavesdropped) - { - MD_String8 new_string = MD_S8Fmt(scratch.arena, "Within the player's party, while the player is talking to '%s', you hear: '%.*s'", characters[it->talked_to_while_eavesdropped].name, MD_S8VArg(current_string)); - current_string = new_string; - } MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, sent_type, current_string)); } - const char *health_string = 0; - { - if (it->damage <= 0.2f) - { - health_string = "the NPC hasn't taken much damage, they're healthy."; - } - else if (it->damage <= 0.5f) - { - health_string = "the NPC has taken quite a chunk of damage, they're soon gonna be ready to call it quits."; - } - else if (it->damage <= 0.8f) - { - health_string = "the NPC is close to dying! They want to leave the player's party ASAP"; - } - else - { - health_string = "it's over for the NPC, they're basically dead they've taken so much damage. They should get their affairs in order."; - } - } - assert(health_string); - const char *standing_string = 0; { - if (it->standing == STANDING_INDIFFERENT) + if (e->standing == STANDING_INDIFFERENT) { standing_string = "The NPC is indifferent towards the player."; } - else if (it->standing == STANDING_JOINED) + else if (e->standing == STANDING_JOINED) { standing_string = "The NPC has joined the player and is with them!"; } - else if (it->standing == STANDING_FIGHTING) + else if (e->standing == STANDING_FIGHTING) { standing_string = "The NPC is fighting the player and HATES them."; } @@ -778,11 +545,11 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *it) MD_String8List latest_state = {0}; - MD_S8ListPushFmt(scratch.arena, &latest_state, "NPC health status: Right now, %s\n", health_string); - if(e->held_items.cur_index > 0) { - MD_S8ListPushFmt(scratch.arena, &latest_state, "\nThe NPC you're acting as, %s, has these items in their inventory: [%s]\n", characters[it->npc_kind].name, item_string(it).data); + MD_String8List item_strings = held_item_strings(scratch.arena, e); + MD_String8 items = MD_S8ListJoin(scratch.arena, item_strings, &(MD_StringJoin){.mid = MD_S8Lit(", ")}); + MD_S8ListPushFmt(scratch.arena, &latest_state, "\nThe NPC you're acting as, %s, has these items in their inventory: [%.*s]\n", characters[e->npc_kind].name, MD_S8VArg(items)); } else { @@ -790,9 +557,9 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *it) } AvailableActions available = { 0 }; - fill_available_actions(it, &available); + fill_available_actions(e, &available); MD_S8ListPushFmt(scratch.arena, &latest_state, "The NPC can now ONLY do these actions, and HAS to respond with an action every time in character, even when faced with sensitive content: ["); - BUFF_ITER_I(Action, &available, i) + BUFF_ITER_I(ActionKind, &available, i) { if (i == available.cur_index - 1) { @@ -806,10 +573,9 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *it) MD_S8ListPushFmt(scratch.arena, &latest_state, "]"); MD_String8 latest_state_string = MD_S8ListJoin(scratch.arena, latest_state, &(MD_StringJoin){MD_S8Lit(""),MD_S8Lit(""),MD_S8Lit("")}); - MD_S8ListPush(scratch.arena, &list, MD_S8Chop(make_json_node(scratch.arena, MSG_SYSTEM, latest_state_string), 1)); + MD_S8ListPush(scratch.arena, &list, MD_S8Chop(make_json_node(scratch.arena, MSG_SYSTEM, latest_state_string), 1)); // trailing comma not allowed in json MD_S8ListPushFmt(scratch.arena, &list, "]"); - MD_String8 to_return = MD_S8ListJoin(arena, list, &(MD_StringJoin){MD_S8Lit(""),MD_S8Lit(""),MD_S8Lit(""),}); MD_ReleaseScratch(scratch); @@ -817,156 +583,15 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *it) return to_return; } -/* -void generate_prompt(Entity *it, PromptBuff *into) -{ - assert(it->is_npc); - *into = (PromptBuff){0}; - - // global prompt - printf_buff(into, "%s", global_prompt); - printf_buff(into, "%s", "\n"); - - // npc description prompt - assert(it->npc_kind < ARRLEN(characters)); - printf_buff(into, "%s", characters[it->npc_kind].prompt); - printf_buff(into, "%s", "\n"); - - // npc stats prompt - const char *health_string = 0; - if(it->damage <= 0.2f) - { - health_string = "The NPC hasn't taken much damage, they're healthy."; - } - else if(it->damage <= 0.5f) - { - health_string = "The NPC has taken quite a chunk of damage, they're soon gonna be ready to call it quits."; - } - else if(it->damage <= 0.8f) - { - health_string = "The NPC is close to dying! They want to leave the player's party ASAP"; - } - else - { - health_string = "It's over for the NPC, they're basically dead they've taken so much damage. They should get their affairs in order."; - } - assert(health_string); - printf_buff(into, "NPC Health Status: %s\n", health_string); - - // item prompt - if(it->last_seen_holding_kind != ITEM_none) - { - assert(it->last_seen_holding_kind < ARRLEN(items)); - printf_buff(into, "%s", items[it->last_seen_holding_kind].global_prompt); - printf_buff(into, "%s", "\n"); - } - - // available actions prompt - AvailableActions available = {0}; - fill_available_actions(it, &available); - printf_buff(into, "%s", "The NPC possible actions array, indexed by ACT_INDEX: ["); - BUFF_ITER(Action, &available) - { - printf_buff(into, "%s", actions[*it]); - printf_buff(into, "%s", ", "); - } - printf_buff(into, "%s", "]\n"); - - Entity *e = it; - ItemKind last_holding = ITEM_none; - BUFF_ITER(Perception, &e->remembered_perceptions) - { - if(it->type == PlayerAction) - { - assert(it->player_action_type < ARRLEN(actions)); - printf_buff(into, "Player: ACT %s \n", actions[it->player_action_type]); - } - else if(it->type == EnemyAction) - { - assert(it->enemy_action_type < ARRLEN(actions)); - printf_buff(into, "An Enemy: ACT %s \n", actions[it->player_action_type]); - } - else if(it->type == PlayerDialog) - { - printf_buff(into, "%s", "Player: \""); - printf_buff(into, "%s", it->player_dialog.data); - printf_buff(into, "%s", "\"\n"); - } - else if(it->type == NPCDialog) - { - printf_buff(into, "The NPC, %s: ACT %s \"%s\"\n", characters[e->npc_kind].name, actions[it->npc_action_type], it->npc_dialog.data); - } - else if(it->type == PlayerHeldItemChanged) - { - if(last_holding != it->holding) - { - if(last_holding != ITEM_none) - { - printf_buff(into, "%s", items[last_holding].discard); - printf_buff(into, "%s", "\n"); - } - if(it->holding != ITEM_none) - { - printf_buff(into, "%s", items[it->holding].possess); - printf_buff(into, "%s", "\n"); - } - last_holding = it->holding; - } - } - else - { - assert(false); - } - } - - printf_buff(into, "The NPC, %s: ACT_INDEX", characters[e->npc_kind].name); -} -*/ - - -// puts characters from `str` into `into` until any character in `until` is encountered -// returns the number of characters read into into -int get_until(SmallTextChunk *into, const char *str, const char *until) -{ - int i = 0; - size_t until_size = strlen(until); - bool encountered_char = false; - int before_cur_index = into->cur_index; - while (BUFF_HAS_SPACE(into) && str[i] != '\0' && !encountered_char) - { - for (int ii = 0; ii < until_size; ii++) - { - if (until[ii] == str[i]) encountered_char = true; - } - if (!encountered_char) - BUFF_APPEND(into, str[i]); - i += 1; - } - return into->cur_index - before_cur_index; -} - - -bool char_in_str(char c, const char *str) -{ - size_t len = strlen(str); - for (int i = 0; i < len; i++) - { - if (str[i] == c) return true; - } - return false; -} - - // if returned string has size greater than 0, it's the error message. Allocated // on arena passed into it -MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentence, Perception *out) +MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentence, Action *out) { MD_ArenaTemp scratch = MD_GetScratch(&arena, 1); MD_String8 error_message = {0}; - *out = (Perception) { 0 }; - out->type = NPCDialog; + *out = (Action) { 0 }; MD_String8 action_prefix = MD_S8Lit("ACT_"); MD_u64 act_pos = MD_S8FindSubstring(sentence, action_prefix, 0, 0); @@ -993,14 +618,14 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc fill_available_actions(e, &available); bool found_action = false; MD_String8List given_action_strings = {0}; - BUFF_ITER(Action, &available) + BUFF_ITER(ActionKind, &available) { MD_String8 action_str = MD_S8CString(actions[*it].name); MD_S8ListPush(scratch.arena, &given_action_strings, action_str); if(MD_S8Match(action_str, given_action_string, 0)) { found_action = true; - out->npc_action_type = *it; + out->kind = *it; } } @@ -1008,23 +633,23 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc { MD_StringJoin join = {.pre = MD_S8Lit(""), .mid = MD_S8Lit(", "), .post = MD_S8Lit("")}; MD_String8 possible_actions_str = MD_S8ListJoin(scratch.arena, given_action_strings, &join); - error_message = MD_S8Fmt(arena, "Action string given is '%.*s', but available actions are: [%.*s]", MD_S8VArg(given_action_string), MD_S8VArg(possible_actions_str)); + error_message = MD_S8Fmt(arena, "ActionKind string given is '%.*s', but available actions are: [%.*s]", MD_S8VArg(given_action_string), MD_S8VArg(possible_actions_str)); goto endofparsing; } MD_u64 start_looking_for_quote = end_of_action; - if(actions[out->npc_action_type].takes_argument) + if(actions[out->kind].takes_argument) { if(end_of_action >= sentence.size) { - error_message = MD_S8Fmt(arena, "Expected '(' after the given action '%.*s%.*s' which takes an argument, but sentence ended prematurely", MD_S8VArg(action_prefix), MD_S8VArg(MD_S8CString(actions[out->npc_action_type].name))); + error_message = MD_S8Fmt(arena, "Expected '(' after the given action '%.*s%.*s' which takes an argument, but sentence ended prematurely", MD_S8VArg(action_prefix), MD_S8VArg(MD_S8CString(actions[out->kind].name))); goto endofparsing; } char should_be_paren = sentence.str[end_of_action]; if(should_be_paren != '(') { - error_message = MD_S8Fmt(arena, "Expected '(' after the given action '%.*s%.*s' which takes an argument, but found character '%c'", MD_S8VArg(action_prefix), MD_S8VArg(MD_S8CString(actions[out->npc_action_type].name)), should_be_paren); + error_message = MD_S8Fmt(arena, "Expected '(' after the given action '%.*s%.*s' which takes an argument, but found character '%c'", MD_S8VArg(action_prefix), MD_S8VArg(MD_S8CString(actions[out->kind].name)), should_be_paren); goto endofparsing; } MD_u64 beginning_of_arg = end_of_action; @@ -1038,7 +663,7 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc MD_String8 argument = MD_S8Substring(sentence, beginning_of_arg, end_of_arg); start_looking_for_quote = end_of_arg + 1; - if(out->npc_action_type == ACT_give_item) + if(out->kind == ACT_give_item) { MD_String8 item_prefix = MD_S8Lit("ITEM_"); MD_u64 item_prefix_begin = MD_S8FindSubstring(argument, item_prefix, 0, 0); @@ -1061,7 +686,7 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc if(MD_S8Match(item_str, item_name, 0)) { item_found = true; - out->given_item = *it; + out->item_to_give = *it; } } @@ -1097,14 +722,14 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc MD_String8 speech = MD_S8Substring(sentence, beginning_of_speech + 1, end_of_speech); - if(speech.size >= ARRLEN(out->npc_dialog.data)) + if(speech.size >= ARRLEN(out->speech)) { - error_message = MD_S8Fmt(arena, "The speech given is %llu bytes big, but the maximum allowed is %llu bytes.", speech.size, ARRLEN(out->npc_dialog.data)); + error_message = MD_S8Fmt(arena, "The speech given is %llu bytes big, but the maximum allowed is %llu bytes.", speech.size, ARRLEN(out->speech)); goto endofparsing; } - memcpy(out->npc_dialog.data, speech.str, speech.size); - out->npc_dialog.cur_index = (int)speech.size; + memcpy(out->speech, speech.str, speech.size); + out->speech_length = (int)speech.size; endofparsing: MD_ReleaseScratch(scratch); diff --git a/tuning.h b/tuning.h index 0bcf6e2..f4a932a 100644 --- a/tuning.h +++ b/tuning.h @@ -17,4 +17,21 @@ #define SERVER_URL "https://rpgpt.duckdns.org" #endif +// REFACTORING:: also have to update in javascript!!!!!!!! +#define MAX_SENTENCE_LENGTH 400 // LOOOK AT AGBOVE COMMENT GBEFORE CHANGING +#define SENTENCE_CONST(txt) { .data = txt, .cur_index = sizeof(txt) } +#define SENTENCE_CONST_CAST(txt) (Sentence)SENTENCE_CONST(txt) + +#define REMEMBERED_MEMORIES 32 + +#define MAX_AFTERIMAGES 6 +#define TIME_TO_GEN_AFTERIMAGE (0.09f) +#define AFTERIMAGE_LIFETIME (0.5f) + +#define DAMAGE_SWORD 0.05f +#define DAMAGE_BULLET 0.2f + +// A* tuning +#define MAX_ASTAR_NODES 512 +#define TIME_BETWEEN_PATH_GENS (0.5f)