Adds a cellular automata with Goucher 4-states rules.

And a simple demo on a square torus grid.
This commit is contained in:
Johann Dreo 2014-12-24 12:51:00 +01:00
commit 013c004807

135
life.py Normal file
View file

@ -0,0 +1,135 @@
from copy import copy
from itertools import permutations
import random
def count( node, states, game, graph ):
"""Count the number of neighbours in each given states, in a single pass."""
nb = {s:0 for s in states}
for neighbor in graph[node]:
for state in states:
if game[neighbor] == state:
nb[state] += 1
# This is the max size of the neighborhood on a rhomb Penrose tiling (P2)
assert( all(nb[s] <= 11 for s in states) )
return nb
class Goucher:
class State: # Should be an Enum in py3k
ground = 0
head = 1
tail = 2
wing = 3
# Available states, the first one is the default "empty" (or "dead") one.
states = [ State.ground, State.head, State.tail, State.wing ]
def __call__(self, node, current, graph ):
"""This is the Goucher 4-states rule.
From: Adam P. Goucher, "Gliders in cellular automata on Penrose tilings", J. Cellular Automata, 2012
Summarized as:
------------------------------------------------------
| Current state | Neighbour condition | Next state |
------------------------------------------------------
| 0 | n1>=1 | n2>=1 | * | 3 |
| 0 | n1>=1 | * | n3>=2 | 3 |
| 1 | * | * | n3>=1 | 2 |
| 1 | * | * | * | 1 |
| 2 | * | * | * | 3 |
| * | * | * | * | 0 |
------------------------------------------------------
"""
# "a" is just a shortcut.
a = self.State()
# Default state, if nothing matches.
next = a.ground
if current[node] is a.ground:
# Count the number of neighbors of each state in one pass.
nb = count( node, [a.head,a.tail,a.wing], current, graph )
if nb[a.head] >= 1 and nb[a.tail] >= 1:
next = a.wing
elif nb[a.head] >= 1 and nb[a.wing] >= 3:
next = a.wing
elif current[node] is a.head:
# It is of no use to compute the number of heads and tails if the current state is not ground.
nb = count( node, [a.wing], current, graph )
if nb[a.wing] >= 1:
next = a.tail
else:
next = a.head
elif current[node] is a.tail:
next = a.wing
# Default to ground, as stated above.
# else:
# next = a.ground
return next
def make_game( graph, state = lambda x: 0 ):
"""Create a new game board, filled with the results of the calls to the given state function.
The given graph should be an iterable with all the nodes.
The given state function should take a node and return a state.
The default state function returns zero.
"""
game = {}
for node in graph:
game[node] = state(node)
return game
def step( current, graph, rule ):
"""Compute one generation of the game.
i.e. apply the given rule function on each node of the given graph board.
The given current board should associate a state to a node.
The given graph should associate each node with its neighbors.
The given rule is a function that takes a node, the current board and the graph and return the next state of the node."""
# Defaults to the first state of the rule.
next = make_game(graph, lambda x : rule.states[0])
for node in graph:
next[node] = rule( node, current, graph )
return next
def play( game, graph, nb_gen, rule ):
for i in range(nb_gen):
game = step( game, graph, rule )
if __name__ == "__main__":
# Simple demo on a square grid torus.
graph = {}
size = 10
for i in range(size):
for j in range(size):
graph[(i,j)] = []
# 2-permutations of 1-axis neighbors directions == all Moore neighborhood vectors around a coordinate.
for di,dj in permutations( (-1,0,1), 2 ):
# Use modulo to avoid limits and create a torus.
graph[ (i,j) ].append( ( (i+di)%size, (j+dj)%size ) )
rule = Goucher()
# Fill a board with random states.
game = make_game( graph, lambda x : random.choice(rule.states) )
# Play and print.
for i in range(5):
print i
for i in range(size):
for j in range(size):
print game[(i,j)],
print ""
game = step(game,graph,rule)