1. Ian George
  2. sketchbook

Source

sketchbook / gol / main.py

import os, sys
import pygame
import random
from pygame.locals import *
from pygame import time

import cProfile
from optparse import OptionParser

from Life import LifeEngine
"""
Game of Life sim

TODO:
FPS limit
"""

APP_NAME = "Game of life v0.5"
TILE_SIZE = 4
WINDOW_SIZE = (640,480)
FILL_SCREEN = True
PERCENT_START_FILL = 5
FPS_LIMIT = 5

FONT_SIZE = 16
TILE_HEAT_COLOUR = (75,75,255,200)
TILE_COLOUR = (200,200,200)
BG_HEAT_ALPHA = 100

class LifeController(object):
    """ Controller class for the simulation, handles I/O, drawing & feeding input to the engine"""
    def __init__(self, profile=False):
        """If profile is set, the simulation will run for 100 iterations and quit displaying cProfile output"""
        #pygame
        if FILL_SCREEN:
            self._screen = pygame.display.set_mode((0,0),FULLSCREEN)
        else:
            self._screen = pygame.display.set_mode((WINDOW_SIZE[0],WINDOW_SIZE[1]))
        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 = LifeEngine(self.tiles, PERCENT_START_FILL)
        self.surface = pygame.display.get_surface()
        self.mouse_pos = (0,0)
        self.mouse_button = False
        self.paused = True
        self.clock = time.Clock()
        self.profile = profile
        self.heat = True

        #gfx
        self.tile_image = pygame.Surface((TILE_SIZE,TILE_SIZE), flags=SRCALPHA)
        self.tile_image.fill(TILE_COLOUR)
        self.tile_hm_image = pygame.Surface((TILE_SIZE,TILE_SIZE), flags=SRCALPHA)
        self.tile_hm_image.fill(TILE_HEAT_COLOUR)

        self.background = pygame.Surface(screen_size)
        self.background = self.background.convert_alpha()
        self.background.fill((0, 0, 0))

        #cache spectrum colours, looking up on the fly is slow
        self.heat_spectrum = []
        spectrum = pygame.image.load('spectrum.bmp')
        for i in xrange(0,255):
            c = spectrum.get_at( (i, 1) )
            h = pygame.Color(c[0],c[1], c[2], BG_HEAT_ALPHA)
            self.heat_spectrum.append(h)

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

        self.draw(False, self.paused)


    def draw(self, update=True, draw_help=False, draw_bg=True):
        """Handles the main part of the drawing function - putting the cells on the screen
        if update=False then it will draw to the screen without updating to the next iteration
        """
        if update:
            self.life_engine.update()

        if draw_bg:
            self._draw_bg()
        
        for (x, y) in self.life_engine.living_cells:
            rect = ((x-1) * TILE_SIZE, (y-1) * TILE_SIZE, TILE_SIZE, TILE_SIZE)
            if self.heat:
                self.surface.blit(self.tile_hm_image, rect)
            else:
                self.surface.blit(self.tile_image, rect)

        self._draw_stats()

        if draw_help:
            self.draw_help()

        pygame.display.flip()

    def draw_help(self):
        """Draws the help text on the screen"""
        lines = 7
        if self.paused:
            paused_text = "p - pauses simulation (currently paused)"
        else:
            paused_text = "p - pauses simulation"

        if self.heat:
            heat_text = "h - toggles heat map (currently on)"
        else:
            heat_text = "h - toggles heat map (currently off)"

        self.__print_help_text(APP_NAME, 1, lines)
        self.__print_help_text(paused_text, 2, lines)
        self.__print_help_text("r - restarts simulation", 3, lines)
        self.__print_help_text(heat_text, 4, lines)
        self.__print_help_text("q - quits", 5, lines)
        self.__print_help_text("", 6, lines)
        self.__print_help_text("Drawing with the mouse when paused adds cells", 7, lines)
        pygame.display.flip()

    def _draw_stats(self):
        """Draws the statistics to the screen"""
        t_width = self.__print_status_text(APP_NAME, 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" % len(self.life_engine.living_cells), t_width + 400)

    def __print_help_text(self, txt, line, total_lines):
        """Helper function to print a line of text for the help info"""
        text_width = 200
        line_height = 25
        text_left = self.life_engine._width / 2 - text_width / 2
        text_top = self.life_engine._height / 2 - total_lines * line_height / 2 + line * line_height
        text = self._font.render(txt, True, (255, 255, 255), (0,0,0))
        rect = text.get_rect()
        rect.left = text_left
        rect.top = text_top
        self.surface.blit(text,rect)

    def __print_status_text(self, txt, left):
        """Helper function to print a block of text for status bar"""
        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 _draw_bg(self):
        """Draws either the heatmap or just fills the bg in black in place"""
        if self.heat:
            hm = self.life_engine.heatmap
            w = self.life_engine._width
            h = self.life_engine._height
            bg = self.background
            bg.fill( (0,0,0) )

            TILE_MULT = 2
            t_size = TILE_SIZE*TILE_MULT
            t_offset = (TILE_SIZE/2) - (t_size/2)

            for y in xrange(h):
                row = hm[y]
                for x in xrange(w):
                    r = (((x-1) * TILE_SIZE) + t_offset, ((y-1) * TILE_SIZE) + t_offset, t_size, t_size)
                    ht = row[x]
                    if ht > 0:
                        bg.fill(self._get_heat(ht), r)
                    
        self.surface.blit(self.background, (0, 0))
        

    def _get_heat(self, heat):
        """Returns the color for a specific heat map value"""
        h = int((heat/20.0) * 254)
        return self.heat_spectrum[h]

    def create_current_cell(self):
        """Creates a cell under the mouse pointer"""
        if self.mousepos:
            tilex = self.mousepos[0]/TILE_SIZE
            tiley = self.mousepos[1]/TILE_SIZE
            if not (tilex,tiley) in self.life_engine.living_cells:
                self.life_engine.cell_born(self.life_engine.cells, tilex, tiley)

    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 == 'r':
                self.life_engine.__init__(self.tiles, PERCENT_START_FILL)
                self.paused = True
                self.draw(False, True)

            if hasattr(event, 'unicode') and event.unicode == 'h':
                self.background.fill( (0,0,0) )
                self.heat = not self.heat
                if self.paused:
                    self.draw(False, True)

            if hasattr(event, 'unicode') and event.unicode == 'p':
                self.paused = not self.paused
                if self.paused:
                    self.draw_help()

            if hasattr(event, 'unicode') and event.unicode == 'n':
                if self.paused:
                    self.draw()

            if event.type == pygame.MOUSEMOTION:
                self.mousepos = event.pos
                b1,b2,b3 = pygame.mouse.get_pressed()

                if self.paused and b1:
                    self.create_current_cell()
                    self.draw(False, False, False)

    def mainloop(self):
        """Main loop!"""
        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(APP_NAME)
    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()