HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Simple Game Of Life with infinite size

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
infinitesimplewithsizegamelife

Problem

I decided to try make a version of the Game Of Life that'd be infinitely big. The idea was to store a dictionary of each Y value, which contain a set of X values (for fast lookup), as opposed to creating a 2D grid, and searching for the coordinate.

I've no way if this idea would scale well or not since I've not done any fancy optimisations. For each generation, it will build a list of every cell and adjacent cell, check how many are adjacent to each of those, and create or remove a cell accordingly.

Currently it's just limited to printing the result, though I'll try get it linked up with pygame sometime if it doesn't turn out horribly slow.

```
class GameOfLife(object):

adjacent = ((-1,-1), (0,-1), (1,-1),
(-1, 0), (1, 0),
(-1, 1), (0, 1), (1, 1))

alive_cell = 'o'
dead_cell = '.'

def __init__(self, rule='B3/S23'):
"""Setup an empty Game Of Life object."""
self._reset()
self.new_rule()

def _reset(self):
self.game_data = {}
self.generations = 0

def new_rule(self, rule='B3/S23'):
"""Store the information for a new rule."""
rules = rule.split('/')

for i in range(len(rules)):
if rules[i].lower().startswith('b'):
self.rule_born = map(int, list(rules[i][1:]))

elif rules[i].lower().startswith('s'):
self.rule_alive = map(int, list(rules[i][1:]))

def paste(self, cells, offset=(0, 0), clear=False):
"""Paste a string to act as cells.

Use 'o' to bring a cell to life, and '.' to kill a cell.
An empty space will not modify the cell under it.
"""
if clear:
self._reset()

y = None
for line in cells.splitlines():

#Ignore any initial empty lives
if y is not None:
y += 1
elif line and y is None:
y = 0

for x in range(len(line)):

Solution

Good work wrapping it as an object, this makes a ton of things easier. adjacent, alive_cell and dead_cell should all be UPPER_SNAKE_CASE though, as they're constants.

You call for a rule parameter in __init__ but then ignore it by not passing it to new_rule and always defaulting that parameter. You also never call new_rule again. Is it intended to be usable on an object at any other time? If not then you should really put all the code from that function into __init__.

Also you can do a much easier loop if you just do for rule in rules. You then access each rule directly rather than using an index. This does technically overwrite the original rule value, but you don't need it any more so it's not going to make a big difference. You don't need to call list since map returns a list. And you should use lower() at the start, before you even split, to save on calling it multiple times later.

You don't validate the rule value at all. What if I passed the rule "You should duplicate if there's 5 spaces around you but die if there's less than 3". Of course that can't work, but you don't tell me that anywhere. Instead the object will be created and move on with no rules to call on. You should raise ValueError if it turns out that at the end of the function either of the rules have not been set and explain what values need to be in the string.

def __init__(self, rule='B3/S23'):
    """Setup an empty Game Of Life object."""
    self._reset()

    rules = rule.lower().split('/')
    for rule in rules:
        if rule.startswith('b'):
            self.rule_born = map(int, rule[1:])

        elif rule.startswith('s'):
            self.rule_alive = map(int, rule[1:])

    try:
        if not (self.rule_born and self.rule_alive):
            raise AttributeError
    except AttributeError:
        raise ValueError("rule must be of format 'B#/S#'.")


This block attempts to test the truthiness of your rules. Python can coerce most objects to boolean values. In the case of lists, an empty list will be False but a list containing any values at all will be True. So, if either of your values is empty then we'll get False here and that will lead us to raising an AttributeError. Why an AttributeError? Because we want to catch that in case we've never even set one of the attributes. If your rule string doesn't include a B in it, then your rule_born wont even exist and trying to access it will cause an AttributeError. This way we can catch the error whether it's due to the relevant letter missing or the letter not setting satisfactory values. You do also have the problem that map(int) can raise a ValueError of its own, I would similarly wrap that in try: except ValueError and re-raise a ValueError with a relevant error message.

Another note about __init__. Why not allow the user to pass a string there to call paste immediately? There's little point to having the object without any values, so adding that string as an optional value would save an added line of code without a lot of complexity.

In your paste function, you could avoid having to use y at all. To ignore the opening blank line/s then just use strip('\n'). Unlike before, this will only remove blank lines and leave spaces intact. Now you can use enumerate to get the index of the line you're on.

cells = cells.strip('\n')
    for y, line in enumerate(cells.splitlines()):
        for x, char in enumerate(line):
            if char == self.alive_cell:
                self.add((x + offset[0], y + offset[1]))
            elif char == self.dead_cell:
                self.remove((x + offset[0], y + offset[1]))


I did the same with line, this is a neater way to simultaneously get the values from your string and the index location of each element.

Code Snippets

def __init__(self, rule='B3/S23'):
    """Setup an empty Game Of Life object."""
    self._reset()

    rules = rule.lower().split('/')
    for rule in rules:
        if rule.startswith('b'):
            self.rule_born = map(int, rule[1:])

        elif rule.startswith('s'):
            self.rule_alive = map(int, rule[1:])

    try:
        if not (self.rule_born and self.rule_alive):
            raise AttributeError
    except AttributeError:
        raise ValueError("rule must be of format 'B#/S#'.")
cells = cells.strip('\n')
    for y, line in enumerate(cells.splitlines()):
        for x, char in enumerate(line):
            if char == self.alive_cell:
                self.add((x + offset[0], y + offset[1]))
            elif char == self.dead_cell:
                self.remove((x + offset[0], y + offset[1]))

Context

StackExchange Code Review Q#108444, answer score: 2

Revisions (0)

No revisions yet.