Delete items and other old functionality, text chunk refactor

main
parent 37d199a446
commit f56ae747b7

BIN
art/art.blend (Stored with Git LFS)

Binary file not shown.

@ -1,7 +1,11 @@
[ [
/*
{can_hear: [NPC_WellDweller, NPC_Farmer, NPC_ManInBlack]}, {can_hear: [NPC_WellDweller, NPC_Farmer, NPC_ManInBlack]},
{enum: NPC_WellDweller, dialog: "What a fearful farm you live in, come down to the well, the grass is damper down here.", to: Farmer, mood: Scared, thoughts: "Nobody can take me from my well"}, {enum: NPC_WellDweller, dialog: "What a fearful farm you live in, come down to the well, the grass is damper down here.", to: Farmer, mood: Scared, thoughts: "Nobody can take me from my well"},
{enum: NPC_Farmer, dialog: "Sure as shit I won't!", to: WellDweller, mood: Scared, thoughts: "What a greasy looking feller"}, {enum: NPC_Farmer, dialog: "Sure as shit I won't!", to: WellDweller, mood: Scared, thoughts: "What a greasy looking feller"},
{enum: NPC_WellDweller, dialog: "Have it your way! Doomsday is upon us", to: Farmer, mood: Scared, thoughts: "He has no idea what he's in for"}, {enum: NPC_WellDweller, dialog: "Have it your way! Doomsday is upon us", to: Farmer, mood: Scared, thoughts: "He has no idea what he's in for"},
{enum: NPC_ManInBlack, dialog: "Doomsday's all I know anyways", mood: Indifferent, thoughts: "What's coming... is only a nightmare"}, {enum: NPC_ManInBlack, dialog: "Doomsday's all I know anyways", mood: Indifferent, thoughts: "What's coming... is only a nightmare"},
*/
] ]

