$ cd /home/martin/workspace/prototypes/boot-to-claude stat -c%s build/kernel.bin echo "budget: $((512*512))"
81424 budget: 262144
1 #include "game.h"
2
3 #include <stdint.h>
4
5 #include "console.h"
6 #include "input.h"
7 #include "serial.h"
8 #include "string.h"
9
10 #define LINE_MAX 64
11 #define WIN_STAGE 5 /* defeat this many wild monsters to become champion */
12
13 /* --- dual console+serial output, same convention as shell.c --- */
14
15 static void w(const char *s) {
16 console_write(s);
17 serial_write(s);
18 }
19
20 static void wln(const char *s) {
21 w(s);
22 w("\n");
23 }
24
25 static void w_int(int v) {
26 char buf[16];
27 w(itoa(v, buf, 10));
28 }
29
30 /* --- line input, same convention as shell.c's read_line (dual keyboard
31 * +serial via input_getchar - see its comment) --- */
32
33 static void echo_char(char c) {
34 console_putc(c);
35 serial_putc(c);
36 }
37
38 static int read_line(char *buf, int max_len) {
39 int len = 0;
40 for (;;) {
41 char c = input_getchar();
42 if (c == '\n') {
43 echo_char('\n');
44 buf[len] = '\0';
45 return len;
46 } else if (c == '\b') {
47 if (len > 0) {
48 len--;
49 echo_char('\b');
50 echo_char(' ');
51 echo_char('\b');
52 }
53 } else if (c >= 32 && c < 127 && len < max_len - 1) {
54 buf[len++] = c;
55 echo_char(c);
56 }
57 }
58 }
59
60 /* --- a small xorshift32 PRNG, perturbed each draw with input_ticks()
61 * (see input.h) - there's no timer interrupt in this kernel, so real
62 * elapsed-wait-time jitter is the only entropy source available. */
63
64 static uint32_t rng_state = 2463534242u; /* must be nonzero */
65
66 static uint32_t rng_next(void) {
67 rng_state ^= input_ticks() + 1;
68 rng_state ^= rng_state << 13;
69 rng_state ^= rng_state >> 17;
70 rng_state ^= rng_state << 5;
71 return rng_state;
72 }
73
74 static int rng_range(int lo, int hi) { /* inclusive */
75 /* CC's codegen only ever emits signed idiv, never true unsigned
76 * division (see docs/toolchain.md) - the rest of the kernel avoids
77 * this by construction (nothing else divides a value that could
78 * have the sign bit set), but rng_next()'s raw uint32_t output can.
79 * Left unmasked, a result with the sign bit set gets `%`'d as if
80 * negative, producing a negative "random" index and an out-of-bounds
81 * moves[] read (garbage move name, huge damage). Clearing the sign
82 * bit first keeps the value in idiv's safe signed range. */
83 uint32_t r = rng_next() & 0x7FFFFFFFu;
84 return lo + (int)(r % (uint32_t)(hi - lo + 1));
85 }
86
87 /* --- game data --- */
88
89 typedef enum { TYPE_FIRE, TYPE_WATER, TYPE_GRASS } mon_type_t;
90
91 typedef struct {
92 const char *name;
93 int power;
94 } move_t;
95
96 typedef struct {
97 const char *name;
98 mon_type_t type;
99 int max_hp;
100 move_t moves[3];
101 } species_t;
102
103 #define NUM_SPECIES 3
104
105 static const species_t SPECIES[NUM_SPECIES] = {
106 { "Emberpup", TYPE_FIRE, 28, { { "Ember", 7 }, { "Scratch", 5 }, { "Flame Burst", 10 } } },
107 { "Aqualing", TYPE_WATER, 30, { { "Water Jet", 7 }, { "Tail Slap", 5 }, { "Tide Wave", 10 } } },
108 { "Leafling", TYPE_GRASS, 32, { { "Vine Whip", 7 }, { "Tackle", 5 }, { "Leaf Storm", 10 } } },
109 };
110
111 /* Rock-paper-scissors: fire > grass > water > fire. */
112 static int type_beats(mon_type_t a, mon_type_t d) {
113 return (a == TYPE_FIRE && d == TYPE_GRASS) ||
114 (a == TYPE_GRASS && d == TYPE_WATER) ||
115 (a == TYPE_WATER && d == TYPE_FIRE);
116 }
117
118 static int compute_damage(mon_type_t attacker_type, mon_type_t defender_type, int power) {
119 int dmg = power;
120 if (type_beats(attacker_type, defender_type)) {
121 w("It's super effective!\n");
122 dmg = dmg * 3 / 2;
123 } else if (type_beats(defender_type, attacker_type)) {
124 w("It's not very effective...\n");
125 dmg = dmg / 2;
126 }
127 dmg += rng_range(-2, 2);
128 if (dmg < 1) {
129 dmg = 1;
130 }
131 return dmg;
132 }
133
134 /* --- status line helpers --- */
135
136 static void print_hp(const char *label, const char *name, int hp, int max_hp) {
137 w(label);
138 w(name);
139 w(": HP ");
140 w_int(hp);
141 w("/");
142 w_int(max_hp);
143 w("\n");
144 }
145
146 /* Parses a line already trimmed of leading spaces: a leading digit is a
147 * numbered menu choice (1-9), otherwise "run"/"quit" are recognized as
148 * words - covers both a human pressing a number key and an AI agent
149 * typing a full word, whichever is more natural for it. Returns the
150 * choice as 1-9, 0 for "run", -1 for "quit", -2 if unrecognized. */
151 static int parse_choice(const char *line) {
152 while (*line == ' ') {
153 line++;
154 }
155 if (*line >= '1' && *line <= '9') {
156 return *line - '0';
157 }
158 if (strcmp(line, "run") == 0) {
159 return 0;
160 }
161 if (strcmp(line, "quit") == 0) {
162 return -1;
163 }
164 return -2;
165 }
166
167 /* Returns the chosen starter index (0..NUM_SPECIES-1), or -1 if the
168 * player typed "quit". */
169 static int choose_starter(void) {
170 char line[LINE_MAX];
171 wln("Choose your starter:");
172 for (int i = 0; i < NUM_SPECIES; i++) {
173 w_int(i + 1);
174 w(") ");
175 w(SPECIES[i].name);
176 w(" (");
177 w(SPECIES[i].type == TYPE_FIRE ? "Fire" : SPECIES[i].type == TYPE_WATER ? "Water" : "Grass");
178 wln(")");
179 }
180 for (;;) {
181 w("> ");
182 read_line(line, sizeof(line));
183 int choice = parse_choice(line);
184 if (choice == -1) {
185 return -1;
186 }
187 if (choice >= 1 && choice <= NUM_SPECIES) {
188 return choice - 1;
189 }
190 wln("invalid choice - type a number, 'run', or 'quit'");
191 }
192 }
193
194 void game_run(void) {
195 char line[LINE_MAX];
196
197 wln("=== Tiny Monsters ===");
198 wln("Defeat 5 wild monsters to become champion. Type 'quit' any time to leave.");
199
200 int player_idx = choose_starter();
201 if (player_idx < 0) {
202 wln("Left the game.");
203 return;
204 }
205 const species_t *player = &SPECIES[player_idx];
206 int player_hp = player->max_hp;
207 int stage = 1;
208
209 for (;;) {
210 int wild_idx = rng_range(0, NUM_SPECIES - 1);
211 const species_t *wild = &SPECIES[wild_idx];
212 int wild_max_hp = wild->max_hp + (stage - 1) * 4;
213 int wild_hp = wild_max_hp;
214
215 w("=== A wild ");
216 w(wild->name);
217 w(" appears! (stage ");
218 w_int(stage);
219 wln(")");
220
221 for (;;) {
222 print_hp("Your ", player->name, player_hp, player->max_hp);
223 print_hp("Wild ", wild->name, wild_hp, wild_max_hp);
224 for (int i = 0; i < 3; i++) {
225 w_int(i + 1);
226 w(") ");
227 w(player->moves[i].name);
228 w(" (");
229 w_int(player->moves[i].power);
230 wln(")");
231 }
232 wln("4) Run");
233 w("> ");
234 read_line(line, sizeof(line));
235 int choice = parse_choice(line);
236
237 if (choice == -1) {
238 wln("Left the game.");
239 return;
240 }
241 if (choice == 0 || choice == 4) {
242 wln("You ran away!");
243 break; /* next encounter, same stage */
244 }
245 if (choice < 1 || choice > 3) {
246 wln("invalid choice - type 1-3 for a move, 'run', or 'quit'");
247 continue;
248 }
249
250 const move_t *mv = &player->moves[choice - 1];
251 w("Your ");
252 w(player->name);
253 w(" uses ");
254 w(mv->name);
255 wln("!");
256 int dmg = compute_damage(player->type, wild->type, mv->power);
257 wild_hp -= dmg;
258 if (wild_hp < 0) {
259 wild_hp = 0;
260 }
261 w("Wild ");
262 w(wild->name);
263 w(" takes ");
264 w_int(dmg);
265 w(" damage. (HP ");
266 w_int(wild_hp);
267 w("/");
268 w_int(wild_max_hp);
269 wln(")");
270
271 if (wild_hp <= 0) {
272 w("Wild ");
273 w(wild->name);
274 wln(" fainted! You win!");
275 stage++;
276 if (stage > WIN_STAGE) {
277 w("*** You defeated ");
278 w_int(WIN_STAGE);
279 wln(" wild monsters - you are the Champion! ***");
280 return;
281 }
282 break; /* next encounter */
283 }
284
285 const move_t *wmv = &wild->moves[rng_range(0, 2)];
286 w("Wild ");
287 w(wild->name);
288 w(" uses ");
289 w(wmv->name);
290 wln("!");
291 int wdmg = compute_damage(wild->type, player->type, wmv->power);
292 player_hp -= wdmg;
293 if (player_hp < 0) {
294 player_hp = 0;
295 }
296 w("Your ");
297 w(player->name);
298 w(" takes ");
299 w_int(wdmg);
300 w(" damage. (HP ");
301 w_int(player_hp);
302 w("/");
303 w_int(player->max_hp);
304 wln(")");
305
306 if (player_hp <= 0) {
307 w("Your ");
308 w(player->name);
309 w(" fainted... Game over! You reached stage ");
310 w_int(stage);
311 wln(".");
312 return;
313 }
314 }
315 }
316 }200 }
201 }
202
203 void vga_put_pixel(int x, int y, uint8_t color) {
204 if (x < 0 || x >= VGA_WIDTH || y < 0 || y >= VGA_HEIGHT) {
205 return;
206 }
207 FRAMEBUFFER[y * VGA_WIDTH + x] = color;
208 }
209
210 void vga_fill_rect(int x, int y, int w, int h, uint8_t color) {
211 for (int row = 0; row < h; row++) {
212 for (int col = 0; col < w; col++) {
213 vga_put_pixel(x + col, y + row, color);
214 }
215 }
216 }
217
218 void vga_draw_rect(int x, int y, int w, int h, uint8_t color) {
219 vga_fill_rect(x, y, w, 1, color);
220 vga_fill_rect(x, y + h - 1, w, 1, color);
221 vga_fill_rect(x, y, 1, h, color);
222 vga_fill_rect(x + w - 1, y, 1, h, color);
223 }
224
225 void vga_draw_char(int x, int y, char c, uint8_t color) {
226 const uint8_t *glyph = font_glyph(c);
227 for (int row = 0; row < FONT_H; row++) {
228 uint8_t bits = glyph[row];
229 for (int col = 0; col < FONT_W; col++) {
230 if (bits & (1 << (FONT_W - 1 - col))) {
231 vga_put_pixel(x + col, y + row, color);
232 }
233 }
234 }
235 }
236
237 void vga_draw_text(int x, int y, const char *s, uint8_t color) {
238 int cx = x;
239 while (*s) {- void vga_draw_rect(int x, int y, int w, int h, uint8_t color) {
- vga_fill_rect(x, y, w, 1, color);
- vga_fill_rect(x, y + h - 1, w, 1, color);
- vga_fill_rect(x, y, 1, h, color);
- vga_fill_rect(x + w - 1, y, 1, h, color);
- }
-
- void vga_draw_char(int x, int y, char c, uint8_t color) {
+ void vga_draw_rect(int x, int y, int w, int h, uint8_t color) {
+ vga_fill_rect(x, y, w, 1, color);
+ vga_fill_rect(x, y + h - 1, w, 1, color);
+ vga_fill_rect(x, y, 1, h, color);
+ vga_fill_rect(x + w - 1, y, 1, h, color);
+ }
+
+ /* Integer sqrt via Newton's method - the circle/ellipse fillers below are
+ * the only callers, and their radii are always small (sprite/HP-bar
+ * scale), so a few iterations from a rough starting guess is plenty. */
+ static int isqrt_approx(int n) {
+ if (n <= 0) {
+ return 0;
+ }
+ int x = n;
+ int y = (x + 1) / 2;
+ while (y < x) {
+ x = y;
+ y = (x + n / x) / 2;
+ }
+ return x;
+ }
+
+ void vga_fill_circle(int cx, int cy, int r, uint8_t color) {
+ for (int dy = -r; dy <= r; dy++) {
+ int dx = isqrt_approx(r * r - dy * dy);
+ vga_fill_rect(cx - dx, cy + dy, dx * 2 + 1, 1, color);
+ }
+ }
+
+ void vga_fill_ellipse(int cx, int cy, int rx, int ry, uint8_t color) {
+ if (ry <= 0) {
+ return;
+ }
+ for (int dy = -ry; dy <= ry; dy++) {
+ /* dx/rx = sqrt(1 - (dy/ry)^2), rearranged to stay in integers. */
+ int dx = isqrt_approx((rx * rx) * (ry * ry - dy * dy) / (ry * ry));
+ vga_fill_rect(cx - dx, cy + dy, dx * 2 + 1, 1, color);
+ }
+ }
+
+ /* Edge function (twice the signed triangle area) - used both to get a
+ * consistent winding-independent inside/outside test and, via its sign,
+ * to handle triangles specified in either winding order. */
+ static int edge_fn(int ax, int ay, int bx, int by, int px, int py) {
+ return (bx - ax) * (py - ay) - (by - ay) * (px - ax);
+ }
+
+ void vga_fill_triangle(int x0, int y0, int x1, int y1, int x2, int y2, uint8_t color) {
+ int min_x = x0 < x1 ? (x0 < x2 ? x0 : x2) : (x1 < x2 ? x1 : x2);
+ int max_x = x0 > x1 ? (x0 > x2 ? x0 : x2) : (x1 > x2 ? x1 : x2);
+ int min_y = y0 < y1 ? (y0 < y2 ? y0 : y2) : (y1 < y2 ? y1 : y2);
+ int max_y = y0 > y1 ? (y0 > y2 ? y0 : y2) : (y1 > y2 ? y1 : y2);
+
+ for (int py = min_y; py <= max_y; py++) {
+ for (int px = min_x; px <= max_x; px++) {
+ int w0 = edge_fn(x1, y1, x2, y2, px, py);
+ int w1 = edge_fn(x2, y2, x0, y0, px, py);
+ int w2 = edge_fn(x0, y0, x1, y1, px, py);
+ if ((w0 >= 0 && w1 >= 0 && w2 >= 0) || (w0 <= 0 && w1 <= 0 && w2 <= 0)) {
+ vga_put_pixel(px, py, color);
+ }
+ }
+ }
+ }
+
+ void vga_draw_char(int x, int y, char c, uint8_t color) {The file /home/martin/workspace/prototypes/boot-to-claude/kernel/vga.c has been updated successfully. (file state is current in your context — no need to Read it back)
- void vga_clear(uint8_t color); - void vga_put_pixel(int x, int y, uint8_t color); - void vga_fill_rect(int x, int y, int w, int h, uint8_t color); - void vga_draw_rect(int x, int y, int w, int h, uint8_t color); + void vga_clear(uint8_t color); + void vga_put_pixel(int x, int y, uint8_t color); + void vga_fill_rect(int x, int y, int w, int h, uint8_t color); + void vga_draw_rect(int x, int y, int w, int h, uint8_t color); + void vga_fill_circle(int cx, int cy, int r, uint8_t color); + void vga_fill_ellipse(int cx, int cy, int rx, int ry, uint8_t color); + void vga_fill_triangle(int x0, int y0, int x1, int y1, int x2, int y2, uint8_t color);
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/vga.h has been updated successfully. (file state is current in your context — no need to Read it back)
#pragma once /* Species index must match game.c's SPECIES[] array order: 0 Emberpup * (fire), 1 Aqualing (water), 2 Leafling (grass). Kept as a plain int * rather than sharing game.c's private mon_type_t/species_t so this * module has no dependency on the game's internal data layout. */ void sprite_init_palette(void); /* Draws species_idx's ~40x36 pixel creature with (x, y) as its * top-left corner. */ void sprite_draw(int species_idx, int x, int y);
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/game/sprites.h (file state is current in your context — no need to Read it back)
#include "sprites.h"
#include "vga.h"
/* Palette layout for the three starters - plain fixed indices (16-27),
* disjoint from battle_gui.c's scene colors (0-10), same convention as
* vga.c's own hand-picked palette assignments elsewhere. Each creature
* is built from vga.c's circle/ellipse/triangle primitives rather than
* a stored pixel array - procedural shapes in the same spirit as
* font.c's stroke-based glyphs, no external art asset involved. */
#define EMBER_BODY 16
#define EMBER_DARK 17
#define EMBER_BELLY 18
#define EMBER_FLAME 19
#define AQUA_BODY 20
#define AQUA_DARK 21
#define AQUA_BELLY 22
#define LEAF_BODY 24
#define LEAF_DARK 25
#define LEAF_BELLY 26
#define LEAF_SPOT 27
void sprite_init_palette(void) {
vga_set_palette_color(EMBER_BODY, 63, 32, 8);
vga_set_palette_color(EMBER_DARK, 48, 20, 4);
vga_set_palette_color(EMBER_BELLY, 63, 55, 35);
vga_set_palette_color(EMBER_FLAME, 63, 50, 10);
vga_set_palette_color(AQUA_BODY, 10, 35, 63);
vga_set_palette_color(AQUA_DARK, 5, 20, 45);
vga_set_palette_color(AQUA_BELLY, 35, 55, 63);
vga_set_palette_color(LEAF_BODY, 15, 45, 15);
vga_set_palette_color(LEAF_DARK, 8, 30, 8);
vga_set_palette_color(LEAF_BELLY, 40, 60, 30);
vga_set_palette_color(LEAF_SPOT, 45, 55, 10);
}
static void draw_emberpup(int x, int y) {
vga_fill_triangle(x + 8, y + 16, x + 15, y + 16, x + 11, y + 4, EMBER_DARK);
vga_fill_triangle(x + 25, y + 16, x + 32, y + 16, x + 29, y + 4, EMBER_DARK);
vga_fill_circle(x + 36, y + 14, 3, EMBER_FLAME);
vga_fill_circle(x + 34, y + 20, 5, EMBER_FLAME);
vga_fill_ellipse(x + 20, y + 24, 13, 11, EMBER_BODY);
vga_fill_ellipse(x + 20, y + 29, 7, 6, EMBER_BELLY);
vga_fill_circle(x + 15, y + 22, 2, 0);
vga_fill_circle(x + 25, y + 22, 2, 0);
}
static void draw_aqualing(int x, int y) {
vga_fill_circle(x + 34, y + 26, 4, AQUA_DARK);
vga_fill_triangle(x + 13, y + 14, x + 27, y + 14, x + 20, y + 3, AQUA_DARK);
vga_fill_ellipse(x + 20, y + 24, 13, 12, AQUA_BODY);
vga_fill_ellipse(x + 20, y + 30, 7, 5, AQUA_BELLY);
vga_fill_circle(x + 15, y + 22, 2, 0);
vga_fill_circle(x + 25, y + 22, 2, 0);
}
static void draw_leafling(int x, int y) {
vga_fill_triangle(x + 20, y + 13, x + 10, y + 3, x + 20, y + 3, LEAF_DARK);
vga_fill_triangle(x + 20, y + 13, x + 30, y + 3, x + 20, y + 3, LEAF_DARK);
vga_fill_circle(x + 20, y + 25, 13, LEAF_BODY);
vga_fill_ellipse(x + 20, y + 30, 7, 5, LEAF_BELLY);
vga_fill_circle(x + 13, y + 27, 2, LEAF_SPOT);
vga_fill_circle(x + 27, y + 24, 2, LEAF_SPOT);
vga_fill_circle(x + 15, y + 23, 2, 0);
vga_fill_circle(x + 25, y + 23, 2, 0);
}
void sprite_draw(int species_idx, int x, int y) {
if (species_idx == 0) {
draw_emberpup(x, y);
} else if (species_idx == 1) {
draw_aqualing(x, y);
} else if (species_idx == 2) {
draw_leafling(x, y);
}
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/game/sprites.c (file state is current in your context — no need to Read it back)
$ cat /home/martin/workspace/prototypes/boot-to-claude/kernel/string.h
#pragma once #include <stddef.h> void *memcpy(void *dst, const void *src, size_t n); void *memset(void *dst, int val, size_t n); size_t strlen(const char *s); int strcmp(const char *a, const char *b); char *strchr(const char *s, int c); /* Writes the decimal (base 10) or hex (base 16) representation of value * into buf (caller-provided, must be large enough) and returns buf. */ char *itoa(int value, char *buf, int base);
#pragma once
/* Graphical (VGA mode 13h) rendering of the battle game, styled after
* the GBA Pokemon games' battle screens - drawn *alongside* game.c's
* existing console/serial text narration, never replacing it: an AI
* agent playing over serial sees exactly the same lines as before this
* existed. species_idx follows sprites.h's convention (0 Emberpup,
* 1 Aqualing, 2 Leafling), matching game.c's SPECIES[] array order. */
void battle_gui_enter(void);
void battle_gui_exit(void);
/* One static screen shown while the text starter-selection prompt is
* on screen; names[i] labels species i. */
void battle_gui_starter_select(const char *names[3]);
void battle_gui_scene(int player_species, int player_hp, int player_max_hp, const char *player_name,
int wild_species, int wild_hp, int wild_max_hp, const char *wild_name);
/* Redraws the message box with up to two lines; line2 may be NULL or
* empty for a single-line message. */
void battle_gui_message(const char *line1, const char *line2);
/* Redraws the message box as a 2x2 move/run menu (move_names[3] plus a
* fixed "Run" fourth slot), mirroring the numbered text menu. */
void battle_gui_menu(const char *move_names[3], const int move_powers[3]);
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/game/battle_gui.h (file state is current in your context — no need to Read it back)
#include "battle_gui.h"
#include "console.h"
#include "sprites.h"
#include "string.h"
#include "vga.h"
/* Scene palette - indices 0-10, disjoint from sprites.c's 16-27. */
#define COL_BLACK 0
#define COL_WHITE 1
#define COL_SKY 2
#define COL_GROUND 3
#define COL_HORIZON 4
#define COL_BOX_BG 5
#define COL_HP_GREEN 6
#define COL_HP_YELLOW 7
#define COL_HP_RED 8
#define COL_HP_BG 9
#define COL_PLATFORM 10
#define HPBOX_W 132
#define HPBOX_H 34
#define MSGBOX_X 8
#define MSGBOX_Y 148
#define MSGBOX_W (VGA_WIDTH - 16)
#define MSGBOX_H 44
static void scene_palette_init(void) {
vga_set_palette_color(COL_BLACK, 0, 0, 0);
vga_set_palette_color(COL_WHITE, 63, 63, 63);
vga_set_palette_color(COL_SKY, 20, 38, 63);
vga_set_palette_color(COL_GROUND, 20, 45, 15);
vga_set_palette_color(COL_HORIZON, 12, 30, 10);
vga_set_palette_color(COL_BOX_BG, 58, 56, 46);
vga_set_palette_color(COL_HP_GREEN, 20, 55, 15);
vga_set_palette_color(COL_HP_YELLOW, 60, 55, 10);
vga_set_palette_color(COL_HP_RED, 55, 10, 10);
vga_set_palette_color(COL_HP_BG, 25, 25, 25);
vga_set_palette_color(COL_PLATFORM, 45, 35, 20);
sprite_init_palette();
}
void battle_gui_enter(void) {
vga_set_graphics_mode();
scene_palette_init();
}
void battle_gui_exit(void) {
vga_set_text_mode();
/* Mode 13h drawing and text-mode character cells share the same
* underlying VGA RAM (see vga.c) - the text screen's old contents
* don't survive a trip through graphics mode, so leave the shell a
* clean slate instead of a garbled one. */
console_clear();
}
/* Concatenates up to 4 strings (any of which may be NULL) into out,
* truncating to n-1 bytes - this kernel has no snprintf, and every
* message this module builds is a small, fixed number of parts. */
static void gui_concat(char *out, int n, const char *a, const char *b, const char *c, const char *d) {
const char *parts[4] = { a, b, c, d };
int k = 0;
for (int p = 0; p < 4; p++) {
const char *s = parts[p];
if (!s) {
continue;
}
for (int i = 0; s[i] && k < n - 1; i++) {
out[k++] = s[i];
}
}
out[k] = '\0';
}
static void draw_background(void) {
vga_fill_rect(0, 0, VGA_WIDTH, 113, COL_SKY);
vga_fill_rect(0, 113, VGA_WIDTH, 4, COL_HORIZON);
vga_fill_rect(0, 117, VGA_WIDTH, VGA_HEIGHT - 117, COL_GROUND);
vga_fill_ellipse(240, 90, 45, 9, COL_PLATFORM);
vga_fill_ellipse(75, 175, 55, 11, COL_PLATFORM);
}
static void draw_hp_box(int x, int y, const char *name, int hp, int max_hp) {
vga_fill_rect(x, y, HPBOX_W, HPBOX_H, COL_BOX_BG);
vga_draw_rect(x, y, HPBOX_W, HPBOX_H, COL_BLACK);
vga_draw_text(x + 4, y + 4, name, COL_BLACK);
int bar_x = x + 4;
int bar_y = y + 17;
int bar_w = HPBOX_W - 8;
int bar_h = 6;
vga_fill_rect(bar_x, bar_y, bar_w, bar_h, COL_HP_BG);
if (max_hp > 0) {
int filled = bar_w * hp / max_hp;
if (filled < 0) {
filled = 0;
}
if (filled > bar_w) {
filled = bar_w;
}
uint8_t hp_color = COL_HP_GREEN;
if (hp * 3 <= max_hp) {
hp_color = COL_HP_RED;
} else if (hp * 3 <= max_hp * 2) {
hp_color = COL_HP_YELLOW;
}
if (filled > 0) {
vga_fill_rect(bar_x, bar_y, filled, bar_h, hp_color);
}
}
vga_draw_rect(bar_x, bar_y, bar_w, bar_h, COL_BLACK);
char hp_buf[8];
char max_buf[8];
char line[24];
gui_concat(line, sizeof(line), itoa(hp, hp_buf, 10), "/", itoa(max_hp, max_buf, 10), NULL);
vga_draw_text(bar_x, bar_y + 9, line, COL_BLACK);
}
void battle_gui_starter_select(const char *names[3]) {
vga_clear(COL_SKY);
vga_fill_rect(0, 140, VGA_WIDTH, VGA_HEIGHT - 140, COL_GROUND);
vga_draw_text(70, 12, "Choose your starter (1-3):", COL_WHITE);
int xs[3] = { 24, 140, 256 };
for (int i = 0; i < 3; i++) {
sprite_draw(i, xs[i], 80);
char label[4] = { (char)('1' + i), ')', ' ', '\0' };
char line[24];
gui_concat(line, sizeof(line), label, names[i], NULL, NULL);
vga_draw_text(xs[i] - 4, 150, line, COL_WHITE);
}
}
void battle_gui_scene(int player_species, int player_hp, int player_max_hp, const char *player_name,
int wild_species, int wild_hp, int wild_max_hp, const char *wild_name) {
draw_background();
sprite_draw(wild_species, 210, 45);
sprite_draw(player_species, 35, 125);
draw_hp_box(8, 8, wild_name, wild_hp, wild_max_hp);
draw_hp_box(180, 100, player_name, player_hp, player_max_hp);
}
static void draw_message_box_frame(void) {
vga_fill_rect(MSGBOX_X, MSGBOX_Y, MSGBOX_W, MSGBOX_H, COL_BOX_BG);
vga_draw_rect(MSGBOX_X, MSGBOX_Y, MSGBOX_W, MSGBOX_H, COL_BLACK);
vga_draw_rect(MSGBOX_X + 2, MSGBOX_Y + 2, MSGBOX_W - 4, MSGBOX_H - 4, COL_BLACK);
}
void battle_gui_message(const char *line1, const char *line2) {
draw_message_box_frame();
vga_draw_text(MSGBOX_X + 10, MSGBOX_Y + 9, line1, COL_BLACK);
if (line2 && line2[0]) {
vga_draw_text(MSGBOX_X + 10, MSGBOX_Y + 24, line2, COL_BLACK);
}
}
void battle_gui_menu(const char *move_names[3], const int move_powers[3]) {
draw_message_box_frame();
int col_w = MSGBOX_W / 2;
for (int i = 0; i < 3; i++) {
int col = i % 2;
int row = i / 2;
int tx = MSGBOX_X + 10 + col * col_w;
int ty = MSGBOX_Y + 9 + row * 16;
char label[4] = { (char)('1' + i), ')', ' ', '\0' };
char pow_buf[8];
char pow_str[12] = { ' ', '(', '\0' };
gui_concat(pow_str, sizeof(pow_str), " (", itoa(move_powers[i], pow_buf, 10), ")", NULL);
char line[28];
gui_concat(line, sizeof(line), label, move_names[i], pow_str, NULL);
vga_draw_text(tx, ty, line, COL_BLACK);
}
vga_draw_text(MSGBOX_X + 10 + col_w, MSGBOX_Y + 9 + 16, "4) Run", COL_BLACK);
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/game/battle_gui.c (file state is current in your context — no need to Read it back)
$ grep -n -i "initializ" /home/martin/workspace/prototypes/boot-to-claude/docs/toolchain.md | head -30
189:Brace initializer lists (`{1, 2, 3}`) are only supported for globals/
191:never brace-initializes a plain stack local, only scalars, so codegen
193:Compound literals (`(type){...}`) and designated initializers
499: separator in call args/declarators/initializers, which already call
549: initializer) left `array_len` at -1** in both the global
559: returning one. Fixed by counting the initializer list's elements once
562: (`a = b;`), a non-brace initializer (`T save = *p;` -
733: (one member a 32-element array) only initialized the *first* member;
737: uninitialized bounds counter) and the outer parse loop's "did we
744:- **A string-literal initializer for a *pointer-typed* struct field- int xs[3] = { 24, 140, 256 };
- for (int i = 0; i < 3; i++) {
- sprite_draw(i, xs[i], 80);
- char label[4] = { (char)('1' + i), ')', ' ', '\0' };
- char line[24];
- gui_concat(line, sizeof(line), label, names[i], NULL, NULL);
- vga_draw_text(xs[i] - 4, 150, line, COL_WHITE);
- }
+ static const int xs[3] = { 24, 140, 256 };
+ for (int i = 0; i < 3; i++) {
+ sprite_draw(i, xs[i], 80);
+ char label[4];
+ label[0] = (char)('1' + i);
+ label[1] = ')';
+ label[2] = ' ';
+ label[3] = '\0';
+ char line[24];
+ gui_concat(line, sizeof(line), label, names[i], NULL, NULL);
+ vga_draw_text(xs[i] - 4, 150, line, COL_WHITE);
+ }The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/battle_gui.c has been updated successfully. (file state is current in your context — no need to Read it back)
- char label[4] = { (char)('1' + i), ')', ' ', '\0' };
- char pow_buf[8];
- char pow_str[12] = { ' ', '(', '\0' };
- gui_concat(pow_str, sizeof(pow_str), " (", itoa(move_powers[i], pow_buf, 10), ")", NULL);
- char line[28];
+ char label[4];
+ label[0] = (char)('1' + i);
+ label[1] = ')';
+ label[2] = ' ';
+ label[3] = '\0';
+ char pow_buf[8];
+ char pow_str[12];
+ gui_concat(pow_str, sizeof(pow_str), " (", itoa(move_powers[i], pow_buf, 10), ")", NULL);
+ char line[28];The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/battle_gui.c has been updated successfully. (file state is current in your context — no need to Read it back)
- #include "game.h" - - #include <stdint.h> - - #include "console.h" - #include "input.h" - #include "serial.h" - #include "string.h" + #include "game.h" + + #include <stdint.h> + + #include "battle_gui.h" + #include "console.h" + #include "input.h" + #include "serial.h" + #include "string.h"
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/game.c has been updated successfully. (file state is current in your context — no need to Read it back)
- /* --- status line helpers --- */
+ /* Concatenates up to 4 strings (any may be NULL) into out, truncating to
+ * n-1 bytes - this kernel has no snprintf, and every graphical message
+ * this module builds is a small, fixed number of parts. Duplicated from
+ * battle_gui.c's identical helper rather than shared, matching this
+ * codebase's existing convention of small per-module helper duplication
+ * (see shell.c's read_line vs. this file's, or its echo_char). */
+ static void gcat(char *out, int n, const char *a, const char *b, const char *c, const char *d) {
+ const char *parts[4] = { a, b, c, d };
+ int k = 0;
+ for (int p = 0; p < 4; p++) {
+ const char *s = parts[p];
+ if (!s) {
+ continue;
+ }
+ for (int i = 0; s[i] && k < n - 1; i++) {
+ out[k++] = s[i];
+ }
+ }
+ out[k] = '\0';
+ }
+
+ /* --- status line helpers --- */The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/game.c has been updated successfully. (file state is current in your context — no need to Read it back)
- static void gcat(char *out, int n, const char *a, const char *b, const char *c, const char *d) {
- const char *parts[4] = { a, b, c, d };
- int k = 0;
- for (int p = 0; p < 4; p++) {
- const char *s = parts[p];
- if (!s) {
- continue;
- }
- for (int i = 0; s[i] && k < n - 1; i++) {
- out[k++] = s[i];
- }
- }
- out[k] = '\0';
- }
+ static void gcat(char *out, int n, const char *a, const char *b, const char *c, const char *d) {
+ int k = 0;
+ if (a) {
+ for (int i = 0; a[i] && k < n - 1; i++) {
+ out[k++] = a[i];
+ }
+ }
+ if (b) {
+ for (int i = 0; b[i] && k < n - 1; i++) {
+ out[k++] = b[i];
+ }
+ }
+ if (c) {
+ for (int i = 0; c[i] && k < n - 1; i++) {
+ out[k++] = c[i];
+ }
+ }
+ if (d) {
+ for (int i = 0; d[i] && k < n - 1; i++) {
+ out[k++] = d[i];
+ }
+ }
+ out[k] = '\0';
+ }The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/game.c has been updated successfully. (file state is current in your context — no need to Read it back)
- static void gui_concat(char *out, int n, const char *a, const char *b, const char *c, const char *d) {
- const char *parts[4] = { a, b, c, d };
- int k = 0;
- for (int p = 0; p < 4; p++) {
- const char *s = parts[p];
- if (!s) {
- continue;
- }
- for (int i = 0; s[i] && k < n - 1; i++) {
- out[k++] = s[i];
- }
- }
- out[k] = '\0';
- }
+ static void gui_concat(char *out, int n, const char *a, const char *b, const char *c, const char *d) {
+ int k = 0;
+ if (a) {
+ for (int i = 0; a[i] && k < n - 1; i++) {
+ out[k++] = a[i];
+ }
+ }
+ if (b) {
+ for (int i = 0; b[i] && k < n - 1; i++) {
+ out[k++] = b[i];
+ }
+ }
+ if (c) {
+ for (int i = 0; c[i] && k < n - 1; i++) {
+ out[k++] = c[i];
+ }
+ }
+ if (d) {
+ for (int i = 0; d[i] && k < n - 1; i++) {
+ out[k++] = d[i];
+ }
+ }
+ out[k] = '\0';
+ }The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/battle_gui.c has been updated successfully. (file state is current in your context — no need to Read it back)
180 * words - covers both a human pressing a number key and an AI agent
181 * typing a full word, whichever is more natural for it. Returns the
182 * choice as 1-9, 0 for "run", -1 for "quit", -2 if unrecognized. */
183 static int parse_choice(const char *line) {
184 while (*line == ' ') {
185 line++;
186 }
187 if (*line >= '1' && *line <= '9') {
188 return *line - '0';
189 }
190 if (strcmp(line, "run") == 0) {
191 return 0;
192 }
193 if (strcmp(line, "quit") == 0) {
194 return -1;
195 }
196 return -2;
197 }
198
199 /* Returns the chosen starter index (0..NUM_SPECIES-1), or -1 if the
200 * player typed "quit". */
201 static int choose_starter(void) {
202 char line[LINE_MAX];
203 wln("Choose your starter:");
204 for (int i = 0; i < NUM_SPECIES; i++) {
205 w_int(i + 1);
206 w(") ");
207 w(SPECIES[i].name);
208 w(" (");
209 w(SPECIES[i].type == TYPE_FIRE ? "Fire" : SPECIES[i].type == TYPE_WATER ? "Water" : "Grass");
210 wln(")");
211 }
212 for (;;) {
213 w("> ");
214 read_line(line, sizeof(line));
215 int choice = parse_choice(line);
216 if (choice == -1) {
217 return -1;
218 }
219 if (choice >= 1 && choice <= NUM_SPECIES) {- static int choose_starter(void) {
- char line[LINE_MAX];
- wln("Choose your starter:");
+ static int choose_starter(void) {
+ char line[LINE_MAX];
+ const char *names[NUM_SPECIES];
+ for (int i = 0; i < NUM_SPECIES; i++) {
+ names[i] = SPECIES[i].name;
+ }
+ battle_gui_starter_select(names);
+ wln("Choose your starter:");The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/game.c has been updated successfully. (file state is current in your context — no need to Read it back)
225 return choice - 1;
226 }
227 wln("invalid choice - type a number, 'run', or 'quit'");
228 }
229 }
230
231 void game_run(void) {
232 char line[LINE_MAX];
233
234 wln("=== Tiny Monsters ===");
235 wln("Defeat 5 wild monsters to become champion. Type 'quit' any time to leave.");
236
237 int player_idx = choose_starter();
238 if (player_idx < 0) {
239 wln("Left the game.");
240 return;
241 }
242 const species_t *player = &SPECIES[player_idx];
243 int player_hp = player->max_hp;
244 int stage = 1;
245
246 for (;;) {
247 int wild_idx = rng_range(0, NUM_SPECIES - 1);
248 const species_t *wild = &SPECIES[wild_idx];
249 int wild_max_hp = wild->max_hp + (stage - 1) * 4;
250 int wild_hp = wild_max_hp;
251
252 w("=== A wild ");
253 w(wild->name);
254 w(" appears! (stage ");
255 w_int(stage);
256 wln(")");
257
258 for (;;) {
259 print_hp("Your ", player->name, player_hp, player->max_hp);
260 print_hp("Wild ", wild->name, wild_hp, wild_max_hp);
261 for (int i = 0; i < 3; i++) {
262 w_int(i + 1);
263 w(") ");
264 w(player->moves[i].name);
265 w(" (");
266 w_int(player->moves[i].power);
267 wln(")");
268 }
269 wln("4) Run");
270 w("> ");
271 read_line(line, sizeof(line));
272 int choice = parse_choice(line);
273
274 if (choice == -1) {
275 wln("Left the game.");
276 return;
277 }
278 if (choice == 0 || choice == 4) {
279 wln("You ran away!");
280 break; /* next encounter, same stage */
281 }
282 if (choice < 1 || choice > 3) {
283 wln("invalid choice - type 1-3 for a move, 'run', or 'quit'");
284 continue;
285 }
286
287 const move_t *mv = &player->moves[choice - 1];
288 w("Your ");
289 w(player->name);
290 w(" uses ");
291 w(mv->name);
292 wln("!");
293 int dmg = compute_damage(player->type, wild->type, mv->power);
294 wild_hp -= dmg;
295 if (wild_hp < 0) {
296 wild_hp = 0;
297 }
298 w("Wild ");
299 w(wild->name);
300 w(" takes ");
301 w_int(dmg);
302 w(" damage. (HP ");
303 w_int(wild_hp);
304 w("/");
305 w_int(wild_max_hp);
306 wln(")");
307
308 if (wild_hp <= 0) {
309 w("Wild ");
310 w(wild->name);
311 wln(" fainted! You win!");
312 stage++;
313 if (stage > WIN_STAGE) {
314 w("*** You defeated ");
315 w_int(WIN_STAGE);
316 wln(" wild monsters - you are the Champion! ***");
317 return;
318 }
319 break; /* next encounter */
320 }
321
322 const move_t *wmv = &wild->moves[rng_range(0, 2)];
323 w("Wild ");
324 w(wild->name);
325 w(" uses ");
326 w(wmv->name);
327 wln("!");
328 int wdmg = compute_damage(wild->type, player->type, wmv->power);
329 player_hp -= wdmg;
330 if (player_hp < 0) {
331 player_hp = 0;
332 }
333 w("Your ");
334 w(player->name);
335 w(" takes ");
336 w_int(wdmg);
337 w(" damage. (HP ");
338 w_int(player_hp);
339 w("/");
340 w_int(player->max_hp);
341 wln(")");
342
343 if (player_hp <= 0) {
344 w("Your ");344 w("Your ");
345 w(player->name);
346 w(" fainted... Game over! You reached stage ");
347 w_int(stage);
348 wln(".");
349 return;
350 }
351 }
352 }
353 }- void game_run(void) {
- char line[LINE_MAX];
-
- wln("=== Tiny Monsters ===");
- wln("Defeat 5 wild monsters to become champion. Type 'quit' any time to leave.");
-
- int player_idx = choose_starter();
- if (player_idx < 0) {
- wln("Left the game.");
- return;
- }
- const species_t *player = &SPECIES[player_idx];
- int player_hp = player->max_hp;
- int stage = 1;
-
- for (;;) {
- int wild_idx = rng_range(0, NUM_SPECIES - 1);
- const species_t *wild = &SPECIES[wild_idx];
- int wild_max_hp = wild->max_hp + (stage - 1) * 4;
- int wild_hp = wild_max_hp;
-
- w("=== A wild ");
- w(wild->name);
- w(" appears! (stage ");
- w_int(stage);
- wln(")");
-
- for (;;) {
- print_hp("Your ", player->name, player_hp, player->max_hp);
- print_hp("Wild ", wild->name, wild_hp, wild_max_hp);
- for (int i = 0; i < 3; i++) {
- w_int(i + 1);
- w(") ");
- w(player->moves[i].name);
- w(" (");
- w_int(player->moves[i].power);
- wln(")");
- }
- wln("4) Run");
- w("> ");
- read_line(line, sizeof(line));
- int choice = parse_choice(line);
-
- if (choice == -1) {
- wln("Left the game.");
- return;
- }
- if (choice == 0 || choice == 4) {
- wln("You ran away!");
- break; /* next encounter, same stage */
- }
- if (choice < 1 || choice > 3) {
- wln("invalid choice - type 1-3 for a move, 'run', or 'quit'");
- continue;
- }
-
- const move_t *mv = &player->moves[choice - 1];
- w("Your ");
- w(player->name);
- w(" uses ");
- w(mv->name);
- wln("!");
- int dmg = compute_damage(player->type, wild->type, mv->power);
- wild_hp -= dmg;
- if (wild_hp < 0) {
- wild_hp = 0;
- }
- w("Wild ");
- w(wild->name);
- w(" takes ");
- w_int(dmg);
- w(" damage. (HP ");
- w_int(wild_hp);
- w("/");
- w_int(wild_max_hp);
- wln(")");
-
- if (wild_hp <= 0) {
- w("Wild ");
- w(wild->name);
- wln(" fainted! You win!");
- stage++;
- if (stage > WIN_STAGE) {
- w("*** You defeated ");
- w_int(WIN_STAGE);
- wln(" wild monsters - you are the Champion! ***");
- return;
- }
- break; /* next encounter */
- }
-
- const move_t *wmv = &wild->moves[rng_range(0, 2)];
- w("Wild ");
- w(wild->name);
- w(" uses ");
- w(wmv->name);
- wln("!");
- int wdmg = compute_damage(wild->type, player->type, wmv->power);
- player_hp -= wdmg;
- if (player_hp < 0) {
- player_hp = 0;
- }
- w("Your ");
- w(player->name);
- w(" takes ");
- w_int(wdmg);
- w(" damage. (HP ");
- w_int(player_hp);
- w("/");
- w_int(player->max_hp);
- wln(")");
-
- if (player_hp <= 0) {
- w("Your ");
- w(player->name);
- w(" fainted... Game over! You reached stage ");
- w_int(stage);
- wln(".");
- return;
- }
- }
- }
- }
+ /* All the game logic and its several early returns live here, unchanged
+ * in behavior from before the graphics existed - game_run() below just
+ * wraps it with a single, guaranteed battle_gui_enter/exit pair instead
+ * of needing a matching exit call before every return. */
+ static void game_run_inner(void) {
+ char line[LINE_MAX];
+
+ wln("=== Tiny Monsters ===");
+ wln("Defeat 5 wild monsters to become champion. Type 'quit' any time to leave.");
+
+ int player_idx = choose_starter();
+ if (player_idx < 0) {
+ wln("Left the game.");
+ return;
+ }
+ const species_t *player = &SPECIES[player_idx];
+ int player_hp = player->max_hp;
+ int stage = 1;
+
+ for (;;) {
+ int wild_idx = rng_range(0, NUM_SPECIES - 1);
+ const species_t *wild = &SPECIES[wild_idx];
+ int wild_max_hp = wild->max_hp + (stage - 1) * 4;
+ int wild_hp = wild_max_hp;
+
+ w("=== A wild ");
+ w(wild->name);
+ w(" appears! (stage ");
+ w_int(stage);
+ wln(")");
+
+ {
+ char msg[48];
+ gcat(msg, sizeof(msg), "A wild ", wild->name, " appears!", NULL);
+ battle_gui_scene(player_idx, player_hp, player->max_hp, player->name,
+ wild_idx, wild_hp, wild_max_hp, wild->name);
+ battle_gui_message(msg, NULL);
+ }
+
+ for (;;) {
+ print_hp("Your ", player->name, player_hp, player->max_hp);
+ print_hp("Wild ", wild->name, wild_hp, wild_max_hp);
+ for (int i = 0; i < 3; i++) {
+ w_int(i + 1);
+ w(") ");
+ w(player->moves[i].name);
+ w(" (");
+ w_int(player->moves[i].power);
+ wln(")");
+ }
+ wln("4) Run");
+
+ {
+ const char *move_names[3];
+ int move_powers[3];
+ for (int i = 0; i < 3; i++) {
+ move_names[i] = player->moves[i].name;
+ move_powers[i] = player->moves[i].power;
+ }
+ battle_gui_scene(player_idx, player_hp, player->max_hp, player->name,
+ wild_idx, wild_hp, wild_max_hp, wild->name);
+ battle_gui_menu(move_names, move_powers);
+ }
+
+ w("> ");
+ read_line(line, sizeof(line));
+ int choice = parse_choice(line);
+
+ if (choice == -1) {
+ wln("Left the game.");
+ return;
+ }
+ if (choice == 0 || choice == 4) {
+ wln("You ran away!");
+ battle_gui_message("You ran away!", NULL);
+ break; /* next encounter, same stage */
+ }
+ if (choice < 1 || choice > 3) {
+ wln("invalid choice - type 1-3 for a move, 'run', or 'quit'");
+ continue;
+ }
+
+ const move_t *mv = &player->moves[choice - 1];
+ w("Your ");
+ w(player->name);
+ w(" uses ");
+ w(mv->name);
+ wln("!");
+ int dmg = compute_damage(player->type, wild->type, mv->power);
+ wild_hp -= dmg;
+ if (wild_hp < 0) {
+ wild_hp = 0;
+ }
+ w("Wild ");
+ w(wild->name);
+ w(" takes ");
+ w_int(dmg);
+ w(" damage. (HP ");
+ w_int(wild_hp);
+ w("/");
+ w_int(wild_max_hp);
+ wln(")");
+
+ {
+ char msg1[48];
+ char msg2[24];
+ char dmg_buf[8];
+ gcat(msg1, sizeof(msg1), "Your ", player->name, " used ", mv->name);
+ gcat(msg2, sizeof(msg2), "-", itoa(dmg, dmg_buf, 10), " HP", NULL);
+ battle_gui_scene(player_idx, player_hp, player->max_hp, player->name,
+ wild_idx, wild_hp, wild_max_hp, wild->name);
+ battle_gui_message(msg1, msg2);
+ }
+
+ if (wild_hp <= 0) {
+ w("Wild ");
+ w(wild->name);
+ wln(" fainted! You win!");
+ battle_gui_message("Wild fainted! You win!", NULL);
+ stage++;
+ if (stage > WIN_STAGE) {
+ w("*** You defeated ");
+ w_int(WIN_STAGE);
+ wln(" wild monsters - you are the Champion! ***");
+ battle_gui_message("You are the Champion!", NULL);
+ return;
+ }
+ break; /* next encounter */
+ }
+
+ const move_t *wmv = &wild->moves[rng_range(0, 2)];
+ w("Wild ");
+ w(wild->name);
+ w(" uses ");
+ w(wmv->name);
+ wln("!");
+ int wdmg = compute_damage(wild->type, player->type, wmv->power);
+ player_hp -= wdmg;
+ if (player_hp < 0) {
+ player_hp = 0;
+ }
+ w("Your ");
+ w(player->name);
+ w(" takes ");
+ w_int(wdmg);
+ w(" damage. (HP ");
+ w_int(player_hp);
+ w("/");
+ w_int(player->max_hp);
+ wln(")");
+
+ {
+ char msg1[48];
+ char msg2[24];
+ char dmg_buf[8];
+ gcat(msg1, sizeof(msg1), "Wild ", wild->name, " used ", wmv->name);
+ gcat(msg2, sizeof(msg2), "-", itoa(wdmg, dmg_buf, 10), " HP", NULL);
+ battle_gui_scene(player_idx, player_hp, player->max_hp, player->name,
+ wild_idx, wild_hp, wild_max_hp, wild->name);
+ battle_gui_message(msg1, msg2);
+ }
+
+ if (player_hp <= 0) {
+ w("Your ");
+ w(player->name);
+ w(" fainted... Game over! You reached stage ");
+ w_int(stage);
+ wln(".");
+ battle_gui_message("Your monster fainted...", "Game over!");
+ return;
+ }
+ }
+ }
+ }
+
+ void game_run(void) {
+ battle_gui_enter();
+ game_run_inner();
+ battle_gui_exit();
+ }The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/game.c has been updated successfully. (file state is current in your context — no need to Read it back)
- kernel/shell/shell.c \ - kernel/game/game.c \ + kernel/shell/shell.c \ + kernel/game/game.c kernel/game/battle_gui.c kernel/game/sprites.c \
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude make -s 2>&1 | tail -80 echo "EXIT:$?"
mkfs_tinyfs: done EXIT:0
$ cd /home/martin/workspace/prototypes/boot-to-claude pkill -9 -f qemu-system-i386 2>/dev/null bash tools/run_tests.sh 2>&1 | tail -40
Exit code 1
$ cd /home/martin/workspace/prototypes/boot-to-claude bash tools/run_tests.sh 2>&1 | tail -40; echo EXIT=$?
run_tests: PASS qemu-system-i386: terminating on signal 15 from pid 1758150 (bash) EXIT=0
$ cd /home/martin/workspace/prototypes/boot-to-claude bash tools/run_serial_tests.sh 2>&1 | tail -40; echo EXIT=$?
ok: shell reachable over serial (no keyboard involved) ok: game reachable, starter menu -> battle screen ok: battle played to completion over serial (Game over!) ok: shell still responsive after the game returned serial_game_test: PASS qemu-system-i386: terminating on signal 15 from pid 1758214 (bash) EXIT=0
$ cd /home/martin/workspace/prototypes/boot-to-claude pkill -9 -f qemu-system-i386 2>/dev/null rm -f build/serial.log build/qemu-monitor.sock nohup $(which qemu-system-i386) -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot > build/qemu.out 2>&1 & sleep 2 tail -c 300 build/serial.log
Exit code 1
$ tail -c 300 /home/martin/workspace/prototypes/boot-to-claude/build/serial.log
(stage 1) Your Emberpup: HP 28/28 Wild Leafling: HP 32/32 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run > run You ran away! === A wild Leafling appears! (stage 1) Your Emberpup: HP 28/28 Wild Leafling: HP 32/32 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run > quit Left the game. >
$ ps aux | grep qemu-system-i386 | grep -v grep
(Bash completed with no output)
$ cat /home/martin/workspace/prototypes/boot-to-claude/build/qemu.out 2>&1 | tail -20 ls -la /home/martin/workspace/prototypes/boot-to-claude/build/serial.log 2>&1
-rw-r----- 1 martin martin 1119 Jul 22 17:03 /home/martin/workspace/prototypes/boot-to-claude/build/serial.log
$ rm -f /home/martin/workspace/prototypes/boot-to-claude/build/serial.log /home/martin/workspace/prototypes/boot-to-claude/build/qemu-monitor.sock /home/martin/workspace/prototypes/boot-to-claude/build/qemu.out
(Bash completed with no output)
$ nohup qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot > build/qemu.out 2>&1 & disown sleep 2 echo done
done
$ pwd; ps aux | grep qemu-system-i386 | grep -v grep; ls -la build/serial.log 2>&1
/home/martin/workspace/prototypes/boot-to-claude martin 1758305 100 0.1 1740904 49504 ? Sl 17:04 0:05 qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot -rw-r----- 1 martin martin 99 Jul 22 17:04 build/serial.log
$ cd /home/martin/workspace/prototypes/boot-to-claude tail -c 200 build/serial.log
tiny-os: kernel entered selftest: running SELFTEST PASS tiny-os shell. type 'help' for commands. >
$ cd /home/martin/workspace/prototypes/boot-to-claude python3 tools/qemu_type.py build/qemu-monitor.sock "play " sleep 1 tail -c 300 build/serial.log
tiny-os: kernel entered selftest: running SELFTEST PASS tiny-os shell. type 'help' for commands. > play === Tiny Monsters === Defeat 5 wild monsters to become champion. Type 'quit' any time to leave. Choose your starter: 1) Emberpup (Fire) 2) Aqualing (Water) 3) Leafling (Grass) >
$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 -c "
import socket, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect('build/qemu-monitor.sock')
time.sleep(0.2)
s.recv(4096)
s.sendall(b'screendump build/shot_starter.ppm\n')
time.sleep(0.5)
s.recv(4096)
s.close()
"
ffmpeg -y -i build/shot_starter.ppm build/shot_starter.png -loglevel error
ls -la build/shot_starter.png-rw-r----- 1 martin martin 3883 Jul 22 17:04 build/shot_starter.png

$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 tools/qemu_type.py build/qemu-monitor.sock "1
"
sleep 1
python3 -c "
import socket, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect('build/qemu-monitor.sock')
time.sleep(0.2)
s.recv(4096)
s.sendall(b'screendump build/shot_battle.ppm\n')
time.sleep(0.5)
s.recv(4096)
s.close()
"
ffmpeg -y -i build/shot_battle.ppm build/shot_battle.png -loglevel error
tail -c 300 build/serial.logfeat 5 wild monsters to become champion. Type 'quit' any time to leave. Choose your starter: 1) Emberpup (Fire) 2) Aqualing (Water) 3) Leafling (Grass) > 1 === A wild Emberpup appears! (stage 1) Your Emberpup: HP 28/28 Wild Emberpup: HP 28/28 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run >

- static void draw_background(void) {
- vga_fill_rect(0, 0, VGA_WIDTH, 113, COL_SKY);
- vga_fill_rect(0, 113, VGA_WIDTH, 4, COL_HORIZON);
- vga_fill_rect(0, 117, VGA_WIDTH, VGA_HEIGHT - 117, COL_GROUND);
- vga_fill_ellipse(240, 90, 45, 9, COL_PLATFORM);
- vga_fill_ellipse(75, 175, 55, 11, COL_PLATFORM);
- }
+ /* The battlefield (sky+ground) occupies everything above the message
+ * box (MSGBOX_Y); the ground band stops right at that line so the
+ * player's platform/sprite never gets clipped by the box drawn on top
+ * of it. */
+ static void draw_background(void) {
+ vga_fill_rect(0, 0, VGA_WIDTH, 95, COL_SKY);
+ vga_fill_rect(0, 95, VGA_WIDTH, 4, COL_HORIZON);
+ vga_fill_rect(0, 99, VGA_WIDTH, MSGBOX_Y - 99, COL_GROUND);
+ vga_fill_ellipse(240, 75, 45, 8, COL_PLATFORM);
+ vga_fill_ellipse(75, 138, 55, 8, COL_PLATFORM);
+ }The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/battle_gui.c has been updated successfully. (file state is current in your context — no need to Read it back)
- draw_background(); - sprite_draw(wild_species, 210, 45); - sprite_draw(player_species, 35, 125); + draw_background(); + sprite_draw(wild_species, 210, 35); + sprite_draw(player_species, 30, 100);
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/battle_gui.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude make -s 2>&1 | tail -40; echo EXIT=$?
mkfs_tinyfs: done EXIT=0
$ cd /home/martin/workspace/prototypes/boot-to-claude pkill -9 -f qemu-system-i386 2>/dev/null sleep 0.3 rm -f build/serial.log build/qemu-monitor.sock build/qemu.out
Exit code 1
$ cd /home/martin/workspace/prototypes/boot-to-claude nohup qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot > build/qemu.out 2>&1 & disown sleep 2 tail -c 200 build/serial.log
tiny-os: kernel entered selftest: running SELFTEST PASS tiny-os shell. type 'help' for commands. >
$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 tools/qemu_type.py build/qemu-monitor.sock "play
1
"
sleep 1
python3 -c "
import socket, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect('build/qemu-monitor.sock')
time.sleep(0.2)
s.recv(4096)
s.sendall(b'screendump build/shot_battle2.ppm\n')
time.sleep(0.5)
s.recv(4096)
s.close()
"
ffmpeg -y -i build/shot_battle2.ppm build/shot_battle2.png -loglevel error
echo okok

$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 tools/qemu_type.py build/qemu-monitor.sock "1
"
sleep 1
python3 -c "
import socket, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect('build/qemu-monitor.sock')
time.sleep(0.2)
s.recv(4096)
s.sendall(b'screendump build/shot_dmg.ppm\n')
time.sleep(0.5)
s.recv(4096)
s.close()
"
ffmpeg -y -i build/shot_dmg.ppm build/shot_dmg.png -loglevel error
tail -c 400 build/serial.logYour Emberpup: HP 28/28 Wild Aqualing: HP 30/30 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run > 1 Your Emberpup uses Ember! It's not very effective... Wild Aqualing takes 2 damage. (HP 28/30) Wild Aqualing uses Tide Wave! It's super effective! Your Emberpup takes 14 damage. (HP 14/28) Your Emberpup: HP 14/28 Wild Aqualing: HP 28/30 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run >

34 0x5F, 0x4F, 0x50, 0x82, 0x54, 0x80, 0xBF, 0x1F,
35 0x00, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
36 0x9C, 0x0E, 0x8F, 0x28, 0x40, 0x96, 0xB9, 0xA3,
37 0xFF,
38 };
39 static const uint8_t MODE13_GC[9] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x05, 0x0F, 0xFF };
40 static const uint8_t MODE13_AC[21] = {
41 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
42 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
43 0x41, 0x00, 0x0F, 0x00, 0x00,- static const uint8_t MODE13_CRTC[25] = {
- 0x5F, 0x4F, 0x50, 0x82, 0x54, 0x80, 0xBF, 0x1F,
- 0x00, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x9C, 0x0E, 0x8F, 0x28, 0x40, 0x96, 0xB9, 0xA3,
- 0xFF,
- };
+ static const uint8_t MODE13_CRTC[25] = {
+ 0x5F, 0x4F, 0x50, 0x82, 0x54, 0x80, 0xBF, 0x1F,
+ 0x00, 0x41, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x9C, 0x0E, 0x8F, 0x28, 0x40, 0x96, 0xB9, 0xA3,
+ 0xFF,
+ };The file /home/martin/workspace/prototypes/boot-to-claude/kernel/vga.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude make -s 2>&1 | tail -40; echo EXIT=$? pkill -9 -f qemu-system-i386 2>/dev/null sleep 0.3
Exit code 1 mkfs_tinyfs: done EXIT=0
$ cd /home/martin/workspace/prototypes/boot-to-claude
rm -f build/serial.log build/qemu-monitor.sock build/qemu.out
nohup qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot > build/qemu.out 2>&1 &
disown
sleep 2
python3 tools/qemu_type.py build/qemu-monitor.sock "play
1
"
sleep 1
python3 -c "
import socket, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect('build/qemu-monitor.sock')
time.sleep(0.2)
s.recv(4096)
s.sendall(b'screendump build/shot_cursor.ppm\n')
time.sleep(0.5)
s.recv(4096)
s.close()
"
ffmpeg -y -i build/shot_cursor.ppm build/shot_cursor.png -loglevel error
echo okok

$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 -c "
from PIL import Image
im = Image.open('build/shot_cursor.ppm')
print(im.size)
"
(640, 400)
$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 -c "
from PIL import Image
im = Image.open('build/shot_cursor.ppm')
crop = im.crop((550,260,640,320)).resize((360,240), Image.NEAREST)
crop.save('build/crop_dot.png')
"
(Bash completed with no output)

$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 -c "
from PIL import Image
im = Image.open('build/shot_starter.ppm')
print(im.size)
crop = im.crop((550,260,640,320)).resize((360,240), Image.NEAREST)
crop.save('build/crop_dot_starter.png')
"
(640, 400)

$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 -c "
from PIL import Image
im = Image.open('build/shot_cursor.ppm')
for y in range(295,305):
row=[]
for x in range(615,635):
row.append(im.getpixel((x,y)))
print(y,row)
"
295 [(80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63)] 296 [(0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63)] 297 [(0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63)] 298 [(232, 224, 184), (232, 224, 184), (232, 224, 184), (232, 224, 184), (232, 224, 184), (232, 224, 184), (232, 224, 184), (0, 0, 0), (0, 0, 0), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63)] 299 [(232, 224, 184), (232, 224, 184), (232, 224, 184), (232, 224, 184), (232, 224, 184), (232, 224, 184), (232, 224, 184), (0, 0, 0), (0, 0, 0), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63)] 300 [(0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (232, 224, 184), (232, 224, 184), (0, 0, 0), (0, 0, 0), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63)] 301 [(0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (232, 224, 184), (232, 224, 184), (0, 0, 0), (0, 0, 0), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63)] 302 [(232, 224, 184), (232, 224, 184), (232, 224, 184), (0, 0, 0), (0, 0, 0), (232, 224, 184), (232, 224, 184), (0, 0, 0), (0, 0, 0), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63)] 303 [(232, 224, 184), (232, 224, 184), (232, 224, 184), (0, 0, 0), (0, 0, 0), (232, 224, 184), (232, 224, 184), (0, 0, 0), (0, 0, 0), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63), (80, 183, 63)] 304 [(232, 224, 184), (232, 224, 184), (232, 224, 184), (0, 0, 0), (0, 0, 0), (232, 224, 184), (232, 224, 184), (0, 0, 0), (0, 0, 0), (80, 183, 63), (80, 183, 63), (255, 255, 255), (255, 255, 255), (255, 255, 255), (255, 255, 255), (255, 255, 255), (255, 255, 255), (255, 255, 255), (255, 255, 255), (80, 183, 63)]
$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 -c "
from PIL import Image
im = Image.open('build/shot_cursor.ppm')
crop = im.crop((600,280,640,320)).resize((320,320), Image.NEAREST)
crop.save('build/crop_dot2.png')
print(im.size)
"
(640, 400)

