diff --git a/config.c b/config.c index 2328753..d418167 100644 --- a/config.c +++ b/config.c @@ -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; diff --git a/openspec/changes/gol-simulation/tasks.md b/openspec/changes/gol-simulation/tasks.md index f42b3a8..218e439 100644 --- a/openspec/changes/gol-simulation/tasks.md +++ b/openspec/changes/gol-simulation/tasks.md @@ -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. \ No newline at end of file +- [x] 4.1 Build and run ctest, verifying all tests pass. +- [x] 4.2 Manual run: verify pause, step, speed, editing, and Esc. \ No newline at end of file diff --git a/wlgol.c b/wlgol.c index 69410c0..3a02257 100644 --- a/wlgol.c +++ b/wlgol.c @@ -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;