8
\$\begingroup\$

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 }
\$\endgroup\$
1
\$\begingroup\$

You offered the code without a specific review question, so I take it to be a generic "please pick nits" sort of review.

Your code has exemplary style, very clear, with nicely chosen identifiers and lovely docstrings. I have no nits to pick. Bravo!

Put another way, every bit of code you write presents an argument to the Gentle Reader, and I am completely buying the argument, I believe it.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.