Skip to content

Instantly share code, notes, and snippets.

@bdw
Created May 28, 2013 21:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bdw/5666475 to your computer and use it in GitHub Desktop.
Save bdw/5666475 to your computer and use it in GitHub Desktop.
Game of Life met pygame en numpy.
#!/usr/bin/env python
import argparse
import numpy
import pygame
pygame.init()
from pygame.locals import QUIT # Dat deze uit z'n namespace moet is echt stom
# 3 regels:
# 1. Levende cellen met minder dan 2 levende buren gaan dood
# 2. Levende cellen met meer dan drie levende buren gaan dood (overpopulatie)
# 3. Dode cellen met precies 3 levende buren worden levend
# Strategie:
# We gebruiken een aantal arrays, te weten:
# Een boolean array met alle cellen (True = levend, False = dood)
#
# Verder, in alle cycli:
# Een array met alle getelde buren
# Een array met alle cellen die doodgaan
# Een array met alle cellen die worden geboren
# En met die gegevens update je de levende cellen
def count_neighbors(live_cells):
"""Deze methode berekent hoeveel buren alle cellen hebben. De
methode is het optellen van 'verschoven' frames.
In totaal worden hier 4 enkele en 4 dubbel verschoven frames met in
totaal 12 verschoven frames. Paargewijs (python __add__ gaat per 2)
worden er vervolgens 7 maal de som van deze frames berekend, ieder (in
principe) in een nieuw frame. Zelfs al zouden de 'verschoven' frames
een copy-on-write aanpassing zijn van de headers (wat volgens mij niet
gebeurt) en zelfs al zouden de sommen van het intermediare resultaat
'in-place' worden uitgevoerd, dan nog is dit een afgrijselijk
innefficient algoritme.
En toch is het sneller dan de sommen in python berekenen. Mijn
oorspronkelijke algoritme bewaarde de tellingen van buren en update
slechts de buren van nieuwgeboren en gestorven cellen (en deed dus
beduidend minder werk, en bovendien in-place). Dat dit monster van een
algoritme sneller is zegt iets tragisch over python.
"""
cells = live_cells.astype(int) # live_cells is boolean, moet int worden
# van een assistent zag ik:
# sum([numpy.roll(numpy.roll(cells, i, 0), j, 1)
# for j in [-1 ,0, 1] for i in [-1, 0, 1]]) - cells
# (parafraserend uit geheugen). Dat is nog erger haha
return (numpy.roll(numpy.roll(cells, -1, 0), -1, 1) + # links boven
numpy.roll(cells, -1, 0) + # boven
numpy.roll(numpy.roll(cells, -1, 0), 1, 1) + # rechts boven
numpy.roll(cells, -1, 1) + # links
numpy.roll(cells, 1, 1) + # rechts
numpy.roll(numpy.roll(cells, 1, 0), -1, 1) + # links onder
numpy.roll(cells, 1, 0) + # onder
numpy.roll(numpy.roll(cells, 1, 0), 1, 1)) # rechts onder
def update(live_cells):
"""Deze methode genereert een nieuwe buffer met de 'game-of-life'"""
neighbors = count_neighbors(live_cells)
dying = (live_cells & (neighbors > 3)) | (live_cells & (neighbors < 2))
born = (~live_cells & (neighbors == 3)) # dit hoeft niet per se zo maar boeit niet
return ((live_cells & ~dying) | born), dying, born
def read_life_file(file_name):
"""Read a genesis file
A genesis file is a text file consisting of rows of '-' alternated by
'o'. The 'o' fields represent living cells, and the other cells are
dead.
"""
with open(file_name) as file_handle:
# hier gebeurt vrij veel.
#
# allereerst genereer ik een array met alle lijnen in een list
# comprehension. Vervolgens maak ik van alle lijnen (min de newline)
# een lijst, dan maak ik er een array van, en die vergelijk ik met 'o'
# zodat ik een array van booleans terugkrijg
return numpy.array([list(line.strip()) for line in file_handle]) == 'o'
def print_generation(generation):
"""Print a generation of life (as would be printed in a life file)"""
# numpy.where maakt een array waarbij alle True values 'o' zijn en alle
# False values een '-'
for row in numpy.where(generation, 'o', '-'):
print(''.join(row))
def random_generation(shape, ratio=0.8):
"""Generate a random generation of a shape (pair of int) with a given
ratio (float) of dead to life cells """
# random array vergeleken met de ratio geeft een boolean array terug
return numpy.random.random(shape) > ratio
# pygame spel dat de game-of-life weergeeft
class Game(object): # object, omdat ik python2.7 gebruik in verband met packages
"""Game simulates the game-of-life on a pygame buffer"""
def __init__(self, genesis, frame_rate=5):
# eerste generatie
self.genesis = genesis
# scherm is twee maal zo groot als 'spelbord'
screen_size = map(lambda x: x * 2, genesis.shape)
self.screen = pygame.display.set_mode(screen_size)
# 3-dimensionale array van pixels
self.buffer = numpy.zeros(genesis.shape + (3,), numpy.uint8)
# lettertype laden
default_font = pygame.font.get_default_font()
self.font = pygame.font.Font(default_font, 20)
# op tijd lopen
self.clock = pygame.time.Clock()
self.frame_rate = frame_rate
# tel aantal generaties
self.count = 0
def start(self):
# plot the first generation
self.buffer[self.genesis] = (0, 0xff, 0)
# loop through all generations
generation = self.genesis
while numpy.any(generation):
# draw old generation first
self.redraw()
# kijk of we moeten stoppen
if pygame.event.peek(QUIT):
break
# TODO: maak dit afhankelijk van 'spelmode'. Ik wil graag nog
# een mode met 'dag-nacht' ritme, waarbij overdag cellen
# makkelijker overleven dan 's nachts. Dat destabiliseert het
# hele spel. Het zou ook leuk zijn om hier een simpel
# ecologisch model mee te maken.
generation, deceased, newborn = update(generation)
# draw the new one
self.buffer[:] = (0, 0, 0) # make it black
self.buffer[generation] = (0, 0xff, 0) # green
self.buffer[deceased] = (0xff, 0, 0) # red
self.buffer[newborn] = (0, 0, 0xff) # blue
self.clock.tick(self.frame_rate) # wait a bit
self.count += 1 # increase generation
def redraw(self):
# nogal wat technische dingetjes. De buffer moet 2 keer zo groot
# worden om op het scherm te worden getekend, want anders zijn de
# individuele cellen bijna niet te zien. Dat kan met scale2x in
# pygame.transform, mmaar daarvoor moet het eerst een Surface
# object zijn (ipv een array). Dus zo gezegd, zo gedaan.
surface = pygame.surfarray.make_surface(self.buffer)
scaled = pygame.transform.scale2x(surface)
self.screen.blit(scaled, (0, 0))
# Schrijf wat statistiekjes op het scherm
text = "Generation {0} ({1:.2f} fps)".format(self.count,
self.clock.get_fps())
rendered = self.font.render(text, True, (0xff, 0xff, 0xff))
# 10 pixels rechts, 10 pixels boven
position = (self.screen.get_width() - rendered.get_width() - 10, 10)
self.screen.blit(rendered, position)
pygame.display.flip()
def benchmark(width=1024,height=1024,iterations=1024):
life = random_generation((width,height),0.8)
for _ in range(iterations):
life, _, _ = update(life)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--width', dest='width', type=int, default=320)
parser.add_argument('--height', dest='height', type=int, default=320)
parser.add_argument('--ratio', dest='ratio', type=float, default=0.9)
parser.add_argument('--file', dest='file', type=str)
parser.add_argument('--frame-rate', dest='frame_rate', type=int, default=5)
parser.add_argument('--benchmark', dest='benchmark', action='store_const',
const=True, default=False)
args = parser.parse_args()
if args.benchmark:
benchmark()
quit(0)
if not args.file is None: # waarom is ipv == ? omdat None een constante is
life = read_life_file(args.file)
else:
life = random_generation((args.width, args.height), args.ratio)
g = Game(life, args.frame_rate)
g.start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment