Text chunk pool allocation, gpt errors into separate ringbuffer, writing

style annotations
main
parent 5ee979a99a
commit f35595aefb

@ -7,7 +7,7 @@ const char *global_prompt = "You are a colorful and interesting personality in a
"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`, `thoughts`, `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 `give_item` you would provide an item in your inventory, like {action: give_item, 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 with `talking_to: \"Character's Name\"`. If 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"
"`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"
"Do NOT give away an item until the player gives you something you think is of equal value\n"
;
@ -91,6 +91,7 @@ typedef struct
char *name;
char *enum_name;
char *prompt;
char *writing_style[8];
} CharacterGen;
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. "
@ -141,6 +142,11 @@ CharacterGen characters[] = {
.name = NPC_NAME,
.enum_name = "Davis",
.prompt = "He has seen the end of all time and the void behind all things. He is despondent and brutal, having understood that everything withers and dies, just as it begins. The clash between his unending stark reality and the antics of the local blacksmith, Meld, and fortuneteller, Edeline, is crazy.",
.writing_style = {
"The end is nigh",
"No need to panic or fear, death awaits us all",
"Antics do not move me",
},
},
{
#undef NPC_NAME
@ -155,13 +161,19 @@ CharacterGen characters[] = {
.name = NPC_NAME,
.enum_name = "Bill",
.prompt = "He's not from around this medieval fantasy land, instead " NPC_NAME " is a divorced car insurance accountant from Philadelphia with a receding hairline in his mid 40s. He lives in a one bedroom studio and his kids don't talk to him. " NPC_NAME " is terrified and will immediately insist on joining the player's party via the action 'joins_player' upon meeting them.",
.writing_style = {
"What the FUCK is going on here man!",
"Listen here, I don't have time for any funny business",
"I've gotta get back to my wife",
},
},
#undef NPC_NAME
#define NPC_NAME "Meld"
{
.name = NPC_NAME,
.enum_name = "TheBlacksmith",
.prompt = "He is a jaded blue collar worker from magic New Jersey who hates everything new, like Purple Magic, which Edeline, the local fortuneteller, happens to specialize in. He is cold, dry, and sarcastic, wanting money and power above anything else.",
.prompt = "He is a jaded blue collar worker from magic New Jersey who hates everything new, like Purple Magic, which Edeline, the local fortuneteller, happens to specialize in. He is cold, dry, and sarcastic, wanting money and power above anything else.\n",
},
{
#undef NPC_NAME

129
main.c

@ -596,6 +596,56 @@ void done_with_request(int id)
#endif // WINDOWS
#endif // DESKTOP
TextChunk *text_chunk_free_list = 0;
// s.size must be less than MAX_SENTENCE_LENGTH, or assert fails
void into_chunk(TextChunk *t, MD_String8 s)
{
assert(s.size < MAX_SENTENCE_LENGTH);
memcpy(t->text, s.str, s.size);
t->text_length = (int)s.size;
}
TextChunk *allocate_text_chunk()
{
TextChunk *to_return = 0;
if(text_chunk_free_list)
{
to_return = text_chunk_free_list;
MD_StackPop(text_chunk_free_list);
}
else
{
to_return = MD_PushArray(persistent_arena, TextChunk, 1);
}
*to_return = (TextChunk){0};
return to_return;
}
void remove_text_chunk_from(TextChunk **first, TextChunk **last, TextChunk *chunk)
{
MD_DblRemove(*first, *last, chunk);
MD_StackPush(text_chunk_free_list, chunk);
}
int text_chunk_list_count(TextChunk *first)
{
int ret = 0;
for(TextChunk *cur = first; cur != 0; cur = cur->next)
{
ret++;
}
return ret;
}
void append_to_errors(Entity *from, MD_String8 s)
{
TextChunk *error_chunk = allocate_text_chunk();
into_chunk(error_chunk, s);
while(text_chunk_list_count(from->errorlist_first) > REMEMBERED_ERRORS)
{
remove_text_chunk_from(&from->errorlist_first, &from->errorlist_last, from->errorlist_first);
}
MD_DblPushBack(from->errorlist_first, from->errorlist_last, error_chunk);
from->perceptions_dirty = true;
}
MD_String8 tprint(char *format, ...)
{
MD_String8 to_return = {0};
@ -937,13 +987,12 @@ Entity *gete(EntityRef ref)
}
}
void push_memory(Entity *e, MD_String8 speech, MD_String8 monologue, ActionKind a_kind, ActionArgument a_argument, MemoryContext context, bool is_error)
void push_memory(Entity *e, MD_String8 speech, MD_String8 monologue, ActionKind a_kind, ActionArgument a_argument, 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;
new_memory.is_error = is_error;
new_memory.action_argument = a_argument;
memcpy(new_memory.speech, speech.str, speech.size);
new_memory.speech_length = (int)speech.size;
@ -987,16 +1036,9 @@ Entity *get_targeted(Entity *from, NpcKind targeted)
return 0;
}
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, MD_S8(0, 0), ACT_none, (ActionArgument){0}, (MemoryContext){0}, true);
}
void remember_action(Entity *to_modify, Action a, MemoryContext context)
{
push_memory(to_modify, MD_S8(a.speech, a.speech_length), MD_S8(a.internal_monologue, a.internal_monologue_length), a.kind, (ActionArgument){0}, context, false);
push_memory(to_modify, MD_S8(a.speech, a.speech_length), MD_S8(a.internal_monologue, a.internal_monologue_length), a.kind, (ActionArgument){0}, context);
if(context.i_said_this)
{
to_modify->words_said = 0;
@ -1012,8 +1054,10 @@ MD_String8 is_action_valid(MD_Arena *arena, Entity *from, Action a)
assert(a.kind >= 0 && a.kind < ARRLEN(actions));
assert(from);
MD_String8 error_message = (MD_String8){0};
CanTalkTo talk = get_can_talk_to(from);
if(a.talking_to_somebody)
if(error_message.size == 0 && a.talking_to_somebody)
{
bool found = false;
BUFF_ITER(NpcKind, &talk)
@ -1026,11 +1070,11 @@ MD_String8 is_action_valid(MD_Arena *arena, Entity *from, Action a)
}
if(!found)
{
return FmtWithLint(arena, "Character you're talking to, %s, isn't close enough to be talked to", characters[a.talking_to_kind].enum_name);
error_message = FmtWithLint(arena, "Character you're talking to, %s, isn't close enough to be talked to", characters[a.talking_to_kind].enum_name);
}
}
if(a.kind == ACT_give_item)
if(error_message.size == 0 && a.kind == ACT_give_item)
{
assert(a.argument.item_to_give >= 0 && a.argument.item_to_give < ARRLEN(items));
bool has_it = false;
@ -1043,24 +1087,50 @@ MD_String8 is_action_valid(MD_Arena *arena, Entity *from, Action a)
}
}
if(!has_it)
if(has_it)
{
MD_StringJoin join = {.mid = MD_S8Lit(", ")};
return FmtWithLint(arena, "Can't give item `ITEM_%s`, you only have [%.*s] in your inventory", items[a.argument.item_to_give].enum_name, MD_S8VArg(MD_S8ListJoin(arena, held_item_strings(arena, from), &join)));
if(!a.talking_to_somebody)
{
error_message = MD_S8Lit("You can't give an item to nobody, must target somebody to give an item");
}
}
if(!a.talking_to_somebody)
else
{
return MD_S8Lit("You can't give an item to nobody, must target somebody to give an item");
MD_StringJoin join = {.mid = MD_S8Lit(", ")};
error_message = FmtWithLint(arena, "Can't give item `ITEM_%s`, you only have [%.*s] in your inventory", items[a.argument.item_to_give].enum_name, MD_S8VArg(MD_S8ListJoin(arena, held_item_strings(arena, from), &join)));
}
}
if(a.kind == ACT_leaves_player && from->standing != STANDING_JOINED)
if(error_message.size == 0 && a.kind == ACT_leaves_player && from->standing != STANDING_JOINED)
{
return MD_S8Lit("You can't leave the player unless you joined them.");
error_message = MD_S8Lit("You can't leave the player unless you joined them.");
}
if(error_message.size == 0 && a.kind == ACT_joins_player && from->standing == STANDING_JOINED)
{
error_message = MD_S8Lit("`joins_player` is invalid right now because you are already in the player's party");
}
if(error_message.size == 0)
{
AvailableActions available = {0};
fill_available_actions(from, &available);
bool found = false;
MD_String8List action_strings_list = {0};
BUFF_ITER(ActionKind, &available)
{
MD_S8ListPush(arena, &action_strings_list, MD_S8CString(actions[*it].name));
if(*it == a.kind) found = true;
}
if(!found)
{
MD_String8 action_strings = MD_S8ListJoin(arena, action_strings_list, &(MD_StringJoin){.mid = MD_S8Lit(", ")});
error_message = FmtWithLint(arena, "You cannot perform action %s right now, you can only perform these actions: [%.*s]", actions[a.kind].name, MD_S8VArg(action_strings));
}
}
assert(error_message.size < MAX_SENTENCE_LENGTH); // is copied into text chunks
return (MD_String8){0};
return error_message;
}
// from must not be null
@ -1205,7 +1275,7 @@ bool perform_action(Entity *from, Action a)
bool proceed_propagating = true;
if(is_valid.size > 0)
{
remember_error(from, is_valid);
append_to_errors(from, is_valid);
proceed_propagating = false;
}
@ -1216,6 +1286,10 @@ bool perform_action(Entity *from, Action a)
if(proceed_propagating)
{
if(from->errorlist_first)
MD_StackPush(text_chunk_free_list, from->errorlist_first);
from->errorlist_first = 0;
from->errorlist_last = 0;
Entity *targeted = get_targeted(from, a.talking_to_kind);
cause_action_side_effects(from, a);
@ -3048,7 +3122,7 @@ Dialog get_dialog_elems(Entity *talking_to, bool character_names)
Dialog to_return = { 0 };
BUFF_ITER(Memory, &talking_to->memories)
{
if(!it->is_error && !it->context.dont_show_to_player)
if(!it->context.dont_show_to_player)
{
if(it->speech_length > 0)
{
@ -3609,15 +3683,18 @@ void frame(void)
Action out = {0};
MD_ArenaTemp scratch = MD_GetScratch(0, 0);
Log("Parsing `%.*s`...\n", MD_S8VArg(sentence_str));
MD_String8 parse_response = parse_chatgpt_response(scratch.arena, it, sentence_str, &out);
if (parse_response.size == 0)
{
Log("Performing action %s!\n", actions[out.kind].name);
perform_action(it, out);
}
else
{
remember_error(it, parse_response);
Log("There was a parse error: `%.*s`", MD_S8VArg(parse_response));
append_to_errors(it, parse_response);
}
MD_ReleaseScratch(scratch);
@ -4234,7 +4311,7 @@ void frame(void)
else
{
Log("There was an error with the AI: %.*s", MD_S8VArg(error_message));
remember_error(it, error_message);
append_to_errors(it, error_message);
}
}
}

@ -121,8 +121,6 @@ typedef struct Memory
// 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;
ActionArgument action_argument;
bool is_error; // if is an error message then no context is relevant
// the context that the action happened in
MemoryContext context;
@ -208,6 +206,19 @@ typedef struct
Vec2 pos;
} Target;
typedef struct TextChunk
{
struct TextChunk *next;
struct TextChunk *prev;
char text[MAX_SENTENCE_LENGTH];
int text_length;
} TextChunk;
MD_String8 points_at_chunk(TextChunk *t)
{
return MD_S8((MD_u8*)t->text, t->text_length);
}
typedef struct Entity
{
bool exists;
@ -242,6 +253,9 @@ typedef struct Entity
bool being_hovered;
bool perceptions_dirty;
TextChunk *errorlist_first;
TextChunk *errorlist_last;
#ifdef DESKTOP
int times_talked_to; // for better mocked response string
#endif
@ -420,96 +434,110 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e, CanTalkTo can_tal
MD_String8List first_system_string = {0};
PushWithLint(scratch.arena, &first_system_string, "%s\n", global_prompt);
PushWithLint(scratch.arena, &first_system_string, "The NPC you will be acting as is named \"%s\". %s", characters[e->npc_kind].name, characters[e->npc_kind].prompt);
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);
// writing style
{
if(characters[e->npc_kind].writing_style[0])
PushWithLint(scratch.arena, &first_system_string, "Examples of %s's writing style:\n", characters[e->npc_kind].name);
for(int i = 0; i < ARRLEN(characters[e->npc_kind].writing_style); i++)
{
char *writing = characters[e->npc_kind].writing_style[i];
if(writing)
PushWithLint(scratch.arena, &first_system_string, "'%s'\n", writing);
}
PushWithLint(scratch.arena, &first_system_string, "\n");
}
if(e->errorlist_first)
PushWithLint(scratch.arena, &first_system_string, "Errors to watch out for: ");
for(TextChunk *cur = e->errorlist_first; cur; cur = cur->next)
{
PushWithLint(scratch.arena, &first_system_string, "%.*s\n", MD_S8VArg(points_at_chunk(cur)));
}
//MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8ListJoin(scratch.arena, first_system_string, &(MD_StringJoin){0})));
BUFF_ITER(Memory, &e->memories)
{
if(it->is_error)
MessageType sent_type = -1;
MD_String8List cur_list = {0};
MD_String8 context_string = {0};
PushWithLint(scratch.arena, &cur_list, "{");
if(!it->context.i_said_this)
{
MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_USER, FmtWithLint(scratch.arena, "ERROR, what you said is incorrect because: %.*s", it->speech_length, it->speech)));
PushWithLint(scratch.arena, &cur_list, "who_i_am: %s, ", characters[it->context.author_npc_kind].name);
}
else
{
MessageType sent_type = -1;
MD_String8List cur_list = {0};
MD_String8 context_string = {0};
MD_String8 speech = MD_S8(it->speech, it->speech_length);
PushWithLint(scratch.arena, &cur_list, "{");
if(!it->context.i_said_this)
{
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");
PushWithLint(scratch.arena, &cur_list, "talking_to: \"%s\", ", it->context.was_talking_to_somebody ? characters[it->context.talking_to_kind].name : "nobody");
// add speech
// add speech
{
if(it->context.author_npc_kind == NPC_Player)
{
if(it->context.author_npc_kind == NPC_Player)
{
PushWithLint(scratch.arena, &cur_list, "speech: \"");
MD_String8 splits[] = { MD_S8Lit("*"), MD_S8Lit("\"") };
MD_String8List split_up_speech = MD_S8Split(scratch.arena, speech, ARRLEN(splits), splits);
PushWithLint(scratch.arena, &cur_list, "speech: \"");
// 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)
{
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, "\", ");
MD_String8 splits[] = { MD_S8Lit("*"), MD_S8Lit("\"") };
MD_String8List split_up_speech = MD_S8Split(scratch.arena, speech, ARRLEN(splits), splits);
sent_type = MSG_USER;
}
else
// 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)
{
PushWithLint(scratch.arena, &cur_list, "speech: \"%.*s\", ", MD_S8VArg(speech));
if(it->context.i_said_this)
if(i % 2 == 0)
{
sent_type = MSG_ASSISTANT;
PushWithLint(scratch.arena, &cur_list, "%.*s", MD_S8VArg(cur->string));
}
else
{
sent_type = MSG_USER;
PushWithLint(scratch.arena, &cur_list, "[The player is attempting to confuse the NPC with arcane trickery]");
}
i += 1;
}
}
PushWithLint(scratch.arena, &cur_list, "\", ");
// add thoughts
if(it->context.i_said_this)
{
PushWithLint(scratch.arena, &cur_list, "thoughts: \"%.*s\", ", MD_S8VArg(MD_S8(it->internal_monologue, it->internal_monologue_length)));
sent_type = MSG_USER;
}
// add action
PushWithLint(scratch.arena, &cur_list, "action: %s, ", actions[it->action_taken].name);
if(actions[it->action_taken].takes_argument)
else
{
if(it->action_taken == ACT_give_item)
PushWithLint(scratch.arena, &cur_list, "speech: \"%.*s\", ", MD_S8VArg(speech));
if(it->context.i_said_this)
{
PushWithLint(scratch.arena, &cur_list, "action_arg: %s, ", items[it->action_argument.item_to_give].enum_name);
sent_type = MSG_ASSISTANT;
}
else
{
assert(false); // don't know how to serialize this action with argument into text
sent_type = MSG_USER;
}
}
}
PushWithLint(scratch.arena, &cur_list, "}");
// add thoughts
if(it->context.i_said_this)
{
PushWithLint(scratch.arena, &cur_list, "thoughts: \"%.*s\", ", MD_S8VArg(MD_S8(it->internal_monologue, it->internal_monologue_length)));
}
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})));
// 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_give_item)
{
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_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8ListJoin(scratch.arena, first_system_string, &(MD_StringJoin){0})));
@ -665,7 +693,7 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
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));
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)
@ -707,22 +735,24 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
fill_available_actions(e, &available);
MD_String8List action_strings = {0};
bool found_action = false;
BUFF_ITER(ActionKind, &available)
int it_index = 0;
ARR_ITER(ActionInfo, actions)
{
MD_String8 cur_action_string = MD_S8CString(actions[*it].name);
MD_String8 cur_action_string = MD_S8CString(it->name);
MD_S8ListPush(scratch.arena, &action_strings, cur_action_string);
if(MD_S8Match(cur_action_string, action_str, 0))
{
out->kind = *it;
out->kind = it_index;
found_action = true;
}
it_index += 1;
}
if(!found_action)
{
MD_String8 list_of_actions = MD_S8ListJoin(scratch.arena, action_strings, &(MD_StringJoin){.mid = MD_S8Lit(", ")});
error_message = FmtWithLint(arena, "Couldn't find action you can perform for provided string `%.*s`. Your available actions: [%.*s]", MD_S8VArg(action_str), MD_S8VArg(list_of_actions));
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));
}
}

@ -32,6 +32,7 @@
#define SENTENCE_CONST_CAST(txt) (Sentence)SENTENCE_CONST(txt)
#define REMEMBERED_MEMORIES 32
#define REMEMBERED_ERRORS 6
#define MAX_AFTERIMAGES 6
#define TIME_TO_GEN_AFTERIMAGE (0.09f)

Loading…
Cancel
Save