$ cd /home/martin/workspace/prototypes/boot-to-claude
pkill -9 -f qemu-system-i386 2>/dev/null
sleep 0.3
rm -f build/serial.log build/qemu-monitor.sock build/qemu.out build/shot_fresh.ppm
nohup qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot > build/qemu.out 2>&1 &
disown
sleep 2
python3 -c "
import socket, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect('build/qemu-monitor.sock')
time.sleep(0.2)
s.recv(4096)
s.sendall(b'screendump build/shot_fresh.ppm\n')
time.sleep(0.5)
s.recv(4096)
s.close()
"
python3 -c "
from PIL import Image
im = Image.open('build/shot_fresh.ppm')
print(im.size)
crop = im.crop((600,280,640,320)).resize((320,320), Image.NEAREST)
crop.save('build/crop_dot_fresh.png')
"Exit code 1
$ ls -la /home/martin/workspace/prototypes/boot-to-claude/build/shot_fresh.ppm 2>&1; cat /home/martin/workspace/prototypes/boot-to-claude/build/qemu.out
ls: cannot access '/home/martin/workspace/prototypes/boot-to-claude/build/shot_fresh.ppm': No such file or directory
$ ps aux | grep qemu-system-i386 | grep -v grep
(Bash completed with no output)
$ rm -f build/serial.log build/qemu-monitor.sock build/qemu.out build/shot_fresh.ppm && echo cleaned
cleaned
$ pwd
/home/martin/workspace/prototypes/boot-to-claude
$ nohup qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot > build/qemu.out 2>&1 & disown sleep 2 ps aux | grep qemu-system-i386 | grep -v grep
martin 1759489 100 0.1 1740904 49388 ? Sl 17:13 0:02 qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot
$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 -c "
import socket, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect('build/qemu-monitor.sock')
time.sleep(0.2)
s.recv(4096)
s.sendall(b'screendump build/shot_fresh.ppm\n')
time.sleep(0.5)
s.recv(4096)
s.close()
"
ls -la build/shot_fresh.ppm-rw-r----- 1 martin martin 864015 Jul 22 17:13 build/shot_fresh.ppm
$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 -c "
from PIL import Image
im = Image.open('build/shot_fresh.ppm')
crop = im.crop((600,280,640,320)).resize((320,320), Image.NEAREST)
crop.save('build/crop_dot_fresh.png')
"(Bash completed with no output)

