Created
January 4, 2014 18:43
-
-
Save Exirel/8258905 to your computer and use it in GitHub Desktop.
Life's Game of John Horton Conway, with implementation OOP & FP - mainly for study purposes. The idea behind world.World class is to show the different part: where the OOP uses the mutability, and where it doesn't.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
from worldfp import calculate_next | |
from world import World | |
def test_calculate_next(): | |
# Three neighbors from True or False is True | |
assert calculate_next(False, [True] * 3 + [False] * 5) | |
assert calculate_next(True, [True] * 3 + [False] * 5) | |
# Two neighbors from X is still X | |
assert calculate_next(True, [True] * 2 + [False] * 6) | |
assert not calculate_next(False, [True] * 2 + [False] * 6) | |
# Any other cases from True is False | |
assert not calculate_next(True, [True] * 1 + [False] * 7) | |
assert not calculate_next(True, [True] * 4 + [False] * 4) | |
assert not calculate_next(True, [True] * 5 + [False] * 3) | |
assert not calculate_next(True, [True] * 6 + [False] * 2) | |
assert not calculate_next(True, [True] * 7 + [False] * 1) | |
assert not calculate_next(True, [True] * 8 + [False] * 0) | |
# Any other cases from False is False | |
assert not calculate_next(False, [True] * 1 + [False] * 7) | |
assert not calculate_next(False, [True] * 4 + [False] * 4) | |
assert not calculate_next(False, [True] * 5 + [False] * 3) | |
assert not calculate_next(False, [True] * 6 + [False] * 2) | |
assert not calculate_next(False, [True] * 7 + [False] * 1) | |
assert not calculate_next(False, [True] * 8 + [False] * 0) | |
def test_world_str(): | |
world = World(3, 3) | |
result = '%s' % world | |
expected = ('0|0|0\n' | |
'0|0|0\n' | |
'0|0|0') | |
assert result == expected | |
world.set_alive(1, 1) | |
result = '%s' % world | |
expected = ('0|0|0\n' | |
'0|1|0\n' | |
'0|0|0') | |
assert result == expected | |
world.set_dead(1, 1) | |
result = '%s' % world | |
expected = ('0|0|0\n' | |
'0|0|0\n' | |
'0|0|0') | |
assert result == expected | |
world.set_alive(1, 2) | |
result = '%s' % world | |
expected = ('0|0|0\n' | |
'0|0|0\n' | |
'0|1|0') | |
assert result == expected | |
def test_world_is_valid(): | |
"""Assertions about World defining valid or invalid positions""" | |
world = World(10, 10) | |
# Valid position | |
assert world.is_valid(0, 0) is True | |
assert world.is_valid(0, 9) is True | |
assert world.is_valid(9, 0) is True | |
assert world.is_valid(9, 9) is True | |
# Invalid position | |
assert world.is_valid(-1, 0) is False | |
assert world.is_valid(0, -1) is False | |
assert world.is_valid(-1, -1) is False | |
assert world.is_valid(10, 0) is False | |
assert world.is_valid(0, 10) is False | |
assert world.is_valid(10, 10) is False | |
def test_world_position(): | |
"""Assertions about World and its positions""" | |
world = World(10, 10) | |
# Position exist but False | |
assert world.position(0, 0) is False | |
assert world.position(9, 9) is False | |
# Position does not exist (out of range) | |
assert world.position(-1, 0) is None | |
assert world.position(0, -1) is None | |
assert world.position(-1, -1) is None | |
assert world.position(10, 0) is None | |
assert world.position(0, 10) is None | |
assert world.position(10, 10) is None | |
def test_world_set_alive(): | |
"""Assertions about setting position alive""" | |
world = World(10, 10) | |
assert world.position(0, 0) is False | |
assert world.position(1, 1) is False | |
world.set_alive(0, 0) | |
# New position alive | |
assert world.position(0, 0) is True | |
# Another position untouched | |
assert world.position(1, 1) is False | |
try: | |
world.set_alive(10, 10) | |
except KeyError: | |
pass | |
else: | |
raise AssertionError('set_alive should raise a ValueError') | |
def test_world_set_dead(): | |
"""Assertions about setting position dead""" | |
world = World(10, 10) | |
world.set_alive(0, 0) # set_alive already tested successfully | |
assert world.position(0, 0) is True | |
assert world.position(1, 1) is False | |
world.set_dead(0, 0) | |
# New position dead | |
assert world.position(0, 0) is False | |
# Another position untouched | |
assert world.position(1, 1) is False | |
try: | |
world.set_dead(10, 10) | |
except KeyError: | |
pass | |
else: | |
raise AssertionError('set_dead should raise a ValueError') | |
def test_world_reset(): | |
world = World(3, 3) | |
world.set_alive(0, 0) | |
assert world.position(0, 0) is True | |
assert world.position(1, 1) is False | |
world.reset() | |
assert world.position(0, 0) is False | |
assert world.position(1, 1) is False | |
def test_world_get_arround(): | |
"""Assertions about getting the position around a given one""" | |
world = World(3, 3) | |
assert world.get_around(0, 0) == [False, False, False] | |
assert world.get_around(1, 0) == [False, False, False, False, False] | |
assert world.get_around(0, 1) == [False, False, False, False, False] | |
assert world.get_around(1, 1) == [False, False, False, | |
False, False, | |
False, False, False] | |
def test_world_calculate(): | |
"""Asserts computation return the expected result. | |
For example this: | |
0|1|0 | |
0|1|0 | |
0|1|0 | |
Will calculate to: | |
0|0|0 | |
1|1|1 | |
0|0|0 | |
""" | |
world = World(3, 3) | |
world.set_alive(1, 0) | |
world.set_alive(1, 1) | |
world.set_alive(1, 2) | |
result = world.calculate() | |
expected = { | |
0: { | |
0: False, | |
1: True, | |
2: False | |
}, | |
1: { | |
0: False, | |
1: True, | |
2: False | |
}, | |
2: { | |
0: False, | |
1: True, | |
2: False | |
} | |
} | |
assert result == expected | |
def test_world_calculate_diff(): | |
world = World(3, 3) | |
# Nothing possible, no change | |
result = world.calculate_diff() | |
assert result == {} | |
# One alone will be dead | |
world.set_alive(1, 1) | |
result = world.calculate_diff() | |
assert result == {1: {1: False}} | |
# But 3 positions alive will add one alive! | |
world.set_alive(1, 2) | |
world.set_alive(2, 1) | |
result = world.calculate_diff() | |
assert result == {2: {2: True}} | |
def test_world_update(): | |
world = World(3, 3) | |
world.set_alive(1, 0) | |
world.set_alive(1, 1) | |
world.set_alive(1, 2) | |
new_state = { | |
0: { | |
1: True, | |
}, | |
1: { | |
0: False, | |
1: True, | |
2: False | |
}, | |
2: { | |
1: True | |
} | |
} | |
# Middle | |
assert world.position(1, 1) | |
# Positions alive | |
assert world.position(1, 0) | |
assert world.position(1, 2) | |
# Positions dead | |
assert world.position(0, 1) == False | |
assert world.position(2, 1) == False | |
# Update | |
world.update(new_state) | |
# Middle untouched | |
assert world.position(1, 1) | |
# Alive positions are now dead | |
assert world.position(1, 0) == False | |
assert world.position(1, 2) == False | |
# Dead positions are now alive | |
assert world.position(0, 1) | |
assert world.position(2, 1) | |
def test_world_update_empty(): | |
world = World(3, 3) | |
world.set_alive(1, 0) | |
world.set_alive(1, 1) | |
world.set_alive(1, 2) | |
expected = '%s' % world | |
world.update({}) | |
result = '%s' % world | |
assert result == expected | |
def test_world_dump(): | |
world = World(3, 3) | |
assert world.dump() == {} | |
world.set_alive(1, 1) | |
assert world.dump() == { | |
1: {1: True} | |
} | |
def test_world_mutate(): | |
world = World(3, 3) | |
world.set_alive(1, 0) | |
world.set_alive(1, 1) | |
world.set_alive(1, 2) | |
actual_state = { | |
1: {0: True, 1: True, 2: True} | |
} | |
next_state = { | |
0: { | |
1: True, | |
}, | |
1: { | |
1: True, | |
}, | |
2: { | |
1: True | |
} | |
} | |
# Before | |
assert world.dump() == actual_state | |
# Mutate | |
world.mutate() | |
# After | |
assert world.dump() == next_state |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
"""A world living the Life's Game of John Horton Conway. | |
This module provide an OOP implementation of a World (a grid) with positions | |
acting by the rules of Life's Game by John Horton Conway (see also on | |
Wikipedia: http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life). | |
The rules are simple: for the next iteration, each cell with exactly 3 alive | |
neighbors will be alive, and each cell with less than 2 or more than 3 alive | |
neighbors will be dead. | |
""" | |
import worldfp | |
class World(object): | |
"""A world is a grid of dead or alive positions within a specific size.""" | |
def __init__(self, size_x, size_y): | |
"""Initialize World with an X, Y size with a board.""" | |
self.size_x = size_x | |
self.size_y = size_y | |
self.board = {} | |
# ------------------------------------------------------------------------- | |
# Mutable | |
# ------------------------------------------------------------------------- | |
def set_alive(self, x, y): | |
"""Sets alive (True) the given position if exists. | |
If position not into the world's limit, a KeyError is raised. | |
""" | |
if not self.is_valid(x, y): | |
raise KeyError('Position (%d, %d) out of world\'s size (%d, %d)' | |
% (x, y, self.size_x, self.size_y)) | |
self.board.setdefault(x, {})[y] = True | |
def set_dead(self, x, y): | |
"""Sets dead (remove) the given position if exists. | |
If position not into the world's limit, a KeyError is raised. | |
""" | |
if not self.is_valid(x, y): | |
raise KeyError('Position (%d, %d) out of world\'s size (%d, %d)' | |
% (x, y, self.size_x, self.size_y)) | |
if self.position(x, y): | |
y_board = self.board.get(x) | |
del y_board[y] | |
def update(self, new_state): | |
"""Update world's board's state with the given new state.""" | |
self.board = worldfp.update(self, new_state) | |
def mutate(self): | |
"""Mutate to the next generation.""" | |
self.update(self.calculate_diff()) | |
def reset(self): | |
"""Resets the world's state.""" | |
self.board = {} | |
# ------------------------------------------------------------------------- | |
# Immutable | |
# ------------------------------------------------------------------------- | |
def is_valid(self, x, y): | |
"""Returns True if the position is into the world's limit.""" | |
return worldfp.is_valid(self, x, y) | |
def position(self, x, y): | |
"""Returns the position's state (True or False) in the world if exists. | |
If position not into the world's limit, None is returned instead. | |
If position not into the world's board, False is returned. | |
""" | |
return worldfp.position(self, x, y) | |
def get_around(self, x, y): | |
"""Returns list of neighbor's states (if exist) for the given position. | |
Returns a list with each neighbor's state if it exists. For example, | |
a border position will have only 5 neighbors, where a corner position | |
will have only 3. | |
""" | |
return worldfp.get_around(self, x, y) | |
def calculate(self): | |
"""Returns the next state of the world by the game's rules. | |
Returns the whole system, not only the modified positions. | |
""" | |
return worldfp.calculate(self) | |
def calculate_diff(self): | |
"""Returns the difference between actual and the next state. | |
Instead of returning the whole world as calculate does, this method | |
only returns the modified position. | |
""" | |
return worldfp.calculate_diff(self) | |
def dump(self): | |
"""Returns only alive positions.""" | |
return worldfp.dump(self) | |
def __str__(self): | |
"""Returns an ASCII representation of the world using only 0 and 1.""" | |
return worldfp.tostring(self) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
from collections import namedtuple | |
World = namedtuple('World', 'size_x size_y board') | |
def calculate_next(actual_state, neighbors): | |
"""Calculates the next state of the given state according to rules.""" | |
return { | |
3: lambda x: True, | |
2: lambda x: x, | |
}.get(sum(1 for neighbors in neighbors if neighbors is True), | |
lambda x: False)(actual_state) | |
calculate_next_change = lambda actual, neighbors: \ | |
actual != calculate_next(actual, neighbors) | |
def select_y_value(line_actual_y, line_new_y, y): | |
return line_actual_y.get(y) if y not in line_new_y else line_new_y.get(y) | |
def is_valid(world, x, y): | |
"""Returns True if the position is into the world's limit.""" | |
return x < world.size_x and y < world.size_y and x >= 0 and y >= 0 | |
def position(world, x, y): | |
"""Returns the position's state (True or False) in the world if exists. | |
If position not into the world's limit, None is returned instead. | |
If position not defined into the world's board, False is returned. | |
""" | |
if not is_valid(world, x, y): | |
return None | |
return x in world.board and world.board.get(x).get(y, False) | |
def get_around(world, x, y): | |
"""Returns list of neighbor's states (if exist) for the given position. | |
Returns a list with each neighbor's state if it exists. For example, | |
a border position will have only 5 neighbors, where a corner position | |
will have only 3. Any other position will have 8 neighbors. | |
""" | |
position_detla = ( | |
(1, -1), (1, 0), (1, 1), # line up | |
(0, -1), (0, 1), # same line, without central position (0, 0) | |
(-1, -1), (-1, 0), (-1, 1) # line down | |
) | |
return [cell for cell in (position(world, x + dx, y + dy) | |
for dx, dy in position_detla) | |
if cell is not None] | |
def calculate(world): | |
"""Returns the next state of the world's board by the game's rules. | |
Returns a complete new board and not only the modified positions. To get | |
only the modified positions, see `calculate_diff`. | |
""" | |
return dict( | |
(x, dict( | |
(y, calculate_next(position(world, x, y), | |
get_around(world, x, y))) | |
for y in xrange(0, world.size_y) | |
)) | |
for x in xrange(0, world.size_x) | |
) | |
def calculate_diff(world): | |
"""Returns the difference between actual world's board and its next state. | |
Instead of returning the whole board as `calculate` does, this function | |
only returns the modified positions. | |
""" | |
return dict( | |
(pos_x, pos_y_line) | |
for pos_x, pos_y_line in dict( | |
(x, dict( | |
(y, not position(world, x, y)) | |
for y in xrange(0, world.size_y) | |
if calculate_next_change(position(world, x, y), | |
get_around(world, x, y)) | |
)) | |
for x in xrange(0, world.size_x) | |
).items() | |
if len(pos_y_line) | |
) | |
def update(world, new_state): | |
"""Returns an updated board's state with the given new state.""" | |
return dict( | |
(x, dict(world.board.get(x).items()) | |
if x not in new_state | |
else dict( | |
(y, select_y_value(world.board.get(x, {}), | |
new_state.get(x, {}), y)) | |
for y in xrange(0, world.size_y) | |
if y in world.board.get(x, {}) or y in new_state.get(x, {}) | |
)) | |
for x in xrange(0, world.size_x) | |
if x in world.board or x in new_state | |
) | |
def dump(world): | |
"""Returns only alive positions of the world's board.""" | |
return dict( | |
(x, dict( | |
(y, True) | |
for y in xrange(0, world.size_y) | |
if position(world, x, y) | |
)) | |
for x in xrange(0, world.size_x) | |
if x in world.board | |
) | |
def tostring(world): | |
"""Returns an ASCII representation of the world using only 0 and 1.""" | |
return '\n'.join( | |
'|'.join('%s' % int(position(world, x, y)) | |
for x in xrange(0, world.size_x)) | |
for y in xrange(0, world.size_y) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment