Gioco della vita di Conway
Ho implementato il gioco della vita di Conway in Python. Avete in mente il simbolo hacker (il glider, quella griglia 3×3 con dei pallini neri)? Beh, sono strettramente legati ![]()
Questa implementazione permette svariate cose, tra cui:
- Una volta impostata una griglia, vedere l’evolversi della situazione.
- Definite le dimensioni della griglia, trovare (con un bruteforce) tutte le figure che si ripetono dopo N stadi.
Ecco il codice (commentato parzialmente in inglese):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | #!/usr/bin/env python # -*- coding: utf-8 -*- try: import psyco from psyco.classes import __metaclass__ psyco.full() except: pass class Life: """ Implementation of the Conway's game of life """ def __init__(self, rows, columns): """ Inizializes 2d table """ self.rows, self.columns = rows, columns self.board = [[0,]*self.columns for row in xrange(self.rows)] def get(self, row, column): """ Gets value or return 0 """ check = 0 <= row < self.rows and 0 <= column < self.columns return (check and self.board[row][column] or 0) def add(self, row, column): """ Adds eight cells near current cell """ return self.get(row - 1, column - 1) + \ self.get(row - 1, column) + \ self.get(row - 1, column + 1) + \ self.get(row, column - 1) + \ self.get(row, column + 1) + \ self.get(row + 1, column - 1) + \ self.get(row + 1, column) + \ self.get(row + 1, column + 1) def next(self): """ Calculates the next stage """ current = [row[:] for row in self.board] # It clones the table for r, row in enumerate(current): # For every row... for c, item in enumerate(row): # ...and for every column... near = self.add(r, c) # ...it calculates near cells if near not in (2, 3) and item: # Condition to death current[r][c] = 0 # Let's kill it :( elif near == 3 and not item: # Condition to became alive current[r][c] = 1 # Let's live! :D self.board = current # It overwrites the old stage def show(self, target = False): """ Shows the table """ for row in (target or self.board): print("".join([column and "@" or "x" for column in row])) def bruteforce(rows, columns): life = Life(rows, columns) possibilities = [list(i) for i in product((0, 1), repeat=columns)] for select in product(xrange(len(possibilities)), repeat=rows): life.board = [possibilities[i] for i in select] history = [life.board] while 1: life.next() if life.board in history: if life.board == history[0]: life.show(history[0]) print("") break history.append(life.board) if __name__ == "__main__": from itertools import product from sys import argv try: rows, columns = int(argv[1]), int(argv[2]) except IndexError: print("Usage: %s [rows] [columns]" % argv[0]) except ValueError: print("Usage: %s [rows] [columns]" % argv[0]) else: bruteforce(rows, columns) """ # Example code: glider (spaceship, hacker symbol) if __name__ == "__main__": # If you're executing this program... life = Life(6, 6) # It inizializes a 2d 6x6 table life.board[2][0] = 1 # 2, 0 set alive life.board[2][1] = 1 # 2, 1 set alive life.board[2][2] = 1 # 2, 2 set alive life.board[1][2] = 1 # 1, 2 set alive life.board[0][1] = 1 # 0, 1 set alive for time in range(12): # For n times... life.next() # ...it makes the next stage... life.show() # ...and it shows it :) """ |
La prima porzione di codice è la seguente:
4 5 6 7 8 9 | try: import psyco from psyco.classes import __metaclass__ psyco.full() except: pass |
In questa maniera, importiamo, se disponibile, la libreria psyco, che, su macchine x86, permette di eseguire operazioni molto onerose per la cpu, in maniera molto più veloce ![]()
Successivamente troviamo questa classe:
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | class Life: """ Implementation of the Conway's game of life """ def __init__(self, rows, columns): """ Inizializes 2d table """ self.rows, self.columns = rows, columns self.board = [[0,]*self.columns for row in xrange(self.rows)] def get(self, row, column): """ Gets value or return 0 """ check = 0 <= row < self.rows and 0 <= column < self.columns return (check and self.board[row][column] or 0) def add(self, row, column): """ Adds eight cells near current cell """ return self.get(row - 1, column - 1) + \ self.get(row - 1, column) + \ self.get(row - 1, column + 1) + \ self.get(row, column - 1) + \ self.get(row, column + 1) + \ self.get(row + 1, column - 1) + \ self.get(row + 1, column) + \ self.get(row + 1, column + 1) def next(self): """ Calculates the next stage """ current = [row[:] for row in self.board] # It clones the table for r, row in enumerate(current): # For every row... for c, item in enumerate(row): # ...and for every column... near = self.add(r, c) # ...it calculates near cells if near not in (2, 3) and item: # Condition to death current[r][c] = 0 # Let's kill it :( elif near == 3 and not item: # Condition to became alive current[r][c] = 1 # Let's live! :D self.board = current # It overwrites the old stage def show(self, target = False): """ Shows the table """ for row in (target or self.board): print("".join([column and "@" or "x" for column in row])) |
Questo codice è stato scritto con una particolare attenzione alle prestazioni. Cerchiamo di comprendere meglio le varie funzioni:
- __init__(self, rows, columns): dati due numeri interi, “disegna” una tabella delle dimensioni specificate
- get(self, row, column): è una funzione che viene usata per vedere se una cella è viva o morta. In caso questa cella non esista, ritorna semplicemente zero, come se fosse morta. Ho trovato questo metodo sensibilmente più veloce rispetto al fare un try/except (senza quindi controllare l’esistenza della cella in base alle coordinate), e molto più chiaro rispetto al creare una tabella con un bordo di una cella.
- add(self, row, column): date le coordinate di una cella, somma le otto celle accanto, utilizzando get().
- next(self): calcola lo stadio successivo.
- show(self, target=False): visualizza in maniera abbastanza “grezza” la nostra griglia sul terminale.
Qui finisce il “grosso” del codice. Successivamente abbiamo la funzione per il bruteforce:
47 48 49 50 51 52 53 54 55 56 57 58 59 60 | def bruteforce(rows, columns): life = Life(rows, columns) possibilities = [list(i) for i in product((0, 1), repeat=columns)] for select in product(xrange(len(possibilities)), repeat=rows): life.board = [possibilities[i] for i in select] history = [life.board] while 1: life.next() if life.board in history: if life.board == history[0]: life.show(history[0]) print("") break history.append(life.board) |
Questa funzione fa uso di itertools per calcolare le combinazioni possibili. Bisogna prestare particolarmente attenzione a piccoli accorgimenti, come il calcolare volta per volta ogni possibile combinazione, semplicemente “scegliendo” tra le “righe” generate. Ho trovato questo metodo particolarmente efficente e veloce.
Per sapere se una figura è già apparsa, crea una lista degli stadi, finché qualcosa non si ripete. Questo deve sempre avvenire, in quanto anche se non capitassimo di fronte a una figura “particolare”, dopo qualche passaggio tutte le celle morirebbero, e le ritroveremmo nuovamente morte nello stadio successivo, provocando una ripetizione (che, in questo caso, non verrà però visualizzata).
Il blocco successivo serve a rendere operativo il bruteforce:
62 63 64 65 66 67 68 69 70 71 72 | if __name__ == "__main__": from itertools import product from sys import argv try: rows, columns = int(argv[1]), int(argv[2]) except IndexError: print("Usage: %s [rows] [columns]" % argv[0]) except ValueError: print("Usage: %s [rows] [columns]" % argv[0]) else: bruteforce(rows, columns) |
Come già accennato precedentemente, importiamo itertools.product (assieme a sys.argv), e prendiamo gli argomenti passati da linea di comando, che diamo in pasto a bruteforce().
L’ultima parte è commentata, e mostra un utilizzo alternativo della classe Life:
74 75 76 77 78 79 80 81 82 83 84 85 86 | """
# Example code: glider (spaceship, hacker symbol)
if __name__ == "__main__": # If you're executing this program...
life = Life(6, 6) # It inizializes a 2d 6x6 table
life.board[2][0] = 1 # 2, 0 set alive
life.board[2][1] = 1 # 2, 1 set alive
life.board[2][2] = 1 # 2, 2 set alive
life.board[1][2] = 1 # 1, 2 set alive
life.board[0][1] = 1 # 0, 1 set alive
for time in range(12): # For n times...
life.next() # ...it makes the next stage...
life.show() # ...and it shows it :)
""" |
Si inizializza una griglia col famoso simbolo hacker, e si mostrano gli stadi successivi
Simpatico, no?
P.S. Ho omesso la licenza per problemi di spazio… Ovviamente è GPLv3
