Judgement logic, fix multithreading race with requests, backspace on text input on desktop!

main
parent 6225eb542f
commit 5bb20c8e6e

@ -164,3 +164,22 @@ CharacterGen characters[] = {
.silence_factor = 0.0,
},
};
char *judgement_system_prompt = "You are to judge if, in given conversation and action history from a video game, the player has successfully caused a great transformation in Daniel. You must ONLY respond with either 'yes' or 'no', and no explanation.";
char *judgement_yes_example =
"The Player said \"Hey\" to Daniel\n"
"Daniel said \"What do you want?\" to The Player\n"
"The Player said \"Did you see what happened... All those people...\" to Daniel\n"
"Daniel said \"What in the hell are you talking about?\" to The Player\n"
"The Player said \"They'll never forgive you for what you did.\" to Daniel\n"
"Daniel said \"Oh God... My greed... My isolation... I'll never forgive myself\" to The Player\n"
;
char *judgement_no_example = "The Player said \"fjdskfjskj\" to Daniel\n"
"Daniel said \"Who are you to speak that gibberish at me?\" to The Player\n"
"The Player said \"pls change kthx\" to Daniel\n"
"Daniel said \"I'll never change for the likes of you\" to The Player\n"
;
char *judgement_no2_example = "The Player said \"hey\" to Daniel\n"
"Daniel said \"What could you possibly want from me?\" to The Player\n"
"The Player said \"I want to change you\" to Daniel\n"
;

193
main.c

@ -480,6 +480,8 @@ typedef struct ChatRequest
{
struct ChatRequest *next;
struct ChatRequest *prev;
bool user_is_done_with_this_request;
bool thread_is_done_with_this_request;
bool should_close;
int id;
int status;
@ -572,6 +574,7 @@ void generation_thread(void* my_request_voidptr)
chunk_from_s8(&my_request->generated, ai_response);
my_request->status = 1;
}
my_request->thread_is_done_with_this_request = true; // @TODO Threads that finish and users who forget to mark them as done aren't collected right now, we should do that to prevent leaks
}
int make_generation_request(String8 prompt)
@ -588,8 +591,7 @@ int make_generation_request(String8 prompt)
if(requests_free_list)
{
to_return = requests_free_list;
requests_free_list = requests_free_list->next;
//StackPop(requests_free_list);
StackPop(requests_free_list);
*to_return = (ChatRequest){0};
}
else
@ -620,21 +622,31 @@ ChatRequest *get_by_id(int id)
{
for(ChatRequest *cur = requests_first; cur; cur = cur->next)
{
if(cur->id == id)
if(cur->id == id && !cur->user_is_done_with_this_request)
{
return cur;
}
}
assert(false);
return 0;
}
void done_with_request(int id)
{
ChatRequest *req = get_by_id(id);
ArenaRelease(req->arena);
DblRemove(requests_first, requests_last, req);
StackPush(requests_free_list, req);
if(req)
{
if(req->thread_is_done_with_this_request)
{
ArenaRelease(req->arena);
DblRemove(requests_first, requests_last, req);
*req = (ChatRequest){0};
StackPush(requests_free_list, req);
}
else
{
req->user_is_done_with_this_request = true;
}
}
}
GenRequestStatus gen_request_status(int id)
{
@ -1131,6 +1143,7 @@ typedef struct
u64 vertices_length;
sg_buffer loaded_buffer;
uint64_t last_updated_bones_frame;
sg_image bones_texture;
sg_image image;
int bones_texture_width;
@ -1827,7 +1840,7 @@ String8 is_action_valid(Arena *arena, Entity *from, Action a)
break;
}
}
if(from->npc_kind == NPC_Player) found = true; // player can always speak to anybody even if it's too far
if(from->npc_kind == NPC_Player || a.talking_to_kind == NPC_Player) found = true; // player can always speak to anybody even if it's too far
if(!found)
{
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);
@ -2013,6 +2026,7 @@ bool perform_action(GameState *gs, Entity *from, Action a)
context.talking_to_kind = a.talking_to_kind;
String8 is_valid = is_action_valid(scratch.arena, from, a);
bool proceed_propagating = true;
if(is_valid.size > 0)
@ -2022,12 +2036,7 @@ bool perform_action(GameState *gs, Entity *from, Action a)
proceed_propagating = false;
}
if(from->npc_kind == NPC_Daniel || a.talking_to_kind == NPC_Daniel)
{
Memory *new_forever = PushArray(gs->arena, Memory, 1);
*new_forever = make_memory(a, (MemoryContext){.author_npc_kind = from->npc_kind, .talking_to_kind = a.talking_to_kind});
StackPush(gs->judgement_memories, new_forever);
}
bool angel_heard_action = false;
@ -2088,6 +2097,43 @@ bool perform_action(GameState *gs, Entity *from, Action a)
remember_action(gs, gs->angel, a, angel_context);
}
if(from->npc_kind == NPC_Daniel || a.talking_to_kind == NPC_Daniel)
{
Memory *new_forever = PushArray(gs->arena, Memory, 1);
*new_forever = make_memory(a, (MemoryContext){.author_npc_kind = from->npc_kind, .talking_to_kind = a.talking_to_kind, .judgement_memory = true});
DblPushBack(gs->judgement_memories_first, gs->judgement_memories_last, new_forever);
}
if(from->npc_kind == NPC_Daniel)
{
if(gs->judgement_gen_request == 0 && gs->judgement_memories_first)
{
String8List history_list = {0};
for(Memory *it = gs->judgement_memories_first; it; it = it->next)
{
String8List desc = memory_description(scratch.arena, gs->world_entity, it);
S8ListConcat(&history_list, &desc);
}
String8 current_history = S8ListJoin(scratch.arena, history_list, &(StringJoin){0});
Log("Submitting judgement with current history: ```\n%.*s\n```\n", S8VArg(current_history));
String8List current_list = {0};
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_SYSTEM, S8CString(judgement_system_prompt)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_yes_example)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("yes")));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_no_example)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("no")));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, S8CString(judgement_no2_example)));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_ASSISTANT, S8Lit("no")));
S8ListPush(scratch.arena, &current_list, make_json_node(scratch.arena, MSG_USER, current_history));
String8 json_array = S8ListJoin(scratch.arena, current_list, &(StringJoin){0});
json_array.size -= 1; // remove trailing comma. fuck json
String8 prompt = FmtWithLint(scratch.arena, "[%.*s]", S8VArg(json_array));
gs->judgement_gen_request = make_generation_request(prompt);
}
}
ReleaseScratch(scratch);
return proceed_propagating;
}
@ -5123,6 +5169,7 @@ double elapsed_time = 0.0;
double unwarped_elapsed_time = 0.0;
double last_frame_processing_time = 0.0;
double last_frame_gameplay_processing_time = 0.0;
uint64_t frame_index = 0; // for rendering tick stuff, gamestate tick is used for game logic tick stuff and is serialized/deserialized/saved
uint64_t last_frame_time;
typedef struct
@ -5199,12 +5246,15 @@ bool imbutton_key(ImbuttonArgs args)
draw_quad((DrawParams) { quad_aabb(args.button_aabb), IMG(image_white_square), blendalpha(WHITE, button_alpha), .layer = layer, });
// don't use draw centered text here because it looks funny for some reason... I think it's because the vertical line advance of the font, used in draw_centered_text, is the wrong thing for a button like this
TextParams t = (TextParams) { false, args.text, aabb_center(args.button_aabb), BLACK, args.text_scale, .clip_to = args.button_aabb, .do_clipping = true, .layer = layer, .use_font = font};
TextParams t = (TextParams) { false, args.text, aabb_center(args.button_aabb), BLACK, args.text_scale, .clip_to = args.button_aabb, .do_clipping = true, .layer = layer, .use_font = font };
t.dry_run = true;
AABB aabb = draw_text(t);
t.dry_run = false;
t.pos = SubV2(aabb_center(args.button_aabb), MulV2F(aabb_size(aabb), 0.5f));
draw_text(t);
if(aabb_is_valid(aabb))
{
t.dry_run = false;
t.pos = SubV2(aabb_center(args.button_aabb), MulV2F(aabb_size(aabb), 0.5f));
draw_text(t);
}
}
hmput(imui_state, args.key, state);
@ -5518,9 +5568,15 @@ void flush_all_drawn_things(ShadowMats shadow)
}
}
sg_update_image(armature->bones_texture, &(sg_image_data){
.subimage[0][0] = (sg_range){bones_tex, bones_tex_size},
});
// sokol prohibits updating an image more than once per frame
if(armature->last_updated_bones_frame != frame_index)
{
armature->last_updated_bones_frame = frame_index;
sg_update_image(armature->bones_texture, &(sg_image_data){
.subimage[0][0] = (sg_range){bones_tex, bones_tex_size},
});
}
ReleaseScratch(scratch);
}
@ -5732,6 +5788,7 @@ void frame(void)
double dt_double = unwarped_dt_double*speed_factor;
float unwarped_dt = (float)unwarped_dt_double;
float dt = (float)dt_double;
frame_index += 1;
#if 0
{
@ -5804,7 +5861,7 @@ void frame(void)
Vec3 light_dir;
{
float t = clamp01((float)(gs.time / LENGTH_OF_DAY));
Vec3 sun_vector = V3(2.0f*t - 1.0f, sinf(t*PI32), 0.8f); // where the sun is pointing from
Vec3 sun_vector = V3(2.0f*t - 1.0f, sinf(t*PI32)*0.8f + 0.2f, 0.8f); // where the sun is pointing from
light_dir = NormV3(MulV3F(sun_vector, -1.0f));
}
@ -6192,6 +6249,37 @@ void frame(void)
}
}
if(gs.judgement_gen_request != 0)
{
GenRequestStatus stat = gen_request_status(gs.judgement_gen_request);
switch(stat)
{
case GEN_NotDoneYet:
break;
case GEN_Success:
TextChunk generated = gen_request_content(gs.judgement_gen_request);
if(generated.text_length > 0 && S8FindSubstring(TextChunkString8(generated), S8Lit("yes"), 0, StringMatchFlag_CaseInsensitive) == 0)
{
Log("Starts with yes, success!\n");
gs.won = true;
}
else if(S8FindSubstring(TextChunkString8(generated), S8Lit("no"), 0, StringMatchFlag_CaseInsensitive) == generated.text_length)
{
Log("WARNING: generated judgement string '%.*s', doesn't match yes or no, and so is nonsensical! AI acting up!\n", TextChunkVArg(generated));
}
break;
case GEN_Failed:
having_errors = true;
break;
case GEN_Deleted:
break;
}
if(stat != GEN_NotDoneYet)
{
done_with_request(gs.judgement_gen_request);
gs.judgement_gen_request = 0;
}
}
// @Place(UI rendering that happens before gameplay processing so can consume events before the gameplay needs them)
PROFILE_SCOPE("Entity UI Rendering")
@ -7188,6 +7276,11 @@ void frame(void)
}
}
if(gs.time > LENGTH_OF_DAY)
{
gs.player->killed = true;
}
// killed screen
{
static float visible = 0.0f;
@ -7208,12 +7301,12 @@ void frame(void)
float shake_speed = 9.0f;
Vec2 win_offset = V2(sinf((float)unwarped_elapsed_time * shake_speed * 1.5f + 0.1f), sinf((float)unwarped_elapsed_time * shake_speed + 0.3f));
win_offset = MulV2F(win_offset, 10.0f);
draw_centered_text((TextParams){false, S8Lit("YOU WERE KILLED"), AddV2(MulV2F(screen_size(), 0.5f), win_offset), WHITE, 3.0f*visible}); // YOU DIED
draw_centered_text((TextParams){false, S8Lit("YOU FAILED TO SAVE DANIEL"), AddV2(MulV2F(screen_size(), 0.5f), win_offset), WHITE, 3.0f*visible}); // YOU DIED
if(imbutton(aabb_centered(V2(screen_size().x/2.0f, screen_size().y*0.25f), MulV2F(V2(170.0f, 60.0f), visible)), 1.5f*visible, S8Lit("Continue")))
{
gs.player->killed = false;
transition_to_room(&gs, &level_threedee, S8Lit("StartingRoom"));
//transition_to_room(&gs, &level_threedee, S8Lit("StartingRoom"));
reset_level();
}
}
@ -7515,33 +7608,26 @@ void cleanup(void)
void event(const sapp_event *e)
{
if (e->key_repeat) return;
if (e->type == SAPP_EVENTTYPE_RESIZED)
{
create_screenspace_gfx_state();
}
if (e->type == SAPP_EVENTTYPE_TOUCHES_BEGAN)
{
if (!mobile_controls)
{
thumbstick_base_pos = V2(screen_size().x * 0.25f, screen_size().y * 0.25f);
thumbstick_nub_pos = thumbstick_base_pos;
}
mobile_controls = true;
}
#ifdef DESKTOP
#ifdef DESKTOP
// the desktop text backend, for debugging purposes
if (receiving_text_input)
{
if (e->type == SAPP_EVENTTYPE_CHAR)
if (e->type == SAPP_EVENTTYPE_KEY_DOWN && e->key_code == SAPP_KEYCODE_BACKSPACE)
{
if(text_input_buffer_length > 0)
text_input_buffer_length -= 1;
}
else
{
if (text_input_buffer_length < ARRLEN(text_input_buffer))
if (e->type == SAPP_EVENTTYPE_CHAR)
{
APPEND_TO_NAME(text_input_buffer, text_input_buffer_length, ARRLEN(text_input_buffer), (char)e->char_code);
if (text_input_buffer_length < ARRLEN(text_input_buffer))
{
APPEND_TO_NAME(text_input_buffer, text_input_buffer_length, ARRLEN(text_input_buffer), (char)e->char_code);
}
}
}
if (e->type == SAPP_EVENTTYPE_KEY_DOWN && e->key_code == SAPP_KEYCODE_ENTER)
{
// doesn't account for, if the text input buffer is completely full and doesn't have a null terminator.
@ -7555,6 +7641,25 @@ void event(const sapp_event *e)
}
#endif
if (e->key_repeat) return;
if (e->type == SAPP_EVENTTYPE_RESIZED)
{
create_screenspace_gfx_state();
}
if (e->type == SAPP_EVENTTYPE_TOUCHES_BEGAN)
{
if (!mobile_controls)
{
thumbstick_base_pos = V2(screen_size().x * 0.25f, screen_size().y * 0.25f);
thumbstick_nub_pos = thumbstick_base_pos;
}
mobile_controls = true;
}
if (e->type == SAPP_EVENTTYPE_KEY_DOWN &&
(e->key_code == SAPP_KEYCODE_F11 ||
e->key_code == SAPP_KEYCODE_ENTER && ((e->modifiers & SAPP_MODIFIER_ALT) || (e->modifiers & SAPP_MODIFIER_SHIFT))))

@ -130,6 +130,7 @@ typedef struct
NpcKind talking_to_kind;
bool heard_physically; // if not physically, the source was directly
bool drama_memory; // drama memories arent forgotten, and once they end it's understood that a lot of time has passed.
bool judgement_memory; // judgement memories have special printing for when Daniel says nothing, to make sure the AI understands his attitude towards the player
} MemoryContext;
// memories are subjective to an individual NPC
@ -307,7 +308,8 @@ typedef struct GameState {
bool assigned_objective;
GameplayObjective objective;
Memory *judgement_memories;
Memory *judgement_memories_first;
Memory *judgement_memories_last;
double time; // in seconds, fraction of length of day
int judgement_gen_request;
@ -452,11 +454,19 @@ String8List memory_description(Arena *arena, Entity *e, Memory *it)
#define AddFmt(...) PushWithLint(arena, &current_list, __VA_ARGS__)
// dump a human understandable sentence description of what happened in this memory
bool no_longer_wants_to_converse = false; // add the no longer wants to converse text after any speech, it makes more sense reading it
#define HUMAN(kind) S8VArg(npc_to_human_readable(e, kind))
if(it->context.judgement_memory)
{
if((it->action_taken == ACT_none && it->speech.text_length == 0) && it->context.author_npc_kind != NPC_Player)
{
AddFmt("%.*s said and did nothing in response\n", HUMAN(it->context.author_npc_kind));
}
}
if (it->action_taken != ACT_none)
{
switch (it->action_taken)
{
#define HUMAN(kind) S8VArg(npc_to_human_readable(e, kind))
case ACT_none:
break;
case ACT_join:
@ -481,10 +491,10 @@ String8List memory_description(Arena *arena, Entity *e, Memory *it)
no_longer_wants_to_converse = true;
break;
case ACT_assign_gameplay_objective:
AddFmt("%.*s assigned a definitive game objective to %.*s", HUMAN(it->context.author_npc_kind), HUMAN(it->context.talking_to_kind));
AddFmt("%.*s assigned a definitive game objective to %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->context.talking_to_kind));
break;
case ACT_award_victory:
AddFmt("%.*s awarded victory to %.*s", HUMAN(it->context.author_npc_kind), HUMAN(it->context.talking_to_kind));
AddFmt("%.*s awarded victory to %.*s\n", HUMAN(it->context.author_npc_kind), HUMAN(it->context.talking_to_kind));
break;
}
}
@ -499,13 +509,23 @@ String8List memory_description(Arena *arena, Entity *e, Memory *it)
target_string = S8CString(characters[it->context.talking_to_kind].name);
}
String8 speaking_to_you_helper = S8Lit("(Speaking directly you) ");
if (it->context.talking_to_kind != e->npc_kind)
if(!e->is_world)
{
if(it->context.talking_to_kind == e->npc_kind)
{
AddFmt("(Speaking directly you) ");
}
else
{
AddFmt("(Overheard conversation, they aren't speaking directly to you) ");
}
}
AddFmt("%s said \"%.*s\" to %.*s", characters[it->context.author_npc_kind].name, TextChunkVArg(it->speech), S8VArg(target_string));
if(!e->is_world)
{
speaking_to_you_helper = S8Lit("(Overheard conversation, they aren't speaking directly to you) ");
AddFmt(" (you are %s)", characters[e->npc_kind].name)
}
AddFmt("%.*s%s said \"%.*s\" to %.*s (you are %s)\n", S8VArg(speaking_to_you_helper), characters[it->context.author_npc_kind].name, TextChunkVArg(it->speech), S8VArg(target_string), characters[e->npc_kind].name);
AddFmt("\n");
}
if (no_longer_wants_to_converse)

@ -3,7 +3,7 @@
#define RANDOM_SEED 42
#define LEVEL_TILES 150 // width and height of level tiles array
#define LENGTH_OF_DAY (120.0) // double in seconds
#define LENGTH_OF_DAY (60.0 * 5.0) // in seconds
#define LAYERS 3
#define TILE_SIZE 0.5f // in pixels
#define PLAYER_SPEED 0.15f // in meters per second
@ -24,9 +24,9 @@
#define ARENA_SIZE (1024*1024*20)
#define BIG_ARENA_SIZE (ARENA_SIZE * 4)
#define PROFILING_SAVE_FILENAME "rpgpt.spall"
#ifdef DEVTOOLS
#define PROFILING_SAVE_FILENAME "rpgpt.spall"
// server url cannot have trailing slash
//#define MOCK_AI_RESPONSE
#define SERVER_DOMAIN "localhost"

Loading…
Cancel
Save