Home
Programmare con Go: scrivere il Gioco della vita

23 Ottobre 2019

Programmare con Go: scrivere il Gioco della vita

di

Per fare pratica con il concetto di slice, mettiamoci alla prova con un classico della simulazione, divertente anche da guardare mentre lavora.

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.

L'autore

  • Nathan Youngman
    Nathan Youngman inizia a programmare come autodidatta e negli anni trasforma la sua passione in un lavoro, specializzandosi prima in ColdFusion e in Ruby on Rails, per poi scoprire Go, di cui è un contributore e un sostenitore attivo.
  • Roger Peppé
    Roger Peppé contribuisce a Go mantenendo progetti open source e organizzando meetup in Inghilterra. Lavora all'infrastruttura software del cloud Go.

Iscriviti alla newsletter

Novità, promozioni e approfondimenti per imparare sempre qualcosa di nuovo

Gli argomenti che mi interessano:
Iscrivendomi dichiaro di aver preso visione dell’Informativa fornita ai sensi dell'art. 13 e 14 del Regolamento Europeo EU 679/2016.

Libri che potrebbero interessarti

Tutti i libri

Programmare con Go

Guida per imparare il linguaggio open source sviluppato da Google

41,15

59,89€ -31%

33,16

34,90€ -5%

24,99

di Nathan Youngman, Roger Peppé

Imparare a programmare con Python

Il manuale per programmatori dai 13 anni in su

28,75

39,99€ -28%

23,75

25,00€ -5%

14,99

di Maurizio Boscaini

Imparare a programmare con JavaScript

Il manuale per programmatori dai 13 anni in su

28,75

39,99€ -28%

23,75

25,00€ -5%

14,99

di Maurizio Boscaini, Massimiliano Masetti


Articoli che potrebbero interessarti

Tutti gli articoli