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.
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.
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 = live_cell population = live_cell population = 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.