Skip to content

Instantly share code, notes, and snippets.

@Exirel
Created January 4, 2014 18:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Exirel/8258905 to your computer and use it in GitHub Desktop.
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.
# -*- 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
# -*- 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)
# -*- 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