Skip to content

Instantly share code, notes, and snippets.

@dgk
Created December 26, 2011 11:36
Show Gist options
  • Save dgk/1520958 to your computer and use it in GitHub Desktop.
Save dgk/1520958 to your computer and use it in GitHub Desktop.
Artificial life simulator
#!/usr/bin/env python
'''
usage: ./pylife.py
or ./pylife.py test
requirements:
PyBrain==0.3
numpy==1.6.1
scipy==0.10.0
'''
import unittest
import random
import curses
import copy
from math import sqrt, fabs
from pybrain.tools.shortcuts import buildNetwork
ACTION_MOVE = 1
ACTION_EAT = 2
ACTION_SKIP = 3
ACTION_REPRODUCE = 4
START_ENERGY = 300
STORE_AUTO_ATTR = 0
AUTO_ATTR_PREFIX = '_'
class Autoattr(object):
def __getattribute__(self, attr):
if not attr.startswith(AUTO_ATTR_PREFIX):
try:
value = object.__getattribute__(self, attr)
except AttributeError:
value = None
if value is None:
autoattr = AUTO_ATTR_PREFIX + attr
if hasattr(self, autoattr):
attr_function = getattr(self, autoattr)
if callable(attr_function):
try:
data = attr_function()
except Exception, ex:
raise
if STORE_AUTO_ATTR:
setattr(self, attr, data)
return data
return object.__getattribute__(self, attr)
MALE = 1
FEMALE = 2
REPRODUCE_ENERGY = 80
_random = random.Random()
def choice(list):
return _random.choice(list)
def main(stdscr):
'''
['COLOR_BLACK', 'COLOR_BLUE', 'COLOR_CYAN', 'COLOR_GREEN', 'COLOR_MAGENTA',
'COLOR_RED', 'COLOR_WHITE', 'COLOR_YELLOW']
'''
if curses.has_colors():
bg = curses.COLOR_BLACK
curses.init_pair(1, curses.COLOR_GREEN, bg)
curses.init_pair(2, curses.COLOR_YELLOW, bg)
curses.init_pair(3, curses.COLOR_RED, bg)
# Clear the screen and display the menu of keys
stdscr.clear()
stdscr_y, stdscr_x = stdscr.getmaxyx()
menu_y=(stdscr_y-3)-1
display_menu(stdscr, menu_y)
# Allocate a subwindow for the Life board and create the board object
subwin=stdscr.subwin(stdscr_y-3, stdscr_x, 0, 0)
board=Ground(subwin)
#board=LifeBoard(subwin, char=ord('*'))
board.display()
# xpos, ypos are the cursor's position
xpos, ypos = board.X/2, board.Y/2
# Main loop:
while (1):
stdscr.move(1+ypos, 1+xpos) # Move the cursor
c=stdscr.getch() # Get a keystroke
if 0<c<256:
c=chr(c)
if c in ' ':
stdscr.addstr(ypos, xpos,
', '.join([str(x) for x in
board.plane[xpos][ypos].agents()]))
stdscr.getch()
stdscr.clear()
board.drawBorder()
display_menu(stdscr, menu_y)
board.display()
if c in 'xX':
board.plane[xpos][ypos].erase()
board.display()
elif c in '$4':
board.add(Herb(), xpos, ypos)
board.display()
elif c in '%5':
board.add(Herbivore(), xpos, ypos)
board.display()
elif c in '&7':
board.add(Carnivore(), xpos, ypos)
board.display()
elif c in 'Cc':
erase_menu(stdscr, menu_y)
stdscr.addstr(menu_y, 6, ' Hit any key to stop continuously '
'updating the screen.')
stdscr.refresh()
# Activate nodelay mode; getch() will return -1
# if no keystroke is available, instead of waiting.
stdscr.nodelay(1)
while (1):
c=stdscr.getch()
if c!=-1: break
stdscr.addstr(0,0, '/'); stdscr.refresh()
board.simulate()
stdscr.addstr(0,0, '+'); stdscr.refresh()
board.display()
stdscr.nodelay(0) # Disable nodelay mode
display_menu(stdscr, menu_y)
elif c in 'Ee':
board.erase()
board.display()
elif c in 'Dd':
stdscr.clear()
board.drawBorder()
display_menu(stdscr, menu_y)
board.display()
elif c in 'Qq': break
elif c in 'Rr':
board.makeRandom()
board.display()
elif c in 'Ss':
board.simulate()
board.display()
else: pass # Ignore incorrect keys
elif c==curses.KEY_UP and ypos>0: ypos=ypos-1
elif c==curses.KEY_DOWN and ypos<board.Y-1: ypos=ypos+1
elif c==curses.KEY_LEFT and xpos>0: xpos=xpos-1
elif c==curses.KEY_RIGHT and xpos<board.X-1: xpos=xpos+1
else: pass # Ignore incorrect keys
def erase_menu(stdscr, menu_y):
"Clear the space where the menu resides"
stdscr.move(menu_y, 0) ; stdscr.clrtoeol()
stdscr.move(menu_y+1, 0) ; stdscr.clrtoeol()
def display_menu(stdscr, menu_y):
"Display the menu of possible keystroke commands"
erase_menu(stdscr, menu_y)
stdscr.addstr(menu_y, 4,
'Use the cursor keys to move, and "$","@" or "%" to create a cell.')
stdscr.addstr(menu_y+1, 4,
'reD)raw screen, E)rase the board, R)andom fill, S)tep once or C)ontinuously, Q)uit')
class Brain(Autoattr):
def __init__(self, agent):
self.agent = agent
inputs = len(agent.inputs)
outputs = len(agent.actions)
self.neurons = buildNetwork(inputs, inputs + outputs, outputs)
def choice(self):
res = list(self.neurons.activate(self.agent.inputs))
return self.agent.actions[res.index(max(res))]
class Agent(Autoattr):
_victims__ = []
visionField = 1
_actions__ = []
def __repr__(self):
return '%s(%s): E%s[%s]' % (self.__class__.__name__,
(self.sex == MALE and 'M' or 'F'),
self.energy, self.age)
def __init__(self):
self.energy = START_ENERGY
self.sex = choice([MALE, FEMALE])
self.direction = 0.0
self.age = 0
class FakeCell:
def seen(self, x):
return []
self.cell = FakeCell()
self.brain = Brain(self)
def _victims(self):
return filter(lambda x: x.__class__ in self._victims__, self.seen)
def _enemies(self):
return filter(lambda x: self.__class__ in x._victims__, self.seen)
def _cospecies(self):
return filter(lambda x: self.__class__ is x.__class__, self.seen)
def _sexpartners(self):
return filter(lambda x: self.sex is not x.sex and
x.energy > REPRODUCE_ENERGY, self.cospecies)
def _seen(self):
return self.cell.seen(self)
def _actions(self):
return self._actions__
def _inputs(self):
seen = self.seen
victims = self.victims
enemies = self.enemies
cospecies = self.cospecies
sexpartners = self.sexpartners
return [
self.age,
self.energy,
self.sex,
self.energy < 100,
self.energy > 1000,
len(seen),
len(victims),
len(enemies),
len(cospecies),
len(sexpartners),
]
def nearest(self, agents):
cellAgents = filter(lambda x: x.cell is self.cell, agents)
if cellAgents:
return cellAgents
cells = dict(map(lambda x: (x.cell, x), agents))
nearest = self.cell.nearest(cells.keys())
return filter(lambda x: x.cell in nearest, agents)
def doEat(self):
if not self.victims:
return
nearest = self.nearest(self.victims)
if nearest:
nearest = choice(nearest)
if nearest.cell is self.cell:
self.eat(nearest)
else:
self.move(nearest.cell)
def doReproduce(self):
if not self.sexpartners:
return
nearest = self.nearest(self.sexpartners)
if nearest:
nearest = choice(nearest)
if nearest.cell is self.cell:
self.reproduce(nearest)
else:
self.move(nearest.cell)
def doMove(self):
if self.victims:
return self.move(choice([x. cell for x in self.victims]))
return self.move(choice(self.cell.neighbours()))
def doSkip(self):
enemies = self.enemies
enemiesCells = dict(map(lambda x: (x.cell, x), enemies)).keys()
neighbours = self.cell.neighbours()
safety = filter(lambda x: x not in enemiesCells, neighbours)
if safety:
self.move(choice(safety))
else:
self.move(choice(neighbours))
def simulate(self):
self.age +=1
self.energy -= 1
if self.energy <= 0:
return self.death()
action = self.brain.choice()
try:
if action == ACTION_MOVE:
self.doMove()
elif action == ACTION_REPRODUCE:
self.doReproduce()
elif action == ACTION_EAT:
self.doEat()
elif action == ACTION_SKIP:
self.doSkip()
except Exception, e:
print e
raise
def eat(self, victim):
self.energy += victim.energy / 2
victim.death()
def death(self):
self.energy = 0
self.cell.content.remove(self)
if issubclass(self.__class__, Animal):
self.cell.add(Herb())
def reproduce(self, partner):
childEnergy = (self.energy + partner.energy) / 2
brain = self.brain if self.energy > partner.energy else partner.brain
self.energy = self.energy / 3 * 2
partner.energy = partner.energy / 3 * 2
child = self.__class__()
child.energy = childEnergy
child.brain.neurons = copy.deepcopy(brain.neurons)
self.cell.add(child)
def move(self, cell):
return self.cell.move(self, cell)
def distance(self, agent):
return self.cell.distance(agent.cell)
class Herb(Agent):
char = ord('$')
color = 1
seen = []
def simulate(self):
if random.random() >= 0.8:
self.energy -= 1
else:
self.energy += 1
if self.energy <= 0:
return self.death()
class Animal(Agent):
_actions__ = []
class Herbivore(Animal):
_actions__ = [ACTION_MOVE, ACTION_EAT, ACTION_REPRODUCE, ACTION_SKIP]
_victims__ = [Herb]
char = ord('%')
color = 2
def eat(self, victim):
self.energy += victim.energy / 3
victim.death()
class Carnivore(Animal):
_actions__ = [ACTION_MOVE, ACTION_EAT, ACTION_REPRODUCE, ]
_victims__ = [Herbivore]
char = ord('&')
color = 3
def eat(self, victim):
self.energy += victim.energy / 2
victim.death()
class Cell:
def __init__(self, ground, x, y):
self.content = []
self.ground = ground
self.x = x
self.y = y
def add(self, agent):
if len(self.agents())<9:
self.content.append(agent)
agent.cell = self
def move(self, agent, cell):
try:
self.content.remove(agent)
cell.add(agent)
except:
pass
raise
def empty(self):
return len(self.content) == 0
def single(self):
return len(self.content) == 1
def distance(self, cell):
if cell is self:
return 0.0
return self.ground.distance(self, cell)
def singleType(self):
if self.single():
return 1
type = self.content[0].__class__
for agent in self.content:
if not isinstance(agent, type):
return 0
return 1
def seen(self, spectator):
visionField = spectator.visionField
neighbours = self.neighbours(visionField)
seen = filter(lambda x: x is not spectator, self.agents())
for neighbour in neighbours:
seen += neighbour.agents()
return seen
def erase(self):
self.content = []
def __repr__(self):
return 'cell@%s:%s' % (self.x, self.y)
def char(self):
if self.empty():
return ' '
elif self.single():
return self.content[0].char
if len(self.content)<10:
return ord(str(len(self.agents())))
return '~'
def color(self):
if self.empty():
z = 0
elif self.single():
z = self.content[0].color
elif self.singleType():
z = self.content[0].color
else:
z = 0
color = curses.color_pair(z)
if z:
color = color | curses.A_BOLD
return color
def agents(self):
return filter(lambda x:x.energy, self.content)
def nearest(self, cells):
distances = dict(map(lambda x: (x, self.distance(x)), cells))
nearest = min(distances.values())
return map(lambda x: x[0],
filter(lambda x: x[1] == nearest, distances.items()))
def neighbours(self, visionField=1):
return self.ground.neighbours(self, visionField)
class Ground:
def __init__(self, scr=None, width=10, height=5):
if scr is not None:
self.scr = scr
Y, X = self.scr.getmaxyx()
self.X, self.Y = X-2, Y-2-1
self.scr.clear()
self.drawBorder()
else:
self.Y = height
self.X = width
self.plane = {}
self.distances = {}
self.cells = []
for x in range(self.X):
self.plane[x] = {}
for y in range(self.Y):
cell = Cell(self, x, y)
self.plane[x][y] = cell
self.cells.append(cell)
def add(self, agent, x, y):
self.plane[x][y].add(agent)
def drawBorder(self):
# Draw a border around the board
border_line='+'+(self.X*'-')+'+'
self.scr.addstr(0, 0, border_line)
self.scr.addstr(self.Y+1,0, border_line)
for y in range(0, self.Y):
self.scr.addstr(1+y, 0, '|')
self.scr.addstr(1+y, self.X+1, '|')
self.scr.refresh()
def display(self):
"""Display the whole board, optionally computing one generation"""
X,Y = self.X, self.Y
for x in range(0, X):
for y in range(0, Y):
cell = self.plane[x][y]
if curses.has_colors():
self.scr.attrset(cell.color())
self.scr.addch(y+1, x+1, cell.char())
self.scr.refresh()
def makeRandom(self):
"Fill the board with a random pattern"
for x in range(self.X):
for y in range(self.Y):
rnd = random.random()
if rnd < 0.07:
self.plane[x][y].add(Herb())
elif rnd < 0.09:
self.plane[x][y].add(Herbivore())
elif rnd < 0.093:
self.plane[x][y].add(Carnivore())
def erase(self):
[x.erase() for x in self.cells]
def simulate(self):
agents = self.agents()
for agent in agents:
if agent.energy:
agent.simulate()
def agents(self):
agents = []
for cell in self.cells:
agents += cell.agents()
return agents
def neighbours(self, cell, visionField):
neighbours = []
for x in range(cell.x-visionField, cell.x+visionField+1):
if x>=0 and x<self.X:
for y in range(cell.y-visionField, cell.y+visionField+1):
if y>=0 and y<self.Y:
if not (x==cell.x and y==cell.y):
neighbours.append(self.plane[x][y])
return neighbours
def distance(self, cell1, cell2):
if not self.distances.has_key(cell1):
self.distances[cell1] = {}
if not self.distances.has_key(cell2):
self.distances[cell2] = {}
if not self.distances[cell1].has_key(cell2) or \
self.distances[cell2].has_key(cell1):
distance = sqrt(fabs(
(float(cell1.x) - float(cell2.x)) ** 2 +
(float(cell1.y) - float(cell2.y)) ** 2
))
self.distances[cell1][cell2] = \
self.distances[cell2][cell1] = distance
return distance
else:
return self.distances[cell1][cell2]
class LifeTester(unittest.TestCase):
def setUp(self):
self.ground = Ground(width=10, height=10)
self.herb = Herb()
self.herb2 = Herb()
self.herbivore = Herbivore()
self.herbivore2 = Herbivore()
self.herbivore3 = Herbivore()
self.carnivore = Carnivore()
self.ground.add(self.herb, 2, 3)
self.ground.add(self.herb2, 5, 5)
self.ground.add(self.herbivore, 2, 3)
self.ground.add(self.herbivore2, 3, 4)
self.ground.add(self.herbivore3, 5, 5)
self.ground.add(self.carnivore, 3, 4)
def testAgentFuncs(self):
cell = self.ground.plane[2][3]
self.failUnlessEqual(self.herb.cell, cell)
self.failUnlessEqual(self.herbivore.cell, cell)
self.failUnlessEqual(0.0, self.herb.direction)
self.failUnlessEqual(START_ENERGY, self.herb.energy)
def testEat(self):
self.herbivore.eat(self.herb)
self.failUnlessEqual(START_ENERGY + START_ENERGY / 3,
self.herbivore.energy)
self.failUnlessEqual(0, self.herb.energy)
def testEatAction(self):
herbivoreActions = self.herbivore.actions
self.failUnlessEqual(type(herbivoreActions), type([]))
self.failUnlessEqual(len(herbivoreActions), 4)
def testAgentActions(self):
herbActions = self.herb.actions
self.failUnlessEqual(type(herbActions), type([]))
self.failUnlessEqual(len(herbActions), 0)
herbActions = self.herb.actions
self.failUnlessEqual(type(herbActions), type([]))
self.failUnlessEqual(len(herbActions), 0)
herbivoreActions = self.herbivore.actions
self.failUnlessEqual(type(herbivoreActions), type([]))
self.failUnlessEqual(len(herbivoreActions), 4)
def testSee(self):
cell = self.ground.plane[2][3]
herbivoreSeen = self.herbivore.seen
self.failUnlessEqual(type(herbivoreSeen), type([]))
self.failUnlessEqual(len(herbivoreSeen), 3)
self.failUnless(self.herb in herbivoreSeen)
herbSeen = self.herb.seen
self.failUnlessEqual(type(herbSeen), type([]))
self.failUnlessEqual(len(herbSeen), 0)
def testVictims(self):
herbivoreVictims = self.herbivore.victims
self.failUnlessEqual(type(herbivoreVictims), type([]))
self.failUnlessEqual(len(herbivoreVictims), 1)
self.failUnless(self.herb in herbivoreVictims)
carnivoreVictims = self.carnivore.victims
self.failUnlessEqual(type(carnivoreVictims), type([]))
self.failUnlessEqual(len(carnivoreVictims), 2)
self.failUnless(self.herbivore in carnivoreVictims)
self.failUnless(self.herbivore2 in carnivoreVictims)
def testEnemies(self):
herbivoreEnemies = self.herbivore.enemies
self.failUnlessEqual(type(herbivoreEnemies), type([]))
self.failUnlessEqual(len(herbivoreEnemies), 1)
self.failUnless(self.carnivore in herbivoreEnemies)
carnivoreEnemies = self.carnivore.enemies
self.failUnlessEqual(type(carnivoreEnemies), type([]))
self.failUnlessEqual(len(carnivoreEnemies), 0)
def testAgentDistances(self):
self.failUnlessEqual(self.herbivore.distance(self.herb), 0.0)
self.assertAlmostEquals(self.herbivore.distance(self.herbivore2),
1.4, 1)
def testAgentMove(self):
srcCell = self.ground.plane[2][3]
dstCell = self.ground.plane[3][3]
self.failUnless(self.herbivore in srcCell.agents())
self.herbivore.move(dstCell)
self.failIf(self.herbivore in srcCell.agents())
self.failUnless(self.herbivore in dstCell.agents())
self.failUnless(dstCell is self.herbivore.cell)
def testCellNearest(self):
cell = self.ground.plane
self.failUnless(cell[1][1] in cell[0][0].nearest(
[cell[1][1], cell[2][2]]))
self.failIf(cell[2][2] in cell[0][0].nearest(
[cell[1][1], cell[2][2]]))
def testCellDistances(self):
cell = self.ground.plane
self.failUnlessEqual(cell[0][0].distance(cell[0][0]), 0.0)
self.failUnlessEqual(cell[0][0].distance(cell[1][0]), 1.0)
self.failUnlessEqual(cell[0][0].distance(cell[0][1]), 1.0)
self.assertAlmostEquals(cell[0][0].distance(cell[1][1]), 1.4, 1)
self.assertAlmostEquals(cell[0][0].distance(cell[2][1]), 2.2, 1)
self.assertAlmostEquals(cell[0][0].distance(cell[2][2]), 2.8, 1)
def testCellNeighbours(self):
plane = self.ground.plane
def arraysEquals(real, expected):
real.sort()
expected.sort()
self.failUnlessEqual(real, expected)
#test 1
cell = plane[2][3]
neighbours = self.ground.neighbours(cell, 1)
expectedNeighbours = [ plane[1][4], plane[2][4], plane[3][4], plane[1][3],
plane[3][3], plane[1][2], plane[2][2], plane[3][2], ]
arraysEquals(neighbours, expectedNeighbours)
#test 2
cell = plane[0][2]
neighbours = self.ground.neighbours(cell, 1)
expectedNeighbours = [ plane[0][1], plane[1][1], plane[1][2],
plane[0][3], plane[1][3], ]
arraysEquals(neighbours, expectedNeighbours)
#test 3
cell = plane[0][0]
neighbours = self.ground.neighbours(cell, 1)
expectedNeighbours = [ plane[0][1], plane[1][0], plane[1][1], ]
arraysEquals(neighbours, expectedNeighbours)
#test 4
cell = plane[9][9]
neighbours = self.ground.neighbours(cell, 1)
expectedNeighbours = [ plane[9][8], plane[8][8], plane[8][9], ]
arraysEquals(neighbours, expectedNeighbours)
def testAgentNearest(self):
nearest = self.herbivore.nearest(self.herbivore.enemies)
self.failUnless(self.carnivore in nearest)
def testAgentDeath(self):
cell = self.ground.plane[3][4]
herbivores = filter(lambda x: isinstance(x, Herbivore), cell.agents())
self.failUnless(herbivores)
self.herbivore2.death()
herbivores = filter(lambda x: isinstance(x, Herbivore), cell.agents())
self.failIf(herbivores)
def testAgentDeathAndBirthHerb(self):
cell = self.ground.plane[3][4]
herbs = filter(lambda x: isinstance(x, Herb), cell.agents())
self.failIf(herbs)
self.herbivore2.death()
herbs = filter(lambda x: isinstance(x, Herb), cell.agents())
self.failUnlessEqual(len(herbs), 1)
herb = herbs[0]
herb.death()
herbs = filter(lambda x: isinstance(x, Herb), cell.agents())
self.failIf(herbs)
def testInputs(self):
self.failUnless(isinstance(self.herbivore.inputs, list))
if __name__ == '__main__':
import sys
if 'test' in sys.argv:
sys.argv = sys.argv[:1] + sys.argv[2:]
unittest.main()
else:
curses.wrapper(main)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment