commit 2376ae060356874550e8b24c9e3712cd9a943acc
Author: Michael Neuper <michael@michaelneuper.com>
Date: Thu, 6 Nov 2025 18:49:45 +0200
initial commit
Diffstat:
| A | Makefile | | | 12 | ++++++++++++ |
| A | README | | | 34 | ++++++++++++++++++++++++++++++++++ |
| A | life.c | | | 305 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
3 files changed, 351 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
@@ -0,0 +1,12 @@
+CC = clang
+CFLAGS = -Wall -Wextra -O2 -lncurses
+TARGET = life
+
+$(TARGET): life.c
+ $(CC) $(CFLAGS) -o $(TARGET) life.c
+
+clean:
+ rm -f $(TARGET)
+
+run:
+ ./life
diff --git a/README b/README
@@ -0,0 +1,34 @@
+GAME OF LIFE
+------------
+A simple tui version of Conway’s Game of Life using ncurses.
+
+Build
+-----
+`make`
+
+Run
+---
+`make run`
+
+Controls
+-------------------
+* q quit
+* e edit grid
+* space / p pause or resume
+* n step one generation (only when paused)
+* + or = speed up
+* - or _ slow down
+
+Editor Controls
+---------------
+Use arrows, `hjkl` or `wasd` to move
+* t toggle cell
+* r randomize grid
+* space / e exit editor
+* enter exit editor
+* q quit program
+
+Notes
+-----
+Grid size is based on the terminal size at launch.
+Resize the terminal before starting if needed.
diff --git a/life.c b/life.c
@@ -0,0 +1,305 @@
+/**
+ * @file life.c
+ * @brief Conway's Game of Life
+ * @author Michael Neuper (michael@michaelneuper.com)
+ * @date 2025-11-06
+ */
+
+#include <ncurses.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+
+#define CURS_COL 1
+#define SLEEP_MSEC 225
+
+/* --- type definitions ----------------------------------------------------- */
+
+typedef enum {
+ DEAD = 0,
+ ALIVE = 1,
+} State;
+
+typedef struct {
+ size_t rows, cols;
+ State **cells;
+} Grid;
+
+/* --- helper macros -------------------------------------------------------- */
+
+#define MSEC_TO_NSEC(nsec) ((nsec) * 1000000)
+#define TOGGLE(a) ((a) = !(a))
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
+/* --- function prototypes: game logic -------------------------------------- */
+
+void game_loop();
+void tick(const Grid *cur, Grid *next);
+size_t count_neighbours(const Grid *grid, size_t row, size_t col);
+
+/* --- function prototypes: drawing ----------------------------------------- */
+
+void init_screen(void);
+void cleanup(void);
+void print_char(size_t row, size_t col, char ch, int color_pair);
+
+/* --- function prototypes: grid -------------------------------------------- */
+
+Grid *init_grid(size_t rows, size_t cols);
+void free_grid(Grid *grid);
+void print_grid(const Grid *grid);
+void set_grid(Grid *grid);
+void randomize_grid(Grid *grid);
+
+/* --- main routine --------------------------------------------------------- */
+
+int main(int argc, char *argv[]) {
+ init_screen();
+ game_loop();
+ cleanup();
+
+ return 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
+void game_loop() {
+ int key;
+ bool paused = false;
+ size_t rows = getmaxy(stdscr) + 1;
+ size_t cols = getmaxx(stdscr) + 1;
+ Grid *cur = init_grid(rows, cols);
+ Grid *next = init_grid(rows, cols);
+
+ if (!(cur && next)) goto err;
+
+ struct timespec ts;
+ ts.tv_sec = 0;
+ ts.tv_nsec = MSEC_TO_NSEC(SLEEP_MSEC);
+
+ set_grid(cur);
+
+ while ((key = getch()) != 'q') {
+ if (key == 'e') {
+ paused = true;
+ set_grid(cur);
+ print_grid(cur);
+ }
+
+ if (key == ' ' || key == 'p') TOGGLE(paused);
+ if (key == '-' || key == '_') ts.tv_nsec *= 2; // decrease tick speed
+ if (key == '+' || key == '=') ts.tv_nsec /= 2; // increase tick speed
+
+ if (paused && key != 'n') continue;
+
+ tick(cur, next);
+
+ // swap the current grid with the next
+ Grid *temp = cur;
+ cur = next;
+ next = temp;
+
+ print_grid(cur);
+
+ if (!paused) nanosleep(&ts, NULL);
+ }
+
+ free_grid(cur);
+ free_grid(next);
+ return;
+
+err:
+ cleanup();
+ if (cur) free_grid(cur);
+ if (next) free_grid(next);
+ fprintf(stderr, "Something terrible has happened.\n");
+ exit(1);
+}
+
+void tick(const Grid *cur, Grid *next) {
+ for (size_t i = 0; i < cur->rows; i++) {
+ for (size_t j = 0; j < cur->cols; j++) {
+ size_t n = count_neighbours(cur, i, j);
+
+ if (cur->cells[i][j]) { // live cell
+ if (n < 2 || n > 3) {
+ next->cells[i][j] = DEAD;
+ } else {
+ next->cells[i][j] = ALIVE;
+ }
+ } else { // dead cell
+ if (n == 3) {
+ next->cells[i][j] = ALIVE;
+ } else {
+ next->cells[i][j] = DEAD;
+ }
+ }
+ }
+ }
+}
+
+size_t count_neighbours(const Grid *grid, size_t row, size_t col) {
+ size_t count = 0;
+
+ for (size_t i = MAX(0, row - 1); i < MIN(row + 1, grid->rows - 1) + 1;
+ i++) {
+ for (size_t j = MAX(0, col - 1); j < MIN(col + 1, grid->cols - 1) + 1;
+ j++) {
+ count += grid->cells[i][j];
+ }
+ }
+
+ return count - grid->cells[row][col];
+}
+
+void init_screen(void) {
+ initscr();
+ noecho();
+ start_color();
+ cbreak(); // disable line buffering
+ curs_set(0); // invisible cursor
+ nodelay(stdscr, TRUE); // make `getch()` non-blocking
+ keypad(stdscr, TRUE);
+
+ attron(A_BOLD);
+}
+
+void cleanup(void) {
+ attroff(A_BOLD);
+ endwin();
+}
+
+void print_char(size_t row, size_t col, char ch, int color_pair) {
+ attron(COLOR_PAIR(color_pair));
+ mvprintw(row, col, "%c", ch);
+ attroff(COLOR_PAIR(color_pair));
+}
+
+Grid *init_grid(size_t rows, size_t cols) {
+ State **cells = malloc(rows * sizeof(State *));
+ Grid *grid = malloc(sizeof(Grid));
+
+ if (!(grid && cells)) goto err;
+
+ for (size_t i = 0; i < rows; i++) {
+ cells[i] = malloc(cols * sizeof(State));
+ if (!cells[i]) goto err; // FIXME memory leak
+
+ for (size_t j = 0; j < cols; j++) {
+ cells[i][j] = DEAD;
+ }
+ }
+
+ grid->cells = cells;
+ grid->rows = rows;
+ grid->cols = cols;
+
+ return grid;
+
+err:
+ if (cells) free(cells);
+ if (grid) free(grid);
+
+ return NULL;
+}
+
+void free_grid(Grid *grid) {
+ for (size_t i = 0; i < grid->rows; i++) {
+ free(grid->cells[i]);
+ }
+
+ free(grid->cells);
+ free(grid);
+}
+
+void print_grid(const Grid *grid) {
+ for (size_t i = 0; i < grid->rows; i++) {
+ for (size_t j = 0; j < grid->cols; j++) {
+ print_char(i, j, grid->cells[i][j] ? '#' : ' ', 0);
+ }
+ }
+
+ refresh();
+}
+
+void set_grid(Grid *grid) {
+ struct {
+ size_t row, col;
+ } curs = {grid->rows / 2, grid->cols / 2};
+
+ init_pair(CURS_COL, COLOR_BLUE, COLOR_BLACK);
+ print_char(curs.row, curs.col, '*', CURS_COL);
+
+ while (true) {
+ int key = getch();
+
+ switch (key) {
+ case KEY_UP:
+ case 'k':
+ case 'w':
+ curs.row -= 1;
+ break;
+
+ case KEY_LEFT:
+ case 'h':
+ case 'a':
+ curs.col -= 1;
+ break;
+
+ case KEY_DOWN:
+ case 'j':
+ case 's':
+ curs.row += 1;
+ break;
+
+ case KEY_RIGHT:
+ case 'l':
+ case 'd':
+ curs.col += 1;
+ break;
+
+ case 't':
+ TOGGLE(grid->cells[curs.row][curs.col]);
+ break;
+
+ case 'r':
+ randomize_grid(grid);
+ break;
+
+ case '\n':
+ case '\r':
+ case ' ':
+ case 'e':
+ return;
+
+ case 'q':
+ goto quit;
+
+ default:
+ continue;
+ }
+
+ curs.row = (curs.row + grid->rows) % grid->rows;
+ curs.col = (curs.col + grid->cols) % grid->cols;
+
+ // re-render the screen
+ print_grid(grid);
+ print_char(curs.row, curs.col, '*', CURS_COL);
+ }
+
+quit:
+ free_grid(grid);
+ cleanup();
+ exit(0);
+}
+
+void randomize_grid(Grid *grid) {
+ for (size_t i = 0; i < grid->rows; i++) {
+ for (size_t j = 0; j < grid->cols; j++) {
+ double rnd = (double)rand() / RAND_MAX;
+
+ grid->cells[i][j] = rnd > 0.8 ? ALIVE : DEAD;
+ }
+ }
+}