/** * @file pool.c * @brief Simple pool (billiards) game for ClaudeOS. * * Uses VGA mode 0x13 (320x200, 256 colors) via the graphics subsystem. * * Controls: * Left/Right arrows (or A/D) - Aim the cue * Up/Down arrows (or W/S) - Adjust shot power * Space or Enter - Shoot * Q or Escape - Quit * * Simplified 8-ball pool: * - 1 cue ball (white) + 7 colored balls * - Pot balls into the 6 pockets * - Pot the cue ball = foul (ball resets) */ #include "syscalls.h" /* ================================================================ * Fixed-point math (16.16 format) * ================================================================ */ typedef int32_t fixed_t; #define FP_SHIFT 16 #define FP_ONE (1 << FP_SHIFT) #define FP_HALF (FP_ONE / 2) #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)) #define FP_DIV(a, b) ((fixed_t)(((int32_t)(a) << FP_SHIFT) / (b))) /** Integer square root (for fixed-point magnitude). */ static uint32_t isqrt(uint32_t n) { if (n == 0) return 0; uint32_t x = n; uint32_t y = (x + 1) / 2; while (y < x) { x = y; y = (x + n / x) / 2; } return x; } static fixed_t fp_sqrt(fixed_t x) { if (x <= 0) return 0; return (fixed_t)isqrt((uint32_t)x << FP_SHIFT); } /* ================================================================ * Simple sin/cos lookup (256 entries for angle 0-255 == 0-360 deg) * Values in 16.16 fixed point. * Using a 64-entry quarter-wave table. * ================================================================ */ /** Pre-computed sine table for angles 0..63 (quarter wave, 0 to PI/2). * Values are 16.16 fixed-point. */ static const fixed_t sin_table_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 }; /** Get sine for angle (0-255 maps to 0-360 degrees), returns 16.16 fixed. */ static fixed_t fp_sin(int angle) { angle = angle & 255; int quadrant = angle >> 6; /* 0-3 */ int idx = angle & 63; fixed_t val; switch (quadrant) { case 0: val = sin_table_q[idx]; break; case 1: val = sin_table_q[64 - idx]; break; case 2: val = -sin_table_q[idx]; break; case 3: val = -sin_table_q[64 - idx]; break; default: val = 0; break; } return val; } static fixed_t fp_cos(int angle) { return fp_sin(angle + 64); } /* ================================================================ * Game constants * ================================================================ */ #define SCREEN_W 320 #define SCREEN_H 200 /* Table dimensions (inner playing area) */ #define TABLE_X 30 #define TABLE_Y 20 #define TABLE_W 260 #define TABLE_H 160 #define TABLE_RIGHT (TABLE_X + TABLE_W) #define TABLE_BOTTOM (TABLE_Y + TABLE_H) /* Bumper/rail width */ #define RAIL_W 6 /* Ball properties */ #define BALL_RADIUS 4 #define NUM_BALLS 8 /* 1 cue + 7 object balls */ /* Pocket properties */ #define POCKET_RADIUS 8 #define NUM_POCKETS 6 /* Physics */ #define FRICTION (FP_ONE - FP_ONE / 100) /* ~0.99 */ #define MIN_SPEED (FP_ONE / 8) /* Below this, stop the ball */ #define MAX_POWER INT_TO_FP(6) /* Colors */ #define COL_FELT GFX_GREEN /* 2: green */ #define COL_RAIL GFX_BROWN /* 6: brown */ #define COL_POCKET GFX_BLACK /* 0: black */ #define COL_CUE_BALL GFX_WHITE /* 15: white */ #define COL_AIM GFX_LIGHT_GREY /* 7 */ #define COL_POWER GFX_LIGHT_RED /* 12 */ #define COL_TEXT GFX_WHITE /* 15 */ #define COL_BG GFX_DARK_GREY /* 8 */ /* Object ball colors */ static const uint32_t ball_colors[7] = { GFX_YELLOW, /* Ball 1: Yellow */ GFX_BLUE, /* Ball 2: Blue */ GFX_RED, /* Ball 3: Red */ GFX_MAGENTA, /* Ball 4: Purple */ GFX_LIGHT_RED, /* Ball 5: Orange-ish */ GFX_LIGHT_GREEN, /* Ball 6: Light Green */ GFX_LIGHT_CYAN, /* Ball 7: Light Cyan */ }; /* ================================================================ * Game state * ================================================================ */ typedef struct { fixed_t x, y; /* Position (fixed-point) */ fixed_t vx, vy; /* Velocity (fixed-point) */ uint32_t color; /* Palette color index */ int active; /* 1 if on table, 0 if potted */ } ball_t; typedef struct { fixed_t x, y; /* Center position */ } pocket_t; static ball_t balls[NUM_BALLS]; static pocket_t pockets[NUM_POCKETS]; static int aim_angle = 0; /* 0-255 */ static int shot_power = 3; /* 1-6 */ static int balls_moving = 0; /* Nonzero while physics is running */ static int score = 0; static int game_over = 0; static int foul = 0; /* Set when cue ball potted */ /* ================================================================ * Initialization * ================================================================ */ static void init_pockets(void) { /* 6 pockets: 4 corners + 2 side midpoints */ pockets[0] = (pocket_t){INT_TO_FP(TABLE_X), INT_TO_FP(TABLE_Y)}; pockets[1] = (pocket_t){INT_TO_FP(TABLE_X + TABLE_W/2), INT_TO_FP(TABLE_Y)}; pockets[2] = (pocket_t){INT_TO_FP(TABLE_RIGHT), INT_TO_FP(TABLE_Y)}; pockets[3] = (pocket_t){INT_TO_FP(TABLE_X), INT_TO_FP(TABLE_BOTTOM)}; pockets[4] = (pocket_t){INT_TO_FP(TABLE_X + TABLE_W/2), INT_TO_FP(TABLE_BOTTOM)}; pockets[5] = (pocket_t){INT_TO_FP(TABLE_RIGHT), INT_TO_FP(TABLE_BOTTOM)}; } static void init_balls(void) { /* Cue ball on left side */ balls[0].x = INT_TO_FP(TABLE_X + TABLE_W / 4); balls[0].y = INT_TO_FP(TABLE_Y + TABLE_H / 2); balls[0].vx = 0; balls[0].vy = 0; balls[0].color = COL_CUE_BALL; balls[0].active = 1; /* Object balls in a triangle formation on right side */ fixed_t start_x = INT_TO_FP(TABLE_X + TABLE_W * 3 / 4); fixed_t start_y = INT_TO_FP(TABLE_Y + TABLE_H / 2); int ball_idx = 1; /* Row 1: 1 ball */ balls[ball_idx].x = start_x; balls[ball_idx].y = start_y; balls[ball_idx].color = ball_colors[0]; balls[ball_idx].active = 1; balls[ball_idx].vx = 0; balls[ball_idx].vy = 0; ball_idx++; /* Row 2: 2 balls */ for (int i = 0; i < 2 && ball_idx < NUM_BALLS; i++) { balls[ball_idx].x = start_x + INT_TO_FP(BALL_RADIUS * 2 + 1); balls[ball_idx].y = start_y + INT_TO_FP((i * 2 - 1) * (BALL_RADIUS + 1)); balls[ball_idx].color = ball_colors[ball_idx - 1]; balls[ball_idx].active = 1; balls[ball_idx].vx = 0; balls[ball_idx].vy = 0; ball_idx++; } /* Row 3: 3 balls */ for (int i = 0; i < 3 && ball_idx < NUM_BALLS; i++) { balls[ball_idx].x = start_x + INT_TO_FP(BALL_RADIUS * 4 + 2); balls[ball_idx].y = start_y + INT_TO_FP((i - 1) * (BALL_RADIUS * 2 + 1)); balls[ball_idx].color = ball_colors[ball_idx - 1]; balls[ball_idx].active = 1; balls[ball_idx].vx = 0; balls[ball_idx].vy = 0; ball_idx++; } /* Row 4: remaining balls */ for (int i = 0; ball_idx < NUM_BALLS; i++) { balls[ball_idx].x = start_x + INT_TO_FP(BALL_RADIUS * 6 + 3); balls[ball_idx].y = start_y + INT_TO_FP((i * 2 - 1) * (BALL_RADIUS + 1)); balls[ball_idx].color = ball_colors[ball_idx - 1]; balls[ball_idx].active = 1; balls[ball_idx].vx = 0; balls[ball_idx].vy = 0; ball_idx++; } } /* ================================================================ * Physics * ================================================================ */ static void check_wall_collisions(ball_t *b) { fixed_t left = INT_TO_FP(TABLE_X + RAIL_W + BALL_RADIUS); fixed_t right = INT_TO_FP(TABLE_RIGHT - RAIL_W - BALL_RADIUS); fixed_t top = INT_TO_FP(TABLE_Y + RAIL_W + BALL_RADIUS); fixed_t bottom = INT_TO_FP(TABLE_BOTTOM - RAIL_W - BALL_RADIUS); if (b->x < left) { b->x = left; b->vx = -b->vx; } if (b->x > right) { b->x = right; b->vx = -b->vx; } if (b->y < top) { b->y = top; b->vy = -b->vy; } if (b->y > bottom) { b->y = bottom; b->vy = -b->vy; } } static void check_pocket(ball_t *b, int ball_idx) { for (int p = 0; p < NUM_POCKETS; p++) { fixed_t dx = b->x - pockets[p].x; fixed_t dy = b->y - pockets[p].y; fixed_t dist_sq = FP_MUL(dx, dx) + FP_MUL(dy, dy); fixed_t pocket_r = INT_TO_FP(POCKET_RADIUS); if (dist_sq < FP_MUL(pocket_r, pocket_r)) { if (ball_idx == 0) { /* Cue ball potted = foul */ foul = 1; b->x = INT_TO_FP(TABLE_X + TABLE_W / 4); b->y = INT_TO_FP(TABLE_Y + TABLE_H / 2); b->vx = 0; b->vy = 0; } else { b->active = 0; b->vx = 0; b->vy = 0; score++; } return; } } } static void check_ball_collisions(void) { for (int i = 0; i < NUM_BALLS; i++) { if (!balls[i].active) continue; for (int j = i + 1; j < NUM_BALLS; j++) { if (!balls[j].active) continue; fixed_t dx = balls[j].x - balls[i].x; fixed_t dy = balls[j].y - balls[i].y; fixed_t dist_sq = FP_MUL(dx, dx) + FP_MUL(dy, dy); fixed_t min_dist = INT_TO_FP(BALL_RADIUS * 2); fixed_t min_dist_sq = FP_MUL(min_dist, min_dist); if (dist_sq < min_dist_sq && dist_sq > 0) { /* Elastic collision */ fixed_t dist = fp_sqrt(dist_sq); if (dist == 0) dist = 1; /* Normal vector */ fixed_t nx = FP_DIV(dx, dist); fixed_t ny = FP_DIV(dy, dist); /* Relative velocity along normal */ fixed_t dvx = balls[i].vx - balls[j].vx; fixed_t dvy = balls[i].vy - balls[j].vy; fixed_t dvn = FP_MUL(dvx, nx) + FP_MUL(dvy, ny); /* Only resolve if balls are approaching */ if (dvn <= 0) continue; /* Update velocities (equal mass elastic collision) */ balls[i].vx -= FP_MUL(dvn, nx); balls[i].vy -= FP_MUL(dvn, ny); balls[j].vx += FP_MUL(dvn, nx); balls[j].vy += FP_MUL(dvn, ny); /* Separate balls */ fixed_t overlap = min_dist - dist; if (overlap > 0) { fixed_t sep = overlap / 2 + FP_ONE / 4; balls[i].x -= FP_MUL(sep, nx); balls[i].y -= FP_MUL(sep, ny); balls[j].x += FP_MUL(sep, nx); balls[j].y += FP_MUL(sep, ny); } } } } } static void update_physics(void) { balls_moving = 0; for (int i = 0; i < NUM_BALLS; i++) { if (!balls[i].active) continue; /* Apply velocity */ balls[i].x += balls[i].vx; balls[i].y += balls[i].vy; /* Apply friction */ balls[i].vx = FP_MUL(balls[i].vx, FRICTION); balls[i].vy = FP_MUL(balls[i].vy, FRICTION); /* Check if ball is still moving */ fixed_t speed_sq = FP_MUL(balls[i].vx, balls[i].vx) + FP_MUL(balls[i].vy, balls[i].vy); if (speed_sq < FP_MUL(MIN_SPEED, MIN_SPEED)) { balls[i].vx = 0; balls[i].vy = 0; } else { balls_moving = 1; } /* Wall collisions */ check_wall_collisions(&balls[i]); /* Pocket check */ check_pocket(&balls[i], i); } /* Ball-ball collisions */ check_ball_collisions(); } /* ================================================================ * Drawing * ================================================================ */ static void draw_table(void) { /* Background */ gfx_clear(COL_BG); /* Rail (border) */ gfx_rect_t rail = {TABLE_X - RAIL_W, TABLE_Y - RAIL_W, TABLE_W + RAIL_W * 2, TABLE_H + RAIL_W * 2, COL_RAIL}; gfx_fill_rect(&rail); /* Felt (playing surface) */ gfx_rect_t felt = {TABLE_X, TABLE_Y, TABLE_W, TABLE_H, COL_FELT}; gfx_fill_rect(&felt); /* Pockets */ for (int i = 0; i < NUM_POCKETS; i++) { gfx_circle_t pocket = { (uint32_t)FP_TO_INT(pockets[i].x), (uint32_t)FP_TO_INT(pockets[i].y), POCKET_RADIUS, COL_POCKET }; gfx_circle(&pocket); } } static void draw_balls(void) { for (int i = 0; i < NUM_BALLS; i++) { if (!balls[i].active) continue; gfx_circle_t c = { (uint32_t)FP_TO_INT(balls[i].x), (uint32_t)FP_TO_INT(balls[i].y), BALL_RADIUS, balls[i].color }; gfx_circle(&c); } } static void draw_aim(void) { if (balls_moving || !balls[0].active) return; /* Draw aim line from cue ball */ int cx = FP_TO_INT(balls[0].x); int cy = FP_TO_INT(balls[0].y); int len = 20 + shot_power * 5; 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 line = {(uint32_t)cx, (uint32_t)cy, (uint32_t)ex, (uint32_t)ey, COL_AIM}; gfx_line(&line); } static void draw_hud(void) { /* Score */ char score_str[32] = "Score: "; int pos = 7; if (score == 0) { score_str[pos++] = '0'; } else { char tmp[8]; int ti = 0; int s = score; while (s > 0) { tmp[ti++] = '0' + (char)(s % 10); s /= 10; } while (ti > 0) score_str[pos++] = tmp[--ti]; } score_str[pos] = '\0'; /* Use pixels directly for HUD text at top */ /* Score on left side of top bar */ int tx = 2; int ty = 2; for (int i = 0; score_str[i]; i++) { gfx_pixel((uint32_t)(tx + i * 6), (uint32_t)ty, COL_TEXT); /* Draw each character as small 4x5 digits — simplified */ } /* Power indicator bar */ gfx_rect_t power_bg = {2, SCREEN_H - 12, 60, 8, COL_BG}; gfx_fill_rect(&power_bg); gfx_rect_t power_bar = {2, SCREEN_H - 12, (uint32_t)(shot_power * 10), 8, COL_POWER}; gfx_fill_rect(&power_bar); /* Foul indicator */ if (foul) { gfx_rect_t foul_bg = {SCREEN_W / 2 - 20, SCREEN_H - 12, 40, 8, GFX_RED}; gfx_fill_rect(&foul_bg); } /* Win message */ if (score >= 7) { game_over = 1; gfx_rect_t win_bg = {SCREEN_W/2 - 30, SCREEN_H/2 - 10, 60, 20, GFX_BLUE}; gfx_fill_rect(&win_bg); } } static void draw_frame(void) { draw_table(); draw_balls(); draw_aim(); draw_hud(); } /* ================================================================ * Input handling * ================================================================ */ static void shoot(void) { if (balls_moving || !balls[0].active) return; fixed_t power = INT_TO_FP(shot_power); balls[0].vx = FP_MUL(power, fp_cos(aim_angle)); balls[0].vy = FP_MUL(power, fp_sin(aim_angle)); foul = 0; } static int handle_input(void) { char c; int32_t n = read(0, &c, 1); if (n <= 0) return 0; switch (c) { case 'q': case 'Q': case 27: /* Escape */ return 1; /* Quit */ 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 (shot_power < 6) shot_power++; break; case 's': case 'S': if (shot_power > 1) shot_power--; break; case ' ': case '\n': case '\r': shoot(); break; default: break; } return 0; } /* ================================================================ * Main * ================================================================ */ int main(void) { /* Enter graphics mode */ gfx_enter(); /* Initialize game */ init_pockets(); init_balls(); /* Main game loop */ while (!game_over) { /* Handle input */ if (handle_input()) break; /* Update physics */ if (balls_moving) { update_physics(); } /* Draw everything */ draw_frame(); /* Frame delay (cooperative yield) */ for (int i = 0; i < 2; i++) yield(); } /* Wait a moment on game over */ if (game_over) { for (int i = 0; i < 200; i++) yield(); } /* Return to text mode */ gfx_leave(); puts("Game over! Final score: "); /* Print score */ char tmp[8]; int ti = 0; int s = score; if (s == 0) { putchar('0'); } else { while (s > 0) { tmp[ti++] = '0' + (char)(s % 10); s /= 10; } while (ti > 0) putchar(tmp[--ti]); } puts("/7\n"); return 0; }