User:Danf/TurtleGraphics: Difference between revisions
Jump to navigation
Jump to search
'x' to re-randomize ... lulz |
features & refactoring: cycle detection, separation of concerns |
||
| Line 1: | Line 1: | ||
<pre> | <pre> | ||
# turtlife.py - Artistic License w/ Attribution -> "(evil) Dan of MOISEBRIDGE" | # turtlife.py - Artistic License w/ Attribution -> "(evil) Dan of MOISEBRIDGE" | ||
# note: press 'n' to advance frame, 'r' to run, 'p' to pause | # note: press 'n' to advance frame, 'r' to run, 'p' to pause | ||
from turtle import Screen, Turtle, mainloop | from turtle import Screen, Turtle, mainloop | ||
from itertools import islice, product, repeat, starmap | from itertools import count, islice, product, repeat, starmap | ||
from random import randint | from collections import deque | ||
from random import choice, randint | |||
from time import sleep | from time import sleep | ||
class Logic(object): | |||
def __init__(self, rules=None): | |||
self.rules = rules | |||
def evaluate(self, o=None): | |||
ret = None | |||
if self.rules == 'life': | |||
ret = self.evaluate_life(o) | |||
elif self.rules == 'prime': | |||
ret = self.evaluate_prime(o) | |||
else: # random | |||
ret = (choice((0, 1)), choice(range(0, 9))) | |||
return ret | |||
def evaluate_life(self, o): | |||
n = o.neighborsum() | |||
if n == 2: | |||
destiny = o.binary_val | |||
elif n == 3: | |||
destiny = 1 | |||
else: | |||
destiny = 0 | |||
return (destiny, n) | |||
def evaluate_prime(self, o): | |||
n = o.neighborsum() | |||
if n in (2, 3): | |||
destiny = 1 | |||
elif n in (5, 7): | |||
destiny = o.binary_val | |||
else: | |||
destiny = 0 | |||
return (destiny, n) | |||
def toggle(self): | |||
if self.rules == 'life': | |||
self.rules = 'prime' | |||
elif self.rules == 'prime': | |||
self.rules = 'life' | |||
else: | |||
self.rules = choice(('prime', 'life')) | |||
class Cell(object): | class Cell(object): | ||
| Line 17: | Line 52: | ||
self.row = row | self.row = row | ||
self.col = col | self.col = col | ||
self. | self.binary_val = 0 | ||
self. | self.cause = 0 | ||
self. | self.logic = colony.logic | ||
self._neighbors = None | self._neighbors = None | ||
def neighbors(self): | def neighbors(self): | ||
if self._neighbors is None: | if self._neighbors is None: | ||
self._neighbors = list(self.colony. | self._neighbors = list(starmap( | ||
lambda x, y: self.colony.cells[x * self.colony.cols + y], | |||
self.colony.neighborhood(self.row, self.col) )) | |||
return self._neighbors | return self._neighbors | ||
def neighborsum(self): | def neighborsum(self): | ||
return sum(o. | return sum(o.binary_val for o in self.neighbors()) | ||
def destiny(self): | def destiny(self): | ||
effect, cause = self.logic.evaluate(self) | |||
return (effect, cause) | |||
def update(self, effect=None, cause=None): | |||
if effect is not None: | |||
self.binary_val = 1 if effect else 0 | |||
def | if cause is not None: | ||
if | self.cause = cause | ||
self. | return self.binary_val | ||
if | |||
self. | |||
return self. | |||
class | class Colony(object): | ||
def __init__(self, rules, displaymode, rows, cols): | def __init__(self, rules, displaymode, rows, cols): | ||
self. | self.logic = Logic(rules) | ||
self.rows = rows | self.rows = rows | ||
self.cols = cols | self.cols = cols | ||
| Line 51: | Line 82: | ||
lambda x, y: Cell(self, x, y), | lambda x, y: Cell(self, x, y), | ||
product(range(rows), range(cols)) )) | product(range(rows), range(cols)) )) | ||
self. | self.state = State(self) | ||
self.display = ColonyDisplay(self, displaymode) | |||
def neighborhood(self, row, col): | def neighborhood(self, row, col): | ||
up = row - 1 if row else self.rows - 1 | up = row - 1 if row else self.rows - 1 | ||
| Line 63: | Line 92: | ||
(row, left), (row, right), | (row, left), (row, right), | ||
(down, left), (down, col), (down, right) ) | (down, left), (down, col), (down, right) ) | ||
def | def randomize(self): | ||
orig_rules = self.logic.rules | |||
lambda x, y: | self.logic.rules = 'random' | ||
self.update() | |||
self.logic.rules = orig_rules | |||
def update(self, sync=True): | |||
self.state.update() | |||
self.display.update() | |||
class ColonyDisplay(object): | |||
def __init__(self, colony, displaymode): | |||
self.displaymode = displaymode | |||
self.cells = colony.cells | |||
self.textdisplay = lambda: colony.state.displaystring() | |||
self.turtles = list(starmap( | |||
lambda x, y: ColonialTurtle(colony, x, y), | |||
product(range(colony.rows), range(colony.cols)) )) | |||
def update(self): | |||
print(self.textdisplay()) | |||
self.turtledisplay() | |||
def turtledisplay(self): | def turtledisplay(self): | ||
for c, t in zip(self.cells, self.turtles): | for c, t in zip(self.cells, self.turtles): | ||
if c. | if c.binary_val: | ||
if c. | if c.cause == 2: | ||
t. | t.set_rgb('blue') | ||
elif c. | elif c.cause == 3: | ||
t. | t.set_rgb('green') | ||
elif c. | elif c.cause == 5: | ||
t. | t.set_rgb('yellow') | ||
elif c. | elif c.cause == 7: | ||
t. | t.set_rgb('red') | ||
else: | else: | ||
if self.displaymode == 'ambient': | if self.displaymode == 'ambient': | ||
t. | t.set_rgb(t.ambience()) | ||
elif self.displaymode == 'fade': | elif self.displaymode == 'fade': | ||
t.set_rgb(0.618) | |||
else: | else: | ||
t. | t.set_rgb('black') | ||
def | class State(object): | ||
for | def __init__(self, colony): | ||
self.colony = colony | |||
def | self.context = None | ||
self. | self.lookback = 1000 | ||
self. | self.gen = 0 | ||
self.history = deque(maxlen=self.lookback) | |||
self.cycle_detector = None | |||
def bitvals(self): | |||
bits = 0 | |||
for o in self.colony.cells: | |||
bits <<= 1 | |||
bits += o.binary_val | |||
return bits | |||
def update_context(self): | |||
prev_context = self.context | |||
self.context = (self.colony.logic.rules, self.colony.rows, self.colony.cols) | |||
return (self.context, prev_context) | |||
def memorialize(self): | |||
current, prev = self.update_context() | |||
if current != prev: | |||
self.history.append((self.gen, (current, prev))) | |||
self.history.append((self.gen, self.bitvals())) | |||
def update(self, sync=True): | |||
dst = (x.destiny() for x in self.colony.cells) | |||
for c, d in zip(self.colony.cells, list(dst) if sync else dst): | |||
effect, cause = d | |||
c.update(effect, cause) | |||
self.memorialize() | |||
self.gen += 1 | |||
def smash(self): | |||
self.history.clear() | |||
self.gen = 0 | |||
def dump(self): | |||
cur, prev = None, None | |||
print('history:') | |||
for gen, entry in self.history: | |||
if type(entry) == tuple: # context | |||
cur, prev = entry | |||
else: | |||
print(cur, gen) | |||
print(self.bitstring(entry)) | |||
def cycler(self): | |||
if self.cycle_detector: # toggle | |||
self.cycle_detector = None | |||
ret = None | |||
else: | |||
self.cycle_detector = dict() | |||
ret = self.cycler_check_history() | |||
return ret | |||
def cycler_check_history(self): | |||
cycle_found = None | |||
for gen, entry in self.history: | |||
if type(entry) == tuple: # context | |||
continue | |||
regen = self.cycler_check_entry(gen, entry) | |||
if gen != regen: | |||
cycle_found = regen | |||
break | |||
return cycle_found | |||
def cycler_check_entry(self, gen, entry): | |||
if entry in self.cycle_detector: # key via history <- self.bitvals() | |||
ret = self.cycle_detector[entry] | |||
else: | |||
self.cycle_detector[entry] = gen | |||
ret = gen | |||
return ret | |||
def bitstring(self, bits=None, wrapped=True): | |||
if bits is None: | |||
bits = self.bitvals() | |||
s = '{0:0{n}b}'.format(bits, n=len(self.colony.cells)) | |||
if wrapped: | |||
s = '\n'.join(map( | |||
lambda x: s[x:x+self.colony.cols], | |||
range(0, len(s), self.colony.cols) )) | |||
return s | |||
def displaystring(self, which=-1): | |||
gen, entry = self.history[which] | |||
if which == -1: | |||
cur = self.context | |||
while type(entry) == tuple: # context | |||
cur, prev = entry | |||
which -= 1 | |||
if which + len(d) < 0: | |||
return "" | |||
gen, entry = self.history[which] | |||
s = ' '.join((str(cur), str(gen))) | |||
return '\n'.join((s, self.bitstring(entry).replace('1','o').replace('0',' '))) | |||
class | class ColonialTurtle(Turtle): | ||
def __init__(self, colony, row, col): | def __init__(self, colony, row, col): | ||
Turtle.__init__(self) | Turtle.__init__(self) | ||
| Line 103: | Line 227: | ||
self.col = col | self.col = col | ||
self.speed(0) | self.speed(0) | ||
self.shape("turtle") | |||
self.shape(" | self.settiltangle(45) | ||
self.resizemode("user") | self.resizemode("user") | ||
self.shapesize( | self.shapesize(1, 1, 0) | ||
self. | self.penup() | ||
self.setx(col) | self.setx(col) | ||
self.sety(row) | self.sety(row) | ||
| Line 119: | Line 242: | ||
( 'green', (0.0, 0.7, 0.0) ), | ( 'green', (0.0, 0.7, 0.0) ), | ||
( 'blue', (0.0, 0.0, 0.7) ) ) ) | ( 'blue', (0.0, 0.0, 0.7) ) ) ) | ||
self. | self.set_rgb('black') | ||
self._neighbors = None | self._neighbors = None | ||
def neighbors(self): | def neighbors(self): | ||
if self._neighbors is None: | if self._neighbors is None: | ||
self._neighbors = list(self.colony. | self._neighbors = list(starmap( | ||
lambda x, y: self.colony.display.turtles[x * self.colony.cols + y], | |||
self.colony.neighborhood(self.row, self.col) )) | |||
return self._neighbors | return self._neighbors | ||
def set_rgb(self, c): | |||
otype = type(c) | |||
if otype is float: | |||
for i in range(3): | |||
self.rgb[i] *= c | |||
else: | |||
if otype is str: | |||
c = self.colors[c] | |||
self.rgb = list(c) | |||
self.color(self.rgb) | |||
def avg_rgb(self, turtles): | def avg_rgb(self, turtles): | ||
rgb = [0.0, 0.0, 0.0] | rgb = [0.0, 0.0, 0.0] | ||
| Line 135: | Line 269: | ||
def ambience(self): | def ambience(self): | ||
return self.avg_rgb(self.neighbors()) | return self.avg_rgb(self.neighbors()) | ||
class ScreenRunner(object): | class ScreenRunner(object): | ||
def __init__(self, rules='prime', displaymode='ambient', rows=16, cols=32): | def __init__(self, rules='prime', displaymode='ambient', delay=0, rows=16, cols=32): | ||
self.delay_milliseconds = delay | |||
self.screen = self.initscreen(rows, cols) | self.screen = self.initscreen(rows, cols) | ||
self. | self.colony = Colony(rules, displaymode, rows, cols) | ||
self. | self.randomize() | ||
self. | self.run() | ||
def initscreen(self, rows, cols): | def initscreen(self, rows, cols): | ||
screen = Screen() | screen = Screen() | ||
| Line 176: | Line 287: | ||
return screen | return screen | ||
def bindkeys(self, screen): | def bindkeys(self, screen): | ||
screen.onkey(self. | screen.onkey(self.cycle_detection, 'c') | ||
screen.onkey(self.dump, 'd') | |||
screen.onkey(self.mode, 'm') | |||
screen.onkey(self.next, 'n') | screen.onkey(self.next, 'n') | ||
screen.onkey(self.pause, 'p') | |||
screen.onkey(self.run, 'r') | screen.onkey(self.run, 'r') | ||
screen.onkey(self. | screen.onkey(self.smash, 's') | ||
screen.onkey(self. | screen.onkey(self.randomize, 'x') | ||
screen.onkey(self.quit, 'q') | screen.onkey(self.quit, 'q') | ||
screen.listen() | screen.listen() | ||
def | def cycle_detection(self): | ||
self. | detected = self.colony.state.cycler() | ||
self. | if detected is not None: | ||
print('cycle detected - gen ', detected) | |||
def dump(self): | |||
self.colony.state.dump() | |||
def mode(self): | |||
self.colony.logic.toggle() | |||
self.colony.update() | |||
def next(self): | def next(self): | ||
self. | self.colony.update() | ||
def pause(self): | |||
self.running = False | |||
def run(self): | def run(self): | ||
self.running = True | self.running = True | ||
self.timer() | self.timer() | ||
def | def randomize(self): | ||
self. | self.colony.randomize() | ||
def smash(self): | |||
self.colony.state.smash() | |||
def | |||
self. | |||
def quit(self): | def quit(self): | ||
exit() | exit() | ||
def timer(self, delay= | def timer(self, delay=None): | ||
if delay is None: | |||
delay = self.delay_milliseconds | |||
if self.running: | if self.running: | ||
self.next() | self.next() | ||
| Line 205: | Line 327: | ||
def main(): | def main(): | ||
ScreenRunner(rules='prime', displaymode='ambient', delay=0, rows=17, cols=23) # delay in milliseconds | |||
return "EVENTLOOP" | return "EVENTLOOP" | ||
| Line 212: | Line 334: | ||
print(msg) | print(msg) | ||
mainloop() | mainloop() | ||
</pre> | |||
Latest revision as of 19:54, 23 January 2015
# turtlife.py - Artistic License w/ Attribution -> "(evil) Dan of MOISEBRIDGE"
# note: press 'n' to advance frame, 'r' to run, 'p' to pause
from turtle import Screen, Turtle, mainloop
from itertools import count, islice, product, repeat, starmap
from collections import deque
from random import choice, randint
from time import sleep
class Logic(object):
def __init__(self, rules=None):
self.rules = rules
def evaluate(self, o=None):
ret = None
if self.rules == 'life':
ret = self.evaluate_life(o)
elif self.rules == 'prime':
ret = self.evaluate_prime(o)
else: # random
ret = (choice((0, 1)), choice(range(0, 9)))
return ret
def evaluate_life(self, o):
n = o.neighborsum()
if n == 2:
destiny = o.binary_val
elif n == 3:
destiny = 1
else:
destiny = 0
return (destiny, n)
def evaluate_prime(self, o):
n = o.neighborsum()
if n in (2, 3):
destiny = 1
elif n in (5, 7):
destiny = o.binary_val
else:
destiny = 0
return (destiny, n)
def toggle(self):
if self.rules == 'life':
self.rules = 'prime'
elif self.rules == 'prime':
self.rules = 'life'
else:
self.rules = choice(('prime', 'life'))
class Cell(object):
def __init__(self, colony, row, col):
self.colony = colony
self.row = row
self.col = col
self.binary_val = 0
self.cause = 0
self.logic = colony.logic
self._neighbors = None
def neighbors(self):
if self._neighbors is None:
self._neighbors = list(starmap(
lambda x, y: self.colony.cells[x * self.colony.cols + y],
self.colony.neighborhood(self.row, self.col) ))
return self._neighbors
def neighborsum(self):
return sum(o.binary_val for o in self.neighbors())
def destiny(self):
effect, cause = self.logic.evaluate(self)
return (effect, cause)
def update(self, effect=None, cause=None):
if effect is not None:
self.binary_val = 1 if effect else 0
if cause is not None:
self.cause = cause
return self.binary_val
class Colony(object):
def __init__(self, rules, displaymode, rows, cols):
self.logic = Logic(rules)
self.rows = rows
self.cols = cols
self.cells = list(starmap(
lambda x, y: Cell(self, x, y),
product(range(rows), range(cols)) ))
self.state = State(self)
self.display = ColonyDisplay(self, displaymode)
def neighborhood(self, row, col):
up = row - 1 if row else self.rows - 1
down = row + 1 if row < self.rows - 1 else 0
left = col - 1 if col else self.cols - 1
right = col + 1 if col < self.cols - 1 else 0
return ( (up, left), (up, col), (up, right),
(row, left), (row, right),
(down, left), (down, col), (down, right) )
def randomize(self):
orig_rules = self.logic.rules
self.logic.rules = 'random'
self.update()
self.logic.rules = orig_rules
def update(self, sync=True):
self.state.update()
self.display.update()
class ColonyDisplay(object):
def __init__(self, colony, displaymode):
self.displaymode = displaymode
self.cells = colony.cells
self.textdisplay = lambda: colony.state.displaystring()
self.turtles = list(starmap(
lambda x, y: ColonialTurtle(colony, x, y),
product(range(colony.rows), range(colony.cols)) ))
def update(self):
print(self.textdisplay())
self.turtledisplay()
def turtledisplay(self):
for c, t in zip(self.cells, self.turtles):
if c.binary_val:
if c.cause == 2:
t.set_rgb('blue')
elif c.cause == 3:
t.set_rgb('green')
elif c.cause == 5:
t.set_rgb('yellow')
elif c.cause == 7:
t.set_rgb('red')
else:
if self.displaymode == 'ambient':
t.set_rgb(t.ambience())
elif self.displaymode == 'fade':
t.set_rgb(0.618)
else:
t.set_rgb('black')
class State(object):
def __init__(self, colony):
self.colony = colony
self.context = None
self.lookback = 1000
self.gen = 0
self.history = deque(maxlen=self.lookback)
self.cycle_detector = None
def bitvals(self):
bits = 0
for o in self.colony.cells:
bits <<= 1
bits += o.binary_val
return bits
def update_context(self):
prev_context = self.context
self.context = (self.colony.logic.rules, self.colony.rows, self.colony.cols)
return (self.context, prev_context)
def memorialize(self):
current, prev = self.update_context()
if current != prev:
self.history.append((self.gen, (current, prev)))
self.history.append((self.gen, self.bitvals()))
def update(self, sync=True):
dst = (x.destiny() for x in self.colony.cells)
for c, d in zip(self.colony.cells, list(dst) if sync else dst):
effect, cause = d
c.update(effect, cause)
self.memorialize()
self.gen += 1
def smash(self):
self.history.clear()
self.gen = 0
def dump(self):
cur, prev = None, None
print('history:')
for gen, entry in self.history:
if type(entry) == tuple: # context
cur, prev = entry
else:
print(cur, gen)
print(self.bitstring(entry))
def cycler(self):
if self.cycle_detector: # toggle
self.cycle_detector = None
ret = None
else:
self.cycle_detector = dict()
ret = self.cycler_check_history()
return ret
def cycler_check_history(self):
cycle_found = None
for gen, entry in self.history:
if type(entry) == tuple: # context
continue
regen = self.cycler_check_entry(gen, entry)
if gen != regen:
cycle_found = regen
break
return cycle_found
def cycler_check_entry(self, gen, entry):
if entry in self.cycle_detector: # key via history <- self.bitvals()
ret = self.cycle_detector[entry]
else:
self.cycle_detector[entry] = gen
ret = gen
return ret
def bitstring(self, bits=None, wrapped=True):
if bits is None:
bits = self.bitvals()
s = '{0:0{n}b}'.format(bits, n=len(self.colony.cells))
if wrapped:
s = '\n'.join(map(
lambda x: s[x:x+self.colony.cols],
range(0, len(s), self.colony.cols) ))
return s
def displaystring(self, which=-1):
gen, entry = self.history[which]
if which == -1:
cur = self.context
while type(entry) == tuple: # context
cur, prev = entry
which -= 1
if which + len(d) < 0:
return ""
gen, entry = self.history[which]
s = ' '.join((str(cur), str(gen)))
return '\n'.join((s, self.bitstring(entry).replace('1','o').replace('0',' ')))
class ColonialTurtle(Turtle):
def __init__(self, colony, row, col):
Turtle.__init__(self)
self.colony = colony
self.row = row
self.col = col
self.speed(0)
self.shape("turtle")
self.settiltangle(45)
self.resizemode("user")
self.shapesize(1, 1, 0)
self.penup()
self.setx(col)
self.sety(row)
self.colors = dict( (
( 'black', (0.0, 0.0, 0.0) ),
( 'grey50', (0.5, 0.5, 0.5) ),
( 'white', (1.0, 1.0, 1.0) ),
( 'red', (0.7, 0.0, 0.0) ),
( 'yellow', (0.7, 0.7, 0.0) ),
( 'green', (0.0, 0.7, 0.0) ),
( 'blue', (0.0, 0.0, 0.7) ) ) )
self.set_rgb('black')
self._neighbors = None
def neighbors(self):
if self._neighbors is None:
self._neighbors = list(starmap(
lambda x, y: self.colony.display.turtles[x * self.colony.cols + y],
self.colony.neighborhood(self.row, self.col) ))
return self._neighbors
def set_rgb(self, c):
otype = type(c)
if otype is float:
for i in range(3):
self.rgb[i] *= c
else:
if otype is str:
c = self.colors[c]
self.rgb = list(c)
self.color(self.rgb)
def avg_rgb(self, turtles):
rgb = [0.0, 0.0, 0.0]
n = len(turtles)
for t in turtles:
for i in range(3):
rgb[i] += t.rgb[i]
return map(lambda x: x/n, rgb)
def ambience(self):
return self.avg_rgb(self.neighbors())
class ScreenRunner(object):
def __init__(self, rules='prime', displaymode='ambient', delay=0, rows=16, cols=32):
self.delay_milliseconds = delay
self.screen = self.initscreen(rows, cols)
self.colony = Colony(rules, displaymode, rows, cols)
self.randomize()
self.run()
def initscreen(self, rows, cols):
screen = Screen()
screen.delay(0)
offset = map(lambda x: x - 0.3, (0, rows, cols, 0))
screen.setworldcoordinates(*offset)
screen.bgcolor(0.0, 0.0, 0.0)
screen.tracer(n=rows*cols)
self.bindkeys(screen)
return screen
def bindkeys(self, screen):
screen.onkey(self.cycle_detection, 'c')
screen.onkey(self.dump, 'd')
screen.onkey(self.mode, 'm')
screen.onkey(self.next, 'n')
screen.onkey(self.pause, 'p')
screen.onkey(self.run, 'r')
screen.onkey(self.smash, 's')
screen.onkey(self.randomize, 'x')
screen.onkey(self.quit, 'q')
screen.listen()
def cycle_detection(self):
detected = self.colony.state.cycler()
if detected is not None:
print('cycle detected - gen ', detected)
def dump(self):
self.colony.state.dump()
def mode(self):
self.colony.logic.toggle()
self.colony.update()
def next(self):
self.colony.update()
def pause(self):
self.running = False
def run(self):
self.running = True
self.timer()
def randomize(self):
self.colony.randomize()
def smash(self):
self.colony.state.smash()
def quit(self):
exit()
def timer(self, delay=None):
if delay is None:
delay = self.delay_milliseconds
if self.running:
self.next()
self.screen.ontimer(lambda: self.timer(delay), delay)
def main():
ScreenRunner(rules='prime', displaymode='ambient', delay=0, rows=17, cols=23) # delay in milliseconds
return "EVENTLOOP"
if __name__ == "__main__":
msg = main()
print(msg)
mainloop()