Implement pool game using graphics subsystem (AI)

This commit is contained in:
AI
2026-02-24 08:05:19 +00:00
parent fc5fe9af63
commit 6a8d561b3e
3 changed files with 592 additions and 7 deletions

View File

@@ -77,7 +77,7 @@ Once a task is completed, it should be checked off.
- [x] Create a UDP and TCP stack.
- [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.
- [ ] Create a simple game of pool. It should use graphics mode to render the game.
- [x] Create a simple game of pool. It should use graphics mode to render the game.
- [ ] 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.

582
apps/pool/pool.c Normal file
View File

@@ -0,0 +1,582 @@
/**
* @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;
}

View File

@@ -30,6 +30,9 @@ Building app: mkfs.fat32
Built: /workspaces/claude-os/build/apps_bin/mkfs.fat32 (5121 bytes)
Building app: mount
Built: /workspaces/claude-os/build/apps_bin/mount (992 bytes)
Building app: pool
/usr/bin/ld: warning: /workspaces/claude-os/build/apps_bin/pool.elf has a LOAD segment with RWX permissions
Built: /workspaces/claude-os/build/apps_bin/pool (2936 bytes)
Building app: sh
/workspaces/claude-os/apps/sh/sh.c:167:17: warning: unused variable 'type' [-Wunused-variable]
167 | int32_t type = readdir(resolved, 0, name);
@@ -39,9 +42,9 @@ Building app: sh
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
[ 4%] Built target initrd
[ 7%] Building C object src/CMakeFiles/kernel.dir/graphics.c.o
[ 9%] Linking C executable ../bin/kernel
[ 97%] Built target kernel
[100%] Generating bootable ISO image
xorriso 1.5.6 : RockRidge filesystem manipulator, libburnia project.
@@ -50,14 +53,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.EEhNkO'
Added to ISO image: directory '/'='/tmp/grub.kHbiEc'
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 : 67.13% done
ISO image produced: 6054 sectors
Written to medium : 6054 sectors at LBA 0
xorriso : UPDATE : Thank you for being patient. Working since 0 seconds.
ISO image produced: 6056 sectors
Written to medium : 6056 sectors at LBA 0
Writing to 'stdio:/workspaces/claude-os/release/claude-os.iso' completed successfully.
[100%] Built target iso