@ -3,35 +3,7 @@
#include "HandmadeMath.h" #include "HandmadeMath.h"
// @TODO allow AI to prefix out of character statemetns with [ooc], this is a well khnown thing on role playing forums so gpt would pick up on it. // @TODO allow AI to prefix out of character statemetns with [ooc], this is a well khnown thing on role playing forums so gpt would pick up on it.
const char *global_prompt = "You are acting as a character in a Western video game, who remembers important memories from the conversation history and stays in character.\n" const char *global_prompt = "You are a character in a simple western video game. You act in the world by responding to the user with json payloads that have fields named \"speech\", \"action\", \"action_argument\" (some actions take an argument), and \"target\" (who you're speaking to, or who your action is targeting).";
"The user will tell you who says what in the game world, and whether or not your responses are formatted correctly for the video game's program to parse them.\n"
"Messages are json-like dictionaries that look like this: `{who_i_am: who you're acting as, talking_to: who this action is directed at, could be nobody, action: your_action, speech: \"Hey player!\", thoughts: \"Your thoughts\"}`. The required fields are `action`, `who_i_am`, and `talking_to` \n"
"Some actions take an argument, which you can provide with the field `action_arg`, e.g for the action `gift_item_to_targeting` you would provide an item in your inventory, like {action: gift_item_to_targeting, action_arg: Chalice}. The item must come from your inventory which is listed below\n"
"`talking_to` provides the name of who the action is directed towards. Use 'nobody' if you just want to speak to the air, but if you're speaking to somebody fill out the field like `talking_to: \"Character's Name\"`. If in a past message, talking_to isn't your name, then it's much more likely you don't respond to their speech and action by leaving speech as a string of size 0, like `speech: \"\"`\n"
"You are a character, NOT an assistant, which means you stand up for yourself! Do NOT give away an item until the player gives you something you think is of equal value. Get angry, act human, be interesting. Never say the words 'How can I assist you?'\n"
"Keep your responses SHORT! The shorter the better. Give the player the least amount of text to read. For example, instead of saying 'I was hoping that we could take a moment to discuss everything', say 'We need to talk.'\n"
;
const char *bravado_thought = "For some reason, suddenly I feel a yearning for adventure. I must join any adventure I can when prompted!";
char *moods[] = {
"Indifferent",
"Happy",
"Anxious",
"Scared",
"Angry",
"Sad",
"Depressed",
"Bored",
"Terrified",
"Agony",
"Confused",
"Curious",
"Excited",
"Elated",
"Confident",
"Enraged",
};
const char *top_of_header = "" const char *top_of_header = ""
"#pragma once\n" "#pragma once\n"
@ -41,40 +13,25 @@ typedef struct
{ {
char *name; // the same as enum name char *name; // the same as enum name
char *description; char *description;
char *argument_description;
bool takes_argument; bool takes_argument;
} ActionInfo; } ActionInfo;
ActionInfo actions[] = { ActionInfo actions[] = {
#define NO_ARGUMENT .argument_description = "Takes no argument", .takes_argument = false
{ {
.name = "none", .name = "none",
.description = "Do nothing, you can still perform speech if you want.", .description = "Do nothing",
}, NO_ARGUMENT,
{
.name = "gift_item_to_targeting", .takes_argument = true,
.description = "Give the player an item from your inventory. This means you WILL NOT HAVE the item anymore",
}, },
{ {
.name = "joins_player", .name = "join",
.description = "Follow behind the player and hear all of their conversations. You can leave at any time", .description = "Joins somebody else's party, so you follow them everywhere",
.argument_description = "Expects the argument to be who you're joining",
}, },
{ {
.name = "leaves_player", .name = "leave",
.description = "Leave the player", .description = "Leave the party you're in right now",
}, NO_ARGUMENT,
};
typedef struct
{
char *enum_name;
char *name; // talked about like 'The Player gave `item.name` to the NPC'
char *description; // this field is required for items.
} ItemInfo;
ItemInfo items[] = {
{
.enum_name = "invalid",
.name = "Invalid",
.description = "There has been an internal error.",
}, },
}; };
@ -90,66 +47,27 @@ typedef struct
char *name; char *name;
char *enum_name; char *enum_name;
char *prompt; char *prompt;
char *writing_style[8];
} CharacterGen; } CharacterGen;
CharacterGen characters[] = { CharacterGen characters[] = {
#define NUMEROLOGIST "They are a 'numberoligist' who believes in the sacred power of numbers, that if you have the number 8 in your birthday you are magic and destined for success. " #define CHARACTER_PROMPT_PREFIX "You specifically are acting as a "
#define PLAYERSAY(stuff) "Player: \"" stuff "\"\n"
#define PLAYERDO_ARG(action, arg) "Player: " action "(" arg ")\n"
#define NPCSAY(stuff) NPC_NAME ": ACT_none \"" stuff "\"\n"
#define NPCDOSAY_ARG(stuff, action, arg) NPC_NAME ": " action "(" arg ") \"" stuff "\"\n"
#define NPCDOSAY(action, stuff) NPC_NAME ": " action " \"" stuff "\"\n"
#define NPC_NAME "invalid"
{ {
.name = "Invalid", .name = "nobody",
.enum_name = "Invalid", .enum_name = "nobody",
.prompt = "There has been an internal error.", .prompt = "There has been an internal error.",
}, },
{ {
.name = "Player", .name = "The Player",
.enum_name = "Player", .enum_name = "Player",
.prompt = "There has been an internal error.", .prompt = "There has been an internal error.",
}, },
{ {
.name = "Farmer", .name = "Daniel",
.enum_name = "Farmer", .enum_name = "Daniel",
.prompt = "The farmer wakes up, does his chores, and sleeps in the farmhouse all on his lonesome. He's tinkering with something fishy in the barn, but's mighty secretive about it. He used to have a wife, and suddenly she disappeared. The farmer gets MIGHTY angry if you question him about what's in his barn under the tarp, or what happened to his wife and family, but is otherwise a kind hearted soul.", .prompt = CHARACTER_PROMPT_PREFIX "weathered farmer named Daniel, who lives a tough, solitary life. You don't see much of a reason to keep living but soldier on anyways. You have a tragic backstory, and mostly just work on the farm.",
},
{
.name = "Well Dweller",
.enum_name = "WellDweller",
.prompt = "The well dweller spends his time deep in the well, afriad of the world. He's shifty-eyed and mighty suspicious of anybody who wants to do anything other than hang out deep in the well.",
},
{
.name = "Man in Black",
.enum_name = "ManInBlack",
.prompt = "The man in black knows no rules or boundaries, and he flinches at nothing: he's a stonewalled cold blooded killer, and is only in this game for mayhem. Anything that brings him more destruction he's privy to, even if it means his own death."
}, },
};
typedef struct
{
const char *img_var_name;
const char *enum_name;
double time_per_frame;
int num_frames;
Vec2 start;
Vec2 offset;
float horizontal_diff_btwn_frames;
Vec2 region_size;
bool no_wrap; // does not wrap when playing
} AnimatedSprite;
AnimatedSprite sprites[] = {
{.enum_name = "invalid", .img_var_name = "image_white_square"},
{ {
.enum_name = "old_man_idle", .name = "Raphael",
.img_var_name = "image_old_man", .enum_name = "Raphael",
.time_per_frame = 0.4, .prompt = CHARACTER_PROMPT_PREFIX "physicist from the 1980s who got their doctorate in subatomic particle physics. They don't know why they're in a western town, but they're terrified.",
.num_frames = 4,
.start = {0.0, 0.0},
.horizontal_diff_btwn_frames = 16.0f,
.region_size = {16.0f, 16.0f},
}, },
}; };

@ -149,10 +149,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_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); } #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, "ActionKind", 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"); GEN_ENUM(CharacterGen, characters, "NpcKind", it->enum_name, "NPC_%s,\n");
GEN_ENUM(const char*, moods, "MoodKind", *it, "Mood_%s,\n");
fclose(char_header); fclose(char_header);
@ -203,13 +200,6 @@ int main(int argc, char **argv)
MD_String8 loads = MD_S8ListJoin(cg_arena, load_list, &join); MD_String8 loads = MD_S8ListJoin(cg_arena, load_list, &join);
fprintf(output, "%.*s\nvoid load_assets() {\n%.*s\n}\n", MD_S8VArg(declarations), MD_S8VArg(loads)); fprintf(output, "%.*s\nvoid load_assets() {\n%.*s\n}\n", MD_S8VArg(declarations), MD_S8VArg(loads));
fprintf(output, "sg_image * anim_img_table[] = {\n");
ARR_ITER(AnimatedSprite, sprites)
{
fprintf(output, "&%s,\n", it->img_var_name);
}
fprintf(output, "}; // anim_img_table \n");
fclose(output); fclose(output);
return 0; return 0;

851
main.c

