1#include <getopt.h>
2#include <stdio.h>
3#include <stdlib.h>
4#include <string.h>
5#include <strings.h>
6
7#define TB_IMPL
8#include "termbox2.h"
9
10#define NONSTD_IMPLEMENTATION
11#include "nonstd.h"
12
13#include "llama.h"
14#include "models.h"
15#include "vectordb.h"
16#include "maps.h"
17
18#define MIN_W 40
19#define MIN_H 12
20#define SIDEBAR_W 40
21#define CP_H 0x2500
22#define CP_V 0x2502
23#define CP_TL 0x250c
24#define CP_TR 0x2510
25#define CP_BL 0x2514
26#define CP_BR 0x2518
27#define MAP_FLOOR_CH '.'
28#define MAP_BORDER_MIN 0x2500
29#define MAP_BORDER_MAX 0x257f
30#define MAP_FLOOR_FG 234
31#define COLOR_WHITE_256 0x0f
32#define COLOR_RED_256 161
33#define COLOR_GREEN_256 0x2e
34#define COLOR_BORDER_256 101
35#define COLOR_CYAN_256 0x33
36#define COLOR_ORANGE_256 0xd0
37#define COLOR_BLUE_256 0x1b
38
39typedef struct {
40 char key;
41 const char *name;
42} InventoryItem;
43
44typedef struct {
45 array(InventoryItem) items;
46} Inventory;
47
48typedef struct {
49 int x;
50 int y;
51 int hp;
52 int hp_max;
53 int ac;
54 int str;
55 int gold;
56 Inventory inventory;
57} Player;
58
59#define DIALOG_HISTORY_MAX 16
60
61typedef struct {
62 char prompt[128];
63 char response[256];
64} DialogEntry;
65
66typedef struct {
67 int open;
68 char input[128];
69 int input_len;
70 int npc_index;
71 const char *npc_name;
72 DialogEntry entries[DIALOG_HISTORY_MAX];
73 int entry_count;
74} Dialog;
75
76typedef struct {
77 const ModelConfig *model_cfg;
78 struct llama_model *model;
79 struct llama_model *embed_model;
80 struct llama_context *embed_ctx;
81 VectorDB *npc_dbs;
82 int *npc_db_loaded;
83 int verbose;
84} GameRuntime;
85
86static void llama_log_callback(enum ggml_log_level level, const char *text, void *user_data) {
87 (void)level;
88 (void)user_data;
89 (void)text;
90}
91
92static int clamp(int value, int min, int max);
93
94static void show_help(const char *prog) {
95 printf("Usage: %s [OPTIONS]\n", prog);
96 printf("Options:\n");
97 printf(" -m, --model <name> Specify model to use (default: first model)\n");
98 printf(" -e, --embed-model <name> Specify model to use for embeddings\n");
99 printf(" -v, --verbose Enable verbose logging\n");
100 printf(" -h, --help Show this help message\n");
101}
102
103static void draw_border(int x, int y, int w, int h, uintattr_t fg) {
104 int ix;
105 int iy;
106
107 for (ix = 0; ix < w; ix++) {
108 tb_set_cell(x + ix, y, CP_H, fg, TB_DEFAULT);
109 tb_set_cell(x + ix, y + h - 1, CP_H, fg, TB_DEFAULT);
110 }
111 for (iy = 0; iy < h; iy++) {
112 tb_set_cell(x, y + iy, CP_V, fg, TB_DEFAULT);
113 tb_set_cell(x + w - 1, y + iy, CP_V, fg, TB_DEFAULT);
114 }
115
116 tb_set_cell(x, y, CP_TL, fg, TB_DEFAULT);
117 tb_set_cell(x + w - 1, y, CP_TR, fg, TB_DEFAULT);
118 tb_set_cell(x, y + h - 1, CP_BL, fg, TB_DEFAULT);
119 tb_set_cell(x + w - 1, y + h - 1, CP_BR, fg, TB_DEFAULT);
120}
121
122static void draw_border_bg(int x, int y, int w, int h, uintattr_t fg,
123 uintattr_t bg) {
124 int ix;
125 int iy;
126
127 for (ix = 0; ix < w; ix++) {
128 tb_set_cell(x + ix, y, CP_H, fg, bg);
129 tb_set_cell(x + ix, y + h - 1, CP_H, fg, bg);
130 }
131 for (iy = 0; iy < h; iy++) {
132 tb_set_cell(x, y + iy, CP_V, fg, bg);
133 tb_set_cell(x + w - 1, y + iy, CP_V, fg, bg);
134 }
135
136 tb_set_cell(x, y, CP_TL, fg, bg);
137 tb_set_cell(x + w - 1, y, CP_TR, fg, bg);
138 tb_set_cell(x, y + h - 1, CP_BL, fg, bg);
139 tb_set_cell(x + w - 1, y + h - 1, CP_BR, fg, bg);
140}
141
142static void get_layout(int w, int h, int *map_x, int *map_y, int *map_w,
143 int *map_h, int *side_x, int *side_y, int *side_w, int *side_h,
144 int *msg1_y, int *msg2_y) {
145 *map_x = 0;
146 *map_y = 0;
147 *map_w = w - SIDEBAR_W;
148 *map_h = h - 2;
149 *side_x = w - SIDEBAR_W;
150 *side_y = 0;
151 *side_w = SIDEBAR_W;
152 *side_h = h - 2;
153 *msg1_y = h - 2;
154 *msg2_y = h - 1;
155}
156
157static void map_init(Map *map, const unsigned char *data, int len) {
158 array(int) line_lengths;
159 int width = 0;
160 int height = 0;
161 int line_len = 0;
162 int i = 0;
163
164 map->data = data;
165 map->len = len;
166 map->cells = NULL;
167 array_init(line_lengths);
168 while (i < len) {
169 uint32_t ch = 0;
170 int consumed = tb_utf8_char_to_unicode(&ch, (const char *)&data[i]);
171 if (consumed <= 0) {
172 i++;
173 continue;
174 }
175 i += consumed;
176 if (ch == '\n') {
177 array_push(line_lengths, line_len);
178 if (line_len > width) {
179 width = line_len;
180 }
181 height++;
182 line_len = 0;
183 } else {
184 line_len++;
185 }
186 }
187 if (line_len > 0 || (len > 0 && data[len - 1] != '\n')) {
188 array_push(line_lengths, line_len);
189 if (line_len > width) {
190 width = line_len;
191 }
192 height++;
193 }
194
195 map->width = width;
196 map->height = height;
197 if (width > 0 && height > 0) {
198 map->cells = ALLOC(u32, (usize)width * (usize)height);
199 for (int y = 0; y < height; y++) {
200 for (int x = 0; x < width; x++) {
201 map->cells[(y * width) + x] = ' ';
202 }
203 }
204 }
205
206 i = 0;
207 int x = 0;
208 int y = 0;
209 while (i < len && y < height) {
210 uint32_t ch = 0;
211 int consumed = tb_utf8_char_to_unicode(&ch, (const char *)&data[i]);
212 if (consumed <= 0) {
213 i++;
214 continue;
215 }
216 i += consumed;
217 if (ch == '\n') {
218 y++;
219 x = 0;
220 continue;
221 }
222 if (map->cells && x < width) {
223 map->cells[(y * width) + x] = ch;
224 }
225 x++;
226 }
227 array_free(line_lengths);
228}
229
230static u32 map_get(const Map *map, int x, int y) {
231 if (!map->cells || x < 0 || y < 0 || x >= map->width || y >= map->height) {
232 return ' ';
233 }
234 return map->cells[(y * map->width) + x];
235}
236
237static void map_set(Map *map, int x, int y, u32 ch) {
238 if (!map->cells || x < 0 || y < 0 || x >= map->width || y >= map->height) {
239 return;
240 }
241 map->cells[(y * map->width) + x] = ch;
242}
243
244static int map_is_walkable(const Map *map, int x, int y) {
245 u32 ch = map_get(map, x, y);
246 return ch == MAP_FLOOR_CH || ch == '$' || ch == 'N'
247 || (ch >= '0' && ch <= '9');
248}
249
250static int npc_index_from_tile(u32 ch) {
251 if (ch >= '0' && ch <= '9') {
252 return (int)(ch - '0');
253 }
254 return -1;
255}
256
257static void map_free(Map *map) {
258 FREE(map->cells);
259}
260
261static void update_camera(const Map *map, int view_w, int view_h,
262 const Player *player, int *cam_x, int *cam_y) {
263 int max_cam_x;
264 int max_cam_y;
265 int margin_x;
266 int margin_y;
267 int next_x = *cam_x;
268 int next_y = *cam_y;
269
270 if (view_w <= 0 || view_h <= 0 || map->width <= 0 || map->height <= 0) {
271 *cam_x = 0;
272 *cam_y = 0;
273 return;
274 }
275
276 margin_x = view_w > 8 ? 3 : view_w / 3;
277 margin_y = view_h > 8 ? 3 : view_h / 3;
278 max_cam_x = map->width - view_w;
279 max_cam_y = map->height - view_h;
280 if (max_cam_x < 0) {
281 max_cam_x = 0;
282 }
283 if (max_cam_y < 0) {
284 max_cam_y = 0;
285 }
286
287 if (player->x < next_x + margin_x) {
288 next_x = player->x - margin_x;
289 } else if (player->x > next_x + view_w - 1 - margin_x) {
290 next_x = player->x - (view_w - 1 - margin_x);
291 }
292 if (player->y < next_y + margin_y) {
293 next_y = player->y - margin_y;
294 } else if (player->y > next_y + view_h - 1 - margin_y) {
295 next_y = player->y - (view_h - 1 - margin_y);
296 }
297
298 *cam_x = clamp(next_x, 0, max_cam_x);
299 *cam_y = clamp(next_y, 0, max_cam_y);
300}
301
302static void draw_map(const Map *map, int map_x, int map_y, int view_w,
303 int view_h, const Player *player, int cam_x, int cam_y) {
304 int ix;
305 int iy;
306
307 for (iy = 0; iy < view_h; iy++) {
308 for (ix = 0; ix < view_w; ix++) {
309 int mx = cam_x + ix;
310 int my = cam_y + iy;
311 u32 ch = map_get(map, mx, my);
312 u32 draw_ch = (ch >= '0' && ch <= '9') ? 'N' : ch;
313 uintattr_t fg = COLOR_WHITE_256;
314 if (ch == MAP_FLOOR_CH) {
315 fg = MAP_FLOOR_FG;
316 } else if (ch == '~') {
317 fg = COLOR_BLUE_256;
318 } else if (ch == '$') {
319 fg = COLOR_ORANGE_256;
320 } else if (ch == 'B' || ch == 'S' || ch == 'G') {
321 fg = COLOR_RED_256;
322 } else if (ch == 'N' || (ch >= '0' && ch <= '9')) {
323 fg = COLOR_CYAN_256;
324 } else if (ch >= MAP_BORDER_MIN && ch <= MAP_BORDER_MAX) {
325 fg = COLOR_BORDER_256;
326 }
327 tb_set_cell(map_x + ix, map_y + iy, draw_ch, fg, TB_DEFAULT);
328 }
329 }
330
331 if (player->x >= cam_x && player->x < cam_x + view_w && player->y >= cam_y
332 && player->y < cam_y + view_h) {
333 int sx = map_x + (player->x - cam_x);
334 int sy = map_y + (player->y - cam_y);
335 tb_set_cell(sx, sy, '@', COLOR_GREEN_256 | TB_BOLD, TB_DEFAULT);
336 }
337}
338
339static void draw_progress_bar(int x, int y, int w, int value, int max) {
340 int filled;
341 int ix;
342 int inner_w = w - 2;
343
344 if (w < 4) {
345 return;
346 }
347 if (max <= 0) {
348 max = 1;
349 }
350 if (value < 0) {
351 value = 0;
352 }
353 if (value > max) {
354 value = max;
355 }
356
357 filled = (inner_w * value) / max;
358 tb_set_cell(x, y, '[', COLOR_WHITE_256, TB_DEFAULT);
359 for (ix = 0; ix < inner_w; ix++) {
360 uintattr_t fg = ix < filled ? COLOR_GREEN_256 : COLOR_WHITE_256;
361 uint32_t ch = ix < filled ? '=' : ' ';
362 tb_set_cell(x + 1 + ix, y, ch, fg, TB_DEFAULT);
363 }
364 tb_set_cell(x + w - 1, y, ']', COLOR_WHITE_256, TB_DEFAULT);
365}
366
367static void inventory_init(Inventory *inv) {
368 array_init(inv->items);
369}
370
371static void inventory_add(Inventory *inv, char key, const char *name) {
372 InventoryItem item = {.key = key, .name = name};
373 array_push(inv->items, item);
374}
375
376static void inventory_free(Inventory *inv) {
377 array_free(inv->items);
378}
379
380static void player_init(Player *player) {
381 player->x = 6;
382 player->y = 4;
383 player->hp = 12;
384 player->hp_max = 12;
385 player->ac = 7;
386 player->str = 16;
387 player->gold = 42;
388 inventory_init(&player->inventory);
389 inventory_add(&player->inventory, 'a', "dagger");
390 inventory_add(&player->inventory, 'b', "ration");
391 inventory_add(&player->inventory, 'c', "potion");
392 inventory_add(&player->inventory, 'd', "scroll");
393}
394
395static void player_free(Player *player) {
396 inventory_free(&player->inventory);
397}
398
399static void draw_stats(int x, int y, const Player *player) {
400 tb_print(x, y, COLOR_WHITE_256 | TB_BOLD, TB_DEFAULT, "Stats");
401 tb_printf(x, y + 2, COLOR_WHITE_256, TB_DEFAULT, "HP %d/%d", player->hp, player->hp_max);
402 draw_progress_bar(x, y + 3, 18, player->hp, player->hp_max);
403 tb_printf(x, y + 4, COLOR_WHITE_256, TB_DEFAULT, "AC: %d", player->ac);
404 tb_printf(x, y + 5, COLOR_WHITE_256, TB_DEFAULT, "Str: %d", player->str);
405 tb_printf(x, y + 6, COLOR_WHITE_256, TB_DEFAULT, "Gold: %d", player->gold);
406}
407
408static void draw_inventory(int x, int y, const Inventory *inv) {
409 InventoryItem item;
410 usize idx = 0;
411
412 tb_print(x, y, COLOR_WHITE_256 | TB_BOLD, TB_DEFAULT, "Inventory");
413 array_foreach(inv->items, item) {
414 tb_printf(x, y + 2 + (int)idx, COLOR_WHITE_256, TB_DEFAULT, "%c) %s", item.key, item.name);
415 idx++;
416 }
417}
418
419static const char *status_msg = "";
420
421static void update_status(const char *message) {
422 status_msg = message ? message : "";
423}
424
425static int draw_wrapped(int x, int y, int max_lines, int box_w, uintattr_t fg,
426 uintattr_t bg, const char *prefix, const char *text) {
427 if (max_lines <= 0 || box_w <= 0 || text == NULL) {
428 return 0;
429 }
430 int lines = 0;
431 int prefix_len = prefix ? (int)strlen(prefix) : 0;
432 if (prefix_len < 0) {
433 prefix_len = 0;
434 }
435 int avail = box_w - 4 - prefix_len;
436 if (avail < 1) {
437 return 0;
438 }
439 char pad[64];
440 int pad_len = prefix_len < (int)sizeof(pad) - 1 ? prefix_len : (int)sizeof(pad) - 1;
441 for (int i = 0; i < pad_len; i++) {
442 pad[i] = ' ';
443 }
444 pad[pad_len] = '\0';
445 const char *p = text;
446 while (*p != '\0' && lines < max_lines) {
447 while (*p == ' ') {
448 p++;
449 }
450 int line_len = 0;
451 int last_space = -1;
452 for (int i = 0; i < avail && p[i] != '\0'; i++) {
453 if (p[i] == '\n') {
454 line_len = i;
455 break;
456 }
457 if (p[i] == ' ') {
458 last_space = i;
459 }
460 line_len = i + 1;
461 }
462 if (line_len == 0) {
463 break;
464 }
465 int cut = line_len;
466 if (cut == avail && p[cut] != '\0' && last_space > 0) {
467 cut = last_space;
468 }
469 char buf[512];
470 int copy_len = cut < (int)sizeof(buf) - 1 ? cut : (int)sizeof(buf) - 1;
471 memcpy(buf, p, (size_t)copy_len);
472 buf[copy_len] = '\0';
473 while (copy_len > 0 && buf[copy_len - 1] == ' ') {
474 buf[copy_len - 1] = '\0';
475 copy_len--;
476 }
477 const char *line_prefix = (lines == 0) ? (prefix ? prefix : "") : pad;
478 tb_printf(x, y + lines, fg, bg, "%s%s", line_prefix, buf);
479 lines++;
480 p += cut;
481 if (*p == '\n') {
482 p++;
483 }
484 }
485 return lines;
486}
487
488static int count_wrapped_lines(int box_w, const char *prefix, const char *text) {
489 if (box_w <= 0 || text == NULL) {
490 return 0;
491 }
492 int prefix_len = prefix ? (int)strlen(prefix) : 0;
493 if (prefix_len < 0) {
494 prefix_len = 0;
495 }
496 int avail = box_w - 4 - prefix_len;
497 if (avail < 1) {
498 return 0;
499 }
500 int lines = 0;
501 const char *p = text;
502 while (*p != '\0') {
503 while (*p == ' ') {
504 p++;
505 }
506 int line_len = 0;
507 int last_space = -1;
508 for (int i = 0; i < avail && p[i] != '\0'; i++) {
509 if (p[i] == '\n') {
510 line_len = i;
511 break;
512 }
513 if (p[i] == ' ') {
514 last_space = i;
515 }
516 line_len = i + 1;
517 }
518 if (line_len == 0) {
519 break;
520 }
521 int cut = line_len;
522 if (cut == avail && p[cut] != '\0' && last_space > 0) {
523 cut = last_space;
524 }
525 lines++;
526 p += cut;
527 if (*p == '\n') {
528 p++;
529 }
530 }
531 return lines;
532}
533
534static void dialog_open(Dialog *dialog, int npc_index, const char *npc_name) {
535 dialog->open = 1;
536 dialog->input_len = 0;
537 dialog->input[0] = '\0';
538 dialog->npc_index = npc_index;
539 dialog->npc_name = npc_name;
540}
541
542static void dialog_close(Dialog *dialog) {
543 dialog->open = 0;
544 dialog->npc_index = -1;
545 dialog->npc_name = NULL;
546}
547
548static void dialog_append(Dialog *dialog, uint32_t ch) {
549 if (ch < 32 || ch > 126) {
550 return;
551 }
552 if (dialog->input_len >= (int)(sizeof(dialog->input) - 1)) {
553 return;
554 }
555 dialog->input[dialog->input_len++] = (char)ch;
556 dialog->input[dialog->input_len] = '\0';
557}
558
559static void dialog_backspace(Dialog *dialog) {
560 if (dialog->input_len <= 0) {
561 return;
562 }
563 dialog->input_len--;
564 dialog->input[dialog->input_len] = '\0';
565}
566
567static void trim_leading(char **text) {
568 while (**text == ' ' || **text == '\t' || **text == '\n' || **text == '\r') {
569 (*text)++;
570 }
571}
572
573static void trim_leading_punct(char **text) {
574 while (**text == '"' || **text == '\'' || **text == '`') {
575 (*text)++;
576 trim_leading(text);
577 }
578}
579
580static void trim_trailing(char *text) {
581 size_t len = strlen(text);
582 while (len > 0) {
583 char ch = text[len - 1];
584 if (ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r') {
585 break;
586 }
587 text[len - 1] = '\0';
588 len--;
589 }
590}
591
592static void strip_any_prefix(char **text, const char *prefix) {
593 if (strncasecmp(*text, prefix, strlen(prefix)) == 0) {
594 *text += strlen(prefix);
595 trim_leading(text);
596 }
597}
598
599
600static char *sanitize_reply(char *reply, const char *name) {
601 if (reply == NULL) {
602 return NULL;
603 }
604 char *start = reply;
605 trim_leading(&start);
606 trim_leading_punct(&start);
607 strip_any_prefix(&start, "Answer:");
608 strip_any_prefix(&start, "NPC:");
609 strip_any_prefix(&start, "Context:");
610 strip_any_prefix(&start, "System:");
611 if (strncmp(start, "<context>", 9) == 0) {
612 start += 9;
613 trim_leading(&start);
614 }
615 char *reminder = strstr(start, "<system-reminder>");
616 if (reminder) {
617 *reminder = '\0';
618 }
619 char *system_tag = strstr(start, "<system");
620 if (system_tag) {
621 *system_tag = '\0';
622 }
623 char *tag = strstr(start, "<|");
624 if (tag) {
625 *tag = '\0';
626 }
627 char *eos = strstr(start, "</s>");
628 if (eos) {
629 *eos = '\0';
630 }
631 char *hash = strstr(start, "###");
632 if (hash) {
633 *hash = '\0';
634 }
635 if (name && name[0] != '\0') {
636 size_t name_len = strlen(name);
637 for (;;) {
638 if (strncasecmp(start, name, name_len) != 0) {
639 break;
640 }
641 start += name_len;
642 while (*start == ':' || *start == '-' || *start == ',') {
643 start++;
644 }
645 trim_leading(&start);
646 trim_leading_punct(&start);
647 }
648 }
649 if (start != reply) {
650 memmove(reply, start, strlen(start) + 1);
651 }
652 trim_trailing(reply);
653 return reply;
654}
655
656static int find_substr_offset(const char *buf, int n, const char *needle) {
657 int needle_len = (int)strlen(needle);
658 if (needle_len <= 0 || n <= 0 || needle_len > n) {
659 return -1;
660 }
661 for (int i = 0; i + needle_len <= n; i++) {
662 int match = 1;
663 for (int j = 0; j < needle_len; j++) {
664 if (buf[i + j] != needle[j]) {
665 match = 0;
666 break;
667 }
668 }
669 if (match) {
670 return i;
671 }
672 }
673 return -1;
674}
675
676static int find_stop_offset(const char *buf, int n) {
677 int stop_at = n;
678 for (int i = 0; i < n; i++) {
679 if (buf[i] == '\n') {
680 stop_at = i;
681 break;
682 }
683 }
684 int off = find_substr_offset(buf, n, "</s>");
685 if (off >= 0 && off < stop_at) {
686 stop_at = off;
687 }
688 off = find_substr_offset(buf, n, "<system-reminder>");
689 if (off >= 0 && off < stop_at) {
690 stop_at = off;
691 }
692 off = find_substr_offset(buf, n, "<system");
693 if (off >= 0 && off < stop_at) {
694 stop_at = off;
695 }
696 off = find_substr_offset(buf, n, "<|");
697 if (off >= 0 && off < stop_at) {
698 stop_at = off;
699 }
700 off = find_substr_offset(buf, n, "###");
701 if (off >= 0 && off < stop_at) {
702 stop_at = off;
703 }
704 off = find_substr_offset(buf, n, "System:");
705 if (off >= 0 && off < stop_at) {
706 stop_at = off;
707 }
708 off = find_substr_offset(buf, n, "User:");
709 if (off >= 0 && off < stop_at) {
710 stop_at = off;
711 }
712 off = find_substr_offset(buf, n, "Assistant:");
713 if (off >= 0 && off < stop_at) {
714 stop_at = off;
715 }
716 return stop_at;
717}
718
719static void append_prompt_context(stringb *sb, const char *npc_name, const char *context,
720 const char *question) {
721 sb_append_cstr(sb, "Context:\n");
722 if (npc_name && npc_name[0] != '\0') {
723 sb_append_cstr(sb, "NPC Name: ");
724 sb_append_cstr(sb, npc_name);
725 sb_append_cstr(sb, "\n");
726 }
727 if (context && context[0] != '\0') {
728 sb_append_cstr(sb, context);
729 }
730 sb_append_cstr(sb, "\nQuestion:\n");
731 sb_append_cstr(sb, question ? question : "");
732}
733
734static char *build_prompt(const ModelConfig *cfg, const char *system, const char *npc_name,
735 const char *context, const char *question) {
736 stringb full = {0};
737 sb_init(&full, 0);
738
739 switch (cfg->prompt_style) {
740 case PROMPT_STYLE_T5:
741 sb_append_cstr(&full, "instruction: ");
742 sb_append_cstr(&full, system ? system : "");
743 sb_append_cstr(&full, "\nquestion: ");
744 sb_append_cstr(&full, question ? question : "");
745 sb_append_cstr(&full, "\ncontext:\n");
746 if (npc_name && npc_name[0] != '\0') {
747 sb_append_cstr(&full, "NPC Name: ");
748 sb_append_cstr(&full, npc_name);
749 sb_append_cstr(&full, "\n");
750 }
751 if (context && context[0] != '\0') {
752 sb_append_cstr(&full, context);
753 }
754 sb_append_cstr(&full, "\nanswer:");
755 break;
756 case PROMPT_STYLE_CHAT:
757 sb_append_cstr(&full, "System:\n");
758 sb_append_cstr(&full, system ? system : "");
759 sb_append_cstr(&full, "\nUser:\n");
760 append_prompt_context(&full, npc_name, context, question);
761 sb_append_cstr(&full, "\nAssistant:");
762 break;
763 case PROMPT_STYLE_PLAIN:
764 default:
765 sb_append_cstr(&full, "System:\n");
766 sb_append_cstr(&full, system ? system : "");
767 sb_append_cstr(&full, "\n");
768 append_prompt_context(&full, npc_name, context, question);
769 sb_append_cstr(&full, "\nAnswer:");
770 break;
771 }
772
773 return full.data;
774}
775
776static char *generate_npc_reply(const GameRuntime *runtime, const GameMap *game_map,
777 int npc_index, const char *prompt) {
778 if (runtime == NULL || prompt == NULL) {
779 return NULL;
780 }
781 const char *fallback = "Demo reply: The old ruins are north of here.";
782 const char *npc_name = NULL;
783 if (game_map && npc_index >= 0 && npc_index < 10) {
784 const char *npc_reply = game_map->npcs[npc_index].reply;
785 npc_name = game_map->npcs[npc_index].name;
786 if (npc_reply && npc_reply[0] != '\0') {
787 fallback = npc_reply;
788 }
789 }
790
791 if (runtime->model == NULL || runtime->model_cfg == NULL || runtime->embed_ctx == NULL
792 || runtime->npc_dbs == NULL || runtime->npc_db_loaded == NULL) {
793 return strdup(fallback);
794 }
795 if (npc_index < 0 || npc_index >= 10 || runtime->npc_db_loaded[npc_index] == 0) {
796 return strdup(fallback);
797 }
798
799 VectorDB *db = &runtime->npc_dbs[npc_index];
800 float query[VDB_EMBED_SIZE];
801 int results[5];
802 for (int i = 0; i < 5; i++) {
803 results[i] = -1;
804 }
805 vdb_embed_query(db, prompt, query);
806 vdb_search(db, query, 5, results);
807
808 size_t context_cap = 1024;
809 size_t context_len = 0;
810 char *context = (char *)malloc(context_cap);
811 if (context == NULL) {
812 return strdup(fallback);
813 }
814 context[0] = '\0';
815 if (runtime->verbose) {
816 fprintf(stderr, "[npc] question: %s\n", prompt);
817 }
818 for (int i = 0; i < 5; i++) {
819 if (results[i] < 0) {
820 continue;
821 }
822 const char *text = db->docs[results[i]].text;
823 if (runtime->verbose) {
824 fprintf(stderr, "[npc] context[%d]: %s\n", i, text);
825 }
826 char header[32];
827 int header_len = snprintf(header, sizeof(header), "Snippet %d:\n", i + 1);
828 size_t text_len = strlen(text);
829 size_t need = context_len + (size_t)header_len + text_len + 2;
830 if (need > context_cap) {
831 while (need > context_cap) {
832 context_cap *= 2;
833 }
834 char *next = (char *)realloc(context, context_cap);
835 if (next == NULL) {
836 free(context);
837 return strdup(fallback);
838 }
839 context = next;
840 }
841 if (header_len > 0) {
842 memcpy(context + context_len, header, (size_t)header_len);
843 context_len += (size_t)header_len;
844 }
845 memcpy(context + context_len, text, text_len);
846 context_len += text_len;
847 context[context_len++] = '\n';
848 context[context_len] = '\0';
849 }
850
851 const char *system_prompt = "You are a helpful NPC. Speak in first person. "
852 "Use only the provided context. If the context does not contain the answer, say \"I don't know.\" "
853 "If asked your name, answer with the NPC Name from the context. "
854 "Do not mention context, system messages, or prompts. Reply with one short sentence.";
855
856 char *full_prompt = build_prompt(runtime->model_cfg, system_prompt, npc_name, context, prompt);
857 if (full_prompt == NULL) {
858 free(context);
859 return strdup(fallback);
860 }
861 free(context);
862
863 if (runtime->verbose) {
864 printf(">> %s\n", full_prompt);
865 }
866
867 const struct llama_vocab *vocab = llama_model_get_vocab(runtime->model);
868 int n_prompt = -llama_tokenize(vocab, full_prompt, strlen(full_prompt), NULL, 0, true, true);
869 llama_token *prompt_tokens = (llama_token *)malloc((size_t)n_prompt * sizeof(llama_token));
870 if (prompt_tokens == NULL) {
871 free(full_prompt);
872 return strdup(fallback);
873 }
874 if (llama_tokenize(vocab, full_prompt, strlen(full_prompt), prompt_tokens, n_prompt, true, true) < 0) {
875 free(full_prompt);
876 free(prompt_tokens);
877 return strdup(fallback);
878 }
879
880 struct llama_context_params ctx_params = llama_context_default_params();
881 ctx_params.n_ctx = runtime->model_cfg->n_ctx;
882 ctx_params.n_batch = runtime->model_cfg->n_batch;
883 ctx_params.embeddings = false;
884
885 struct llama_context *ctx = llama_init_from_model(runtime->model, ctx_params);
886 if (ctx == NULL) {
887 free(full_prompt);
888 free(prompt_tokens);
889 return strdup(fallback);
890 }
891
892 struct llama_sampler_chain_params sparams = llama_sampler_chain_default_params();
893 struct llama_sampler *smpl = llama_sampler_chain_init(sparams);
894 if (runtime->model_cfg->top_k > 0) {
895 llama_sampler_chain_add(smpl, llama_sampler_init_top_k(runtime->model_cfg->top_k));
896 }
897 if (runtime->model_cfg->top_p > 0.0f && runtime->model_cfg->top_p < 1.0f) {
898 llama_sampler_chain_add(smpl, llama_sampler_init_top_p(runtime->model_cfg->top_p, 1));
899 }
900 if (runtime->model_cfg->min_p > 0.0f) {
901 llama_sampler_chain_add(smpl, llama_sampler_init_min_p(runtime->model_cfg->min_p, 1));
902 }
903 llama_sampler_chain_add(smpl, llama_sampler_init_penalties(
904 runtime->model_cfg->repeat_last_n,
905 runtime->model_cfg->repeat_penalty,
906 runtime->model_cfg->freq_penalty,
907 runtime->model_cfg->presence_penalty));
908 llama_sampler_chain_add(smpl, llama_sampler_init_temp(runtime->model_cfg->temperature));
909 llama_sampler_chain_add(smpl, llama_sampler_init_dist(runtime->model_cfg->seed));
910
911 struct llama_batch batch = llama_batch_get_one(prompt_tokens, n_prompt);
912
913 if (llama_model_has_encoder(runtime->model)) {
914 if (llama_encode(ctx, batch)) {
915 llama_sampler_free(smpl);
916 free(full_prompt);
917 free(prompt_tokens);
918 llama_free(ctx);
919 return strdup(fallback);
920 }
921 llama_token decoder_start = llama_model_decoder_start_token(runtime->model);
922 if (decoder_start == LLAMA_TOKEN_NULL) {
923 decoder_start = llama_vocab_bos(vocab);
924 }
925 batch = llama_batch_get_one(&decoder_start, 1);
926 }
927
928 int n_pos = 0;
929 llama_token new_token_id;
930 size_t out_cap = 256;
931 size_t out_len = 0;
932 char *out = (char *)malloc(out_cap);
933 if (out == NULL) {
934 llama_sampler_free(smpl);
935 free(full_prompt);
936 free(prompt_tokens);
937 llama_free(ctx);
938 return strdup(fallback);
939 }
940 out[0] = '\0';
941 int n_predict = runtime->model_cfg->n_predict > 0 ? runtime->model_cfg->n_predict : 64;
942 if (n_predict > 64) {
943 n_predict = 64;
944 }
945 while (n_pos + batch.n_tokens < n_prompt + n_predict) {
946 if (llama_decode(ctx, batch)) {
947 break;
948 }
949 n_pos += batch.n_tokens;
950 new_token_id = llama_sampler_sample(smpl, ctx, -1);
951 if (llama_vocab_is_eog(vocab, new_token_id)) {
952 break;
953 }
954 char buf[128];
955 int n = llama_token_to_piece(vocab, new_token_id, buf, sizeof(buf), 0, true);
956 if (n < 0) {
957 break;
958 }
959 int stop_at = find_stop_offset(buf, n);
960 if (out_len == 0 && stop_at == 0 && n > 0 && buf[0] == '\n') {
961 batch = llama_batch_get_one(&new_token_id, 1);
962 continue;
963 }
964 if (out_len + (size_t)stop_at + 1 > out_cap) {
965 while (out_len + (size_t)stop_at + 1 > out_cap) {
966 out_cap *= 2;
967 }
968 char *next = (char *)realloc(out, out_cap);
969 if (next == NULL) {
970 break;
971 }
972 out = next;
973 }
974 memcpy(out + out_len, buf, (size_t)stop_at);
975 out_len += (size_t)stop_at;
976 out[out_len] = '\0';
977 if (stop_at != n) {
978 break;
979 }
980 batch = llama_batch_get_one(&new_token_id, 1);
981 }
982
983 llama_sampler_free(smpl);
984 free(full_prompt);
985 free(prompt_tokens);
986 llama_free(ctx);
987
988 if (out_len == 0) {
989 free(out);
990 return strdup(fallback);
991 }
992 return out;
993}
994
995static void dialog_submit(Dialog *dialog, const GameMap *game_map, const GameRuntime *runtime) {
996 if (dialog->input_len == 0) {
997 return;
998 }
999 {
1000 const char *npc_name = NULL;
1001 char *reply = generate_npc_reply(runtime, game_map, dialog->npc_index, dialog->input);
1002 const char *fallback = "";
1003 if (game_map && dialog->npc_index >= 0 && dialog->npc_index < 10) {
1004 npc_name = game_map->npcs[dialog->npc_index].name;
1005 fallback = game_map->npcs[dialog->npc_index].reply;
1006 if (fallback == NULL) {
1007 fallback = "";
1008 }
1009 }
1010 reply = sanitize_reply(reply, npc_name);
1011 if (reply == NULL || reply[0] == '\0') {
1012 free(reply);
1013 reply = NULL;
1014 }
1015 const char *reply_text = reply != NULL ? reply : fallback;
1016 if (dialog->entry_count >= DIALOG_HISTORY_MAX) {
1017 for (int i = 1; i < DIALOG_HISTORY_MAX; i++) {
1018 dialog->entries[i - 1] = dialog->entries[i];
1019 }
1020 dialog->entry_count = DIALOG_HISTORY_MAX - 1;
1021 }
1022 snprintf(dialog->entries[dialog->entry_count].prompt,
1023 sizeof(dialog->entries[dialog->entry_count].prompt), "%s", dialog->input);
1024 snprintf(dialog->entries[dialog->entry_count].response,
1025 sizeof(dialog->entries[dialog->entry_count].response), "%s", reply_text);
1026 dialog->entry_count++;
1027 free(reply);
1028 }
1029 dialog->input_len = 0;
1030 dialog->input[0] = '\0';
1031}
1032
1033static void update_npc_status(const GameMap *game_map, int npc_index) {
1034 static char status_buf[128];
1035 const char *name = NULL;
1036 if (game_map && npc_index >= 0 && npc_index < 10) {
1037 name = game_map->npcs[npc_index].name;
1038 }
1039 if (name && name[0] != '\0') {
1040 snprintf(status_buf, sizeof(status_buf), "You approach %s.", name);
1041 } else {
1042 snprintf(status_buf, sizeof(status_buf), "You approach the NPC.");
1043 }
1044 update_status(status_buf);
1045}
1046
1047static void render(const Map *map, const Player *player, int *cam_x,
1048 int *cam_y, int *out_view_w, int *out_view_h, const Dialog *dialog) {
1049 int w;
1050 int h;
1051 int map_x;
1052 int map_y;
1053 int map_w;
1054 int map_h;
1055 int side_x;
1056 int side_y;
1057 int side_w;
1058 int side_h;
1059 int stats_x;
1060 int stats_y;
1061 int stats_w;
1062 int stats_h;
1063 int inv_x;
1064 int inv_y;
1065 int inv_w;
1066 int inv_h;
1067 int msg1_y;
1068 int msg2_y;
1069 int view_w;
1070 int view_h;
1071 int draw_w;
1072 int draw_h;
1073 int pad_x;
1074 int pad_y;
1075
1076 w = tb_width();
1077 h = tb_height();
1078 get_layout(w, h, &map_x, &map_y, &map_w, &map_h, &side_x, &side_y, &side_w, &side_h, &msg1_y, &msg2_y);
1079
1080 tb_clear();
1081 if (w < MIN_W || h < MIN_H || map_w < 8 || map_h < 3) {
1082 tb_print(1, 1, COLOR_RED_256 | TB_BOLD, TB_DEFAULT, "Window too small. Resize to at least 40x12.");
1083 tb_present();
1084 *out_view_w = map_w;
1085 *out_view_h = map_h;
1086 return;
1087 }
1088
1089 view_w = map_w - 2;
1090 view_h = map_h - 2;
1091 draw_w = view_w;
1092 draw_h = view_h;
1093 if (map->width < draw_w) {
1094 draw_w = map->width;
1095 }
1096 if (map->height < draw_h) {
1097 draw_h = map->height;
1098 }
1099 pad_x = view_w > draw_w ? (view_w - draw_w) / 2 : 0;
1100 pad_y = view_h > draw_h ? (view_h - draw_h) / 2 : 0;
1101
1102 draw_border(map_x, map_y, map_w, map_h, COLOR_WHITE_256);
1103 update_camera(map, view_w, view_h, player, cam_x, cam_y);
1104 draw_map(map, map_x + 1 + pad_x, map_y + 1 + pad_y, draw_w, draw_h, player, *cam_x, *cam_y);
1105
1106 stats_x = side_x;
1107 stats_y = side_y;
1108 stats_w = side_w;
1109 stats_h = 11;
1110 inv_x = side_x;
1111 inv_y = side_y + stats_h;
1112 inv_w = side_w;
1113 inv_h = side_h - stats_h;
1114 if (stats_w >= 12 && stats_h >= 9) {
1115 draw_border(stats_x, stats_y, stats_w, stats_h, COLOR_WHITE_256);
1116 draw_stats(stats_x + 2, stats_y + 1, player);
1117 }
1118 if (inv_w >= 12 && inv_h >= 7) {
1119 draw_border(inv_x, inv_y, inv_w, inv_h, COLOR_WHITE_256);
1120 draw_inventory(inv_x + 2, inv_y + 1, &player->inventory);
1121 }
1122
1123 tb_print(2, msg1_y, COLOR_GREEN_256, TB_DEFAULT, status_msg);
1124 tb_print(2, msg2_y, COLOR_WHITE_256, TB_DEFAULT, "Move: arrows Quit: q/ESC");
1125
1126 if (dialog->open) {
1127 int box_w = map_w - 4;
1128 int box_h = 12;
1129 int box_x = map_x + 2;
1130 int box_y = map_y + map_h - box_h - 1;
1131 if (box_w > w - 2) {
1132 box_w = w - 2;
1133 box_x = 1;
1134 }
1135 if (box_h > h - 2) {
1136 box_h = h - 2;
1137 box_y = 1;
1138 }
1139 if (box_w < 20) {
1140 box_w = 20;
1141 box_x = map_x + 1;
1142 }
1143 if (box_y < map_y + 1) {
1144 box_y = map_y + 1;
1145 }
1146 for (int iy = 0; iy < box_h; iy++) {
1147 for (int ix = 0; ix < box_w; ix++) {
1148 tb_set_cell(box_x + ix, box_y + iy, ' ', COLOR_WHITE_256, 19);
1149 }
1150 }
1151 draw_border_bg(box_x, box_y, box_w, box_h, COLOR_WHITE_256, 19);
1152 {
1153 int input_y = box_y + box_h - 3;
1154 int footer_y = box_y + box_h - 2;
1155 int log_y = box_y + 1;
1156 int max_lines = input_y - log_y;
1157 int max_text = box_w - 4 - 5;
1158 int line = 0;
1159
1160 if (max_text < 0) {
1161 max_text = 0;
1162 }
1163 int start = dialog->entry_count;
1164 if (start < 0) {
1165 start = 0;
1166 }
1167 int used_lines = 0;
1168 for (int i = dialog->entry_count - 1; i >= 0; i--) {
1169 const char *prompt_text = dialog->entries[i].prompt;
1170 const char *response_text = dialog->entries[i].response;
1171 const char *name = dialog->npc_name && dialog->npc_name[0] != '\0' ? dialog->npc_name : "NPC";
1172 char prefix_you[16];
1173 char prefix_npc[64];
1174 snprintf(prefix_you, sizeof(prefix_you), "You: ");
1175 snprintf(prefix_npc, sizeof(prefix_npc), "%s: ", name);
1176 int need = count_wrapped_lines(box_w, prefix_you, prompt_text)
1177 + count_wrapped_lines(box_w, prefix_npc, response_text);
1178 if (used_lines + need > max_lines && used_lines > 0) {
1179 break;
1180 }
1181 used_lines += need;
1182 start = i;
1183 if (used_lines >= max_lines) {
1184 break;
1185 }
1186 }
1187 for (int i = start; i < dialog->entry_count && line + 1 <= max_lines; i++) {
1188 const char *prompt_text = dialog->entries[i].prompt;
1189 const char *response_text = dialog->entries[i].response;
1190 const char *name = dialog->npc_name && dialog->npc_name[0] != '\0' ? dialog->npc_name : "NPC";
1191 char prefix_you[16];
1192 char prefix_npc[64];
1193 snprintf(prefix_you, sizeof(prefix_you), "You: ");
1194 snprintf(prefix_npc, sizeof(prefix_npc), "%s: ", name);
1195 int used = draw_wrapped(box_x + 2, log_y + line, max_lines - line, box_w,
1196 COLOR_WHITE_256, 19, prefix_you, prompt_text);
1197 line += used;
1198 if (line >= max_lines) {
1199 break;
1200 }
1201 used = draw_wrapped(box_x + 2, log_y + line, max_lines - line, box_w,
1202 COLOR_GREEN_256, 19, prefix_npc, response_text);
1203 line += used;
1204 if (line >= max_lines) {
1205 break;
1206 }
1207 }
1208
1209 tb_printf(box_x + 2, input_y, COLOR_WHITE_256, 19, "Say: %s", dialog->input);
1210 {
1211 int cursor_x = box_x + 2 + 5 + dialog->input_len;
1212 int cursor_y = input_y;
1213 if (cursor_x < box_x + box_w - 1) {
1214 tb_set_cell(cursor_x, cursor_y, '_', COLOR_WHITE_256 | TB_BOLD, 19);
1215 }
1216 }
1217 tb_print(box_x + 2, footer_y, COLOR_WHITE_256, 19, "Enter: send ESC: close");
1218 }
1219 }
1220
1221 tb_present();
1222 *out_view_w = view_w;
1223 *out_view_h = view_h;
1224}
1225
1226static int clamp(int value, int min, int max) {
1227 if (value < min) {
1228 return min;
1229 }
1230 if (value > max) {
1231 return max;
1232 }
1233 return value;
1234}
1235
1236int main(int argc, char **argv) {
1237 const char *model_name = NULL;
1238 const char *embed_model_name = NULL;
1239 const ModelConfig *model_cfg = NULL;
1240 struct llama_model *embed_model = NULL;
1241 struct llama_model *gen_model = NULL;
1242 struct llama_context *embed_ctx = NULL;
1243 int tb_ready = 0;
1244 int llama_ready = 0;
1245 int exit_code = 0;
1246 int verbose = 0;
1247
1248 static struct option long_options[] = {
1249 {"model", required_argument, 0, 'm'},
1250 {"embed-model", required_argument, 0, 'e'},
1251 {"verbose", no_argument, 0, 'v'},
1252 {"help", no_argument, 0, 'h'},
1253 {0, 0, 0, 0}
1254 };
1255
1256 int opt;
1257 int option_index = 0;
1258 while ((opt = getopt_long(argc, argv, "m:e:vh", long_options, &option_index)) != -1) {
1259 switch (opt) {
1260 case 'm':
1261 model_name = optarg;
1262 break;
1263 case 'e':
1264 embed_model_name = optarg;
1265 break;
1266 case 'v':
1267 verbose = 1;
1268 break;
1269 case 'h':
1270 show_help(argv[0]);
1271 return 0;
1272 default:
1273 fprintf(stderr, "Usage: %s [-m model] [-v] [-h]\n", argv[0]);
1274 return 1;
1275 }
1276 }
1277
1278 if (model_name != NULL) {
1279 model_cfg = get_model_by_name(model_name);
1280 if (model_cfg == NULL) {
1281 fprintf(stderr, "Unknown model '%s'\n", model_name);
1282 return 1;
1283 }
1284 } else {
1285 model_cfg = &models[0];
1286 }
1287
1288 Player player = {0};
1289 array(GameMap) maps;
1290 GameMap map1 = {0};
1291 GameMap *current_map = NULL;
1292 VectorDB *npc_dbs = NULL;
1293 int *npc_db_loaded = NULL;
1294 int running = 1;
1295 int view_w = 0;
1296 int view_h = 0;
1297 int cam_x = 0;
1298 int cam_y = 0;
1299 Dialog dialog = {0};
1300 GameRuntime runtime = {0};
1301
1302 player_init(&player);
1303 array_init(maps);
1304 map1 = make_map1();
1305 array_push(maps, map1);
1306 current_map = &maps.data[0];
1307 map_init(¤t_map->map, current_map->data, current_map->len);
1308
1309 if (verbose == 0) {
1310 llama_log_set(llama_log_callback, NULL);
1311 }
1312
1313 npc_dbs = (VectorDB *)calloc(10, sizeof(VectorDB));
1314 npc_db_loaded = (int *)calloc(10, sizeof(int));
1315 if (npc_dbs == NULL || npc_db_loaded == NULL) {
1316 fprintf(stderr, "Failed to allocate NPC vector databases\n");
1317 exit_code = 1;
1318 goto cleanup;
1319 }
1320
1321 llama_backend_init();
1322 ggml_backend_load_all();
1323 llama_ready = 1;
1324 const ModelConfig *embed_cfg = NULL;
1325 if (embed_model_name != NULL) {
1326 embed_cfg = get_model_by_name(embed_model_name);
1327 if (embed_cfg == NULL) {
1328 fprintf(stderr, "Unknown embedding model '%s'\n", embed_model_name);
1329 exit_code = 1;
1330 goto cleanup;
1331 }
1332 } else if (model_cfg->embed_model_name != NULL) {
1333 embed_cfg = get_model_by_name(model_cfg->embed_model_name);
1334 }
1335 if (embed_cfg == NULL) {
1336 embed_cfg = model_cfg;
1337 }
1338
1339 struct llama_model_params gen_params = llama_model_default_params();
1340 gen_params.n_gpu_layers = model_cfg->n_gpu_layers;
1341 gen_params.use_mmap = model_cfg->use_mmap;
1342 gen_model = llama_model_load_from_file(model_cfg->filepath, gen_params);
1343 if (gen_model == NULL) {
1344 fprintf(stderr, "Unable to load generation model\n");
1345 exit_code = 1;
1346 goto cleanup;
1347 }
1348
1349 struct llama_model_params embed_params = llama_model_default_params();
1350 embed_params.n_gpu_layers = embed_cfg->n_gpu_layers;
1351 embed_params.use_mmap = embed_cfg->use_mmap;
1352 embed_model = llama_model_load_from_file(embed_cfg->filepath, embed_params);
1353 if (embed_model == NULL) {
1354 fprintf(stderr, "Unable to load embedding model\n");
1355 exit_code = 1;
1356 goto cleanup;
1357 }
1358
1359 struct llama_context_params cparams = llama_context_default_params();
1360 cparams.n_ctx = embed_cfg->n_ctx;
1361 cparams.n_batch = embed_cfg->n_batch;
1362 cparams.embeddings = true;
1363 embed_ctx = llama_init_from_model(embed_model, cparams);
1364 if (embed_ctx == NULL) {
1365 fprintf(stderr, "Failed to create embedding context\n");
1366 exit_code = 1;
1367 goto cleanup;
1368 }
1369
1370 for (int i = 0; i < 10; i++) {
1371 const char *vdb_path = current_map->npcs[i].vdb_path;
1372 if (vdb_path == NULL || vdb_path[0] == '\0') {
1373 continue;
1374 }
1375 vdb_init(&npc_dbs[i], embed_ctx);
1376 VectorDBErrorCode vdb_rc = vdb_load(&npc_dbs[i], vdb_path);
1377 if (vdb_rc != VDB_SUCCESS) {
1378 fprintf(stderr, "Failed to load vector database %s: %s\n", vdb_path, vdb_error(vdb_rc));
1379 vdb_free(&npc_dbs[i]);
1380 continue;
1381 }
1382 npc_db_loaded[i] = 1;
1383 }
1384
1385 runtime.model_cfg = model_cfg;
1386 runtime.model = gen_model;
1387 runtime.embed_model = embed_model;
1388 runtime.embed_ctx = embed_ctx;
1389 runtime.npc_dbs = npc_dbs;
1390 runtime.npc_db_loaded = npc_db_loaded;
1391 runtime.verbose = verbose;
1392
1393 if (tb_init() != TB_OK) {
1394 fprintf(stderr, "Failed to init termbox.\n");
1395 exit_code = 1;
1396 goto cleanup;
1397 }
1398 tb_ready = 1;
1399
1400 tb_set_input_mode(TB_INPUT_ESC);
1401 tb_set_output_mode(TB_OUTPUT_256);
1402 update_status("You feel like you have a lot of potential.");
1403 while (running) {
1404 struct tb_event ev;
1405
1406 render(¤t_map->map, &player, &cam_x, &cam_y, &view_w, &view_h, &dialog);
1407 tb_poll_event(&ev);
1408 if (ev.type == TB_EVENT_KEY) {
1409 if (dialog.open) {
1410 if (ev.key == TB_KEY_ESC) {
1411 dialog_close(&dialog);
1412 } else if (ev.key == TB_KEY_ENTER) {
1413 dialog_submit(&dialog, current_map, &runtime);
1414 } else if (ev.key == TB_KEY_BACKSPACE || ev.key == TB_KEY_BACKSPACE2) {
1415 dialog_backspace(&dialog);
1416 } else if (ev.ch) {
1417 dialog_append(&dialog, ev.ch);
1418 }
1419 } else {
1420 if (ev.key == TB_KEY_ESC || ev.ch == 'q') {
1421 running = 0;
1422 } else if (ev.key == TB_KEY_ARROW_UP) {
1423 int next_y = player.y - 1;
1424 u32 target = map_get(¤t_map->map, player.x, next_y);
1425 int npc_index = npc_index_from_tile(target);
1426 if (target == 'N' || npc_index >= 0) {
1427 const char *npc_name = current_map && npc_index >= 0 && npc_index < 10
1428 ? current_map->npcs[npc_index].name
1429 : NULL;
1430 dialog_open(&dialog, npc_index, npc_name);
1431 update_npc_status(current_map, npc_index);
1432 } else if (map_is_walkable(¤t_map->map, player.x, next_y)) {
1433 player.y = next_y;
1434 }
1435 } else if (ev.key == TB_KEY_ARROW_DOWN) {
1436 int next_y = player.y + 1;
1437 u32 target = map_get(¤t_map->map, player.x, next_y);
1438 int npc_index = npc_index_from_tile(target);
1439 if (target == 'N' || npc_index >= 0) {
1440 const char *npc_name = current_map && npc_index >= 0 && npc_index < 10
1441 ? current_map->npcs[npc_index].name
1442 : NULL;
1443 dialog_open(&dialog, npc_index, npc_name);
1444 update_npc_status(current_map, npc_index);
1445 } else if (map_is_walkable(¤t_map->map, player.x, next_y)) {
1446 player.y = next_y;
1447 }
1448 } else if (ev.key == TB_KEY_ARROW_LEFT) {
1449 int next_x = player.x - 1;
1450 u32 target = map_get(¤t_map->map, next_x, player.y);
1451 int npc_index = npc_index_from_tile(target);
1452 if (target == 'N' || npc_index >= 0) {
1453 const char *npc_name = current_map && npc_index >= 0 && npc_index < 10
1454 ? current_map->npcs[npc_index].name
1455 : NULL;
1456 dialog_open(&dialog, npc_index, npc_name);
1457 update_npc_status(current_map, npc_index);
1458 } else if (map_is_walkable(¤t_map->map, next_x, player.y)) {
1459 player.x = next_x;
1460 }
1461 } else if (ev.key == TB_KEY_ARROW_RIGHT) {
1462 int next_x = player.x + 1;
1463 u32 target = map_get(¤t_map->map, next_x, player.y);
1464 int npc_index = npc_index_from_tile(target);
1465 if (target == 'N' || npc_index >= 0) {
1466 const char *npc_name = current_map && npc_index >= 0 && npc_index < 10
1467 ? current_map->npcs[npc_index].name
1468 : NULL;
1469 dialog_open(&dialog, npc_index, npc_name);
1470 update_npc_status(current_map, npc_index);
1471 } else if (map_is_walkable(¤t_map->map, next_x, player.y)) {
1472 player.x = next_x;
1473 }
1474 }
1475 if (map_get(¤t_map->map, player.x, player.y) == '$') {
1476 player.gold += 10;
1477 map_set(¤t_map->map, player.x, player.y, MAP_FLOOR_CH);
1478 update_status("You pick up 10 gold.");
1479 }
1480 }
1481 player.x = clamp(player.x, 0, current_map->map.width > 1 ? current_map->map.width - 1 : 0);
1482 player.y = clamp(player.y, 0, current_map->map.height > 1 ? current_map->map.height - 1 : 0);
1483 } else if (ev.type == TB_EVENT_RESIZE) {
1484 player.x = clamp(player.x, 0, current_map->map.width > 1 ? current_map->map.width - 1 : 0);
1485 player.y = clamp(player.y, 0, current_map->map.height > 1 ? current_map->map.height - 1 : 0);
1486 }
1487 }
1488
1489cleanup:
1490 player_free(&player);
1491 for (size_t i = 0; i < maps.length; i++) {
1492 map_free(&maps.data[i].map);
1493 }
1494 array_free(maps);
1495 if (tb_ready) {
1496 tb_shutdown();
1497 }
1498 for (int i = 0; i < 10; i++) {
1499 if (npc_db_loaded && npc_db_loaded[i]) {
1500 vdb_free(&npc_dbs[i]);
1501 }
1502 }
1503 free(npc_db_loaded);
1504 free(npc_dbs);
1505 if (embed_ctx != NULL) {
1506 llama_free(embed_ctx);
1507 }
1508 if (embed_model != NULL) {
1509 llama_model_free(embed_model);
1510 }
1511 if (gen_model != NULL) {
1512 llama_model_free(gen_model);
1513 }
1514 if (llama_ready) {
1515 llama_backend_free();
1516 }
1517 return exit_code;
1518}