Created
May 28, 2013 21:54
-
-
Save bdw/5666475 to your computer and use it in GitHub Desktop.
Game of Life met pygame en numpy.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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