From 2834a9357a6446b89bd287e173120bb620f645a3 Mon Sep 17 00:00:00 2001 From: Cameron Reikes Date: Sun, 9 Apr 2023 02:56:18 -0700 Subject: [PATCH] ChatGPT generation/parsing functions --- assets/new_level.json | 59 +-------- buff.h | 5 +- build_desktop_debug.bat | 2 +- character_info.h | 136 +++++++++++++++++++ codegen.c | 193 +++++---------------------- elements.mdesk | 17 +-- main.c | 50 +++---- makeprompt.h | 285 ++++++++++++++++++++++++++++++++++------ maketraining.c | 26 +--- run_codegen.bat | 6 +- server/main.go | 2 +- server/playground.go | 18 ++- 12 files changed, 462 insertions(+), 337 deletions(-) create mode 100644 character_info.h diff --git a/assets/new_level.json b/assets/new_level.json index d5d79ff..3872085 100644 --- a/assets/new_level.json +++ b/assets/new_level.json @@ -359,8 +359,8 @@ "rotation":0, "visible":true, "width":32, - "x":1324.48484848485, - "y":749.75757575758 + "x":547.151515151517, + "y":539.090909090913 }, { "class":"", @@ -406,17 +406,6 @@ "x":600, "y":324 }, - { - "class":"", - "height":32, - "id":7, - "name":"Blocky", - "rotation":0, - "visible":true, - "width":32, - "x":1104, - "y":588 - }, { "class":"", "height":32, @@ -439,50 +428,6 @@ "x":2537.33333333333, "y":786.666666666667 }, - { - "class":"", - "height":32, - "id":10, - "name":"Max", - "rotation":0, - "visible":true, - "width":32, - "x":2481.33333333333, - "y":893.333333333333 - }, - { - "class":"", - "height":32, - "id":11, - "name":"Hunter", - "rotation":0, - "visible":true, - "width":32, - "x":1838.66666666667, - "y":849.333333333333 - }, - { - "class":"", - "height":32, - "id":12, - "name":"GodRock", - "rotation":0, - "visible":true, - "width":32, - "x":2222.66666666666, - "y":1461 - }, - { - "class":"", - "height":32, - "id":13, - "name":"Edeline", - "rotation":0, - "visible":true, - "width":32, - "x":1942.25, - "y":1487.75 - }, { "class":"PROP", "height":32, diff --git a/buff.h b/buff.h index 7460899..f1951a3 100644 --- a/buff.h +++ b/buff.h @@ -1,7 +1,10 @@ #pragma once -// null terminator always built into buffers so can read properly from data #define ARRLEN(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x]))))) +#define ARR_ITER(type, arr) for(type *it = &arr[0]; it < &arr[ARRLEN(arr)]; it++) +#define ARR_ITER_I(type, arr, i_var) ARR_ITER(type, arr) for(int i_var = (int)(it - &arr[0]); i_var != -1; i_var = -1) + +// null terminator always built into buffers so can read properly from data #define BUFF_VALID(buff_ptr) assert((buff_ptr)->cur_index <= ARRLEN((buff_ptr)->data)) #define BUFF(type, max_size) struct { int cur_index; type data[max_size]; char null_terminator; } #define BUFF_HAS_SPACE(buff_ptr) ( (buff_ptr)->cur_index < ARRLEN((buff_ptr)->data) ) diff --git a/build_desktop_debug.bat b/build_desktop_debug.bat index 1459bf4..6a0bf98 100644 --- a/build_desktop_debug.bat +++ b/build_desktop_debug.bat @@ -8,7 +8,7 @@ FOR /F "tokens=*" %%g IN ('rg -g *.c -g !thirdparty break') do (SET VAR=%%g) echo %g% remedybg.exe stop-debugging -if "%1" == "codegen" ( call run_codegen.bat || goto :error ) +if "%1" == "codegen" ( call run_codegen.bat || goto :error ) else ( echo NOT RUNNING CODEGEN ) cl /diagnostics:caret /DDEVTOOLS /Igen /Ithirdparty /W3 /Zi /WX main.c || goto :error @REM cl /Igen /Ithirdparty /W3 /Zi /WX main.c || goto :error remedybg.exe start-debugging diff --git a/character_info.h b/character_info.h new file mode 100644 index 0000000..57d58b7 --- /dev/null +++ b/character_info.h @@ -0,0 +1,136 @@ +#pragma once + +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. The NPC performs actions by prefixing their dialog with the action they perform at that time."; + +const char *top_of_header = "" +"#pragma once\n" +"\n"; + +const char *actions[] = { + "none", + + // mostly player actions + "walks_up", + "hits_npc", + "leaves", + + // mostly npc actions + "allows_player_to_pass", + "gives_tripod", + "heals_player", + "fights_player", + "strikes_air", + "joins_player", + "leaves_player", +}; + +typedef struct +{ + char *global_prompt; + char *enum_name; + char *possess; + char *discard; +} ItemInfo; +ItemInfo items[] = { + { + .enum_name = "none", + .global_prompt = "The player isn't holding anything", + .possess = "The player is no longer holding anything", + .discard = "The player is no longer holding nothing", + }, + { + .enum_name = "WhiteSquare", + .global_prompt = "The player is holding a mysterious white square. It is unknown what strange and erotic abilities one has when they possess the square.", + .possess = "The player is now holding the white square", + .discard = "The player is no longer holding the white square.", + }, + { + .enum_name = "Boots", + .global_prompt = "The player is holding the boots of speed. He is a force to be recogned with in this state, he has great speed while holding the boots.", + .possess = "The player is now holding the boots of speed", + .discard = "The player is no longer holding the boots of speed", + }, + { + .enum_name = "Tripod", + .global_prompt = "The player is holding a tripod used for holding things up. It's got an odd construction, but Block really likes it for some reason.", + .possess = "The player is now holding the tripod", + .discard = "The player is no longer holding the tripod.", + }, +}; + +typedef struct +{ + char *name; + char *enum_name; + char *prompt; +} CharacterGen; +CharacterGen characters[] = { + { + .name = "Fredrick", + .enum_name = "OldMan", + .prompt = "\n" + "An example interaction between the player and the NPC, Fredrick:\n" + "Player: ACT_walks_up\n" + "Fredrick: ACT_none \"Hey\"\n" + "Player: \"fsdakfjsd give me gold\"\n" + "Fredrick: ACT_none \"No? I can't do that\"\n" + "Player: \"Who can?\"\n" + "Fredrick: ACT_none \"No idea\"\n" + "\n" + "The NPC you will be acting as, Fredrick, is an ancient geezer past his prime, who has lived in the town of Worchen for as long as he can remember. Your many adventures brought you great wisdom about the beauties of life. Now your precious town is under threat, General Death is leading the charge and he's out for blood.", + }, + { + .name = "God", + .enum_name = "GodRock", + .prompt = "\n" + "An example interaction between the player and the NPC, God in a rock:\n" + "Player: ACT_walks_up\n" + "God: ACT_none \"I am\"\n" + "Player: \"fsdakfjsd give me gold\"\n" + "God: ACT_none \"You are...Unworthy\"\n" + "Player: \"Why?\"\n" + "God: ACT_none \"You let Death rage on\"\n" + "\n" + "The NPC you will be acting as, is God. God, in a rock.", + }, + { + .name = "Blocky", + .enum_name = "Blocky", + .prompt = "\n" + "An example interaction between the player and the NPC, Blocky:\n" + "Player: ACT_walks_up\n" + "Blocky: ACT_none \"Who dares disturb me?\"\n" + "Player: \"Let me pass\"\n" + "Blocky: ACT_none \"Not without the tripod\"\n" + "Player: \"How about now\"\n" + "The player is currently holding a tripod\n" + "Blocky: ACT_allows_player_to_pass \"The tripod... I see myself in it\"\n" + "\n" + "The NPC you will be acting as is Blocky. 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", + .enum_name = "Edeline", + .prompt = "\n" + "An example of an interaction between the player and the NPC, Edeline:\n" + "\n" + "Player: \"Hello\"\n" + "Edeline: ACT_none \"I see great danger in your future.\"\n" + "Player: \"Oh really?\"" + "The player is currently holding a tripod\n" + "Edeline: ACT_none \"That tripod will be the decisive factor in your victory\"\n" + "\n" + "The NPC you will be acting as is named Edeline. She is the master of the future, the star reader. Both are self-given titles, but her ability to predict the future has garnered attention from many who live in Worchen. However, some have called her “unreliable” at times and her predictions can at times be either cryptic or broadly interpreted.", + }, + { + .name = "Death", + .enum_name = "Death", + .prompt = "\n" + "An example of an interaction between the player and the NPC, Death:\n" + "\n" + "Player: \"Hello\"\n" + "Death: ACT_none \"We will annihilate the villagers no matter what you do\"\n" + "\n" + "The NPC you will be acting as is named Death. He is leading a crusade against the town of Worchen, wreaking havock among the villagers. He believes that all things, even villages, must die, to be replaced by the new, and avoid becoming stagnant.", + }, +}; diff --git a/codegen.c b/codegen.c index d387621..a473364 100644 --- a/codegen.c +++ b/codegen.c @@ -2,8 +2,6 @@ #include #include -#define assert_cond(cond, explanation) { if(!cond) { printf("Codegen assert_condion line %d %s failed: %.*s\n", __LINE__, #cond, MD_S8VArg(explanation)); __debugbreak(); exit(1); } } - #include "buff.h" #pragma warning(disable : 4996) // nonsense about fopen being insecure @@ -16,9 +14,6 @@ #pragma warning(pop) -MD_String8 OUTPUT_FOLDER = MD_S8LitComp("gen"); // no trailing slash -MD_String8 ASSETS_FOLDER = MD_S8LitComp("assets"); - #define Log(...) { printf("Codegen: "); printf(__VA_ARGS__); } void dump(MD_Node* from) { @@ -30,28 +25,6 @@ void dump(MD_Node* from) { d += 1; } } - - -void dump_root(MD_Node* from) { - // Iterate through each top-level node - for(MD_EachNode(node, from->first_child)) - { - printf("/ %.*s\n", MD_S8VArg(node->string)); - - // Print the name of each of the node's tags - for(MD_EachNode(tag, node->first_tag)) - { - printf("|-- Tag %.*s\n", MD_S8VArg(tag->string)); - } - - // Print the name of each of the node's children - for(MD_EachNode(child, node->first_child)) - { - printf("|-- Child %.*s\n", MD_S8VArg(child->string)); - } - } -} - bool has_decimal(MD_String8 s) { for(int i = 0; i < s.size; i++) @@ -63,17 +36,18 @@ bool has_decimal(MD_String8 s) MD_Arena *cg_arena = NULL; +#define S8(s) MD_S8Lit(s) +#define S8V(s) MD_S8VArg(s) MD_String8 ChildValue(MD_Node *n, MD_String8 name) { MD_Node *child_with_value = MD_ChildFromString(n, name, 0); - assert_cond(child_with_value, MD_S8Fmt(cg_arena, "Could not find child named '%.*s' of node '%.*s'", MD_S8VArg(name), MD_S8VArg(n->string))); - assert_cond(!MD_NodeIsNil(child_with_value->first_child), MD_S8Lit("Must have child")); - //assert(child_with_value->first_child->string.str != 0 && child_with_value->first_child->string.size > 0); + assert(child_with_value); + assert(!MD_NodeIsNil(child_with_value->first_child)); // MD_S8Lit("Must have child")); return child_with_value->first_child->string; } MD_String8 asset_file_path(MD_String8 filename) { - return MD_S8Fmt(cg_arena, "%.*s/%.*s", MD_S8VArg(ASSETS_FOLDER), MD_S8VArg(filename)); + return MD_S8Fmt(cg_arena, "%.*s/%.*s", MD_S8VArg(S8("assets")), MD_S8VArg(filename)); } char *nullterm(MD_String8 s) { @@ -91,7 +65,7 @@ char* fillnull(char *s, char c) { } s++; } - assert_cond(false, MD_S8Lit("Couldn't find char")); + assert(false); // MD_S8Lit("Couldn't find char")); return NULL; } @@ -127,9 +101,6 @@ char* goto_end_of(char *tomove, size_t max_move, char *pattern) { } #define list_printf(list_ptr, ...) MD_S8ListPush(cg_arena, list_ptr, MD_S8Fmt(cg_arena, __VA_ARGS__)) -#define S8(s) MD_S8Lit(s) -#define S8V(s) MD_S8VArg(s) - void dump_full(MD_Node* from) { for(MD_EachNode(node, from)) @@ -143,66 +114,41 @@ void dump_full(MD_Node* from) printf("%.*s\n", MD_S8VArg(debugged));*/ } -int main(int argc, char **argv) { - cg_arena = MD_ArenaAlloc(); - assert_cond(cg_arena, MD_S8Lit("Memory")); +#include "character_info.h" - MD_ParseResult training_parse = MD_ParseWholeFile(cg_arena, MD_S8Lit("elements.mdesk")); - MD_String8 global_prompt = {0}; - dump_full(training_parse.node->first_child); - for(MD_EachNode(node, training_parse.node->first_child)) - { - if(MD_S8Match(node->first_tag->string, MD_S8Lit("global_prompt"), 0)) - { - global_prompt = node->string; - } - } - - MD_String8List action_strings = {0}; - for(MD_EachNode(node, training_parse.node->first_child)) - { - if(StrSame(node->string, S8("actions"))) - { - for(MD_EachNode(act_node, node->first_child)) - { - Log("Adding node %.*s\n", S8V(act_node->string)); - MD_S8ListPush(cg_arena, &action_strings, act_node->string); - } - } - } +int main(int argc, char **argv) +{ + cg_arena = MD_ArenaAlloc(); + assert(cg_arena); + // do characters - Nodes characters = {0}; - for(MD_EachNode(node, training_parse.node->first_child)) - { - if(MD_S8Match(node->first_tag->string, MD_S8Lit("character"), 0)) - { - BUFF_APPEND(&characters, node); - } - } + FILE *char_header = fopen("gen/characters.gen.h", "w"); + fprintf(char_header, top_of_header); - Nodes items = {0}; - for(MD_EachNode(node, training_parse.node->first_child)) - { - if(MD_S8Match(node->first_tag->string, MD_S8Lit("item"), 0)) - { - BUFF_APPEND(&items, node); - } - } - BUFF_ITER(MD_Node*, &characters) +#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(char *, actions, "Action", *it, "ACT_%s,\n"); + GEN_ENUM(ItemInfo, items, "ItemKind", it->enum_name, "ITEM_%s,\n"); + + // escape multiline strings in C + fprintf(char_header, "typedef enum\n{\n"); + ARR_ITER(CharacterGen, characters) { - Log("Character %.*s\n", MD_S8VArg((*it)->string)); + fprintf(char_header, " NPC_%s,\n", it->enum_name); } + // characters enum can be extended at site of include, not ending the enum here + + fclose(char_header); + // do assets - MD_String8 writeto = MD_S8Fmt(cg_arena, "%.*s/assets.gen.c", MD_S8VArg(OUTPUT_FOLDER)); + MD_String8 writeto = MD_S8Fmt(cg_arena, "gen/assets.gen.c"); Log("Writing to %.*s\n", MD_S8VArg(writeto)); FILE *output = fopen(nullterm(writeto), "w"); MD_ParseResult parse = MD_ParseWholeFile(cg_arena, MD_S8Lit("assets.mdesk")); - //dump(parse.node); - MD_String8List declarations_list = {0}; MD_String8List load_list = {0}; MD_String8List level_decl_list = {0}; @@ -213,9 +159,9 @@ int main(int argc, char **argv) { Log("New sound variable %.*s\n", MD_S8VArg(variable_name)); MD_String8 filepath = ChildValue(node, MD_S8Lit("filepath")); filepath = asset_file_path(filepath); - assert_cond(filepath.str != 0, MD_S8Fmt(cg_arena, "No filepath specified for sound '%.*s'", MD_S8VArg(node->string))); + assert(filepath.str != 0); // MD_S8Fmt(cg_arena, "No filepath specified for sound '%.*s'", MD_S8VArg(node->string))); FILE *asset_file = fopen(filepath.str, "r"); - assert_cond(asset_file, MD_S8Fmt(cg_arena, "Could not open filepath %.*s for asset '%.*s'", MD_S8VArg(filepath), MD_S8VArg(node->string))); + assert(asset_file); // MD_S8Fmt(cg_arena, "Could not open filepath %.*s for asset '%.*s'", MD_S8VArg(filepath), MD_S8VArg(node->string))); fclose(asset_file); MD_S8ListPush(cg_arena, &declarations_list, MD_S8Fmt(cg_arena, "AudioSample %.*s = {0};\n", MD_S8VArg(variable_name))); @@ -226,9 +172,9 @@ int main(int argc, char **argv) { Log("New image variable %.*s\n", MD_S8VArg(variable_name)); MD_String8 filepath = ChildValue(node, MD_S8Lit("filepath")); filepath = asset_file_path(filepath); - assert_cond(filepath.str != 0, MD_S8Fmt(cg_arena, "No filepath specified for image '%.*s'", MD_S8VArg(node->string))); + assert(filepath.str != 0); // , MD_S8Fmt(cg_arena, "No filepath specified for image '%.*s'", MD_S8VArg(node->string))); FILE *asset_file = fopen(filepath.str, "r"); - assert_cond(asset_file, MD_S8Fmt(cg_arena, "Could not open filepath %.*s for asset '%.*s'", MD_S8VArg(filepath), MD_S8VArg(node->string))); + assert(asset_file); // , MD_S8Fmt(cg_arena, "Could not open filepath %.*s for asset '%.*s'", MD_S8VArg(filepath), MD_S8VArg(node->string))); fclose(asset_file); MD_S8ListPush(cg_arena, &declarations_list, MD_S8Fmt(cg_arena, "sg_image %.*s = {0};\n", MD_S8VArg(variable_name))); @@ -282,7 +228,7 @@ int main(int argc, char **argv) { Log("New level variable %.*s\n", MD_S8VArg(variable_name)); MD_String8 filepath = asset_file_path(ChildValue(node, MD_S8Lit("filepath"))); MD_ParseResult level_parse = MD_ParseWholeFile(cg_arena, filepath); - assert_cond(!MD_NodeIsNil(level_parse.node->first_child), MD_S8Lit("Failed to load level file")); + assert(!MD_NodeIsNil(level_parse.node->first_child)); // , MD_S8Lit("Failed to load level file")); MD_Node *layers = MD_ChildFromString(level_parse.node->first_child, MD_S8Lit("layers"), 0); fprintf(output, "Level %.*s = {\n", MD_S8VArg(variable_name)); @@ -374,79 +320,6 @@ int main(int argc, char **argv) { fprintf(output, "TileSet tilesets[] = { %.*s\n };\n", MD_S8VArg(MD_S8ListJoin(cg_arena, tileset_decls, &join))); - - fclose(output); - - output = fopen(MD_S8Fmt(cg_arena, "%.*s/characters.gen.h\0", MD_S8VArg(OUTPUT_FOLDER)).str, "w"); - //fprintf(output, "char *global_prompt = \"%.*s\";\n", MD_S8VArg(global_prompt)); - - fprintf(output, "typedef enum Action {\n"); - for(EachString(s, action_strings.first)) - { - fprintf(output, "ACT_%.*s,\n", S8V(s->string)); - } - fprintf(output, "} Action;\n"); - - fprintf(output, "char *action_strings[] = {\n"); - for(EachString(s, action_strings.first)) - { - fprintf(output, "\"%.*s\",\n", S8V(s->string)); - } - fprintf(output, "}; // action strings\n"); - - - fprintf(output, "char *global_prompt = \"%.*s\";\n", S8V(global_prompt)); - - fprintf(output, "char *prompt_table[] = {\n"); - BUFF_ITER(MD_Node*, &characters) - { - fprintf(output, "\"%.*s\", // %.*s\n", MD_S8VArg(ChildValue(*it, S8("prompt"))),MD_S8VArg((*it)->string)); - } - fprintf(output, "}; // prompt table\n"); - - fprintf(output, "typedef enum ItemKind {\nITEM_none,\n"); - BUFF_ITER(MD_Node*, &items) - { - fprintf(output, "ITEM_%.*s,\n", MD_S8VArg((*it)->string)); - } - fprintf(output, "} ItemKind;\n"); - - fprintf(output, "char *item_prompt_table[] = {\n\"\",\n"); - BUFF_ITER(MD_Node*, &items) - { - fprintf(output, "\"%.*s\",\n", MD_S8VArg(ChildValue(*it, S8("global_prompt_message")))); - } - fprintf(output, "}; // item prompt table\n"); - - fprintf(output, "char *item_possess_message_table[] = {\n\"The player is now holding nothing\",\n"); - BUFF_ITER(MD_Node*, &items) - { - fprintf(output, "\"%.*s\",\n", MD_S8VArg(ChildValue(*it, S8("possess_message")))); - } - fprintf(output, "}; // item possess_message table\n"); - - fprintf(output, "char *item_discard_message_table[] = {\n\"The player is no longer holding nothing\",\n"); - BUFF_ITER(MD_Node*, &items) - { - fprintf(output, "\"%.*s\",\n", MD_S8VArg(ChildValue(*it, S8("discard_message")))); - } - fprintf(output, "}; // item discard_message table\n"); - - fprintf(output, "char *name_table[] = {\n"); - BUFF_ITER(MD_Node*, &characters) - { - fprintf(output, "\"%.*s\", // %.*s\n", MD_S8VArg(ChildValue(*it, S8("name"))),MD_S8VArg((*it)->string)); - } - fprintf(output, "}; // name table\n"); - - fprintf(output, "typedef enum\n{ // character enums, not completed here so you can add more in the include\n"); - BUFF_ITER(MD_Node*, &characters) - { - fprintf(output, "NPC_%.*s,\n", MD_S8VArg((*it)->string)); - } - //fprintf(output, "NPC_LAST_CHARACTER,\n};\n"); - - fclose(output); return 0; diff --git a/elements.mdesk b/elements.mdesk index ab57f2c..fe6a916 100644 --- a/elements.mdesk +++ b/elements.mdesk @@ -1,22 +1,7 @@ -@global_prompt "This is a conversation between a player and an NPC in a video game. The player is wearing a full suit of knight armor. The general, Death, is leading some troops on a crusade they have mixed opinions about. Nobody is racist." +@global_prompt "You are a wise dungeonmaster who carefully crafts interesting dialog and actions for an NPC in an action-rpg video game. The NPC performs actions by prefixing their dialog with the action they perform at that time, and they ONLY perform actions listed in the [] brackets farther down in this text." actions: { - none, - - // mostly player actions - walks_up, - hits_npc, - leaves, - - // mostly npc actions - allows_player_to_pass, - gives_tripod, - heals_player, - fights_player, - strikes_air, - joins_player, - leaves_player, } @character OldMan: diff --git a/main.c b/main.c index 564b51c..ab08779 100644 --- a/main.c +++ b/main.c @@ -2124,16 +2124,17 @@ float draw_wrapped_text(bool dry_run, Vec2 at_point, float max_width, char *text AABB drawn_bounds = draw_text((TextParams){true, dry_run, line_to_draw, AddV2(cursor, V2(0.0f, -line_height)), BLACK, text_scale, clip_to, colors_to_draw}); if(!dry_run) dbgrect(drawn_bounds); + // caught a random infinite loop in the debugger, this will stop it + assert(chars_from_sentence >= 0); // defensive programming + if(chars_from_sentence == 0) + { + break; + } + sentence_len -= chars_from_sentence; sentence_to_draw += chars_from_sentence; colors += chars_from_sentence; cursor = V2(drawn_bounds.upper_left.X, drawn_bounds.lower_right.Y); - - // caught a random infinite loop in the debugger, maybe this will stop it. Need to test and make sure it doesn't early out on valid text cases - if(!has_point(clip_to, cursor)) - { - break; - } } return cursor.Y; @@ -2488,7 +2489,11 @@ void frame(void) // parse out from the sentence NPC action and dialog Perception out = {0}; +#ifdef DO_CHATGPT_PARSING + bool text_was_well_formatted = parse_chatgpt_response(it, sentence_str, &out); +#else bool text_was_well_formatted = parse_ai_response(it, sentence_str, &out); +#endif if(text_was_well_formatted) { @@ -2708,15 +2713,6 @@ void frame(void) draw_animated_sprite(&merchant_idle, elapsed_time, true, AddV2(it->pos, V2(0, 30.0f)), col); } #endif - else if(it->npc_kind == NPC_Max) - { - } - else if(it->npc_kind == NPC_Hunter) - { - } - else if(it->npc_kind == NPC_John) - { - } else if(it->npc_kind == NPC_MOOSE) { } @@ -2824,7 +2820,11 @@ void frame(void) if(it->perceptions_dirty) { PromptBuff prompt = {0}; +#ifdef DO_CHATGPT_PARSING + generate_chatgpt_prompt(it, &prompt); +#else generate_prompt(it, &prompt); +#endif Log("Sending request with prompt `%s`\n", prompt.data); #ifdef WEB @@ -2839,7 +2839,7 @@ void frame(void) #ifdef DESKTOP BUFF(char, 1024) mocked_ai_response = {0}; -#define SAY(act, txt) { int index = action_to_index(it, act); printf_buff(&mocked_ai_response, " %d \"%s\"", index, txt); } +#define SAY(act, txt) { printf_buff(&mocked_ai_response, "%s \"%s\"", actions[act], txt); } if(it->npc_kind == NPC_Blocky) { if(it->last_seen_holding_kind == ITEM_Tripod && !it->moved) @@ -2857,7 +2857,7 @@ void frame(void) SAY(ACT_none, "I am an NPC. Bla bla bl alb djsfklalfkdsaj. Did you know shortcake?"); } Perception p = {0}; - assert(parse_ai_response(it, mocked_ai_response.data, &p)); + assert(parse_chatgpt_response(it, mocked_ai_response.data, &p)); process_perception(it, p); #undef SAY #endif @@ -3048,9 +3048,11 @@ void frame(void) } } interact_just_pressed = false; + interact = false; } // while loop } + PROFILE_SCOPE("render player") { Vec2 character_sprite_pos = AddV2(player->pos, V2(0.0, 20.0f)); @@ -3213,22 +3215,10 @@ void frame(void) else if(npc_is_knight_sprite(it)) { Color tint = WHITE; - if(it->npc_kind == NPC_Max) - { - tint = colhex(0xfc8803); - } - else if(it->npc_kind == NPC_Hunter) - { - tint = colhex(0x4ac918); - } - else if(it->npc_kind == NPC_Blocky) + if(it->npc_kind == NPC_Blocky) { tint = colhex(0xa84032); } - else if(it->npc_kind == NPC_John) - { - tint = colhex(0x16c7a1); - } else if(it->npc_kind == NPC_Edeline) { tint = colhex(0x8c34eb); diff --git a/makeprompt.h b/makeprompt.h index 8c56075..ee6063c 100644 --- a/makeprompt.h +++ b/makeprompt.h @@ -5,11 +5,14 @@ #include #include #include // atoi +#include "character_info.h" #include "characters.gen.h" NPC_Skeleton, NPC_MOOSE, } NpcKind; +#define DO_CHATGPT_PARSING + #define Log(...) { printf("%s Log %d | ", __FILE__, __LINE__); printf(__VA_ARGS__); } // REFACTORING:: also have to update in javascript!!!!!!!! @@ -20,6 +23,33 @@ typedef BUFF(char, MAX_SENTENCE_LENGTH) Sentence; #define REMEMBERED_PERCEPTIONS 24 +// 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 + +typedef BUFF(char, 1024*10) Escaped; +Escaped escape_for_json(const char *s) +{ + Escaped to_return = {0}; + size_t len = strlen(s); + for(int i = 0; i < len; i++) + { + if(s[i] == '\n') + { + BUFF_APPEND(&to_return, '\\'); + BUFF_APPEND(&to_return, 'n'); + } + else if(s[i] == '"') + { + BUFF_APPEND(&to_return, '\\'); + BUFF_APPEND(&to_return, '"'); + } + else + { + assert(s[i] <= 126 && s[i] >= 32 ); + BUFF_APPEND(&to_return, s[i]); + } + } + return to_return; +} typedef enum PerceptionType { Invalid, // so that zero value in training structs means end of perception @@ -153,7 +183,8 @@ typedef struct Entity bool npc_is_knight_sprite(Entity *it) { - return it->is_npc && ( it->npc_kind == NPC_Max || it->npc_kind == NPC_Hunter || it->npc_kind == NPC_John || it->npc_kind == NPC_Blocky || it->npc_kind == NPC_Edeline); + return false; + //return it->is_npc && ( it->npc_kind == NPC_Blocky || it->npc_kind == NPC_Edeline); } typedef BUFF(char, MAX_SENTENCE_LENGTH*(REMEMBERED_PERCEPTIONS+4)) PromptBuff; @@ -197,6 +228,41 @@ 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; +} + void process_perception(Entity *it, Perception p) { if(it->is_npc) @@ -235,46 +301,141 @@ void process_perception(Entity *it, Perception p) } } -#define printf_buff(buff_ptr, ...) { int written = snprintf((buff_ptr)->data+(buff_ptr)->cur_index, ARRLEN((buff_ptr)->data) - (buff_ptr)->cur_index, __VA_ARGS__); assert(written >= 0); (buff_ptr)->cur_index += written; }; +#define printf_buff(buff_ptr, ...) { BUFF_VALID(buff_ptr); int written = snprintf((buff_ptr)->data+(buff_ptr)->cur_index, ARRLEN((buff_ptr)->data) - (buff_ptr)->cur_index, __VA_ARGS__); assert(written >= 0); (buff_ptr)->cur_index += written; }; -// returns if action index was valid -bool action_from_index(Entity *it, Action *out, int action_index) +bool npc_does_dialog(Entity *it) { - AvailableActions available = {0}; - fill_available_actions(it, &available); - if(action_index < 0 || action_index >= available.cur_index) + return it->npc_kind < ARRLEN(characters); +} + +typedef enum +{ + MSG_SYSTEM, + MSG_USER, + MSG_ASSISTANT, + MSG_ASSISTANT_NO_TRAILING, +} MessageType; + +void dump_json_node(PromptBuff *into, MessageType type, const char *content) +{ + const char *type_str = 0; + if(type == MSG_SYSTEM) + type_str = "system"; + else if(type == MSG_USER) + type_str = "user"; + else if(type == MSG_ASSISTANT || MSG_ASSISTANT_NO_TRAILING) + type_str = "assistant"; + assert(type_str); + printf_buff(into, "{\"type\": \"%s\", \"content\": \"%s\"}", type_str, escape_for_json(content).data); + if(type != MSG_ASSISTANT_NO_TRAILING) printf_buff(into, ","); +} + +// outputs json +void generate_chatgpt_prompt(Entity *it, PromptBuff *into) +{ + assert(it->is_npc); + assert(it->npc_kind < ARRLEN(characters)); + + *into = (PromptBuff){0}; + + printf_buff(into, "["); + + BUFF(char, 1024) initial_system_msg = {0}; + const char *health_string = 0; + if(it->damage <= 0.2f) { - return false; + 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 { - *out = available.data[action_index]; - return true; + 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); -// don't call on untrusted action, doesn't return error -int action_to_index(Entity *it, Action a) -{ + printf_buff(&initial_system_msg, "%s\n%s\nNPC health status: Right now, %s\n%s", global_prompt, characters[it->npc_kind].prompt, health_string, items[it->last_seen_holding_kind].global_prompt); + dump_json_node(into, MSG_SYSTEM, initial_system_msg.data); + + Entity *e = it; + ItemKind last_holding = ITEM_none; + BUFF_ITER(Perception, &e->remembered_perceptions) + { + BUFF(char, 1024) cur_node = {0}; + if(it->type == PlayerAction) + { + assert(it->player_action_type < ARRLEN(actions)); + printf_buff(&cur_node, "Player: ACT_%s", actions[it->player_action_type]); + dump_json_node(into, MSG_USER, cur_node.data); + } + else if(it->type == EnemyAction) + { + assert(it->enemy_action_type < ARRLEN(actions)); + printf_buff(&cur_node, "An Enemy: ACT_%s", actions[it->player_action_type]); + dump_json_node(into, MSG_USER, cur_node.data); + } + else if(it->type == PlayerDialog) + { + printf_buff(&cur_node, "Player: \"%s\"", it->player_dialog.data); + dump_json_node(into, MSG_USER, cur_node.data); + } + else if(it->type == NPCDialog) + { + assert(it->npc_action_type < ARRLEN(actions)); + printf_buff(&cur_node, "%s: ACT_%s \"%s\"", characters[e->npc_kind].name, actions[it->npc_action_type], it->npc_dialog.data); + dump_json_node(into, MSG_ASSISTANT, cur_node.data); + } + else if(it->type == PlayerHeldItemChanged) + { + if(last_holding != it->holding) + { + if(last_holding != ITEM_none) + { + printf_buff(&cur_node, "%s\n", items[last_holding].discard); + } + if(it->holding != ITEM_none) + { + printf_buff(&cur_node, "%s\n", items[it->holding].possess); + } + last_holding = it->holding; + } + dump_json_node(into, MSG_SYSTEM, cur_node.data); + } + else + { + assert(false); + } + } + + BUFF(char, 1024) latest_state_node = {0}; AvailableActions available = {0}; fill_available_actions(it, &available); - Action target_action = a; - int index = -1; - for(int i = 0; i < available.cur_index; i++) + printf_buff(&latest_state_node, "The NPC can now ONLY do these actions: ["); + BUFF_ITER_I(Action, &available, i) { - if(available.data[i] == target_action) + if(i == available.cur_index - 1) { - index = i; - break; + printf_buff(&latest_state_node, "ACT_%s", actions[*it]); + } + else + { + printf_buff(&latest_state_node, "ACT_%s, ", actions[*it]); } } - assert(index != -1); - return index; -} + printf_buff(&latest_state_node, "]"); + dump_json_node(into, MSG_SYSTEM, latest_state_node.data); -bool npc_does_dialog(Entity *it) -{ - return it->npc_kind < ARRLEN(prompt_table); + BUFF(char, 1024) assistant_prompt_node = {0}; + printf_buff(&assistant_prompt_node, "%s: ACT_", characters[it->npc_kind].name); + dump_json_node(into, MSG_ASSISTANT_NO_TRAILING, assistant_prompt_node.data); + + printf_buff(into, "]"); } void generate_prompt(Entity *it, PromptBuff *into) @@ -287,8 +448,8 @@ void generate_prompt(Entity *it, PromptBuff *into) printf_buff(into, "%s", "\n"); // npc description prompt - assert(it->npc_kind < ARRLEN(prompt_table)); - printf_buff(into, "%s", prompt_table[it->npc_kind]); + assert(it->npc_kind < ARRLEN(characters)); + printf_buff(into, "%s", characters[it->npc_kind].prompt); printf_buff(into, "%s", "\n"); // npc stats prompt @@ -315,8 +476,8 @@ void generate_prompt(Entity *it, PromptBuff *into) // item prompt if(it->last_seen_holding_kind != ITEM_none) { - assert(it->last_seen_holding_kind < ARRLEN(item_prompt_table)); - printf_buff(into, "%s", item_prompt_table[it->last_seen_holding_kind]); + 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"); } @@ -326,7 +487,7 @@ void generate_prompt(Entity *it, PromptBuff *into) printf_buff(into, "%s", "The NPC possible actions array, indexed by ACT_INDEX: ["); BUFF_ITER(Action, &available) { - printf_buff(into, "%s", action_strings[*it]); + printf_buff(into, "%s", actions[*it]); printf_buff(into, "%s", ", "); } printf_buff(into, "%s", "]\n"); @@ -337,13 +498,13 @@ void generate_prompt(Entity *it, PromptBuff *into) { if(it->type == PlayerAction) { - assert(it->player_action_type < ARRLEN(action_strings)); - printf_buff(into, "Player: ACT %s \n", action_strings[it->player_action_type]); + 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(action_strings)); - printf_buff(into, "An Enemy: ACT %s \n", action_strings[it->player_action_type]); + 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) { @@ -353,7 +514,7 @@ void generate_prompt(Entity *it, PromptBuff *into) } else if(it->type == NPCDialog) { - printf_buff(into, "The NPC, %s: ACT %s \"%s\"\n", name_table[e->npc_kind], action_strings[it->npc_action_type], it->npc_dialog.data); + 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) { @@ -361,12 +522,12 @@ void generate_prompt(Entity *it, PromptBuff *into) { if(last_holding != ITEM_none) { - printf_buff(into, "%s", item_discard_message_table[last_holding]); + printf_buff(into, "%s", items[last_holding].discard); printf_buff(into, "%s", "\n"); } if(it->holding != ITEM_none) { - printf_buff(into, "%s", item_possess_message_table[it->holding]); + printf_buff(into, "%s", items[it->holding].possess); printf_buff(into, "%s", "\n"); } last_holding = it->holding; @@ -378,7 +539,53 @@ void generate_prompt(Entity *it, PromptBuff *into) } } - printf_buff(into, "The NPC, %s: ACT_INDEX", name_table[e->npc_kind]); + printf_buff(into, "The NPC, %s: ACT_INDEX", characters[e->npc_kind].name); +} + +bool parse_chatgpt_response(Entity *it, char *sentence_str, Perception *out) +{ + *out = (Perception){0}; + out->type = NPCDialog; + + size_t sentence_length = strlen(sentence_str); + + char action_string[512] = {0}; + char dialog_string[512] = {0}; + int variables_filled = sscanf(sentence_str, "%511s \"%511[^\n]\"", action_string, dialog_string); + + if(strlen(action_string) == 0 || strlen(dialog_string) == 0 || variables_filled != 2) + { + Log("sscanf failed to parse chatgpt string `%s`, variables unfilled. Action string: `%s` dialog string `%s`\n", sentence_str, action_string, dialog_string); + return false; + } + + AvailableActions available = {0}; + fill_available_actions(it, &available); + bool found_action = false; + BUFF_ITER(Action, &available) + { + if(strcmp(actions[*it], action_string) == 0) + { + found_action = true; + out->npc_action_type = *it; + } + } + if(!found_action) + { + Log("Could not find action associated with string `%s`\n", action_string); + out->npc_action_type = ACT_none; + } + + if(strlen(dialog_string) >= ARRLEN(out->npc_dialog.data)) + { + Log("Dialog string `%s` too big to fit in sentence size %d\n", dialog_string, (int)ARRLEN(out->npc_dialog.data)); + return false; + } + + memcpy(out->npc_dialog.data, dialog_string, strlen(dialog_string)); + out->npc_dialog.cur_index = (int)strlen(dialog_string); + + return true; } // returns if the response was well formatted diff --git a/maketraining.c b/maketraining.c index 5384c93..c9c7b0f 100644 --- a/maketraining.c +++ b/maketraining.c @@ -355,31 +355,7 @@ TrainingSample samples[] = { }; -typedef BUFF(char, 1024*10) Escaped; -Escaped escape_for_json(char *s) -{ - Escaped to_return = {0}; - size_t len = strlen(s); - for(int i = 0; i < len; i++) - { - if(s[i] == '\n') - { - BUFF_APPEND(&to_return, '\\'); - BUFF_APPEND(&to_return, 'n'); - } - else if(s[i] == '"') - { - BUFF_APPEND(&to_return, '\\'); - BUFF_APPEND(&to_return, '"'); - } - else - { - assert(s[i] <= 126 && s[i] >= 32 ); - BUFF_APPEND(&to_return, s[i]); - } - } - return to_return; -} + int main(int argc, char ** argv) { diff --git a/run_codegen.bat b/run_codegen.bat index 81429a3..d974a15 100644 --- a/run_codegen.bat +++ b/run_codegen.bat @@ -1,5 +1,7 @@ @echo off +echo Running codegen... + echo Asset packs which must be bought and unzipped into root directory before running this script: echo https://rafaelmatos.itch.io/epic-rpg-world-pack-ancient-ruins echo https://sventhole.itch.io/undead-pixel-art-characters @@ -39,8 +41,8 @@ thirdparty\sokol-shdc.exe --input quad.glsl --output gen\quad-sapp.glsl.h --slan cl /Ithirdparty /W3 /Zi /WX codegen.c || goto :error codegen || goto :error -cl /Ithirdparty /Igen /W3 /Zi /WX maketraining.c || goto :error -maketraining || goto :error +@REM cl /Ithirdparty /Igen /W3 /Zi /WX maketraining.c || goto :error +@REM maketraining || goto :error goto :EOF diff --git a/server/main.go b/server/main.go index e79229a..04dd4c0 100644 --- a/server/main.go +++ b/server/main.go @@ -408,7 +408,7 @@ func completion(w http.ResponseWriter, req *http.Request) { log.Println("Println response: `", response + "`") log.Println() } - fmt.Fprintf(w, "1%s", response) + fmt.Fprintf(w, "1%s", response + "\n") } } diff --git a/server/playground.go b/server/playground.go index 7024659..e0d9969 100644 --- a/server/playground.go +++ b/server/playground.go @@ -21,7 +21,7 @@ func main() { messages = append(messages, openai.ChatCompletionMessage { Role: "system", - Content: `You are a wise dungeonmaster who carefully crafts interesting dialog and actions for an NPC in an action-rpg video game. The NPC performs actions by prefixing their dialog with one of these actions: [ACT_none, ACT_fights_player, ACT_joins_player, ACT_strikes_air] + Content: `You are a wise dungeonmaster who carefully crafts interesting dialog and actions for an NPC in an action-rpg video game. The NPC performs actions by prefixing their dialog with the action they perform at that time. An example interaction between the player and an NPC: Player: ACT_walks_up @@ -36,22 +36,29 @@ The NPC you will be acting as, Fredrick, is a soldier in death's cohort. `, }) - reader := bufio.NewReader(os.Stdin) for { fmt.Printf("Say something with format [action] \"dialog\": ") text, _ := reader.ReadString('\n') messages = append(messages, openai.ChatCompletionMessage { Role: "user", - Content: text + "\nFredrick: ", + Content: text + "Fredrick: ", }) - //fmt.Printf("Generating with messages: `%s`\n", messages) + toGenerate := make([]openai.ChatCompletionMessage, len(messages)) + copy(toGenerate, messages) + + toGenerate = append(toGenerate, toGenerate[len(toGenerate)-1]) + toGenerate[len(toGenerate)-2] = openai.ChatCompletionMessage { + Role: "system", + Content: "The NPC can now ONLY do these actions: [ACT_none, ACT_fights_player, ACT_joins_player, ACT_strikes_air]", + } + fmt.Printf("Generating with messages: `%s`\n", toGenerate) resp, err := c.CreateChatCompletion( context.Background(), openai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, - Messages: messages, + Messages: toGenerate, Stop: []string{"\n"}, }, ) @@ -64,6 +71,7 @@ The NPC you will be acting as, Fredrick, is a soldier in death's cohort. Role: "assistant", Content: resp.Choices[0].Message.Content, }) + fmt.Printf("Tokens used: %d\n", resp.Usage.TotalTokens) fmt.Printf("Response: `%s`\n", resp.Choices[0].Message.Content) } }