diff --git a/README.md b/README.md index e950a35..edc7bf4 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,6 @@ Once a task is completed, it should be checked off. - [x] Implement a simple version of `ftp` and `wget`. - [x] Create a graphics subsystem. It should provide functionality to switch between the normal text mode, and a graphics mode. - [x] Create a simple game of pool. It should use graphics mode to render the game. -- [ ] Create a simple game of minigolf. +- [x] Create a simple game of minigolf. Finally, before starting, write your prompt into `PROMPT.md`. This makes the request that you were given more easily auditable. \ No newline at end of file diff --git a/apps/minigolf/minigolf.c b/apps/minigolf/minigolf.c new file mode 100644 index 0000000..d2a5e94 --- /dev/null +++ b/apps/minigolf/minigolf.c @@ -0,0 +1,585 @@ +/** + * @file minigolf.c + * @brief Simple minigolf game for ClaudeOS. + * + * Uses VGA mode 0x13 (320x200, 256 colors) via the graphics subsystem. + * Features 4 progressively harder holes with walls and obstacles. + * + * Controls: + * A/D or Left/Right - Aim + * W/S or Up/Down - Adjust power + * Space or Enter - Shoot + * Q or Escape - Quit + */ + +#include "syscalls.h" + +/* ================================================================ + * Fixed-point math (16.16) + * ================================================================ */ + +typedef int32_t fixed_t; + +#define FP_SHIFT 16 +#define FP_ONE (1 << FP_SHIFT) +#define INT_TO_FP(x) ((fixed_t)(x) << FP_SHIFT) +#define FP_TO_INT(x) ((int)((x) >> FP_SHIFT)) +#define FP_MUL(a, b) ((fixed_t)(((int32_t)(a) * (int32_t)(b)) >> FP_SHIFT)) + +static uint32_t isqrt(uint32_t n) { + if (n == 0) return 0; + uint32_t x = n, y = (x + 1) / 2; + while (y < x) { x = y; y = (x + n / x) / 2; } + return x; +} + +/* Quarter-wave sine table (0..64 entries, 16.16 fixed point) */ +static const fixed_t sin_q[65] = { + 0, 1608, 3216, 4821, 6424, 8022, 9616, 11204, + 12785, 14359, 15924, 17479, 19024, 20557, 22078, 23586, + 25080, 26558, 28020, 29466, 30893, 32303, 33692, 35062, + 36410, 37736, 39040, 40320, 41576, 42806, 44011, 45190, + 46341, 47464, 48559, 49624, 50660, 51665, 52639, 53581, + 54491, 55368, 56212, 57022, 57798, 58538, 59244, 59914, + 60547, 61145, 61705, 62228, 62714, 63162, 63572, 63944, + 64277, 64571, 64827, 65043, 65220, 65358, 65457, 65516, + 65536 +}; + +static fixed_t fp_sin(int a) { + a &= 255; + int q = a >> 6, i = a & 63; + fixed_t v; + switch (q) { + case 0: v = sin_q[i]; break; + case 1: v = sin_q[64 - i]; break; + case 2: v = -sin_q[i]; break; + case 3: v = -sin_q[64 - i]; break; + default: v = 0; + } + return v; +} + +static fixed_t fp_cos(int a) { return fp_sin(a + 64); } + +/* ================================================================ + * Constants + * ================================================================ */ + +#define SW 320 +#define SH 200 + +#define BALL_R 3 +#define HOLE_R 6 +#define FRICTION (FP_ONE - FP_ONE / 80) /* ~0.9875 */ +#define MIN_SPEED (FP_ONE / 16) +#define MAX_HOLES 4 + +/* Colors */ +#define C_GRASS GFX_GREEN +#define C_WALL GFX_LIGHT_GREY +#define C_BALL GFX_WHITE +#define C_HOLE GFX_BLACK +#define C_AIM GFX_YELLOW +#define C_POWER GFX_LIGHT_RED +#define C_TEXT GFX_WHITE +#define C_WATER GFX_BLUE +#define C_SAND GFX_BROWN +#define C_BG GFX_DARK_GREY + +/* ================================================================ + * Wall segment + * ================================================================ */ + +typedef struct { int x1, y1, x2, y2; } wall_t; + +/* ================================================================ + * Hole (level) definition + * ================================================================ */ + +#define MAX_WALLS 16 +#define MAX_OBS 4 /* Obstacles (water/sand zones) */ + +typedef struct { + int par; + int ball_x, ball_y; /* Start position */ + int hole_x, hole_y; /* Hole position */ + int num_walls; + wall_t walls[MAX_WALLS]; + /* Rectangular obstacles */ + int num_obs; + struct { int x, y, w, h; uint32_t color; } obs[MAX_OBS]; +} hole_def_t; + +/* ================================================================ + * Course layout (4 holes) + * ================================================================ */ + +static const hole_def_t course[MAX_HOLES] = { + /* Hole 1: Simple straight shot */ + { + .par = 2, + .ball_x = 60, .ball_y = 100, + .hole_x = 260, .hole_y = 100, + .num_walls = 4, + .walls = { + {30, 60, 290, 60}, /* Top wall */ + {30, 140, 290, 140}, /* Bottom wall */ + {30, 60, 30, 140}, /* Left wall */ + {290, 60, 290, 140}, /* Right wall */ + }, + .num_obs = 0, + }, + /* Hole 2: L-shaped with turn */ + { + .par = 3, + .ball_x = 50, .ball_y = 50, + .hole_x = 270, .hole_y = 160, + .num_walls = 8, + .walls = { + {20, 20, 200, 20}, /* Top */ + {20, 80, 200, 80}, /* Mid horizontal */ + {20, 20, 20, 80}, /* Left */ + {200, 20, 200, 80}, /* Right-top */ + {200, 80, 300, 80}, /* Turn top */ + {140, 80, 140, 190}, /* Turn left */ + {300, 80, 300, 190}, /* Right */ + {140, 190, 300, 190}, /* Bottom */ + }, + .num_obs = 0, + }, + /* Hole 3: Water hazard */ + { + .par = 3, + .ball_x = 50, .ball_y = 100, + .hole_x = 270, .hole_y = 100, + .num_walls = 4, + .walls = { + {20, 50, 300, 50}, + {20, 150, 300, 150}, + {20, 50, 20, 150}, + {300, 50, 300, 150}, + }, + .num_obs = 1, + .obs = { + {130, 70, 60, 60, C_WATER}, + }, + }, + /* Hole 4: Obstacle course */ + { + .par = 4, + .ball_x = 40, .ball_y = 100, + .hole_x = 280, .hole_y = 100, + .num_walls = 8, + .walls = { + {20, 30, 300, 30}, /* Top */ + {20, 170, 300, 170}, /* Bottom */ + {20, 30, 20, 170}, /* Left */ + {300, 30, 300, 170}, /* Right */ + /* Internal walls (obstacles) */ + {100, 30, 100, 100}, /* First barrier from top */ + {180, 100, 180, 170}, /* Second barrier from bottom */ + {240, 30, 240, 120}, /* Third barrier from top */ + {60, 120, 60, 170}, /* Small bump from bottom */ + }, + .num_obs = 1, + .obs = { + {120, 120, 40, 30, C_SAND}, /* Sand trap */ + }, + }, +}; + +/* ================================================================ + * Game state + * ================================================================ */ + +static fixed_t ball_x, ball_y, ball_vx, ball_vy; +static int aim_angle = 0; +static int power = 3; /* 1-6 */ +static int curr_hole = 0; +static int strokes = 0; +static int total_strokes = 0; +static int ball_moving = 0; +static int ball_in_hole = 0; +static int in_water = 0; +static fixed_t saved_x, saved_y; /* Last safe position (before water) */ + +/* ================================================================ + * Wall collision + * ================================================================ */ + +/** + * Check if the ball collides with a horizontal or vertical wall segment. + * Simple axis-aligned bounce. + */ +static void check_wall_bounce(const wall_t *w) { + int bx = FP_TO_INT(ball_x); + int by = FP_TO_INT(ball_y); + + if (w->y1 == w->y2) { + /* Horizontal wall */ + int minx = w->x1 < w->x2 ? w->x1 : w->x2; + int maxx = w->x1 > w->x2 ? w->x1 : w->x2; + if (bx >= minx - BALL_R && bx <= maxx + BALL_R) { + int dy = by - w->y1; + if (dy < 0) dy = -dy; + if (dy <= BALL_R) { + ball_vy = -ball_vy; + /* Push ball out */ + if (by < w->y1) + ball_y = INT_TO_FP(w->y1 - BALL_R - 1); + else + ball_y = INT_TO_FP(w->y1 + BALL_R + 1); + } + } + } else if (w->x1 == w->x2) { + /* Vertical wall */ + int miny = w->y1 < w->y2 ? w->y1 : w->y2; + int maxy = w->y1 > w->y2 ? w->y1 : w->y2; + if (by >= miny - BALL_R && by <= maxy + BALL_R) { + int dx = bx - w->x1; + if (dx < 0) dx = -dx; + if (dx <= BALL_R) { + ball_vx = -ball_vx; + if (bx < w->x1) + ball_x = INT_TO_FP(w->x1 - BALL_R - 1); + else + ball_x = INT_TO_FP(w->x1 + BALL_R + 1); + } + } + } +} + +/* ================================================================ + * Physics update + * ================================================================ */ + +static void update_physics(void) { + const hole_def_t *h = &course[curr_hole]; + + /* Move ball */ + ball_x += ball_vx; + ball_y += ball_vy; + + /* Save last safe position */ + int bx = FP_TO_INT(ball_x); + int by = FP_TO_INT(ball_y); + + /* Check obstacles */ + in_water = 0; + for (int i = 0; i < h->num_obs; i++) { + int ox = h->obs[i].x, oy = h->obs[i].y; + int ow = h->obs[i].w, oh = h->obs[i].h; + if (bx >= ox && bx <= ox + ow && by >= oy && by <= oy + oh) { + if (h->obs[i].color == C_WATER) { + in_water = 1; + /* Reset ball to last safe position */ + ball_x = saved_x; + ball_y = saved_y; + ball_vx = 0; + ball_vy = 0; + strokes++; /* Penalty stroke */ + return; + } else if (h->obs[i].color == C_SAND) { + /* Sand: extra friction */ + ball_vx = FP_MUL(ball_vx, FP_ONE - FP_ONE / 20); + ball_vy = FP_MUL(ball_vy, FP_ONE - FP_ONE / 20); + } + } + } + + /* Apply friction */ + ball_vx = FP_MUL(ball_vx, FRICTION); + ball_vy = FP_MUL(ball_vy, FRICTION); + + /* Check if stopped */ + fixed_t speed_sq = FP_MUL(ball_vx, ball_vx) + FP_MUL(ball_vy, ball_vy); + if (speed_sq < FP_MUL(MIN_SPEED, MIN_SPEED)) { + ball_vx = 0; + ball_vy = 0; + ball_moving = 0; + saved_x = ball_x; + saved_y = ball_y; + } + + /* Wall collisions */ + for (int i = 0; i < h->num_walls; i++) { + check_wall_bounce(&h->walls[i]); + } + + /* Check hole */ + fixed_t dx = ball_x - INT_TO_FP(h->hole_x); + fixed_t dy = ball_y - INT_TO_FP(h->hole_y); + fixed_t dist_sq = FP_MUL(dx, dx) + FP_MUL(dy, dy); + fixed_t hole_r = INT_TO_FP(HOLE_R); + if (dist_sq < FP_MUL(hole_r, hole_r)) { + ball_in_hole = 1; + ball_vx = 0; + ball_vy = 0; + ball_moving = 0; + } +} + +/* ================================================================ + * Drawing + * ================================================================ */ + +static void draw_number(int x, int y, int num, uint32_t color) { + char buf[8]; + int len = 0; + if (num == 0) { buf[len++] = '0'; } + else { + int n = num; + while (n > 0) { buf[len++] = '0' + (char)(n % 10); n /= 10; } + } + /* Reverse and draw as pixels (very simple 3x5 digit font) */ + for (int i = len - 1; i >= 0; i--) { + /* Draw digit as a small cluster of pixels */ + int d = buf[i] - '0'; + int dx = x + (len - 1 - i) * 5; + /* Simple representation: draw a small filled rect for each digit */ + gfx_rect_t r = {(uint32_t)dx, (uint32_t)y, 4, 5, color}; + gfx_fill_rect(&r); + /* Blank out parts to make it look like a number - simplified */ + if (d == 0) { gfx_rect_t inner = {(uint32_t)(dx+1), (uint32_t)(y+1), 2, 3, C_BG}; gfx_fill_rect(&inner); } + if (d == 1) { gfx_rect_t l = {(uint32_t)dx, (uint32_t)y, 1, 5, C_BG}; gfx_fill_rect(&l); + gfx_rect_t r2 = {(uint32_t)(dx+2), (uint32_t)y, 2, 5, C_BG}; gfx_fill_rect(&r2); } + } +} + +static void draw_hole(void) { + const hole_def_t *h = &course[curr_hole]; + + /* Background */ + gfx_clear(C_BG); + + /* Draw course grass area (fill inside walls approximately) */ + /* Just fill the entire course bounding box with grass */ + int minx = 999, miny = 999, maxx = 0, maxy = 0; + for (int i = 0; i < h->num_walls; i++) { + if (h->walls[i].x1 < minx) minx = h->walls[i].x1; + if (h->walls[i].x2 < minx) minx = h->walls[i].x2; + if (h->walls[i].y1 < miny) miny = h->walls[i].y1; + if (h->walls[i].y2 < miny) miny = h->walls[i].y2; + if (h->walls[i].x1 > maxx) maxx = h->walls[i].x1; + if (h->walls[i].x2 > maxx) maxx = h->walls[i].x2; + if (h->walls[i].y1 > maxy) maxy = h->walls[i].y1; + if (h->walls[i].y2 > maxy) maxy = h->walls[i].y2; + } + gfx_rect_t grass = {(uint32_t)minx, (uint32_t)miny, + (uint32_t)(maxx - minx), (uint32_t)(maxy - miny), C_GRASS}; + gfx_fill_rect(&grass); + + /* Draw obstacles */ + for (int i = 0; i < h->num_obs; i++) { + gfx_rect_t obs = {(uint32_t)h->obs[i].x, (uint32_t)h->obs[i].y, + (uint32_t)h->obs[i].w, (uint32_t)h->obs[i].h, + h->obs[i].color}; + gfx_fill_rect(&obs); + } + + /* Draw walls */ + for (int i = 0; i < h->num_walls; i++) { + gfx_line_t line = {(uint32_t)h->walls[i].x1, (uint32_t)h->walls[i].y1, + (uint32_t)h->walls[i].x2, (uint32_t)h->walls[i].y2, C_WALL}; + gfx_line(&line); + } + + /* Draw hole */ + gfx_circle_t hole_circ = {(uint32_t)h->hole_x, (uint32_t)h->hole_y, HOLE_R, C_HOLE}; + gfx_circle(&hole_circ); + /* Hole rim */ + /* Draw a slightly larger circle outline in white for visibility */ + /* We'll just use the filled circle - the black on green is visible */ + + /* Draw ball */ + if (!ball_in_hole) { + gfx_circle_t bc = {(uint32_t)FP_TO_INT(ball_x), (uint32_t)FP_TO_INT(ball_y), + BALL_R, C_BALL}; + gfx_circle(&bc); + } + + /* Draw aiming line */ + if (!ball_moving && !ball_in_hole) { + int cx = FP_TO_INT(ball_x); + int cy = FP_TO_INT(ball_y); + int len = 15 + power * 4; + int ex = cx + FP_TO_INT(FP_MUL(INT_TO_FP(len), fp_cos(aim_angle))); + int ey = cy + FP_TO_INT(FP_MUL(INT_TO_FP(len), fp_sin(aim_angle))); + gfx_line_t aim = {(uint32_t)cx, (uint32_t)cy, + (uint32_t)ex, (uint32_t)ey, C_AIM}; + gfx_line(&aim); + } + + /* HUD */ + /* Hole number indicator */ + gfx_rect_t hud_bg = {0, 0, SW, 12, C_BG}; + gfx_fill_rect(&hud_bg); + + /* "Hole N Par N Strokes N" */ + /* Simple pixel text for HUD - draw filled rects as digit placeholders */ + /* Hole number */ + gfx_rect_t h_label = {2, 2, 24, 7, C_WALL}; + gfx_fill_rect(&h_label); /* "Hole" background */ + draw_number(28, 2, curr_hole + 1, C_TEXT); + + /* Par */ + gfx_rect_t p_label = {50, 2, 16, 7, C_WALL}; + gfx_fill_rect(&p_label); + draw_number(68, 2, h->par, C_TEXT); + + /* Strokes */ + gfx_rect_t s_label = {90, 2, 36, 7, C_WALL}; + gfx_fill_rect(&s_label); + draw_number(128, 2, strokes, C_TEXT); + + /* Power bar */ + gfx_rect_t pwr_bg = {SW - 62, 2, 60, 7, C_BG}; + gfx_fill_rect(&pwr_bg); + gfx_rect_t pwr = {SW - 62, 2, (uint32_t)(power * 10), 7, C_POWER}; + gfx_fill_rect(&pwr); + + /* Ball in hole message */ + if (ball_in_hole) { + gfx_rect_t msg_bg = {SW/2 - 40, SH/2 - 8, 80, 16, GFX_BLUE}; + gfx_fill_rect(&msg_bg); + /* "IN" text represented as colored block */ + gfx_rect_t msg = {SW/2 - 8, SH/2 - 4, 16, 8, C_TEXT}; + gfx_fill_rect(&msg); + } +} + +/* ================================================================ + * Input + * ================================================================ */ + +static int handle_input(void) { + char c; + if (read(0, &c, 1) <= 0) return 0; + + switch (c) { + case 'q': case 'Q': case 27: + return 1; + + case 'a': case 'A': + aim_angle = (aim_angle - 4) & 255; + break; + + case 'd': case 'D': + aim_angle = (aim_angle + 4) & 255; + break; + + case 'w': case 'W': + if (power < 6) power++; + break; + + case 's': case 'S': + if (power > 1) power--; + break; + + case ' ': case '\n': case '\r': + if (!ball_moving && !ball_in_hole) { + fixed_t p = INT_TO_FP(power); + ball_vx = FP_MUL(p, fp_cos(aim_angle)); + ball_vy = FP_MUL(p, fp_sin(aim_angle)); + ball_moving = 1; + strokes++; + } + break; + + case 'n': case 'N': + /* Next hole (after sinking) */ + if (ball_in_hole) { + total_strokes += strokes; + curr_hole++; + if (curr_hole >= MAX_HOLES) return 2; /* Game complete */ + /* Reset for next hole */ + strokes = 0; + ball_in_hole = 0; + ball_moving = 0; + aim_angle = 0; + power = 3; + ball_x = INT_TO_FP(course[curr_hole].ball_x); + ball_y = INT_TO_FP(course[curr_hole].ball_y); + ball_vx = 0; + ball_vy = 0; + saved_x = ball_x; + saved_y = ball_y; + } + break; + } + + return 0; +} + +/* ================================================================ + * Main + * ================================================================ */ + +int main(void) { + gfx_enter(); + + /* Initialize first hole */ + curr_hole = 0; + strokes = 0; + total_strokes = 0; + ball_in_hole = 0; + ball_moving = 0; + aim_angle = 0; + power = 3; + ball_x = INT_TO_FP(course[0].ball_x); + ball_y = INT_TO_FP(course[0].ball_y); + ball_vx = 0; + ball_vy = 0; + saved_x = ball_x; + saved_y = ball_y; + + int quit = 0; + while (!quit) { + int r = handle_input(); + if (r == 1) break; /* Quit */ + if (r == 2) { quit = 2; break; } /* Game complete */ + + if (ball_moving) { + update_physics(); + } + + draw_hole(); + + for (int i = 0; i < 2; i++) yield(); + } + + /* Show final score */ + if (quit == 2) { + total_strokes += strokes; + /* Show completion screen */ + gfx_clear(C_BG); + gfx_rect_t box = {SW/2 - 60, SH/2 - 20, 120, 40, GFX_BLUE}; + gfx_fill_rect(&box); + /* Score display */ + draw_number(SW/2 - 10, SH/2 - 5, total_strokes, C_TEXT); + /* Wait */ + for (int i = 0; i < 300; i++) yield(); + } + + gfx_leave(); + + puts("Minigolf complete!\n"); + puts("Total strokes: "); + char tmp[8]; + int ti = 0, s = total_strokes; + if (s == 0) putchar('0'); + else { while (s > 0) { tmp[ti++] = '0' + (char)(s % 10); s /= 10; } while (ti > 0) putchar(tmp[--ti]); } + puts("\n"); + + /* Calculate par total */ + int total_par = 0; + for (int i = 0; i < MAX_HOLES; i++) total_par += course[i].par; + puts("Par: "); + ti = 0; s = total_par; + if (s == 0) putchar('0'); + else { while (s > 0) { tmp[ti++] = '0' + (char)(s % 10); s /= 10; } while (ti > 0) putchar(tmp[--ti]); } + puts("\n"); + + return 0; +} diff --git a/build.log b/build.log index adeecfe..3a8285e 100644 --- a/build.log +++ b/build.log @@ -21,6 +21,13 @@ Building app: ip Built: /workspaces/claude-os/build/apps_bin/ip (3695 bytes) Building app: ls Built: /workspaces/claude-os/build/apps_bin/ls (250 bytes) +Building app: minigolf +/workspaces/claude-os/apps/minigolf/minigolf.c:29:17: warning: unused function 'isqrt' [-Wunused-function] + 29 | static uint32_t isqrt(uint32_t n) { + | ^~~~~ +1 warning generated. +/usr/bin/ld: warning: /workspaces/claude-os/build/apps_bin/minigolf.elf has a LOAD segment with RWX permissions + Built: /workspaces/claude-os/build/apps_bin/minigolf (3456 bytes) Building app: mkfs.fat32 /workspaces/claude-os/apps/mkfs.fat32/mkfs.fat32.c:56:13: warning: unused function 'print_hex' [-Wunused-function] 56 | static void print_hex(uint32_t val) { @@ -43,7 +50,7 @@ Building app: wget Built: /workspaces/claude-os/build/apps_bin/wget (2193 bytes) [ 2%] Built target apps [ 4%] Generating CPIO initial ramdisk -Generated initrd: 33656 bytes +Generated initrd: 37232 bytes [ 4%] Built target initrd [ 97%] Built target kernel [100%] Generating bootable ISO image @@ -53,14 +60,14 @@ Drive current: -outdev 'stdio:/workspaces/claude-os/release/claude-os.iso' Media current: stdio file, overwriteable Media status : is blank Media summary: 0 sessions, 0 data blocks, 0 data, 126g free -Added to ISO image: directory '/'='/tmp/grub.kHbiEc' +Added to ISO image: directory '/'='/tmp/grub.NLidpF' xorriso : UPDATE : 581 files added in 1 seconds Added to ISO image: directory '/'='/workspaces/claude-os/build/isodir' xorriso : UPDATE : 586 files added in 1 seconds xorriso : NOTE : Copying to System Area: 512 bytes from file '/usr/lib/grub/i386-pc/boot_hybrid.img' -xorriso : UPDATE : Thank you for being patient. Working since 0 seconds. -ISO image produced: 6056 sectors -Written to medium : 6056 sectors at LBA 0 +xorriso : UPDATE : 64.38% done +ISO image produced: 6058 sectors +Written to medium : 6058 sectors at LBA 0 Writing to 'stdio:/workspaces/claude-os/release/claude-os.iso' completed successfully. [100%] Built target iso