Capstone: Conway’s Game of Life, without a single loop
A whole simulation in two lines of array arithmetic.
np.rollboolean masksastypeshifted sumswrap-aroundsimulationWatch it happen
Play it through, or step back and forth yourself.
grid34 x 22 · generation 0 · 136 aliveThree rules, applied to every cell at once. A live cell with 2 or 3 live neighbours survives; a dead cell with exactly 3 comes alive; everything else dies. That's the whole of Conway's Game of Life.
The idea
Conway's Game of Life has three rules and produces gliders, oscillators and — famously — universal computation. It's also the perfect final exercise for this track, because the obvious implementation is a nest of loops and the NumPy one is two lines.
The rules
- A live cell with 2 or 3 live neighbours stays alive.
- A dead cell with exactly 3 live neighbours becomes alive.
- Everything else dies or stays dead.
Crucially, every cell is decided simultaneously from the current state. You can't update in place as you sweep — cells later in the sweep would see the new values, and the simulation would be wrong. That constraint is exactly what array thinking gives you for free.
Counting neighbours without looping
The insight: instead of asking each cell about its eight neighbours, shift the entire grid in each of the eight directions and add the copies up. Every cell then holds its own neighbour count.
n = sum(
np.roll(np.roll(grid, dy, axis=0), dx, axis=1)
for dy in (-1, 0, 1)
for dx in (-1, 0, 1)
if (dy, dx) != (0, 0)
)That loop runs eight times regardless of grid size — it's a loop over directions, not over cells. On a million-cell grid it's still eight operations.
np.roll wraps around the edges, which makes the board a torus and means there are no boundary cases to special-case. A glider that leaves the right edge comes back on the left.

Applying the rules
With the counts in hand, the three rules collapse into one boolean expression:
grid = ((n == 3) | ((grid == 1) & (n == 2))).astype(np.uint8)Read it out loud: "exactly three neighbours, or already alive with exactly two". That's rules 1 and 2; rule 3 is everything the expression doesn't select. No branches, no loops, and every cell decided from the same snapshot — the simultaneity is automatic, because the right-hand side is fully evaluated before the assignment happens.

What this capstone uses
Nearly the whole track. Boolean masks and combining them with & and | (lesson 7), astype and uint8 (lesson 3), shifting views (lessons 4 and 5), broadcasting when you compare an array to a scalar (lesson 13), and the vectorisation habit that made you look for the loop-free version at all (lesson 14).
Work through the exercises in order — they build the simulation one piece at a time, and the last one runs it.
Going further
Once it works, try: hard edges instead of wrapping (pad with zeros rather than rolling); other rulesets like HighLife, which adds "a dead cell with 6 neighbours also revives"; counting neighbours with a convolution instead; or tracking how many generations until the board settles.
Practice
Write it yourself. The answer is there when you want it.
Putting the kettle on…
Starting up…
Write it yourself
not gradedWrite life_step(g): count each cell's eight neighbours by rolling the grid one step in every direction and summing, then return the cells with exactly three neighbours, plus the live cells with two. Run it four times from grid.copy(), print the board with show, and finish with the number of cells still alive.
Your turn
4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Shift grid one row upwards, wrapping around, using np.roll.
Build the neighbour count array for grid — the sum of all eight shifted copies. Same shape as grid.
Given that count, apply the rules and return the next generation as a uint8 array: alive where the count is exactly 3, or where the cell is already alive and the count is exactly 2.
Run the simulation for 10 generations from grid and return how many cells are alive at the end.
