Re-use
Given that both the Langton's Ant and the Game of Life use a grid, I decided to code a generic grid_diplayer
code and use it both for GoL (Game of Life) and Langton's Ant to put into practice the principle of code re-use.
Rules of Game of Life
Rules of Langton's Ant
Pure logic
The code inside the ant_logic
and life_logic
cannot make contact with the outside world, any state not contained in the grid can be handled with a specific state
variable that is returned along with the grid.
grid_displayer
grid_displayer
shows the board after each update.
Commands
In the case of Game of Life I also added a bit of interactivity (in grid_displayer
):
- SPACEBAR : Pause / Resume
- ENTER : Start Again from a random board
- + : Increase the x and y dimension by 10
- - : Decrease the x and y dimension by 10
(Please note that + and - may create un-aesthetic effects if the size is not a sub-multiple of the screen size, just increase or decrease more to get a nicer effect)
grid_displayer
import sys, pygame
import random
from itertools import count
import life_logic
import ant_logic as ant
def show_grid(grid, screen, screen_size, color_decider):
"""
Shows the `grid` on the `screen`.
The colour of each cell is given by color_decider,
a function of the form (cell -> rgb_triplet)
"""
number_of_squares = len(grid)
square_size = screen_size[0] // number_of_squares
for y, row in enumerate(grid):
for x, item in enumerate(row):
pygame.draw.rect(screen, color_decider(item), (x * square_size, y * square_size, square_size, square_size), 0)
def animate_grid(grid, grid_updater, color_decider, screen_size=(600, 600), state={}):
"""
Repeatedly calls `show_grid` to show a continually updating grid.
"""
pygame.init()
screen = pygame.display.set_mode( screen_size )
for ticks in count(0):
user_inputs = pygame.event.get()
# if user_inputs: print(repr(user_inputs))
show_grid(grid, screen, screen_size, color_decider)
grid, state = grid_updater(grid, user_inputs, ticks, state)
pygame.display.flip()
def main():
def life_next_board(current, inputs, _, state={}):
for event in inputs:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_DOWN:
state["size"] -= 10
if event.key == pygame.K_UP:
state["size"] += 10
if event.key == pygame.K_SPACE:
state["paused"] = not state["paused"]
if event.key == pygame.K_RETURN:
return life_logic.random_grid(state["size"]), state
if not state["paused"]:
grid = life_logic.near_map(life_logic.rules, current)
else:
grid = current
return grid, state
def life_color_decider(cell):
return (255, 255, 255) if cell else (0, 0, 0)
if raw_input("Do you want the [G]ame of Life or the [L]angton's Ant? ").upper().startswith("L"):
animate_grid(
grid = [ [True for _ in range(ant.GRID_SIZE)] for _ in range(ant.GRID_SIZE)],
grid_updater = ant.ant_step,
color_decider = lambda x: (255, 255, 255) if x else (0, 0, 0),
state = {"ant" : {"x":ant.GRID_SIZE//2, "y":ant.GRID_SIZE//2, "heading":1}, "count":0 }
)
else:
animate_grid(
grid = life_logic.random_grid(life_logic.BOARD_SIZE),
grid_updater = life_next_board,
color_decider = life_color_decider,
state = {"paused" : False, "size" : 30}
)
if __name__ == "__main__":
main()
life_logic
import doctest
import random
BOARD_SIZE = 30
def enumerate_2d(matrix):
"""
Yields ((x, y), value) pairs from a given input matrix.
>>> list( enumerate_2d( [ ["a", "b", "c"],
... ["d", "e", "f"] ] ) )
[((0, 0), 'a'), ((1, 0), 'b'), ((2, 0), 'c'), ((0, 1), 'd'), ((1, 1), 'e'), ((2, 1), 'f')]
"""
for y, row in enumerate(matrix):
for x, item in enumerate(row):
yield (x, y), item
def nears(x, y, matrix):
"""
Yields all the neightbours of the given x, y coordinates in the matrix.
Wraps around.
>>> list(nears(3, 1, [ [1, 2, 3, 4 ],
... [5, 6, 7, 8 ],
... [9, 10, 11, 12] ]))
[11, 3, 12, 4, 7]
>>> list(nears(0, 0, [ [1, 2, 3, 4 ],
... [5, 6, 7, 8 ],
... [9, 10, 11, 12] ]))
[6, 8, 10, 12, 5, 9, 2, 4]
"""
for c in ( (x + 1, y + 1),
(x - 1, y + 1),
(x + 1, y - 1),
(x - 1, y - 1),
(x , y + 1),
(x , y - 1),
(x + 1, y ),
(x - 1, y ) ):
try:
yield matrix[c[1]][c[0]]
except IndexError:
pass
def near_map(func, matrix):
"""
Given a function (current, neightbours) -> new_current
Maps the matrix by giving it as arguments the current and its neightbours.
Wraps around.
>>> near_map( (lambda this, nears: this + sum(nears)), [
... [1, 2, 3, 4, 5 ],
... [6, 7, 8, 9, 10],
... [11, 12, 13, 14, 15] ])
[[69, 63, 72, 81, 57], [69, 63, 72, 81, 57], [61, 57, 63, 69, 48]]
"""
return list(chunks_of(len(matrix[0]), \
[func(i, nears(x, y, matrix)) for (x, y), i in enumerate_2d(matrix)]))
def chunks_of(n, list_):
"""
Yield successive n-sized chunks from list_.
>>> list(chunks_of(2, "abcdef"))
['ab', 'cd', 'ef']
"""
for i in range(0, len(list_), n):
yield list_[i:i + n]
def rules(me, nears):
"""
Should the current cell `me` survive based on its neightbours (`nears`)?
>>> rules(False, [True, True, True])
True
>>> rules(True, [False, False, True, False])
False
"""
return sum(nears) in (2, 3) if me else sum(nears) == 3
def next_board(current):
"""
Next board of Game of Life.
"""
return near_map(rules, current)
def random_grid(size):
"""
Generates a random square starting grid for the Game of Life of the given `size`.
"""
return [ [random.randint(0, 1) for _ in range(size)] for _ in range(size)]
if __name__ == "__main__":
doctest.testmod()
ant_logic
GRID_SIZE = 100
ORIENTATION_TO_DELTA = {
0 : (0 ,-1),
1 : ( 1, 0),
2 : ( 0, 1),
3 : (-1, 0)
}
def ant_step(board, _, __, state):
x, y = state["ant"]["x"], state["ant"]["y"]
offset = 1 if board[y][x] else -1 # White is true
heading = (state["ant"]["heading"] + offset) % 4 # 4 cardinal directions
delta_x, delta_y = ORIENTATION_TO_DELTA[heading]
new_board = board[:]; new_board[y][x] = not board[y][x]
# if state["count"] % 100 == 0: print(state["count"])
return new_board, {"ant" : {"x": x + delta_x, "y" : y + delta_y, "heading": heading}, "count":state["count"] +1 }