$ grep -n "^build" /home/martin/workspace/prototypes/boot-to-claude/.gitignore
1:build/
$ cd /home/martin/workspace/prototypes/boot-to-claude python3 tools/qemu_type.py build/qemu-monitor.sock "play 1 3 3 3 3 3 3 " sleep 1 tail -c 900 build/serial.log
berpup uses Flame Burst! Your Emberpup takes 11 damage. (HP 7/28) Your Emberpup: HP 7/28 Wild Emberpup: HP 9/28 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run > 3 Your Emberpup uses Flame Burst! Wild Emberpup takes 8 damage. (HP 1/28) Wild Emberpup uses Scratch! Your Emberpup takes 4 damage. (HP 3/28) Your Emberpup: HP 3/28 Wild Emberpup: HP 1/28 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run > 3 Your Emberpup uses Flame Burst! Wild Emberpup takes 8 damage. (HP 0/28) Wild Emberpup fainted! You win! === A wild Emberpup appears! (stage 2) Your Emberpup: HP 3/28 Wild Emberpup: HP 32/32 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run > 3 Your Emberpup uses Flame Burst! Wild Emberpup takes 11 damage. (HP 21/32) Wild Emberpup uses Scratch! Your Emberpup takes 6 damage. (HP 0/28) Your Emberpup fainted... Game over! You reached stage 2. > 3 unknown command (type 'help') >
$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 tools/qemu_type.py build/qemu-monitor.sock "play
1
"
sleep 1
python3 -c "
import socket, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect('build/qemu-monitor.sock')
time.sleep(0.2); s.recv(4096)
s.sendall(b'screendump build/shot_low.ppm\n')
time.sleep(0.5); s.recv(4096)
s.close()
"
ffmpeg -y -i build/shot_low.ppm build/shot_low.png -loglevel error
tail -c 200 build/serial.logrpup (Fire) 2) Aqualing (Water) 3) Leafling (Grass) > 1 === A wild Emberpup appears! (stage 1) Your Emberpup: HP 28/28 Wild Emberpup: HP 28/28 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run >
$ cd /home/martin/workspace/prototypes/boot-to-claude python3 tools/qemu_type.py build/qemu-monitor.sock "3 3 3 3 3 " sleep 1 tail -c 500 build/serial.log
d Emberpup takes 8 damage. (HP 0/28) Wild Emberpup fainted! You win! === A wild Aqualing appears! (stage 2) Your Emberpup: HP 5/28 Wild Aqualing: HP 34/34 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run > 3 Your Emberpup uses Flame Burst! It's not very effective... Wild Aqualing takes 6 damage. (HP 28/34) Wild Aqualing uses Water Jet! It's super effective! Your Emberpup takes 10 damage. (HP 0/28) Your Emberpup fainted... Game over! You reached stage 2. > 3 unknown command (type 'help') >
- void battle_gui_message(const char *line1, const char *line2) {
- draw_message_box_frame();
- vga_draw_text(MSGBOX_X + 10, MSGBOX_Y + 9, line1, COL_BLACK);
- if (line2 && line2[0]) {
- vga_draw_text(MSGBOX_X + 10, MSGBOX_Y + 24, line2, COL_BLACK);
- }
- }
+ /* There's no timer interrupt in this kernel (see input.h/game.c's RNG
+ * comment) - a real "sleep milliseconds" isn't available, so this is a
+ * fixed busy-wait spin, calibrated by measurement rather than a clock.
+ * Without it, each message would be overwritten by the next draw call
+ * before a human watching the screen could read it - the text/serial
+ * side has no equivalent gap (an AI agent isn't reading pixels), so this
+ * only affects the graphical panel's pacing, not the protocol. */
+ static void gui_pause(void) {
+ for (volatile uint32_t i = 0; i < 120000000u; i++) {
+ }
+ }
+
+ void battle_gui_message(const char *line1, const char *line2) {
+ draw_message_box_frame();
+ vga_draw_text(MSGBOX_X + 10, MSGBOX_Y + 9, line1, COL_BLACK);
+ if (line2 && line2[0]) {
+ vga_draw_text(MSGBOX_X + 10, MSGBOX_Y + 24, line2, COL_BLACK);
+ }
+ gui_pause();
+ }The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/battle_gui.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude make -s 2>&1 | tail -40; echo EXIT=$?
mkfs_tinyfs: done EXIT=0
$ cd /home/martin/workspace/prototypes/boot-to-claude pkill -9 -f qemu-system-i386 2>/dev/null sleep 0.3 rm -f build/serial.log build/qemu-monitor.sock build/qemu.out nohup qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot > build/qemu.out 2>&1 & disown sleep 2 echo started
Exit code 1
$ ps aux | grep qemu-system-i386 | grep -v grep
(Bash completed with no output)
$ pkill -9 -f qemu-system-i386 || true
Exit code 1
$ rm -f /home/martin/workspace/prototypes/boot-to-claude/build/serial.log /home/martin/workspace/prototypes/boot-to-claude/build/qemu-monitor.sock /home/martin/workspace/prototypes/boot-to-claude/build/qemu.out nohup qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot > build/qemu.out 2>&1 & disown sleep 2 ps aux | grep qemu-system-i386 | grep -v grep
martin 1759978 101 0.1 1740904 49620 ? Sl 17:16 0:02 qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot
$ pwd; cd /home/martin/workspace/prototypes/boot-to-claude date +%s.%N
/home/martin/workspace/prototypes/boot-to-claude 1784733397.715963302
$ cd /home/martin/workspace/prototypes/boot-to-claude
python3 tools/qemu_type.py build/qemu-monitor.sock "play
1
"
sleep 1
T0=$(date +%s.%N)
python3 tools/qemu_type.py build/qemu-monitor.sock "1
"
# wait for the move-menu text to reappear (proves both gui_pause calls for this exchange finished)
python3 - <<'EOF'
import time
t0=time.time()
last=""
while time.time()-t0 < 15:
with open("build/serial.log","rb") as f:
data = f.read().decode(errors="replace")
if data.count("4) Run") >= 2:
print("elapsed", time.time()-t0)
break
time.sleep(0.1)
EOFelapsed 2.0033085346221924
$ pkill -9 -f qemu-system-i386 || true sleep 0.3 cd /home/martin/workspace/prototypes/boot-to-claude time bash tools/run_serial_tests.sh 2>&1 | tail -20
Exit code 1
$ cd /home/martin/workspace/prototypes/boot-to-claude time bash tools/run_serial_tests.sh 2>&1 | tail -20; echo EXIT=$?
ok: shell reachable over serial (no keyboard involved) ok: game reachable, starter menu -> battle screen ok: battle played to completion over serial (Game over!) ok: shell still responsive after the game returned serial_game_test: PASS qemu-system-i386: terminating on signal 15 from pid 1760067 (bash) real 0m16.587s user 0m16.575s sys 0m0.079s EXIT=0
$ cd /home/martin/workspace/prototypes/boot-to-claude time bash tools/run_tests.sh 2>&1 | tail -30; echo EXIT=$?
run_tests: PASS qemu-system-i386: terminating on signal 15 from pid 1760100 (bash) real 0m14.256s user 0m14.596s sys 0m0.285s EXIT=0
$ pkill -9 -f qemu-system-i386 || true sleep 0.3 cd /home/martin/workspace/prototypes/boot-to-claude rm -f build/serial.log build/qemu-monitor.sock build/qemu.out nohup qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot > build/qemu.out 2>&1 & disown sleep 2 ps aux | grep qemu-system-i386 | grep -v grep
(Bash completed with no output)
$ cd /home/martin/workspace/prototypes/boot-to-claude nohup qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot > build/qemu.out 2>&1 & disown sleep 2 ps aux | grep qemu-system-i386 | grep -v grep
martin 1760251 101 0.1 1740904 49336 ? Sl 17:17 0:02 qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial file:build/serial.log -monitor unix:build/qemu-monitor.sock,server,nowait -no-reboot
$ cd /home/martin/workspace/prototypes/boot-to-claude python3 tools/qemu_type.py build/qemu-monitor.sock "play 1 3 3 3 3 3 3 3 " tail -c 400 build/serial.log
tiny-os: kernel entered selftest: running SELFTEST PASS tiny-os shell. type 'help' for commands. > play === Tiny Monsters === Defeat 5 wild monsters to become champion. Type 'quit' any time to leave. Choose your starter: 1) Emberpup (Fire) 2) Aqualing (Water) 3) Leafling (Grass) > 1 === A wild Emberpup appears! (stage 1)
$ sleep 12 tail -c 900 /home/martin/workspace/prototypes/boot-to-claude/build/serial.log
(Grass) > 1 === A wild Emberpup appears! (stage 1) Your Emberpup: HP 28/28 Wild Emberpup: HP 28/28 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run > 3 Your Emberpup uses Flame Burst! Wild Emberpup takes 11 damage. (HP 17/28) Wild Emberpup uses Flame Burst! Your Emberpup takes 12 damage. (HP 16/28) Your Emberpup: HP 16/28 Wild Emberpup: HP 17/28 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run > 3 Your Emberpup uses Flame Burst! Wild Emberpup takes 8 damage. (HP 9/28) Wild Emberpup uses Flame Burst! Your Emberpup takes 10 damage. (HP 6/28) Your Emberpup: HP 6/28 Wild Emberpup: HP 9/28 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run > 3 Your Emberpup uses Flame Burst! Wild Emberpup takes 8 damage. (HP 1/28) Wild Emberpup uses Flame Burst! Your Emberpup takes 8 damage. (HP 0/28) Your Emberpup fainted... Game over! You reached stage 1. > 3 unknown command (type 'help') >
$ pkill -9 -f qemu-system-i386 || true cat /home/martin/workspace/prototypes/boot-to-claude/docs/game.md
Exit code 1
1 # Tiny Monsters (`kernel/game/`)
2
3 A small turn-based, Pokemon-like battle game built into the kernel,
4 reachable from the shell's `play` command. It exists specifically to be
5 playable by an external agent (an AI agent, or any other program) that
6 only has a serial connection to the machine - no keyboard, no display.
7
8 ## Dual input: keyboard *or* serial
9
10 Every other input path in this kernel (the shell) used to read
11 exclusively from the polled PS/2 keyboard (`kernel/keyboard.c`). Serial
12 (`kernel/serial.c`, COM1) was write-only - used for the existing
13 dual-output logging (`console_write` + `serial_write` side by side
14 everywhere), never for input.
15
16 `kernel/input.c` adds that: `serial_has_char()`/`serial_getc()` (serial
17 RX) and `keyboard_has_char()` (a non-blocking peek at the PS/2
18 controller) let `input_getchar()` poll both peripherals and return
19 whichever produces a character first. Both the shell (`kernel/shell/
20 shell.c`) and the game (`kernel/game/game.c`) read all their input
21 through `input_getchar()`, so a human at the keyboard and an agent
22 connected over serial can drive either one interchangeably, in the same
23 session, with no configuration - matching the existing dual-*output*
24 convention rather than adding a new, separate input mode.
25
26 ## Playing over serial
27
28 Boot with a bidirectional serial backend instead of (or in addition to)
29 a display - e.g.:
30
31 ```
32 qemu-system-i386 -drive file=build/disk.img,format=raw \
33 -display none -serial unix:/tmp/tinyos-serial.sock,server,nowait
34 ```
35
36 Then connect to that socket and treat it as a plain line-oriented text
37 stream: read until `SELFTEST PASS` and the shell's `> ` prompt, send
38 `play\n`, read the starter menu, send a digit, read the battle screen,
39 and so on - exactly what `tools/serial_game_test.py` does (see
40 Testing below). No special protocol; it's the same text a human would
41 see and type at a terminal.
42
43 ## Rules
44
45 - **Starters**: three species, one of each type - `Emberpup` (Fire),
46 `Aqualing` (Water), `Leafling` (Grass). The wild encounter pool draws
47 from the same three.
48 - **Types**: a simple rock-paper-scissors cycle, Fire beats Grass beats
49 Water beats Fire. A type-advantaged move does 1.5x damage ("It's super
50 effective!"); a disadvantaged one does 0.5x ("It's not very
51 effective..."); same/no relationship is 1x.
52 - **Battle**: the player always attacks first each turn; if the wild
53 monster survives, it counterattacks with a random one of its own three
54 moves. Damage is `move power`, type-adjusted, `+/-2` random variance,
55 minimum 1.
56 - **Progression**: HP does *not* regenerate between encounters - only
57 fainting ends a run. Each wild monster defeated increases `stage`
58 (their HP scales up slightly with it); reach stage 6 (5 wins) to become
59 Champion. Your own monster fainting ends the run immediately.
60 - **Commands** at any prompt: a digit picks a starter or a move; `run`
61 flees the current encounter (a fresh one follows, same stage); `quit`
62 leaves the game and returns to the shell immediately.
63
64 ## Testing
65
66 Two complementary automated tests, both driving a full session against
67 a headless QEMU instance and asserting on the resulting output - see
68 each script's own header comment for the exact mechanism:
69
70 - `tools/run_tests.sh` - the shell's existing keyboard-driven suite,
71 extended with a `play` pass that checks the deterministic parts of the
72 flow (menu structure, `run`/`quit`) via QEMU monitor `sendkey`
73 injection (`tools/qemu_type.py`), the same way every other shell
74 command is tested.
75 - `tools/run_serial_tests.sh` (+ `tools/serial_game_test.py`) - the
76 scenario this feature actually exists for: no keyboard input at all,
77 driven entirely over a `-serial unix:...` socket. Plays a real battle
78 to a real end state (a faint or a win - battle outcomes are
79 RNG-dependent, so this accepts either) to prove the whole shell+game
80 loop works end to end over serial alone, then confirms the shell is
81 still responsive afterward.
82 - ## Playing over serial + ## Graphical battle screen + + When there's a real display attached (`make run-gui`, or any QEMU + invocation with a display backend), the game also renders a GBA-Pokemon- + style battle screen: sky/ground background, both combatants' sprites and + platforms, HP bars, and a dialogue/menu box - drawn via direct VGA mode + 0x13 register programming (`kernel/vga.c`, `docs/toolchain.md`-adjacent + bare-metal register I/O, no BIOS/VBE calls). Purely additive: every + `console_write`/`serial_write` call and every `input_getchar()` read + this file already made are unchanged, so an agent playing over serial + sees exactly the same text stream whether or not anyone is watching the + graphical side. `kernel/game/sprites.c` draws the three starters + procedurally (circles/ellipses/triangles from `vga.c`, the same + hand-authored-primitives approach as `kernel/font.c`'s stroke-based + glyphs) rather than from a stored image asset. + + Returning to the shell restores 80x25 text mode, including the real + BIOS-loaded font glyphs (mode 0x13 drawing and text-mode character data + share the same underlying VGA RAM - see `vga.c`'s comments on plane 2 + save/restore). + + Since there's no timer interrupt in this kernel, each battle message + holds on screen for a fixed busy-wait spin (`battle_gui.c`'s + `gui_pause`) rather than a calibrated real-time delay - long enough to + read, uncalibrated by design (nothing here depends on its exact + duration; an AI agent isn't reading pixels). + + ## Playing over serial
The file /home/martin/workspace/prototypes/boot-to-claude/docs/game.md has been updated successfully. (file state is current in your context — no need to Read it back)
$ grep -n "game\.c\|game/\|input\.c\|vga\|font\." /home/martin/workspace/prototypes/boot-to-claude/README.md
71: keyboard.c + input.c -> shell/shell.c PS/2 or serial RX -> command REPL 75: game/game.c shell's `play` - see docs/game.md 108: serial_game_test.py drives kernel/game/ over a serial socket, no keyboard 137:the same idea, but for `kernel/input.c`'s *other* input path: no
<tool_use_error>InputValidationError: Read was called with input that could not be parsed as JSON.
You sent (first 94 of 94 bytes): {"file_path": "/home/martin/workspace/prototypes/boot-to-claude/README.md", "offset": 55, 115}
Common causes: unescaped backslashes in file paths (use / or \\), unescaped control characters, or truncated output. Retry with valid JSON.</tool_use_error>55 `gcc` by default. That bootstrap step never runs again after the first
56 `build/toolchain/{as,cc,ld}` exist: `nasm`/`ld`/`objcopy` aren't needed
57 at all, and `gcc` never touches OS code, only `toolchain/`'s own source.
58 No cross-compiler needed.
59
60 ## Architecture
61
62 ```
63 BIOS -> boot/boot.S (MBR, 512B) real mode
64 -> boot/stage2.S real mode -> protected mode
65 -> kernel/entry.S -> kmain.c 32-bit protected mode, flat, no paging
66 gdt.c / idt.c kernel-owned GDT; IDT covers CPU
67 exceptions only - no PIC/IRQs, ever
68 console.c / serial.c VGA text + COM1, dual-output throughout
69 memory.c free-list kmalloc/kfree over a 1MB heap
70 ata.c -> fs/tinyfs.c polled ATA PIO -> tinyfs filesystem
71 keyboard.c + input.c -> shell/shell.c PS/2 or serial RX -> command REPL
72 lang/lexer.c (shared)
73 -> lang/script/{parser,ast,eval}.c scripting language (tree-walking)
74 -> lang/compiler/{parser,codegen}.c compiler -> lang/vm.c (bytecode VM)
75 game/game.c shell's `play` - see docs/game.md
76 ```
77
78 Deliberately single-tasking: no scheduler, no preemption, no interrupts
79 enabled after boot, no process/syscall boundary. The shell and both
80 language runtimes execute directly in kernel context. This keeps the OS
81 layer small so the interesting work is in the filesystem and language
82 implementations - see [`docs/`](docs/) for their designs:
83
84 - [`docs/tinyfs.md`](docs/tinyfs.md) - on-disk filesystem format
85 - [`docs/scripting-language.md`](docs/scripting-language.md) - the `tsh` language
86 - [`docs/bytecode-isa.md`](docs/bytecode-isa.md) - the compiled language + VM ISA
87 - [`docs/toolchain.md`](docs/toolchain.md) - the self-hosted assembler/compiler/linker
88 - [`docs/game.md`](docs/game.md) - the `play` battle game, playable over serial only
89
90 ## Repo layout
91
92 ```
93 boot/ stage1 (MBR) + stage2 boot loaders
94 kernel/ the kernel itself (see architecture diagram above)
95 kernel/fs/ tinyfs (shared: freestanding in the kernel, native in mkfs_tinyfs)
96 kernel/lang/ shared lexer + the two language implementations
97 kernel/shell/ interactive command REPL
98 toolchain/ self-hosted AS/CC/LD (see docs/toolchain.md) - host-side
99 build infrastructure, not OS code, but self-authored
100 like everything else in this repo
101 tools/ host-side build/test infrastructure (not OS code):
102 mkfs_tinyfs.c formats + seeds build/disk.img at build time
103 bootstrap_selfhost.sh builds+verifies toolchain/ (Stage1/2/3)
104 run_tests.sh headless QEMU test harness (keyboard-driven)
105 run_serial_tests.sh same, but driven entirely over serial (see docs/game.md)
106 run_toolchain_tests.sh host-side AS/CC/LD test harness
107 qemu_type.py scripts keystrokes into QEMU via its HMP monitor
108 serial_game_test.py drives kernel/game/ over a serial socket, no keyboard
109 tests/ fixtures (*.tsh, *.tc) + tests/expected/ serial-output fixtures
110 tests/toolchain/ AS/CC/LD test fixtures (see run_toolchain_tests.sh)
111 docs/ design docs, listed above
112 ```
113
114 ## Testing
115
116 There's no hardware and no display in CI, so the serial port is the test
117 oracle throughout: `kernel/selftest.c` runs a boot-time self-test (string
118 ops, the allocator including coalescing, and a tinyfs create/write/read/
119 delete roundtrip) and prints `SELFTEST PASS`/`FAIL`.
120
121 ```
122 tools/run_tests.sh
123 ```
124
125 boots `build/disk.img` headless (`-display none -serial file:...`), waits
126 for `SELFTEST PASS`, then drives the shell by scripting keystrokes into
127 QEMU's HMP monitor over a unix socket (`tools/qemu_type.py sendkey ...`)
128 - the only way to feed the kernel's polled PS/2 driver without real
129 hardware - and asserts on the resulting serial output for every shell
130 command, `run hello.tsh`, `build`+`exec` on `hello.tc`, and the `play`
131 battle game's menu structure.
132
133 ```
134 tools/run_serial_tests.sh
135 ```
136
137 the same idea, but for `kernel/input.c`'s *other* input path: no
138 keyboard/HMP injection at all, driven entirely over a second, genuinely
139 bidirectional serial connection (`-serial unix:...,server,nowait`) via
140 `tools/serial_game_test.py` - the actual scenario that path exists for
141 (an external agent with only a serial connection). See
142 [`docs/game.md`](docs/game.md).
143
144 ## Boot budget
145
146 The kernel image is capped at a fixed number of disk sectors (`boot/
147 stage2.S`'s `KERNEL_SECTORS`, currently 512 = 256KB), read via BIOS
148 INT13h in bounded chunks (SeaBIOS silently misbehaves on very large
149 single extended-read requests). `kernel/fs/tinyfs.h`'s
150 `TINYFS_FS_START_LBA` reserves exactly that much room before the
151 filesystem region begins. The Makefile fails the build loudly if the
152 kernel image ever grows past its budget - if you hit that, bump
153 `KERNEL_SECTORS`, the Makefile's `KERNEL_MAX_SECTORS`, and
154 `TINYFS_FS_START_LBA` together.
155 - keyboard.c + input.c -> shell/shell.c PS/2 or serial RX -> command REPL
- lang/lexer.c (shared)
- -> lang/script/{parser,ast,eval}.c scripting language (tree-walking)
- -> lang/compiler/{parser,codegen}.c compiler -> lang/vm.c (bytecode VM)
- game/game.c shell's `play` - see docs/game.md
- ```
+ keyboard.c + input.c -> shell/shell.c PS/2 or serial RX -> command REPL
+ lang/lexer.c (shared)
+ -> lang/script/{parser,ast,eval}.c scripting language (tree-walking)
+ -> lang/compiler/{parser,codegen}.c compiler -> lang/vm.c (bytecode VM)
+ vga.c / font.c mode 0x13 VGA driver + hand-authored bitmap font
+ game/game.c, battle_gui.c, sprites.c shell's `play` - see docs/game.md
+ ```The file /home/martin/workspace/prototypes/boot-to-claude/README.md has been updated successfully. (file state is current in your context — no need to Read it back)
- kernel/shell/ interactive command REPL + kernel/shell/ interactive command REPL + kernel/game/ the `play` battle game + its VGA battle-screen rendering
The file /home/martin/workspace/prototypes/boot-to-claude/README.md has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude rm -f build/shot_*.ppm build/shot_*.png build/crop_*.png build/after_restore.* build/qemu.out pkill -9 -f qemu-system-i386 || true echo cleaned
Exit code 1
$ cd /home/martin/workspace/prototypes/boot-to-claude make clean make -s 2>&1 | tail -60 echo BUILD_EXIT=$?
rm -rf build bootstrap: stage1 (host gcc) bootstrap: stage2 (via stage1) bootstrap: stage3 (via stage2) bootstrap: verifying stage2 == stage3 as: identical cc: identical ld: identical bootstrap: PASS - self-hosted toolchain installed at build/toolchain mkfs_tinyfs: done BUILD_EXIT=0
$ cd /home/martin/workspace/prototypes/boot-to-claude bash tools/run_tests.sh 2>&1 | tail -20; echo T1=$?
run_tests: PASS qemu-system-i386: terminating on signal 15 from pid 1760815 (bash) T1=0
$ cd /home/martin/workspace/prototypes/boot-to-claude bash tools/run_serial_tests.sh 2>&1 | tail -20; echo T2=$?
ok: shell reachable over serial (no keyboard involved) ok: game reachable, starter menu -> battle screen ok: battle played to completion over serial (Game over!) ok: shell still responsive after the game returned serial_game_test: PASS qemu-system-i386: terminating on signal 15 from pid 1760928 (bash) T2=0
$ cd /home/martin/workspace/prototypes/boot-to-claude bash tools/run_toolchain_tests.sh 2>&1 | tail -30; echo T3=$?
PASS t01_basic (5) PASS t02_control (7) PASS t02_types (42) PASS t03_recursion (55) PASS t04_pointers (31) PASS t05_structs (17) PASS t06_switch (139) PASS t07_comma (53) PASS t08_forward_struct (6) PASS t09_local_init (81) PASS t10_short (6) PASS t11_array_size (155) PASS t12_local_array_size (4) PASS t13_struct_copy (212) PASS t14_struct_return (44) PASS t15_escapes (158) run_toolchain_tests: PASS T3=0
$ cd /home/martin/workspace/prototypes/boot-to-claude git status --short
M Makefile M README.md M docs/game.md M kernel/game/game.c M kernel/vga.c M kernel/vga.h ?? kernel/game/battle_gui.c ?? kernel/game/battle_gui.h ?? kernel/game/sprites.c ?? kernel/game/sprites.h
$ cd /home/martin/workspace/prototypes/boot-to-claude git add Makefile README.md docs/game.md kernel/game/game.c kernel/vga.c kernel/vga.h kernel/game/battle_gui.c kernel/game/battle_gui.h kernel/game/sprites.c kernel/game/sprites.h git status --short
M Makefile M README.md M docs/game.md A kernel/game/battle_gui.c A kernel/game/battle_gui.h M kernel/game/game.c A kernel/game/sprites.c A kernel/game/sprites.h M kernel/vga.c M kernel/vga.h
$ cd /home/martin/workspace/prototypes/boot-to-claude git commit -m "$(cat <<'EOF' Add a GBA Pokemon-style graphical battle screen to the game battle_gui.c renders the whole scene in VGA mode 13h - sky/ground background, both combatants on platforms, HP bars, and a bordered dialogue/menu box - drawn alongside game.c's existing console+serial text narration, never replacing it: an AI agent playing over serial sees exactly the same byte stream as before this existed. Verified via tools/run_tests.sh and tools/run_serial_tests.sh against a full clean rebuild (including the toolchain bootstrap). sprites.c draws the three starters procedurally, from vga.c's new circle/ellipse/triangle primitives - the same hand-authored-shapes approach as font.c's stroke-based glyphs, no image asset involved. Since there's no timer interrupt in this kernel, each battle message holds on screen via a fixed busy-wait spin rather than a calibrated delay, long enough for a human to read it before the next draw overwrites it - measured at roughly a second per message, adding harmless latency (not content) to serial/AI-driven play. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0165e38p645KdVscGAtTxcpe EOF )" git log --oneline -5
[main a4a64cb] Add a GBA Pokemon-style graphical battle screen to the game 10 files changed, 520 insertions(+), 4 deletions(-) create mode 100644 kernel/game/battle_gui.c create mode 100644 kernel/game/battle_gui.h create mode 100644 kernel/game/sprites.c create mode 100644 kernel/game/sprites.h a4a64cb Add a GBA Pokemon-style graphical battle screen to the game 377d751 Add VGA mode 13h graphics driver and hand-authored bitmap font 4fafcaa Ignore recordings/ (local screencast output, not source) 4246ba7 Add a Pokemon-like battle game, playable entirely over serial 900c4cf Phase T9: docs + cleanup