Source

sketchbook / gol / main0.4.py

Full commit
import os, sys
import pygame
from pygame.locals import *
from pygame import time

import cProfile
from optparse import OptionParser
"""
Game of Life sim

"""

# Life engine that handles all of the life calcs implementing:
#    Random and sequential access for state data
#    Acts as a generator so that next() that calculates the next iteration of the game

# LifeController class that handles updating the life engine and drawing to screen
# Will implement animation and heat mapping

TILE_SIZE = 5
WINDOW_SIZE = (640,480)
FONT_SIZE = 16

INITIAL_STATE = [
    (16,15),
    (16,16),
    (16,17),
    (20,19),
    (20,18),
    (20,16),
    (21,19),
    (22,19),
    (23,18),
    (20,17),
    (18,17),
    (17,17),
    ]



class Life(object):
    """
    _width: the width of the grid
    _height: the height of the grid
    cells: {(int x, int y) : (bool current, bool changed)}
    iteration: number of times update has been called
    """
    def __init__(self, dimensions):
        self._width = dimensions[0]
        self._height = dimensions[1]
        self.cells = {}
        self.changed = {}
        self.iteration = 1

        for (x,y) in self.tile_range():
            if (x,y) in INITIAL_STATE:
                self.cells[(x,y)] = True
                self.changed[(x,y)] = True
            else:
                self.cells[(x,y)] = False
                self.changed[(x,y)] = False

    def next_iteration(self, cells, changed):
        """
        Each cell checks the 9 cells surrounding it for existing cells.
        We have already checked 1,2,4,5,7,8 in the previous iteration 
        so we store them in neighbours[] requiring only a check of 3,6,9
        1 2 3
        4 5 6
        7 8 9
        This is only done on a row-by-row basis
        """

        future_state = {}

        for y in xrange(self._height):
            neighbours = []
            for x in xrange(self._width):
                for (nx,ny,cellno) in self.gen_neighbours(x,y):
                    if cells[(nx, ny)]:
                        neighbours.append(cellno)
                nlen = len(neighbours)

                if 5 in neighbours: nlen -= 1
        
                if cells[(x,y)]:
                    if nlen > 3: 
                        future_state[(x,y)] = False
                        changed[(x,y)] = True
                    elif nlen < 2: 
                        future_state[(x,y)] = False
                        changed[(x,y)] = True
                    elif nlen in [2,3]: 
                        future_state[(x,y)] = True
                        changed[(x,y)] = False
                else:
                    if nlen == 3:
                        future_state[(x,y)] = True
                        changed[(x,y)] = True
                    else:
                        future_state[(x,y)] = False
                        changed[x,y] = False

                #store current neighbours as matches for next iteration
                neighbours = [x-1 for x in neighbours if x not in [1,4,7]]
        self.cells = future_state

    def gen_neighbours(self, x, y):
        r_edge = self._width - 1
        b_edge = self._height - 1

        i = 1
        #if x is at left edge loop thru all 9
        if x == 0:
            for yy in xrange(y-1, y+2):
                for xx in xrange(-1, 2):
                    if yy > b_edge: 
                        yy -= b_edge
                    elif yy < 0: 
                        yy += b_edge
                    if xx < 0: 
                        xx += r_edge
                    yield (xx,yy,i)
                    i += 1
        #otherwise, just the rightmost 3 of the 9 neighbours
        else:
            x += 1
            for yy in xrange(y-1, y+2):
                if yy > b_edge: 
                    yy -= b_edge
                elif yy < 0: 
                    yy += b_edge
                if x > r_edge:
                    x -= r_edge
                yield (x,yy,i*3)
                i += 1

    def update(self):
        self.next_iteration(self.cells, self.changed)
        self.iteration += 1

    def tile_range(self):
        """Outputs the entire tile range"""
        for y in xrange(self._height):
            for x in xrange(self._width):
                yield(x,y)    

