From 932ca4409a658919f681edb3286c5a8215d925b5 Mon Sep 17 00:00:00 2001 From: jrpotter Date: Sun, 31 May 2015 20:58:09 -0400 Subject: [PATCH] Functional Game of Life --- src/cam.py | 36 +++++++++++++++++++++++--- src/life.py | 30 ++++++++++++++++++++++ src/neighborhood.py | 27 +++++++++++--------- src/ruleset.py | 61 ++++++++++++++++++++++++--------------------- 4 files changed, 110 insertions(+), 44 deletions(-) create mode 100644 src/life.py diff --git a/src/cam.py b/src/cam.py index 58b95a0..2dd1148 100644 --- a/src/cam.py +++ b/src/cam.py @@ -2,7 +2,11 @@ """ -from cell_plane import CellPlane +import copy +import numpy as np +import ruleset as rs +import cell_plane as cp +import neighborhood as nh class CAM: @@ -18,9 +22,10 @@ class CAM: """ """ + cps = max(cps, 1) self._dimen = dimen - self._planes = [CellPlane(dimen) for i in cps] - self._master = self._planes[0].grid if cps > 0 else None + self._planes = [cp.CellPlane(dimen) for i in range(cps)] + self.master = self._planes[0].grid def tick(self, ruleset, neighborhood, *args): @@ -31,5 +36,28 @@ class CAM: is placed into the master grid. Depending on the timing specifications set by the user, this may also change secondary cell planes (the master is always updated on each tick). """ - self._master = ruleset.update(self._master, self.master, neighborhood, *args) + self.master[:] = rs.Ruleset.update(self.master, ruleset, neighborhood, *args) + + + def randomize(self): + """ + Set the master grid to a random configuration. + """ + @np.vectorize + def v_random(cell): + cell.value = np.random.random_integers(0, 1) + return cell + + v_random(self.master) + + + def console_display(self): + """ + A convenience method used to display the grid onto the console. + + This should not be called frequently; I haven't benchmarked it but I do not anticipate this + running very well for larger grids. + """ + vfunc = np.vectorize(lambda x: int(x.value)) + print(vfunc(self.master)) diff --git a/src/life.py b/src/life.py new file mode 100644 index 0000000..695bf12 --- /dev/null +++ b/src/life.py @@ -0,0 +1,30 @@ +import cam +import ruleset as rs +import neighborhood as nh + +def game_of_life(cell, neighbors): + """ + Rules of the Game of Life. + + Note we ignore the second component of the neighbors tuples since + life depends on all neighbors + """ + total = sum(map(lambda x: int(x[0].value), neighbors)) + if cell.value: + if total < 2 or total > 3: + return False + else: + return True + elif total == 3: + return True + else: + return False + +if __name__ == '__main__': + c = cam.CAM(1, (10, 10)) + c.randomize() + c.console_display() + r = rs.Ruleset(rs.Rule.SATISFY) + n = nh.Neighborhood.moore(c.master, True) + c.tick(r, n, game_of_life) + c.console_display() diff --git a/src/neighborhood.py b/src/neighborhood.py index f94b27a..b8f4bfa 100644 --- a/src/neighborhood.py +++ b/src/neighborhood.py @@ -61,7 +61,7 @@ class Neighborhood: return 0 - def __init__(self): + def __init__(self, grid, wrap_around=True): """ Sets up an empty neighborhood. @@ -69,10 +69,12 @@ class Neighborhood: Note the offsets have a tuple as a key representing the position being offsetted by, and as a value, the current state the given cell at the offset is checked to be. """ + self.grid = grid self.offsets = {} + self.wrap_around = wrap_around - def neighbors(self, cell, grid, wrap_around=True): + def neighbors(self, cell): """ Returns all cells in the given neighborhood. @@ -83,12 +85,12 @@ class Neighborhood: for k in sorted(self.offsets.keys()): position = [sum(x) for x in zip(cell.index, k)] for i in range(len(position)): - if wrap_around: - position[i] = position[i] % grid.shape[i] - elif i < 0 or i >= grid.shape[i]: + if self.wrap_around: + position[i] = position[i] % self.grid.shape[i] + elif i < 0 or i >= self.grid.shape[i]: break else: - cells.append((grid[tuple(position)], self.offsets[k])) + cells.append((self.grid[tuple(position)], self.offsets[k])) return cells @@ -111,7 +113,7 @@ class Neighborhood: @classmethod - def moore(cls, dimen, value=True): + def moore(cls, grid, wrap_around=True, value=True): """ Returns a neighborhood corresponding to the Moore neighborhood. @@ -122,19 +124,19 @@ class Neighborhood: Note the center cell is excluded, so the total number of offsets are 3^N - 1. """ offsets = {} - variants = ([-1, 0, 1],) * dimen + variants = ([-1, 0, 1],) * len(grid.shape) for current in itertools.product(*variants): if any(current): offsets[current] = value - m_neighborhood = cls() + m_neighborhood = cls(grid, wrap_around) m_neighborhood.extend(offsets) return m_neighborhood @classmethod - def neumann(cls, dimen, value=True): + def neumann(cls, grid, wrap_around=True, value=True): """ Returns a neighborhood corresponding to the Von Neumann neighborhood. @@ -145,14 +147,15 @@ class Neighborhood: Note the center cell is excluded, so the total number of offsets are 2N. """ offsets = {} - variant = [0] * dimen + variant = [0] * len(grid.shape) for i in range(len(variant)): for j in [-1, 1]: variant[i] = j offsets[tuple(variant)] = value variant[i] = 0 - n_neighborhood = cls() + n_neighborhood = cls(grid, wrap_around) n_neighborhood.extend(offsets) return n_neighborhood + diff --git a/src/ruleset.py b/src/ruleset.py index 9f079cf..f0667b2 100644 --- a/src/ruleset.py +++ b/src/ruleset.py @@ -5,9 +5,12 @@ @author: jrpotter @date: May 31st, 2015 """ -from enum import Enum +import copy +import enum +import numpy as np -class Rule(Enum): + +class Rule(enum.Enum): MATCH = 0 TOLERATE = 1 SATISFY = 2 @@ -26,22 +29,41 @@ class Ruleset: a neighborhood instance's offsets member. """ - def __init__(self, method, wrap_around=True): + @staticmethod + @np.vectorize + def update(cell, rules, neighborhood, *args): + """ + Allow for batch processing of rules. + + We choose our processing function based on the specified rule and update every cell in the grid simultaneously + via a vectorization. + """ + tmp = copy.deepcopy(cell) + if rules.method == Rule.MATCH: + tmp.value = rules.matches(cell, neighborhood, *args) + elif rules.method == Rule.TOLERATE: + tmp.value = rules.tolerate(cell, neighborhood, *args) + elif rules.method == Rule.SATISFY: + tmp.value = rules.satisfies(cell, neighborhood, *args) + + return tmp + + + def __init__(self, method): """ """ self.method = method - self.wrap_around = wrap_around - def _matches(self, cell, grid, neighborhood): + def matches(self, cell, neighborhood): """ Determines that neighborhood matches expectation exactly. Note this is just like the tolerate method with a tolerance of 1, but recoding allows for short circuiting. """ - residents = neighborhood.neighbors(cell, grid, self.wrap_around) + residents = neighborhood.neighbors(cell) for resident in residents: if resident[0].value != resident[1]: return False @@ -49,7 +71,7 @@ class Ruleset: return True - def _tolerate(self, cell, grid, neighborhood, tolerance): + def tolerate(self, cell, neighborhood, tolerance): """ Determines that neighborhood matches expectation within tolerance. @@ -57,7 +79,7 @@ class Ruleset: consider this cell to be alive. Note tolerance must be a value 0 <= t <= 1. """ matches = 0 - residents = neighborhood.neighbors(cell, grid, self.wrap_around) + residents = neighborhood.neighbors(cell) for resident in residents: if resident[0].value == resident[1]: matches += 1 @@ -65,32 +87,15 @@ class Ruleset: return (matches / len(residents)) >= tolerance - def _satisfies(self, cell, grid, neighborhood, valid_func): + def satisfies(self, cell, neighborhood, valid_func): """ Allows custom function to relay next state of given cell. The passed function is supplied the list of 2-tuple elements, of which the first is a Cell and the second is the expected state as declared in the Neighborhood, as well as the grid and cell in question. """ - residents = neighborhood.neighbors(cell, grid, self.wrap_around) + residents = neighborhood.neighbors(cell) - return valid_func(cell, grid, residents) + return valid_func(cell, residents) - @np.vectorize - def update(self, cell, *args): - """ - Allow for batch processing of rules. - - We choose our processing function based on the specified rule and update every cell in the grid simultaneously - via a vectorization. - """ - if self.method == Rule.MATCH: - cell.value = self._matches(cell, *args) - elif self.method == Rule.TOLERATE: - cell.value = self._tolerate(cell, *args) - elif self.method == Rule.SATISFY: - cell.value = self._satisfy(cell, *args) - - return cell -