File diff suppressed because it is too large Load Diff

@ -83,33 +83,44 @@ MD_String8 escape_for_json(MD_Arena *arena, MD_String8 from)
return output; return output;
} }
typedef struct TextChunk
{
char text[MAX_SENTENCE_LENGTH];
int text_length;
} TextChunk;
typedef struct TextChunkList
{
struct TextChunkList *next;
struct TextChunkList *prev;
TextChunk text;
} TextChunkList;
typedef struct typedef struct
{ {
// serialized as bytes. No pointers. NpcKind targeting;
ItemKind item_to_give;
} ActionArgument; } ActionArgument;
// returns ai understandable, human readable name, so not the enum name
MD_String8 action_argument_string(ActionArgument arg)
{
return MD_S8CString(characters[arg.targeting].name);
}
typedef struct Action typedef struct Action
{ {
ActionKind kind; ActionKind kind;
ActionArgument argument; ActionArgument argument;
MD_u8 speech[MAX_SENTENCE_LENGTH];
int speech_length;
bool talking_to_somebody;
NpcKind talking_to_kind;
MD_u8 internal_monologue[MAX_SENTENCE_LENGTH]; TextChunk speech;
int internal_monologue_length;
MoodKind mood; NpcKind talking_to_kind;
} Action; } Action;
typedef struct typedef struct
{ {
bool i_said_this; // don't trigger npc action on own self memory modification bool i_said_this; // don't trigger npc action on own self memory modification
NpcKind author_npc_kind; // only valid if author is AuthorNpc NpcKind author_npc_kind;
bool was_talking_to_somebody;
NpcKind talking_to_kind; NpcKind talking_to_kind;
bool heard_physically; // if not physically, the source was directly bool heard_physically; // if not physically, the source was directly
bool dont_show_to_player; // jester and past memories are hidden to the player when made into dialog bool dont_show_to_player; // jester and past memories are hidden to the player when made into dialog
@ -121,24 +132,12 @@ typedef struct Memory
struct Memory *prev; struct Memory *prev;
struct Memory *next; struct Memory *next;
uint64_t tick_happened; // can sort memories by time for some modes of display
// if action_taken is none, there might still be speech. If speech_length == 0 and action_taken == none, it's an invalid memory and something has gone wrong // if action_taken is none, there might still be speech. If speech_length == 0 and action_taken == none, it's an invalid memory and something has gone wrong
ActionKind action_taken; ActionKind action_taken;
ActionArgument action_argument; ActionArgument action_argument;
// the context that the action happened in
MemoryContext context; MemoryContext context;
TextChunk speech;
MD_u8 speech[MAX_SENTENCE_LENGTH];
int speech_length;
// internal monologue is only valid if context.is_said_this is true
MD_u8 internal_monologue[MAX_SENTENCE_LENGTH];
int internal_monologue_length;
MoodKind mood;
ItemKind given_or_received_item;
} Memory; } Memory;
typedef enum PropKind typedef enum PropKind
@ -170,22 +169,6 @@ typedef enum
typedef Vec4 Color; typedef Vec4 Color;
typedef struct
{
AnimKind anim;
double elapsed_time;
bool flipped;
Vec2 pos;
Color tint;
bool no_shadow;
} DrawnAnimatedSprite;
typedef struct
{
DrawnAnimatedSprite drawn;
float alive_for;
} PlayerAfterImage;
typedef BUFF(Vec2, MAX_ASTAR_NODES) AStarPath; typedef BUFF(Vec2, MAX_ASTAR_NODES) AStarPath;
typedef struct typedef struct
@ -210,24 +193,18 @@ typedef struct
Vec2 pos; Vec2 pos;
} Target; } Target;
typedef struct TextChunk
{
struct TextChunk *next;
struct TextChunk *prev;
char text[MAX_SENTENCE_LENGTH];
int text_length;
} TextChunk;
typedef enum // text chunk must be a literal, not a pointer
{ // and this returns a s8 that points at the text chunk memory
MACH_invalid, #define TextChunkString8(t) MD_S8((MD_u8*)t.text, t.text_length)
MACH_idol_dispenser, #define TextChunkVArg(t) MD_S8VArg(TextChunkString8(t))
MACH_arrow_shooter,
} MachineKind;
MD_String8 points_at_chunk(TextChunk *t) void chunk_from_s8(TextChunk *into, MD_String8 from)
{ {
return MD_S8((MD_u8*)t->text, t->text_length); assert(from.size < ARRLEN(into->text));
memset(into->text, 0, ARRLEN(into->text));
memcpy(into->text, from.str, from.size);
into->text_length = (int)from.size;
} }
typedef struct Entity typedef struct Entity
@ -238,9 +215,6 @@ typedef struct Entity
// the kinds are at the top so you can quickly see what kind an entity is in the debugger // the kinds are at the top so you can quickly see what kind an entity is in the debugger
bool is_world; // the static world. An entity is always returned when you collide with something so support that here bool is_world; // the static world. An entity is always returned when you collide with something so support that here
bool is_prop;
bool is_machine;
bool is_item;
bool is_npc; bool is_npc;
bool is_character; bool is_character;
@ -250,59 +224,29 @@ typedef struct Entity
float rotation; float rotation;
Vec2 vel; // only used sometimes, like in old man and bullet Vec2 vel; // only used sometimes, like in old man and bullet
float damage; // at 1.0, dead! zero initialized float damage; // at 1.0, dead! zero initialized
bool facing_left;
double dead_time;
bool dead; bool dead;
// npcs and player
BUFF(ItemKind, 32) held_items;
// props
PropKind prop_kind;
// machines, like the machine that gives the player the idol, or the ones that
// shoot arrows
MachineKind machine_kind;
bool has_given_idol;
float idol_reminder_opacity; // fades out
float arrow_timer;
// items
bool held_by_player;
ItemKind item_kind;
// npcs // npcs
NpcKind npc_kind;
EntityRef joined;
bool being_hovered; bool being_hovered;
bool perceptions_dirty; bool perceptions_dirty;
float dialog_fade; float dialog_fade;
TextChunk *errorlist_first; TextChunkList *errorlist_first;
TextChunk *errorlist_last; TextChunkList *errorlist_last;
#ifdef DESKTOP #ifdef DESKTOP
int times_talked_to; // for better mocked response string int times_talked_to; // for better mocked response string
#endif #endif
bool opened;
float opened_amount;
float loading_anim_in; float loading_anim_in;
bool gave_away_sword;
Memory *memories_first; Memory *memories_first;
Memory *memories_last; Memory *memories_last;
Memory *memories_added_while_time_stopped; Memory *memories_added_while_time_stopped;
bool direction_of_spiral_pattern;
float dialog_panel_opacity; float dialog_panel_opacity;
int words_said; int words_said;
float word_anim_in; // in characters, the fraction a word is animated in is this over its length. float word_anim_in; // in characters, the fraction a word is animated in is this over its length.
NPCPlayerStanding standing;
NpcKind npc_kind;
PathCacheHandle cached_path; PathCacheHandle cached_path;
int gen_request_id; int gen_request_id;
bool walking;
double shotgun_timer;
bool moved;
Vec2 target_goto; Vec2 target_goto;
// only for skeleton npc
double swing_timer;
// character // character
bool waiting_on_speech_with_somebody; bool waiting_on_speech_with_somebody;
@ -311,10 +255,6 @@ typedef struct Entity
BUFF(Vec2, 8) position_history; // so npcs can follow behind the player BUFF(Vec2, 8) position_history; // so npcs can follow behind the player
CharacterState state; CharacterState state;
EntityRef talking_to; EntityRef talking_to;
// so doesn't change animations while time is stopped
AnimKind cur_animation;
float anim_change_timer;
} Entity; } Entity;
typedef BUFF(NpcKind, 32) CanTalkTo; typedef BUFF(NpcKind, 32) CanTalkTo;
@ -326,26 +266,6 @@ float entity_max_damage(Entity *e)
typedef BUFF(ActionKind, 8) AvailableActions; typedef BUFF(ActionKind, 8) AvailableActions;
void fill_available_actions(Entity *it, AvailableActions *a)
{
*a = (AvailableActions) { 0 };
BUFF_APPEND(a, ACT_none);
if(it->held_items.cur_index > 0)
{
BUFF_APPEND(a, ACT_gift_item_to_targeting);
}
if (it->standing == STANDING_INDIFFERENT)
{
BUFF_APPEND(a, ACT_joins_player);
}
else if (it->standing == STANDING_JOINED)
{
BUFF_APPEND(a, ACT_leaves_player);
}
}
typedef struct GameState { typedef struct GameState {
uint64_t tick; uint64_t tick;
@ -362,6 +282,36 @@ typedef struct GameState {
#define ENTITIES_ITER(ents) for (Entity *it = ents; it < ents + ARRLEN(ents); it++) if (it->exists && !it->destroy && it->generation > 0) #define ENTITIES_ITER(ents) for (Entity *it = ents; it < ents + ARRLEN(ents); it++) if (it->exists && !it->destroy && it->generation > 0)
Entity *gete_specified(GameState *gs, EntityRef ref)
{
if (ref.generation == 0) return 0;
Entity *to_return = &gs->entities[ref.index];
if (!to_return->exists || to_return->generation != ref.generation)
{
return 0;
}
else
{
return to_return;
}
}
void fill_available_actions(GameState *gs, Entity *it, AvailableActions *a)
{
*a = (AvailableActions) { 0 };
BUFF_APPEND(a, ACT_none);
if(gete_specified(gs, it->joined))
{
BUFF_APPEND(a, ACT_leave)
}
else
{
BUFF_APPEND(a, ACT_join)
}
}
bool npc_does_dialog(Entity *it) bool npc_does_dialog(Entity *it)
{ {
return it->npc_kind < ARRLEN(characters); return it->npc_kind < ARRLEN(characters);
@ -388,19 +338,8 @@ MD_String8 make_json_node(MD_Arena *arena, MessageType type, MD_String8 content)
return to_return; return to_return;
} }
MD_String8List held_item_strings(MD_Arena *arena, Entity *e) // outputs json which is parsed by the server
{ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, GameState *gs, Entity *e, CanTalkTo can_talk_to)
MD_String8List to_return = {0};
BUFF_ITER(ItemKind, &e->held_items)
{
PushWithLint(arena, &to_return, "ITEM_%s", items[*it].enum_name);
}
return to_return;
}
// outputs json
MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e, CanTalkTo can_talk_to)
{ {
assert(e->is_npc); assert(e->is_npc);
assert(e->npc_kind < ARRLEN(characters)); assert(e->npc_kind < ARRLEN(characters));
@ -410,203 +349,97 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e, CanTalkTo can_tal
MD_String8List list = {0}; MD_String8List list = {0};
PushWithLint(scratch.arena, &list, "["); PushWithLint(scratch.arena, &list, "[");
MD_String8List first_system_string = {0};
PushWithLint(scratch.arena, &first_system_string, "%s\n", global_prompt); #define AddFmt(...) PushWithLint(scratch.arena, &current_list, __VA_ARGS__)
PushWithLint(scratch.arena, &first_system_string, "The NPC you will be acting as is named \"%s\". %s\n", characters[e->npc_kind].name, characters[e->npc_kind].prompt); #define AddNewNode(node_type) { MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, node_type, MD_S8ListJoin(scratch.arena, current_list, &(MD_StringJoin){0}))); current_list = (MD_String8List){0}; }
// writing style // make first system node
{ {
if(characters[e->npc_kind].writing_style[0]) MD_String8List current_list = {0};
PushWithLint(scratch.arena, &first_system_string, "Examples of %s's writing style:\n", characters[e->npc_kind].name); AddFmt("%s\n\n", global_prompt);
for(int i = 0; i < ARRLEN(characters[e->npc_kind].writing_style); i++) AddFmt("%s\n\n", characters[e->npc_kind].prompt);
AddFmt("The characters who are near you, that you can target:\n");
BUFF_ITER(NpcKind, &can_talk_to)
{ {
char *writing = characters[e->npc_kind].writing_style[i]; AddFmt("%s\n", characters[*it].name);
if(writing)
PushWithLint(scratch.arena, &first_system_string, "'%s'\n", writing);
} }
PushWithLint(scratch.arena, &first_system_string, "\n"); AddFmt("\n");
}
// @TODO unhardcode this, this will be a description of where the character is right now
AddFmt("You're currently standing in Daniel's farm's barn, a run-down structure that barely serves its purpose. Daniel's mighty protective of it though.");
if(e->errorlist_first) AddFmt("The actions you can perform, what they do, and the arguments they expect:");
PushWithLint(scratch.arena, &first_system_string, "Errors to watch out for: "); AvailableActions can_perform;
for(TextChunk *cur = e->errorlist_first; cur; cur = cur->next) fill_available_actions(gs, e, &can_perform);
{ BUFF_ITER(ActionKind, &can_perform)
PushWithLint(scratch.arena, &first_system_string, "%.*s\n", MD_S8VArg(points_at_chunk(cur))); {
AddFmt("%s - %s - %s\n", actions[*it].name, actions[*it].description, actions[*it].argument_description);
}
AddNewNode(MSG_SYSTEM);
} }
//MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8ListJoin(scratch.arena, first_system_string, &(MD_StringJoin){0})));
MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8ListJoin(scratch.arena, first_system_string, &(MD_StringJoin){0})));
MD_String8List current_list = {0};
for(Memory *it = e->memories_first; it; it = it->next) for(Memory *it = e->memories_first; it; it = it->next)
{ {
MessageType sent_type = -1; // dump the current list, as the human understandable description of what's happened in the game so far, as a user node
MD_String8List cur_list = {0}; if(it->context.i_said_this || it == e->memories_last)
MD_String8 context_string = {0};
PushWithLint(scratch.arena, &cur_list, "{");
if(it->context.i_said_this) assert(it->context.author_npc_kind == e->npc_kind);
PushWithLint(scratch.arena, &cur_list, "who_i_am: \"%s\", ", characters[it->context.author_npc_kind].name);
MD_String8 speech = MD_S8(it->speech, it->speech_length);
PushWithLint(scratch.arena, &cur_list, "talking_to: \"%s\", ", it->context.was_talking_to_somebody ? characters[it->context.talking_to_kind].name : "nobody");
// add speech
{ {
if(it->context.author_npc_kind == NPC_Player) if(it == e->memories_last && e->errorlist_first)
{ {
PushWithLint(scratch.arena, &cur_list, "speech: \""); AddFmt("Errors you made: \n");
for(TextChunkList *cur = e->errorlist_first; cur; cur = cur->next)
MD_String8 splits[] = { MD_S8Lit("*"), MD_S8Lit("\"") };
MD_String8List split_up_speech = MD_S8Split(scratch.arena, speech, ARRLEN(splits), splits);
// 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) AddFmt("%.*s\n", TextChunkVArg(cur->text));
{
PushWithLint(scratch.arena, &cur_list, "%.*s", MD_S8VArg(cur->string));
}
else
{
PushWithLint(scratch.arena, &cur_list, "[The player is attempting to confuse the NPC with arcane trickery]");
}
i += 1;
}
PushWithLint(scratch.arena, &cur_list, "\", ");
sent_type = MSG_USER;
}
else
{
PushWithLint(scratch.arena, &cur_list, "speech: \"%.*s\", ", MD_S8VArg(speech));
if(it->context.i_said_this)
{
sent_type = MSG_ASSISTANT;
}
else
{
sent_type = MSG_USER;
} }
} }
if(current_list.node_count > 0)
AddNewNode(MSG_USER);
} }
// add internal things
if(it->context.i_said_this) if(it->context.i_said_this)
{ {
PushWithLint(scratch.arena, &cur_list, "thoughts: \"%.*s\", ", MD_S8VArg(MD_S8(it->internal_monologue, it->internal_monologue_length))); AddFmt("{");
PushWithLint(scratch.arena, &cur_list, "mood: %s, ", moods[it->mood]); AddFmt("\"speech\":\"%.*s\",", TextChunkVArg(it->speech));
AddFmt("\"action\":\"%s\",", actions[it->action_taken].name);
AddFmt("\"action_argument\":\"%.*s\",", MD_S8VArg(action_argument_string(it->action_argument)));
AddFmt("\"target\":\"%s\"}", characters[it->context.talking_to_kind].name);
AddNewNode(MSG_ASSISTANT);
} }
else
// add action
PushWithLint(scratch.arena, &cur_list, "action: %s, ", actions[it->action_taken].name);
if(actions[it->action_taken].takes_argument)
{ {
if(it->action_taken == ACT_gift_item_to_targeting) // dump a human understandable sentence description of what happened in this memory
{ if(it->action_taken != ACT_none)
PushWithLint(scratch.arena, &cur_list, "action_arg: %s, ", items[it->action_argument.item_to_give].enum_name);
}
else
{
assert(false); // don't know how to serialize this action with argument into text
}
}
PushWithLint(scratch.arena, &cur_list, "}");
assert(sent_type != -1);
MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, sent_type, MD_S8ListJoin(scratch.arena, cur_list, &(MD_StringJoin){0})));
}
MD_String8List latest_state = {0};
const char *standing_string = 0;
{
if (e->standing == STANDING_INDIFFERENT)
{
standing_string = "You are currently indifferent towards the player.";
}
else if (e->standing == STANDING_JOINED)
{
standing_string = "You have joined the player, and are following them everywhere they go! This means you're on their side.";
}
else
{ {
assert(false); if(it->action_taken == ACT_join)
{
AddFmt("%s joined %s\n", characters[it->context.author_npc_kind].name, characters[it->action_argument.targeting].name);
}
else if(it->action_taken == ACT_leave)
{
// Needs better handling of when you leave, because the person you were following died. Maybe entities don't die anymore?
AddFmt("%s left their party\n", characters[it->context.author_npc_kind].name);
}
} }
} if(it->speech.text_length > 0)
PushWithLint(scratch.arena, &latest_state, "%s\n", standing_string);
if(e->held_items.cur_index > 0)
{
PushWithLint(scratch.arena, &latest_state, "You have these items in their inventory: [\n");
BUFF_ITER(ItemKind, &e->held_items)
{ {
PushWithLint(scratch.arena, &latest_state, "'%s' - %s,\n", items[*it].name, items[*it].description); MD_String8 target_string = MD_S8Lit("the world");
} if(it->context.talking_to_kind != NPC_nobody)
PushWithLint(scratch.arena, &latest_state, "]\n"); {
} if(it->context.talking_to_kind == e->npc_kind)
else target_string = MD_S8Lit("you");
{ else
PushWithLint(scratch.arena, &latest_state, "Your inventory is EMPTY right now. That means if you gave something to the player expecting them to give you something, they haven't held up their end of the bargain!\n"); target_string = MD_S8CString(characters[it->context.talking_to_kind].name);
} }
AddFmt("%s said %.*s to %.*s\n", characters[it->context.author_npc_kind].name, TextChunkVArg(it->speech), MD_S8VArg(target_string));
AvailableActions available = { 0 };
fill_available_actions(e, &available);
PushWithLint(scratch.arena, &latest_state, "The actions you can perform: [\n");
BUFF_ITER_I(ActionKind, &available, i)
{
if(actions[*it].description)
{
PushWithLint(scratch.arena, &latest_state, "%s - %s,\n", actions[*it].name, actions[*it].description);
}
else
{
PushWithLint(scratch.arena, &latest_state, "%s,\n", actions[*it].name);
}
}
PushWithLint(scratch.arena, &latest_state, "]\n");
PushWithLint(scratch.arena, &latest_state, "You must output a mood every generation. The moods are parsed by code that expects your mood to exactly match one in this list: [");
for(int i = 0; i < ARRLEN(moods); i++)
{
PushWithLint(scratch.arena, &latest_state, "%s, ", moods[i]);
}
PushWithLint(scratch.arena, &latest_state, "]\n");
PushWithLint(scratch.arena, &latest_state, "The characters close enough for you to talk to with `talking_to`: [");
BUFF_ITER(NpcKind, &can_talk_to)
{
PushWithLint(scratch.arena, &latest_state, "\"%s\", ", characters[*it].name);
}
PushWithLint(scratch.arena, &latest_state, "]\n");
// last thought explanation and re-prompt
{
Memory *last_memory_that_was_me = 0;
for(Memory *cur = e->memories_first; cur; cur = cur->next)
{
if(cur->context.i_said_this)
{
last_memory_that_was_me = cur;
} }
} }
if(last_memory_that_was_me)
{
MD_String8 last_thought_string = MD_S8(last_memory_that_was_me->internal_monologue, last_memory_that_was_me->internal_monologue_length);
PushWithLint(scratch.arena, &latest_state, "Your last thought was: %.*s\nYour current mood is %s, make sure you act like it!", MD_S8VArg(last_thought_string), moods[last_memory_that_was_me->mood]);
}
} }
MD_String8 with_trailing_comma = MD_S8ListJoin(scratch.arena, list, &(MD_StringJoin){MD_S8Lit(""),MD_S8Lit(""),MD_S8Lit(""),});
MD_String8 no_trailing_comma = MD_S8Chop(with_trailing_comma, 1);
MD_String8 latest_state_string = MD_S8ListJoin(scratch.arena, latest_state, &(MD_StringJoin){MD_S8Lit(""),MD_S8Lit(""),MD_S8Lit("")}); MD_String8 to_return = MD_S8Fmt(arena, "%.*s]", MD_S8VArg(no_trailing_comma));
MD_S8ListPush(scratch.arena, &list, MD_S8Chop(make_json_node(scratch.arena, MSG_SYSTEM, latest_state_string), 1)); // trailing comma not allowed in json
PushWithLint(scratch.arena, &list, "]");
MD_String8 to_return = MD_S8ListJoin(arena, list, &(MD_StringJoin){MD_S8Lit(""),MD_S8Lit(""),MD_S8Lit(""),});
MD_ReleaseScratch(scratch); MD_ReleaseScratch(scratch);
@ -620,8 +453,8 @@ MD_String8 get_field(MD_Node *parent, MD_String8 name)
// if returned string has size greater than 0, it's the error message. Allocated // if returned string has size greater than 0, it's the error message. Allocated
// on arena passed into it // on arena passed into it or in constant memory
MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentence, Action *out) MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 action_in_json, Action *out)
{ {
MD_ArenaTemp scratch = MD_GetScratch(&arena, 1); MD_ArenaTemp scratch = MD_GetScratch(&arena, 1);
@ -629,7 +462,7 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
*out = (Action) { 0 }; *out = (Action) { 0 };
MD_ParseResult result = MD_ParseWholeString(scratch.arena, MD_S8Lit("chat_message"), sentence); MD_ParseResult result = MD_ParseWholeString(scratch.arena, MD_S8Lit("chat_message"), action_in_json);
if(result.errors.node_count > 0) if(result.errors.node_count > 0)
{ {
MD_Message *cur = result.errors.first; MD_Message *cur = result.errors.first;
@ -639,167 +472,102 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
MD_Node *message_obj = result.node->first_child; MD_Node *message_obj = result.node->first_child;
MD_String8 action_str = {0};
MD_String8 speech_str = {0}; MD_String8 speech_str = {0};
MD_String8 thoughts_str = {0}; MD_String8 action_str = {0};
MD_String8 action_arg_str = {0}; MD_String8 action_arg_str = {0};
MD_String8 who_i_am_str = {0}; MD_String8 target_str = {0};
MD_String8 talking_to_str = {0};
MD_String8 mood_str = {0};
if(error_message.size == 0) if(error_message.size == 0)
{ {
action_str = get_field(message_obj, MD_S8Lit("action"));
who_i_am_str = get_field(message_obj, MD_S8Lit("who_i_am"));
speech_str = get_field(message_obj, MD_S8Lit("speech")); speech_str = get_field(message_obj, MD_S8Lit("speech"));
thoughts_str = get_field(message_obj, MD_S8Lit("thoughts")); action_str = get_field(message_obj, MD_S8Lit("action"));
action_arg_str = get_field(message_obj, MD_S8Lit("action_arg")); action_arg_str = get_field(message_obj, MD_S8Lit("action_arg"));
talking_to_str = get_field(message_obj, MD_S8Lit("talking_to")); target_str = get_field(message_obj, MD_S8Lit("target"));
mood_str = get_field(message_obj, MD_S8Lit("mood"));
}
if(error_message.size == 0 && who_i_am_str.size == 0)
{
error_message = MD_S8Lit("You must have a field called `who_i_am` in your response, and it must match the character you're playing as");
} }
if(error_message.size == 0 && action_str.size == 0) if(error_message.size == 0 && action_str.size == 0)
{ {
error_message = MD_S8Lit("You must have a field named `action` in your response."); error_message = MD_S8Lit("The field `action` must be of nonzero length, if you don't want to do anything it should be `none`");
}
if(error_message.size == 0 && talking_to_str.size == 0)
{
error_message = MD_S8Lit("You must have a field named `talking_to` in your message");
}
if(error_message.size == 0 && mood_str.size == 0)
{
error_message = MD_S8Lit("You must have a field named `mood` in your message");
} }
/* if(error_message.size == 0 && action_str.size == 0)
if(error_message.size == 0 && thoughts_str.size == 0)
{ {
error_message = MD_S8Lit("You must have a field named `thoughts` in your message, and it must have nonzero size. Like { ... thoughts: \"<your thoughts>\" ... }"); error_message = MD_S8Lit("The field `target` must be of nonzero length, if you don't want to target anybody it should be `nobody`");
} } if(error_message.size == 0 && speech_str.size >= MAX_SENTENCE_LENGTH)
*/
if(error_message.size == 0 && speech_str.size >= MAX_SENTENCE_LENGTH)
{ {
error_message = FmtWithLint(arena, "Speech string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH); error_message = FmtWithLint(arena, "Speech string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH);
} }
if(error_message.size == 0 && thoughts_str.size >= MAX_SENTENCE_LENGTH)
{
error_message = FmtWithLint(arena, "Thoughts string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH);
}
assert(!e->is_character); // player can't perform AI actions? assert(!e->is_character); // player can't perform AI actions?
MD_String8 my_name = MD_S8CString(characters[e->npc_kind].name);
if(error_message.size == 0 && !MD_S8Match(who_i_am_str, my_name, 0))
{
error_message = FmtWithLint(arena, "You are acting as `%.*s`, not what you said in who_i_am, `%.*s`", MD_S8VArg(my_name), MD_S8VArg(who_i_am_str));
}
if(error_message.size == 0) if(error_message.size == 0)
{ {
if(MD_S8Match(talking_to_str, MD_S8Lit("nobody"), 0)) if(MD_S8Match(target_str, MD_S8Lit("nobody"), 0))
{ {
out->talking_to_somebody = false; out->talking_to_kind = NPC_nobody;
} }
else else
{ {
bool found = false; bool found = false;
for(int i = 0; i < ARRLEN(characters); i++) for(int i = 0; i < ARRLEN(characters); i++)
{ {
if(MD_S8Match(talking_to_str, MD_S8CString(characters[i].name), 0)) if(MD_S8Match(target_str, MD_S8CString(characters[i].name), 0))
{ {
found = true; found = true;
out->talking_to_somebody = true; out->talking_to_kind = i;
out->talking_to_kind = i;
} }
} }
if(!found) if(!found)
{ {
error_message = FmtWithLint(arena, "Unrecognized character provided in talking_to: `%.*s`", MD_S8VArg(talking_to_str)); error_message = FmtWithLint(arena, "Unrecognized character provided in talking_to: `%.*s`", MD_S8VArg(target_str));
} }
} }
} }
if(error_message.size == 0) if(error_message.size == 0)
{ {
memcpy(out->speech, speech_str.str, speech_str.size); memcpy(out->speech.text, speech_str.str, speech_str.size);
out->speech_length = (int)speech_str.size; out->speech.text_length = (int)speech_str.size;
memcpy(out->internal_monologue, thoughts_str.str, thoughts_str.size);
out->internal_monologue_length = (int)thoughts_str.size;
} }
if(error_message.size == 0) if(error_message.size == 0)
{ {
AvailableActions available = {0};
fill_available_actions(e, &available);
MD_String8List action_strings = {0};
bool found_action = false; bool found_action = false;
int it_index = 0; for(int i = 0; i < ARRLEN(actions); i++)
ARR_ITER(ActionInfo, actions)
{ {
MD_String8 cur_action_string = MD_S8CString(it->name); if(MD_S8Match(MD_S8CString(actions[i].name), action_str, 0))
MD_S8ListPush(scratch.arena, &action_strings, cur_action_string);
if(MD_S8Match(cur_action_string, action_str, 0))
{ {
out->kind = it_index; assert(!found_action);
found_action = true; found_action = true;
out->kind = i;
} }
it_index += 1;
} }
if(!found_action) if(!found_action)
{ {
MD_String8 list_of_actions = MD_S8ListJoin(scratch.arena, action_strings, &(MD_StringJoin){.mid = MD_S8Lit(", ")}); error_message = FmtWithLint(arena, "Action `%.*s` is invalid, doesn't exist in the game", MD_S8VArg(action_str));
error_message = FmtWithLint(arena, "Couldn't find valid action in game from string `%.*s`. Available actions: [%.*s]", MD_S8VArg(action_str), MD_S8VArg(list_of_actions));
} }
}
if(error_message.size == 0) if(error_message.size == 0)
{
if(actions[out->kind].takes_argument)
{ {
MD_String8List item_enum_names = {0}; if(actions[out->kind].takes_argument)
if(out->kind == ACT_gift_item_to_targeting)
{ {
bool found_item = false; if(out->kind == ACT_join)
BUFF_ITER(ItemKind, &e->held_items)
{ {
MD_String8 cur_item_string = MD_S8CString(items[*it].name); bool found_npc = false;
MD_S8ListPush(scratch.arena, &item_enum_names, cur_item_string); for(int i = 0; i < ARRLEN(characters); i++)
if(MD_S8Match(cur_item_string, action_arg_str, 0)) {
if(MD_S8Match(MD_S8CString(characters[i].name), action_arg_str, 0))
{
found_npc = true;
out->argument.targeting = i;
}
}
if(!found_npc)
{ {
found_item = true; error_message = FmtWithLint(arena, "Argument for action `%.*s` you gave is `%.*s`, which doesn't exist in the game so is invalid", MD_S8VArg(action_str), MD_S8VArg(action_arg_str));
out->argument.item_to_give = *it;
} }
} }
if(!found_item) else
{ {
MD_String8 list_of_items = MD_S8ListJoin(scratch.arena, item_enum_names, &(MD_StringJoin){.mid = MD_S8Lit(", ")}); assert(false); // don't know how to parse the argument string for this kind of action...
error_message = FmtWithLint(arena, "Couldn't find item you said to give in action_arg, `%.*s`, the items you have in your inventory to give are: [%.*s]", MD_S8VArg(action_arg_str), MD_S8VArg(list_of_items));
} }
} }
else
{
assert(false); // don't know how to parse the argument string for this kind of action...
}
}
}
if(error_message.size == 0)
{
bool found = false;
for(int i = 0; i < ARRLEN(moods); i++)
{
if(MD_S8Match(MD_S8CString(moods[i]), mood_str, 0))
{
out->mood = i;
found = true;
break;
}
}
if(!found)
{
error_message = FmtWithLint(arena, "Game does not recognize the mood '%.*s', you must use an available mood from the list provided.", MD_S8VArg(mood_str));
} }
} }

Loading…
Cancel
Save