feat: add Game of Life simulation mechanics

Implement Conway rules with tick timing, pause/resume, single stepping,
speed control, and paused-only cell editing with drag painting. Update
default key bindings for pause/step/speed. Mark verification complete.
This commit is contained in:
2026-11-08 15:47:46 +01:00
parent 00a2b6b03e
commit cb974854a3
3 changed files with 126 additions and 18 deletions
+13 -2
View File
@@ -60,11 +60,14 @@ int config_parse_mods(const char *s, uint32_t *out_mods, char **out_key) {
while (t) {
char *tt = trim(t);
if (*tt) {
char *alias = tt;
if (strcmp(tt, "+") == 0) alias = "plus";
else if (strcmp(tt, "-") == 0) alias = "minus";
if (n == cap) {
cap = cap ? cap * 2 : 4;
toks = realloc(toks, cap * sizeof(char *));
}
toks[n++] = strdup(tt);
toks[n++] = strdup(alias);
}
t = strtok_r(NULL, "+", &save);
}
@@ -129,6 +132,10 @@ config_t config_defaults(void) {
{"space", ACTION_ROTATE},
{"escape", ACTION_CANCEL},
{"ctrl+q", ACTION_QUIT},
{"p", ACTION_PAUSE},
{"n", ACTION_STEP},
{"+", ACTION_SPEED_UP},
{"-", ACTION_SPEED_DOWN},
};
int ndef = sizeof(defs) / sizeof(defs[0]);
for (int i = 0; i < ndef; i++) {
@@ -152,7 +159,11 @@ static const char *default_file_content =
"\"4\" = \"select:toad\"\n"
"\"space\" = \"rotate\"\n"
"\"escape\" = \"cancel\"\n"
"\"ctrl+q\" = \"quit\"\n";
"\"ctrl+q\" = \"quit\"\n"
"\"p\" = \"pause\"\n"
"\"n\" = \"step\"\n"
"\"+\" = \"speed_up\"\n"
"\"-\" = \"speed_down\"\n";
int config_ensure_default_file(const char *path) {
if (!path) return -1;
+8 -8
View File
@@ -2,20 +2,20 @@
## 1. Simulation core
- [ ] 1.1 Implement Conway tick using a second grid buffer and swap, verifying a known pattern (blinker) oscillates.
- [ ] 1.2 Add tick timing in the frame loop with adjustable interval, verifying ticks occur at configured rate.
- [x] 1.1 Implement Conway tick using a second grid buffer and swap, verifying a known pattern (blinker) oscillates.
- [x] 1.2 Add tick timing in the frame loop with adjustable interval, verifying ticks occur at configured rate.
## 2. Controls
- [ ] 2.1 Wire pause/step/speed actions to simulation state, verifying pause stops ticks and step advances one generation.
- [ ] 2.2 Add default key bindings for p/n/+/- and verify config tests pass.
- [x] 2.1 Wire pause/step/speed actions to simulation state, verifying pause stops ticks and step advances one generation.
- [x] 2.2 Add default key bindings for p/n/+/- and verify config tests pass.
## 3. Editing
- [ ] 3.1 Implement paused-only cell editing on left/right click, verifying clicks change cell state only when paused.
- [ ] 3.2 Implement drag painting while paused, verifying drag sets/clears cells.
- [x] 3.1 Implement paused-only cell editing on left/right click, verifying clicks change cell state only when paused.
- [x] 3.2 Implement drag painting while paused, verifying drag sets/clears cells.
## 4. Verification
- [ ] 4.1 Build and run ctest, verifying all tests pass.
- [ ] 4.2 Manual run: verify pause, step, speed, editing, and Esc.
- [x] 4.1 Build and run ctest, verifying all tests pass.
- [x] 4.2 Manual run: verify pause, step, speed, editing, and Esc.
+105 -8
View File
@@ -48,11 +48,18 @@ double current_scale = 1.0;
int need_resize = 1;
grid_t grid;
grid_t grid_back;
int sim_paused = 1;
double sim_interval = 0.05; /* 20 tps */
double sim_accumulator = 0.0;
struct timespec sim_last_time;
int sim_time_initialized = 0;
int mouse_left_down = 0;
int mouse_right_down = 0;
double cursor_x = 0, cursor_y = 0;
int click_pending = 0;
double click_x = 0, click_y = 0;
int show_grid_lines = 1;
int sim_paused = 1;
struct timespec mytime;
struct canvas
@@ -229,6 +236,8 @@ const struct wl_buffer_listener wl_buffer_listener = {
void redraw(); // forward declaration
static void simulation_tick(void);
static void handle_key(uint32_t key, uint32_t state, xkb_keysym_t sym, uint32_t mods, void *user_data)
{
(void)key;
@@ -266,12 +275,25 @@ static void handle_key(uint32_t key, uint32_t state, xkb_keysym_t sym, uint32_t
break;
case ACTION_PAUSE:
sim_paused = !sim_paused;
sim_accumulator = 0;
printf("paused=%d\n", sim_paused);
break;
case ACTION_STEP:
printf("step requested\n");
if (sim_paused) {
simulation_tick();
printf("stepped\n");
}
break;
case ACTION_SPEED_UP:
sim_interval /= 2.0;
if (sim_interval < 0.025) sim_interval = 0.025;
printf("interval=%g\n", sim_interval);
break;
case ACTION_SPEED_DOWN:
sim_interval *= 2.0;
if (sim_interval > 1.0) sim_interval = 1.0;
printf("interval=%g\n", sim_interval);
break;
case ACTION_TOGGLE_GRID:
show_grid_lines = !show_grid_lines;
break;
@@ -283,12 +305,24 @@ static void handle_key(uint32_t key, uint32_t state, xkb_keysym_t sym, uint32_t
static void handle_button(uint32_t button, uint32_t state, double x, double y, void *user_data)
{
(void)user_data;
if (state != WL_POINTER_BUTTON_STATE_PRESSED) return;
if (button != BTN_LEFT) return;
click_x = x;
click_y = y;
click_pending = 1;
printf("click at %.1f %.1f\n", x, y);
if (button == BTN_LEFT) {
if (state == WL_POINTER_BUTTON_STATE_PRESSED) {
mouse_left_down = 1;
click_x = x;
click_y = y;
click_pending = 1;
} else {
mouse_left_down = 0;
}
}
if (button == BTN_RIGHT) {
if (state == WL_POINTER_BUTTON_STATE_PRESSED) {
mouse_right_down = 1;
} else {
mouse_right_down = 0;
}
}
printf("button %u state %u at %.1f %.1f\n", button, state, x, y);
}
void wl_callack_done(void *data, struct wl_callback *callback, uint32_t time)
@@ -342,6 +376,7 @@ static void canvas_resize(int log_w, int log_h, double scale) {
}
grid_resize(&grid, log_w / 8, log_h / 8);
grid_resize(&grid_back, grid.cols, grid.rows);
need_resize = 0;
printf("canvas resized logical=%dx%d phys=%dx%d scale=%g grid=%dx%d\n", log_w, log_h, phys_w, phys_h, scale, grid.cols, grid.rows);
}
@@ -462,6 +497,39 @@ static void fill_rect(int x0, int y0, int w, int h, unsigned char r, unsigned ch
}
}
static int count_neighbors(int c, int r) {
int n = 0;
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
if (dr == 0 && dc == 0) continue;
n += grid_get(&grid, c + dc, r + dr);
}
}
return n;
}
static void simulation_tick(void) {
if (grid_back.cols != grid.cols || grid_back.rows != grid.rows) {
grid_free(&grid_back);
grid_init(&grid_back, grid.cols, grid.rows);
}
grid_clear(&grid_back);
for (int r = 0; r < grid.rows; r++) {
for (int c = 0; c < grid.cols; c++) {
int alive = grid_get(&grid, c, r);
int nb = count_neighbors(c, r);
int next = 0;
if (alive && (nb == 2 || nb == 3)) next = 1;
if (!alive && nb == 3) next = 1;
grid_set(&grid_back, c, r, next);
}
}
/* swap cell buffers */
unsigned char *tmp = grid.cells;
grid.cells = grid_back.cells;
grid_back.cells = tmp;
}
void redraw()
{
if (need_resize) {
@@ -474,6 +542,35 @@ void redraw()
cursor_x = pointer_data.x;
cursor_y = pointer_data.y;
/* tick timing */
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (!sim_time_initialized) {
sim_last_time = now;
sim_time_initialized = 1;
}
double dt = (now.tv_sec - sim_last_time.tv_sec) + (now.tv_nsec - sim_last_time.tv_nsec) / 1e9;
sim_last_time = now;
if (!sim_paused) {
sim_accumulator += dt;
while (sim_accumulator >= sim_interval) {
simulation_tick();
sim_accumulator -= sim_interval;
}
}
/* paused-only editing */
if (sim_paused && active_tool < 0) {
if (mouse_left_down || mouse_right_down) {
int cx = (int)(cursor_x / 8.0);
int cy = (int)(cursor_y / 8.0);
if (cx >= 0 && cy >= 0 && cx < grid.cols && cy < grid.rows) {
if (mouse_left_down) grid_set(&grid, cx, cy, 1);
if (mouse_right_down) grid_set(&grid, cx, cy, 0);
}
}
}
/* handle pending click placement */
if (click_pending) {
click_pending = 0;