Skip to content

Instantly share code, notes, and snippets.

@speezepearson
Last active August 29, 2015 13:58
Show Gist options
  • Save speezepearson/10014982 to your computer and use it in GitHub Desktop.
Save speezepearson/10014982 to your computer and use it in GitHub Desktop.
A Game of Life simulator in PyGUI.
#!/usr/bin/python
#
# A package that runs Conway's Game of Life. Simple GUI interface.
# Requires PyGUI and srptools (a small collection of libraries I wrote, available via my Github account, speezepearson.
#
import pickle
from GUI import (Application, View, Document, Window, Task, Frame,
Slider, Label, Button, ViewBase)
from GUI.Files import FileType
from GUI.Geometry import pt_in_rect
from GUI.StdColors import black, white, red, blue
from srptools.pyguiutils import (StateChangingButton, TitledItemFrame,
ControlWindow)
from math import log
CELL_SIZE = 8 # Size of a grid cell in pixels
# These layout options probably shouldn't be changed. Things look funny
# if they're not at good values. Possibly a surmountable problem, but
# not one I feel like doing at the moment.
SPEED_FRAME_HEIGHT = 50
WINDOW_PADDING = 20
SLIDER_WIDTH = 300
LABEL_WIDTH = 50
BUTTON_WIDTH = 50
DOC_W = 100
DOC_H = DOC_W
def coordinates_around( i, j, imax, jmax ):
"""Returns the neighbor coordinates to (i,j)."""
for ox in (-1,0,1):
for oy in (-1,0,1):
yield ((i+ox)%imax, (j+oy)%jmax)
def _suppress( *args ):
pass
ViewBase.setup_menus = _suppress
# Kluuuuuuuudge.
# Keystrokes just give some weird error message about ignoring an exception
# related to ViewBase having no 'setup_menus' attribute. So... we do this.
class SpeedSlider( Slider ):
def __init__( self, document, label=None ):
self.label = label
self.document = document
Slider.__init__( self, orient='h', max_value=10, live=False,
discrete=False, width=300, value=10*log(4*document.speed,240) )
def action( self ):
speed = 1/4. * 240**(self.value/10)
self.document.set_speed( speed )
self.label.text = 'Speed: %.2f' %speed
class ConwayApp( Application ):
def __init__( self ):
Application.__init__( self )
self.file_type = FileType( name="Conway Document", suffix="cny" )
def open_app( self ):
self.new_cmd()
def make_document( self, fileref ):
return ConwayDocument()
def make_window( self, document ):
"""Makes a window associated with a document, and a control window."""
# First, let's make the control window.
startstop_frame = TitledItemFrame( 'Start/Stop',
StateChangingButton( (('Start', document.start),
('Stop', document.stop)) ) )
slider_frame = TitledItemFrame( 'Speed: 1.00', SpeedSlider(document) )
slider_frame.label.width += 30
slider_frame.item.label = slider_frame.label
control_window = ControlWindow( omnipresent=[startstop_frame, slider_frame] )
control_window.show()
win = Window( document=document, resizable=False )
cell_view = CellView( document )
win.place( cell_view, left=WINDOW_PADDING, top=WINDOW_PADDING )
win.shrink_wrap()
win.show()
class ConwayDocument( Document ):
def __init__( self ):
Document.__init__( self )
self.grid = None
self.speed = None
self.updater = None
self.going = False
def new_contents( self, width=DOC_W, height=DOC_H ):
self.grid = Grid( width, height )
self.set_speed( 1 )
def read_contents( self, from_file ):
states = pickle.load(from_file)
self.grid = Grid(len(states), len(states[0]))
for i in range(self.grid.width):
for j in range(self.grid.height):
if states[i][j]:
self.grid.toggle( i, j )
self.set_speed( 1 )
def write_contents( self, to_file ):
# We'd like to just pickle self.cells, but neighbor lists are circular, so Pickle will explode.
# We'll just record the states instead.
states = [ [self.grid[i,j].on for j in range(self.grid.height)] for i in range(self.grid.width) ]
pickle.dump( states, to_file )
def start( self ):
self.going = True
self.updater.start()
def stop( self ):
self.going = False
self.updater.stop()
def set_speed( self, speed ):
if self.going:
self.updater.stop()
self.speed = speed
self.updater = Task( self.step, interval=1./speed, repeat=True, start=self.going )
def toggle( self, i, j ):
self.grid.toggle( i, j )
self.notify_views('cell_toggled', i, j )
self.changed()
def step( self ):
self.grid.step()
for i, j in self.grid.recently_toggled:
self.notify_views('cell_toggled', i, j )
self.changed()
class CellView( View ):
def __init__( self, document ):
w,h = document.grid.width*CELL_SIZE, document.grid.height*CELL_SIZE
self.first_draw = True
View.__init__( self, model=document, size=(w,h), border=True )
def draw( self, canvas, update_rect ):
if self.first_draw:
print "Drawing."
self.first_draw = False
canvas.pencolor = red
canvas.newpath()
for x in range( CELL_SIZE, self.model.grid.width*CELL_SIZE+1, CELL_SIZE ):
# print "x =", x
canvas.moveto( x, 0 )
canvas.lineto( x, self.model.grid.height*CELL_SIZE )
for y in range( CELL_SIZE, self.model.grid.height*CELL_SIZE+1, CELL_SIZE ):
# print "y =", y
canvas.moveto( 0, y )
canvas.lineto( self.model.grid.width*CELL_SIZE, y )
canvas.stroke()
for i in range(self.model.grid.width):
for j in range(self.model.grid.height):
draw_cell( i, j, canvas, (black if self.model.grid[i,j].on else white) )
for (i,j) in self.model.grid.recently_toggled:
draw_cell( i, j, canvas, (black if self.model.grid[i,j].on else white) )
def cell_toggled( self, model, i, j ):
# print "Cell %f, %f toggled!" %changed_cell
left = i * CELL_SIZE
top = j * CELL_SIZE
right = left + CELL_SIZE-1
bottom = top + CELL_SIZE-1
# print "Passing (%.2f,%.2f,%.2f,%.2f)" %(left,top,right,bottom)
self.invalidate_rect( (left,top,right,bottom) )
def mouse_down( self, event ):
"""Toggles the cell that was clicked on."""
x,y = event.position
i,j = int((x / CELL_SIZE)), int((y / CELL_SIZE))
# print "Click! ", x,y, i,j
self.model.toggle(i,j)
def draw_cell( i, j, canvas, fillcolor ):
canvas.fillcolor = fillcolor
canvas.newpath()
left,right = i*CELL_SIZE, (i+1)*CELL_SIZE - 1
top,bottom = j*CELL_SIZE, (j+1)*CELL_SIZE - 1
canvas.rect( (left,top,right,bottom) )
canvas.fill()
#canvas.newpath()
#canvas.moveto( right, top )
#canvas.lineto( right, bottom )
#canvas.lineto( left, bottom )
#canvas.stroke()
class Grid( object ):
def __init__( self, width, height ):
self.width = width
self.height = height
self.cells = [[Cell() for j in range(height)] for i in range(width)]
for i in range(width):
for j in range(height):
for k,l in coordinates_around(i, j, width, height):
if (k==i) and (l==j):
continue
self.cells[i][j].neighbors.append( self.cells[k][l] )
self.interesting_cells = set([])
self.recently_toggled = set([])
def __getitem__( self, ij ):
i, j = ij
return self.cells[i][j]
def toggle( self, i, j ):
# print 'Toggled %d, %d!' %(i,j)
c = self.cells[i][j]
c.toggle()
for k,l in coordinates_around(i, j, self.width, self.height):
self.interesting_cells.add( (k,l) )
self.recently_toggled.add( (i,j) )
def step( self ):
"""Steps forward by one generation."""
toggle_cells = [ (i,j) for (i,j) in self.interesting_cells if self.cells[i][j].should_toggle() ]
self.interesting_cells = set([])
self.recently_toggled = set([])
for i,j in toggle_cells:
self.toggle( i, j )
class Cell( object ):
def __init__( self, on=False ):
self.on = on
self.neighbors = []
def on_neighbors( self ):
result = 0
for n in self.neighbors:
if n.on:
result += 1
return result
def toggle( self ):
# print "Toggled! Going from",self.on,"to",not self.on
self.on = not self.on
def should_toggle( self ):
"""Returns whether the cell should toggle next generation."""
if self.on:
return self.on_neighbors() not in (2,3)
else:
return self.on_neighbors() == 3
if __name__ == '__main__':
app = ConwayApp()
app.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment