Created
December 26, 2011 11:36
-
-
Save dgk/1520958 to your computer and use it in GitHub Desktop.
Artificial life simulator
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
#!/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