Recreating Conway’s Game of Life in Go, with a twist

Support the Open-Source Project used in this blog post to teach you Go.

Today, we will be recreating Conway’s Game of Life but with some slight progamically induced twists.

Let’s get started with our package and import statements, which we define at the top of every Go program.

package main

import ("fmt" 
"math/rand" 
"os" 
"os/exec" 
"runtime" 
"time" 
"golang.org/x/term"
)

These lines import various packages that our program will use. In Go, you need to explicitly import the packages you want to use in your code.

The fmt package provides basic input/output functionality, rand is used for generating random numbers, os and exec are used for executing system commands, runtime provides information about the runtime environment, time is used for handling time-related operations, and golang.org/x/term is a third-party package used for getting the terminal size.

Now, let’s start writing our application.

const universeHeight = 20 var universeWidth int

Above, we define a constant universeHeight and a variable universeWidth. Constants are immutable values that are known at compile-time, while variables can be changed during runtime.

In this case, universeHeight is a constant set to 20, represented by the fixed height of our Game of Life universe.

universeWidth, however, is a variable that will be set dynamically based on the user's terminal size

type Cell booltype Universe [][]Cell

Above, we show how Go allows you to define your custom types using the type keyword. In this code, we define two new types: Cell and Universe.

Support the Open-Source Project used in this blog post to teach you Go.

Cell is a boolean type that represents whether a cell in the Game of Life is alive or dead. Universe is a 2D slice of Cell values, representing the entire game board.

var dir = [][]int
{
{-1, -1}, {0, -1}, {1, -1},{1, 0}, {1, 1},{0, 1}, {-1, 1}, {-1, 0},
}

This line defines a variable dir that holds a 2D slice of integer values.

These values represent the relative positions of neighboring cells in the Game of Life universe.

Each sub-slice represents the row and column offset from a given cell.

This allows for directions to be stored and used by the cells.

func newUniverse() Universe {
universe := make(Universe, universeHeight)
 for i := range universe {
  universe[i] = make([]Cell, universeWidth)
 for j := range universe[i] {
  universe[i][j] = rand.Intn(2) == 0}
}
 return universe
}

This is a function newUniverse that returns a new Universe with random cell values. It first creates a 2D slice of the correct size using make.

Then, it iterates over each row and column, assigning a random boolean value (true or false) to each cell using the rand.Intn(2) function, which generates a random integer between 0 and 1 (inclusive).

If the neighbor's position is within the bounds of the universe and the cell is alive, it increments the count variable.

Finally, it subtracts 1 from the count if the current cell is alive (since we don't want to count it as a neighbor of itself).

func (u Universe) next() Universe {
newUniverse := make(Universe, universeHeight)   
 
for i := range newUniverse {
newUniverse[i] = make([]Cell, universeWidth)   
     
for j := range newUniverse[i] 
{ neighbors := countNeighbors(u, j, i)            
cell := u[i][j] newUniverse[i][j] = cell && 
(neighbors == 2 || neighbors == 3) || !cell && neighbors == 3}}

return newUniverse
}

The next method is a receiver function for the Universe type. It takes the current universe and applies the rules of the Game of Life to generate the next generation.

It does this by iterating over each cell in the universe, counting the number of alive neighbors for that cell using the countNeighbors function and then apply the rules to determine whether the cell should be alive or dead in the next generation.

func printUniverse(u *Universe) {
for i := range *u { 
for j := range (*u)[i] { 
  if (*u)[i][j] { fmt.Print("█")} 
  else {                
fmt.Print(" ")
}} 
fmt.Println()
}}

The printUniverse function takes a pointer to a Universe and prints it to the terminal. It iterates over each cell in the universe, printing a Unicode block character (█) for alive cells and a space for dead cells.

func shiftUniverse(u Universe) Universe {    
newUniverse := make(Universe, universeHeight)    
for i := range newUniverse {        
newUniverse[i] = make([]Cell, universeWidth)        copy(newUniverse[i], u[i][1:])        
newUniverse[i][universeWidth-1] = u[i][0]    
}    
return newUniverse
}

The shiftUniverse function creates a new universe by shifting the cells in the current universe from one position to the left.

Support the Open-Source Project used in this blog post to teach you Go.

It does this by creating a new universe and copying the values from the current universe, starting from the second column.

The first column is then copied to the last column, creating a wrap-around effect.

func main() {    
rand.Seed(time.Now().UnixNano())    
width, _ := getTerminalSize()    
universeWidth = width    
universeA := make(Universe, universeHeight)    

for i := range universeA {        
universeA[i] = make([]Cell, universeWidth)    
}    
universeB := newUniverse()    
universes := []*Universe{&universeA, &universeB
}    

currentIndex := 0    

for {        
clearScreen()        
printUniverse(universes[currentIndex])
        
time.Sleep(200 * time.Millisecond)
        universes[(currentIndex+1)%2].updateFrom(universes[currentIndex])        
currentIndex = (currentIndex + 1) % 2    
}}

This is the main function, which serves as the entry point for our program. It first seeds the random number generator with the current time to ensure different random values on each run.

Then, it retrieves the width of the terminal using the getTerminalSize function and assigns it to the universeWidth variable.

Next, it creates two universes: universeA (an empty universe) and universeB (a random universe).

These universes are stored in a slice of pointers (universes), and a currentIndex variable keeps track of which universe is currently being displayed.

The program then enters an infinite loop where it:

  1. Clears the terminal screen using the clearScreen function.

  2. Prints the current universe to the terminal using the printUniverse function.

  3. Waits for 200 milliseconds to create an animation effect.

  4. Updates the next universe by calling the updateFrom method, which applies the Game of Life rules and shifts the universe.

  5. Toggles the currentIndex to switch between the two universes for the next iteration.

This loop continues indefinitely, creating an animated display of the Game of Life simulation in the terminal window.

And there you have it!

A comprehensive breakdown of the Game of Life implementation in Go, complete with code snippets and explanations of the various concepts and functions used.

I hope this blog post helps you better understand the code and teaches you some valuable Go programming concepts along the way.

  • Nick

Reply

or to participate.