dashn9/conway-game-of-life: The Game of Life That Changes as It Reads Itself

A tiny Rust terminal simulator that updates cells in place, turning grid order into the hidden mechanic.

8 min read · dashn9/conway-game-of-life

A terminal window sits on a white desk and shows a small cellular grid partway through an update. The cells behind a left-to-right sweep are already darkened while the cells ahead are still blank, which makes the simulation feel like it is rewriting itself in real time.
A terminal-first Life clone with the update visible mid-flight, not after the fact.
Key Takeaways

The rule that leaks through the grid

On paper, this is Conway’s Game of Life. In practice, it is a different machine. The surprise is simple: should_i_live writes the new state back into the same grid it is still reading, so the top-left of the board can affect the bottom-right before the generation is finished.

An interactive way to see why a cellular automaton needs a second buffer if you want the rules to stay pure.

That is the whole thesis. A cellular automaton is supposed to be blind to the order you inspect it in. This repo makes order matter, which means the code is no longer just simulating Life. It is simulating a sweep through Life.

Inside the Rust loop

The shape of the program is spare. main.rs calls start_game(8, 8), and the engine in game.rs seeds a Vec<Vec<bool>>, clears the terminal, renders the board, sleeps for two seconds, then walks every row and column again.

fn start_game(rows: u16, columns: u16) {
    let mut world = seed_world(rows, columns);

    loop {
        print!("\x1B[2J\x1B[1;1H");
        render_world(&world);
        std::thread::sleep(std::time::Duration::from_secs(2));

        for row in 0..rows {
            for column in 0..columns {
                should_i_live(&mut world, row, column, rows, columns);
            }
        }
    }
}

The neighbor lookup is explicit, not clever. fetch_neighbours_positions checks each edge and corner by hand, returning only the cells that actually exist. That is readable, but it also means a tiny mismatch between rows and columns can slip through, which is exactly the kind of bug that shows up in small grid code.

The terminal rendering is equally bare bones. ANSI clear-screen escape codes do the work, so the whole experience stays focused on the board instead of on a framework. There is nowhere for the logic to hide.

Why this is not canonical Game of Life

Canonical Life reads from one board and writes to another. That separation is not an implementation detail, it is the rule that keeps the math honest. This repo skips the second board, so the current cell can change what later cells think the world looks like.

DimensionCanonical Game of Lifedashn9/conway-game-of-life
Update modelRead from one grid, write to a separate next gridRead and write the same grid in place
Traversal orderIrrelevant to the resultChanges the result as the sweep advances
Boundary handlingUsually wrapped, buffered, or hidden behind helpersExplicit edge checks in neighbor lookup
DeterminismStable for a given seed and rule setStable only if you keep the sweep order fixed
Teaching valueReference implementation of the rulesA live demonstration of why buffering matters
A close-up shows a single row of cells being rewritten from left to right, with a narrow sweep bar crossing the grid. Cells behind the bar are already changed while cells ahead remain original, which explains why in-place updates make traversal order matter.
The same board, but now the sweep itself changes the outcome.

If you want the canonical behavior, you need a double buffer. If you want a teaching example of what goes wrong when you skip one, this repo is perfect. It turns a familiar automaton into a visible argument about state.

A beginner Rust project wearing a classic skin

The repo is small enough to read in one sitting, which is part of the value. There is one dependency, rand, a terminal renderer, and a single engine file carrying nearly all the logic. That makes it a clean Rust exercise in indexing, mutation, and loop structure.

That also makes it a useful reminder that small simulation code can still be subtle. A grid of booleans looks harmless until you realize that every choice about traversal, bounds, and mutation changes the meaning of the program. This is not a polished Game of Life app. It is a compact lesson in why correctness depends on separating observation from update.