class LifeController(object):
    def __init__(self, profile=False):
        #pygame
        self._screen = pygame.display.set_mode((WINDOW_SIZE[0],WINDOW_SIZE[1]))#,FULLSCREEN)
        self._font = pygame.font.SysFont("DejaVu Sans Mono", FONT_SIZE)
        screen_size = self._screen.get_size()

        #internal state
        self.tiles = (screen_size[0]/TILE_SIZE, (screen_size[1]-FONT_SIZE)/TILE_SIZE)
        self.life_engine = Life(self.tiles)
        self.surface = pygame.display.get_surface()
        self.mouse_pos = (0,0)
        self.paused = True
        self.clock = time.Clock()
        self.profile = profile

        #gfx
        self.tile_images = {}
        self.tile_images[True] = pygame.Surface((TILE_SIZE,TILE_SIZE))
        self.tile_images[True].fill((50,255,100))
        self.tile_images[False] = pygame.Surface((TILE_SIZE,TILE_SIZE))
        self.tile_images[False].fill((0,0,0))

        #stats
        self.fps = 0
        self._cell_count = 0

        self.draw(False)

    def draw(self, update=True):
        if update:
            self.life_engine.update()

        self._cell_count = 0

        for (x,y) in self.life_engine.tile_range():
            cell = self.life_engine.cells[(x,y)]
            if cell:
                self._cell_count += 1

            if self.life_engine.changed[(x,y)]:
                rect = ((x-1) * TILE_SIZE, (y-1) * TILE_SIZE, TILE_SIZE, TILE_SIZE)
                self.surface.blit(self.tile_images[cell], rect)

        self._draw_stats()
        pygame.display.flip()

    def _draw_stats(self):
        t_width = self.__print_status_text("Game of Life 0.3", 5)
        self.__print_status_text("Day %s" % self.life_engine.iteration, t_width + 100)
        self.__print_status_text("%.2f FPS" % self.fps, t_width + 250)
        self.__print_status_text("%s Cells" % self._cell_count, t_width + 400)

    def __print_status_text(self, txt, left):
        text = self._font.render(txt, True, (200, 20, 20), (0, 0, 0))
        rect = text.get_rect()
        rect.bottom = self.surface.get_rect().height
        rect.left = left
        fill_rect = rect
        fill_rect.width += 10
        fill_rect.left -= 5
        self.surface.fill((0,0,0), fill_rect)
        self.surface.blit(text, rect)
        return rect.right

    def toggle_current_tile(self):
        if self.mousepos:
            tilex = self.mousepos[0]/TILE_SIZE
            tiley = self.mousepos[1]/TILE_SIZE
            self.life_engine.cells[tilex, tiley] = not self.life_engine.cells[tilex, tiley]
            self.life_engine.changed[tilex, tiley] = True
            self.draw(False)

    def input(self,events):
        """Deals with kepresses and mouse events"""
        for event in events:
            if hasattr(event, 'unicode') and event.unicode == 'q':
                sys.exit(0)
            if hasattr(event, 'unicode') and event.unicode == 'p':
                self.paused = not self.paused
            if hasattr(event, 'unicode') and event.unicode == 'n':
                if self.paused:
                    self.draw()
            if hasattr(event, 'pos'):
                self.mousepos = event.pos
            if hasattr(event, 'button') and event.type == pygame.MOUSEBUTTONUP:
                self.toggle_current_tile()

    def mainloop(self):
        done = False
        while not done:
            self.input(pygame.event.get())
            if not self.paused:
                self.clock.tick()
                self.fps = self.clock.get_fps()            
                self.draw()
            if self.profile and self.life_engine.iteration > 100:
                done = True


if __name__ == "__main__":
    parser = OptionParser()
    parser.add_option("--profile", action="store_true", dest="profile", default="False", help="Profile 100 iterations (default: False)")
    (options, args) = parser.parse_args()

    pygame.init()
    pygame.display.set_caption('Game of life 0.4')
    lc = LifeController()

    if options.profile is True:
        lc.profile = True
        cProfile.run('lc.mainloop()')
        pygame.quit()
        raw_input("Press Enter to exit")        
    else:
        lc.mainloop()
        pygame.quit()