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