Ricreare il gioco Life del matematico John Conway usando le slice di Go
La sfida di questa pagina consiste nel realizzare una simulazione di sottopopolazione, sovrappopolazione e riproduzione di cellule chiamata Gioco della vita di Conway. La simulazione viene giocata su una griglia bidimensionale di cellule. Pertanto, questa sfida si baserà su slice.
Ogni cellula ha otto cellule adiacenti in orizzontale, verticale e diagonale. Per ciascuna generazione, le cellule vivono o muoiono sulla base del numero di cellule circostanti.
Un nuovo universo
Per la prima implementazione del Gioco della vita, limitiamo l’universo a una dimensione fissa. Decidiamo le dimensioni della griglia e definiamo alcune costanti:
const (
width = 80
height = 15
)
Poi, definiamo il tipo Universe
che conterrà un campo bidimensionale di cellule. Con un tipo booleano, ogni cellula potrà essere viva (true
) o morta (false
):
type Universe [][]bool
Utilizziamo slice invece che array, in modo che l’universo possa essere condiviso e anche modificato tramite funzioni o metodi.
Scriviamo una funzione NewUniverse
che utilizzi make
per allocare e restituire un Universe
dotato di height
righe, ognuna delle quali ha width
colonne.
func NewUniverse() Universe
Le slice appena allocate contengono, per default, tutti valori zero, che, nel caso dei booleani, è false
. Quindi all’inizio l’universo è completamente privo di vita.
Osservare l’universo
Scriviamo un metodo per visualizzare l’universo sullo schermo utilizzando il package fmt
. Rappresentiamo le cellule vive con un asterisco e le cellule morte con uno spazio. Ricordiamo di andare a capo dopo aver terminato di visualizzare la riga corrente:
func (u Universe) Show()
Scriviamo una funzione main
per creare un NewUniverse
e poi mostriamolo con Show
. Prima di procedere, assicuriamoci di poter eseguire il programma anche quando l’universo è vuoto.
Introdurre le cellule vive
Scriviamo un metodo Seed
che depositi casualmente delle cellule vive (true
) nel 25% degli spazi disponibili:
func (u Universe) Seed()
Ricordiamo di importare il package math/rand
per utilizzare la funzione Intn
. Al termine, aggiorniamo main
per popolare l’universo con Seed
e visualizziamo il risultato con Show
.
Implementazione delle regole del gioco
Le regole del Gioco della vita di Conway sono le seguenti.
- Una cellula che non ha almeno due cellule vicine, muore isolata.
- Una cellula che ha due o tre cellule vicine, passa alla generazione successiva.
- Una cellula che ha più di tre cellule vicine, muore soffocata.
- In uno spazio vuoto che ha esattamente tre cellule vive vicine, nasce una cellula viva.
Per implementare le regole, suddividiamole in tre passi, ognuno dei quali può diventare un metodo.
- Un modo per determinare se una cellula è viva.
- Il conteggio delle cellule vicine.
- La logica per determinare se una cellula sarà viva o morta nella generazione successiva.
Viva o morta?
Dovrebbe essere facile determinare se una cellula è viva o morta. Basta cercare in una cella della slice Universe
. Se il valore booleano è true
, la cellula è viva.
Scriviamo un metodo Alive
di tipo Universe
con la seguente signature:
func (u Universe) Alive(x, y int) bool
Sorge una complicazione quando la cellula si trova all’esterno dell’universo. In (–1,–1), la cellula è viva o morta? In una griglia 80 × 15, la cellula (80,15) è viva o morta?
Per risolvere questo problema, facciamo in modo che l’universo sia curvato su se stesso. La cella posta sopra (0,0) sarà (0,14) invece di (0,–1), cosa che può essere calcolata sommando height
a y
. Se y
supera l’altezza della griglia, possiamo utilizzare l’operatore modulo (%
). Utilizziamo %
per dividere y
per height
e utilizziamo il resto. Lo stesso vale per x
e width
.
Conteggio dei vicini
Scriviamo un metodo per contare il numero di cellule vive vicine a una data cellula, da 0 a 8. Invece di accedere direttamente ai dati dell’universo, utilizziamo il metodo Alive
, in modo che l’universo risulti piegato in base alle regole appena descritte:
func (u Universe) Neighbors(x, y int) int
Facciamo attenzione a contare solo le cellule adiacenti e non la cellula in questione.
La logica del gioco
Ora che possiamo determinare se una cellula ha due, tre o più vicini, possiamo implementare le regole elencate all’inizio di questo paragrafo. Scriviamo un metodo Next
per svolgere esattamente questa operazione:
func (u Universe) Next(x, y int) bool
Non modifichiamo direttamente l’universo. Piuttosto, restituiamo il fatto che la cellula debba essere viva o morta nella generazione successiva.
Un universo parallelo
Per completare la simulazione, dobbiamo creare un ciclo che esamini ogni cellula dell’universo e determini quale dovrà essere il suo stato successivo (Next
).
C’è un problema: quando contiamo le cellule vicine, il conteggio deve basarsi sullo stato attuale dell’universo. Se modifichiamo direttamente l’universo, ogni modifica influenzerà il conteggio necessario per stabilire la condizione delle cellule circostanti.
Una soluzione semplice consiste nel creare due universi delle stesse dimensioni. Leggiamo la situazione dall’universo A e inseriamo la generazione successiva nell’universo B. Scriviamo una funzione Step
per svolgere questa operazione:
func Step(a, b Universe)
Una volta che l’universo B contiene la generazione successiva, possiamo scambiare gli universi e ripetere l’operazione:
a, b = b, a
Per cancellare lo schermo prima di visualizzare una nuova generazione, possiamo usare il codice "\x0c"
che è una particolare sequenza di escape ANSI. Poi visualizziamo l’universo e utilizziamo la funzione Sleep
del package time
per rallentare l’animazione. All’esterno di Go Playground, possiamo aver bisogno di un altro meccanismo per cancellare lo schermo, come per esempio "\033[H"
in Mac OS X.
Ora abbiamo tutto ciò che serve per scrivere interamente il Gioco della vita ed eseguirlo in Go Playground. Chi non si sentisse sicuro può aiutarsi con la soluzione, qui sotto.
La soluzione
life.go
package main
import (
"fmt"
"math/rand"
"time"
)
const (
width = 80
height = 15
)
// Universe è un campo bidimensionale di cellule.
type Universe [][]bool
// NewUniverse restituisce un universo vuoto.
func NewUniverse() Universe {
u := make(Universe, height)
for i := range u {
u[i] = make([]bool, width)
}
return u
}
// Seed inserisce casualmente cellule vive dell'universo.
func (u Universe) Seed() {
for i := 0; i < (width * height / 4); i++ {
u.Set(rand.Intn(width), rand.Intn(height), true)
}
}
// Set imposta lo stato della cellula specificata.
func (u Universe) Set(x, y int, b bool) {
u[y][x] = b
}
// Alive indica se la cellula specificata è viva.
// Se le coordinate sono esterne all'universo, rientra dall'inizio.
func (u Universe) Alive(x, y int) bool {
x = (x + width) % width
y = (y + height) % height
return u[y][x]
}
// Neighbors conta le cellule vive adiacenti.
func (u Universe) Neighbors(x, y int) int {
n := 0
for v := -1; v <= 1; v++ {
for h := -1; h <= 1; h++ {
if !(v == 0 && h == 0) && u.Alive(x+h, y+v) {
n++
}
}
}
return n
}
// Next restituisce lo stato successivo della cellula specificata.
func (u Universe) Next(x, y int) bool {
n := u.Neighbors(x, y)
return n == 3 || n == 2 && u.Alive(x, y)
}
// String restituisce l'universo sotto forma di stringa.
func (u Universe) String() string {
var b byte
buf := make([]byte, 0, (width+1)*height)
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
b = ' '
if u[y][x] {
b = '*'
}
buf = append(buf, b)
}
buf = append(buf, '\n')
}
return string(buf)
}
// Show cancella lo schermo e mostra l'universo.
func (u Universe) Show() {
fmt.Print("\x0c", u.String())
}
// Step aggiorna lo stato del prossimo universo (b)
// traendolo dall'universo corrente (a).
func Step(a, b Universe) {
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
b.Set(x, y, a.Next(x, y))
}
}
}
func main() {
a, b := NewUniverse(), NewUniverse()
a.Seed()
for i := 0; i < 300; i++ {
Step(a, b)
a.Show()
time.Sleep(time.Second / 30)
a, b = b, a // Scambia gli universi
}
}
Il programma del Gioco della vita e gli altri presenti in Programmare con Go sono scaricabili dal sito web del libro o dal relativo repository GitHub.
Questo articolo richiama contenuti dal capitolo 20 di Programmare con Go.