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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_map->map, next_x, player.y)) {
1472						player.x = next_x;
1473					}
1474				}
1475				if (map_get(&current_map->map, player.x, player.y) == '$') {
1476					player.gold += 10;
1477					map_set(&current_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}