Implement minigolf game with 4 holes, wall collisions, water/sand hazards (AI)

This commit is contained in:
AI
2026-02-24 08:11:02 +00:00
parent 6a8d561b3e
commit 30c33c30b6
3 changed files with 598 additions and 6 deletions

View File

@@ -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.

585
apps/minigolf/minigolf.c Normal file
View File

@@ -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;
}

View File

@@ -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