Microbit Life
Conway's Game of Life is a programming classic used in katas, to teach TDD ideas and even features in the annual code retreat events around the world. It's a wonderful example of fairly simple code giving rise to some complicated and often beautiful behaviour. It's also a good demonstration piece because it doesn't require any human intervention to run and will often carry on generating new patterns long after it has been started.
You're way ahead of me, aren't you?
If it's such a programming classic, my thoughts went, it should be easy - or at least possible - to write it in Python for the microbit? And so the challenge began to form in my head.
Recursion
Traditionally, game of life is often implemented using recursion or other exotic device to make the code more expressive and beautiful and use fewer lines of code. My first cut of the program tried a recursive algorithm but I found that even though the program worked, occasionally it would crash with a stack space error. This seemed to be related to the current and evolving pattern, derived from the random starting configuration. Not all patterns would crash the program, only a select few but in each case it seemed to be that the program interpreter on the microbit was running out of stack.
To get around this and still keep a hope of having minimal code, I tried falling back to Python list comprehensions. This worked slightly better but our random startup code still could generate an initial pattern that could crash the interpreter.
Code
So, what I have as a working solution can be seen below.
# Conways game of life for the microbit
# Shows a random animal each time before
# generating a random population and "evolving" it.
# If all the population dies, a new population is
# automatically generated (after the obligatory sad face)
#
# Reset the current population at any time by pressing
# the A button
#
# For the rules see:
# https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
import sys
import random
from microbit import *
# Population refresh rate
refresh_rate_in_ms = 1000
# board dimensions
width, height = 5, 5
# likelihood that a cell will be alive when randomly initialised
random_population_pc = 50
# cell states
dead_cell = 0 # debug set to 2 to see all dimly lit.
live_cell = 9
# is this x, y location on the board?
def is_legal_location(x, y):
return 0 <= x < width and 0 <= y < height
# is this cell in the population in a matching state.
def is_cell(population, x, y, state):
return is_legal_location(x, y, population) and population[x][y] == state
# ugly but works with microbit
# tried list comprehensions to be more pyton-y
# but microbit complains about stack for
# certain configurations
def count_live_neighbours(x, y, population):
live_neighbours = 0
# to the left...
if is_cell(population, x - 1, y - 1, live_cell):
live_neighbours += 1
if is_cell(population, x - 1, y, live_cell):
live_neighbours += 1
if is_cell(population, x - 1, y + 1, live_cell):
live_neighbours += 1
# above
if is_cell(population, x, y - 1, live_cell):
live_neighbours += 1
# below
if is_cell(population, x, y + 1, live_cell):
live_neighbours += 1
# to the right...
if is_cell(population, x + 1, y - 1, live_cell):
live_neighbours += 1
if is_cell(population, x + 1, y, live_cell):
live_neighbours += 1
if is_cell(population, x + 1, y + 1, live_cell):
live_neighbours += 1
return live_neighbours
# run through the survivors, births and deaths
def create_next_generation(population):
# nothing yet...
next_generation = empty_population()
# apply the rules of the game...
for x in range(0, width):
for y in range(0, height):
live_neighbours = count_live_neighbours(x, y, population)
# Any live cell...
if population[x][y] == live_cell:
# ... with fewer than two live neighbours dies, as if caused by under-population.
if live_neighbours < 2:
next_generation[x][y] = dead_cell
# ... with two or three live neighbours lives on to the next generation.
if live_neighbours in [2, 3]:
next_generation[x][y] = live_cell
# ... with more than three live neighbours dies, as if by over-population.
if live_neighbours > 3:
next_generation[x][y] = dead_cell
# Any dead cell...
if population[x][y] == dead_cell:
# ... with exactly three live neighbours becomes a live cell, as if by reproduction.
if live_neighbours == 3:
next_generation[x][y] = live_cell
return next_generation
# empty initialisation
def empty_population():
population = [[0 for x in range(width)] for y in range(height)]
# start with all dead cells
for x in range(0, width):
for y in range(0, height):
population[x][y] = dead_cell
return population
# test configuration that will repeat indefinitely
def blinker():
population = empty_population()
population[1][0] = live_cell
population[1][1] = live_cell
population[1][2] = live_cell
# randomly initialise
def random_population():
population = empty_population()
for x in range(0, width):
for y in range(0, height):
probability = random.randint(0, 100)
if probability > random_population_pc:
population[x][y] = live_cell
else:
population[x][y] = dead_cell
return population
# are all locations dead?
def is_extinct(population):
living = 0
for x in range(0, width):
for y in range(0, height):
if population[x][y] == live_cell:
living += 1
return (living == 0)
def display_population(population):
# display.clear()
for x in range(0, width):
for y in range(0, height):
display.set_pixel(x, y, population[x][y])
def display_random_animal():
animals = [
Image.DUCK,
Image.RABBIT,
Image.COW,
Image.PACMAN,
Image.TORTOISE,
Image.BUTTERFLY,
Image.GIRAFFE,
Image.SNAKE
]
display.show(random.choice(animals))
# Game start...
# display.scroll("Game of Life")
display_random_animal()
sleep(2 * refresh_rate_in_ms)
display.clear()
reset_population = True
regenerate_population = True
living_cells = random_population()
# Game loop
while True:
# has the population died?
if is_extinct(living_cells):
display.clear()
display.show(Image.SAD)
sleep(refresh_rate_in_ms)
reset_population = True
else:
display_population(living_cells)
sleep(refresh_rate_in_ms)
# button a resets the population
if button_a.was_pressed():
# display.scroll("resetting")
reset_population = True
if reset_population:
living_cells = random_population()
reset_population = False
display.clear()
# regenerate a new population
if regenerate_population:
living_cells = create_next_generation(living_cells)
It's true that there is a lot of repeated list iteration code and it feels like it could be shorter but I have left it like this based on feedback from some beginning coders who found this version easier to follow.