DEV Community

loading...
Cover image for Coding Conway's Game of Life in Go

Coding Conway's Game of Life in Go

Samuel Grasse-Haroldsen
Full-stack developer fluent in Hungarian looking for exciting challenges in the international industry!
・6 min read

Have you heard of Conway's Game of Life? It is a 0 player simulation that is absolutely fascinating to watch. Based on only a few simple rules, we can create a visualization of this simulation and watch it to our heart's content. Here's a cool website where you can experiment with the game and watch what happens: Conway's Game of Life.

The Game

The simulation takes place on a 2D grid. Cells on this grid are either dead or alive. With each iteration of the game, cells live and die based on the rules of the game.

The Rules

  1. A living cell with 2-3 neighbors will continue to live.
  2. A dead cell with exactly 3 neighbors comes to life.

Any other cell will die or stay dead (if it has more than 3 neighbors or less than 2 neighbors).

Requirements

For a 2 dimensional grid we will need an array of arrays to represent each row of cells. Because a cell can only be in one of two states (no quantum funny business here), we will use booleans as the type in our array of arrays. As far as packages go, we will need fmt to print our array, math/rand to generate our initial grid, and time so we can observe each iteration of the game. I will also make use of some unicode characters to make the display of our game a little more visually appealing.

Let's Start!

First we need to make our grid. Let's import the packages we need and define some constants for the dimensions of the grid. You could make these whatever you want, just be aware that it might look strange depending on the size of your terminal.

While we are defining constants, let's go ahead and declare a few more like the Unicode characters to represent dead vs living cells (I'll be using brown and green squares but feel free to use whatever you want), as well as an ANSI escape sequence to clear our screen each iteration (we want the appearance of animation). We'll also declare a sleepIteration const so we can easily change how long each iteration should be displayed (this will be useful when debugging.

Check out this website to search for the UTF-8 codes for different symbols/characters: Unicode

package main

import (
    "fmt"
    "math/rand"
    "time
)

const (
        width          = 40
        height         = 20
        sleepIteration = 100
        ansiEscapeSeq  = "\033c\x0c"   
        brownSquare    = "\xF0\x9F\x9F\xAB"    
        greenSquare    = "\xF0\x9F\x9F\xA9"

)
Enter fullscreen mode Exit fullscreen mode

The Grid

Let's go ahead and make a new type to represent the grid of cells. Feel free to call this whatever makes the most sense to you: world, universe, plane, grid, etc. I know I mentioned arrays earlier, but we will be utilizing slices as they are much more flexible and fun (in my opinion)!

type World [][]bool
Enter fullscreen mode Exit fullscreen mode

Now that we have a type to represent a slice of slices that each hold a boolean value, we can make some methods to go along with our new World type.

World Methods

We will want a few different methods to interact with our World type:

  1. Display: This will be the method responsible for displaying our World
  2. Seed: This will initialize our World with data (we will utilize the rand package in this method)
  3. Alive: This method will return true if the given cell, x y coordinates, is alive and false otherwise
  4. Neighbors: This method will return how many neighbors (alive ones) a given cell has
  5. Next: This method will determine the next state of a given cell

We will also need two functions:

  1. MakeWorld: This will initialize our World type
  2. Step: takes in two Worlds and sets the Next state of one World by reading the current state of the other.

Let's go ahead and go through each function and method.

MakeWorld

func MakeWorld() World {
        w := make(World, height)
        for i := range w {
                w[i] = make([]bool, width)
        }
        return w
}
Enter fullscreen mode Exit fullscreen mode

Our MakeWorld function returns our newly created world. First we are using the make reserved word to create a World type with height number of slices (the number of rows). Then we are looping through each row and setting the row to be a slice containing boolean values of width length (columns).

Seed

func (w World) Seed() {
        for _, row := range w {
                for i := range row {
                        if rand.Intn(4) == 1 {
                                row[i] = true
                        }
                }
        }
}
Enter fullscreen mode Exit fullscreen mode

In our Seed method we are looping through each row and column and setting the cell value to true if the rand.Intn(4) returns 1 (this will set roughly 25% of the cells to alive.

Display

func (w World) Display() {
        for _, row := range w {
                for _, cell := range row {
                        switch {
                        case cell: 
                                fmt.Printf(greenSquare)
                        default:
                                fmt.Printf(brownSquare)
                        }
                }
                fmt.Printf("\n")
        }
}
Enter fullscreen mode Exit fullscreen mode

Again, we are looping though each slice in our slice and displaying a greenSquare if the cell is alive and the brownSqaure if not. We also print a new line at the end of the inner "cell loop" so we can see where each row ends.

Alive

func (w World) Alive(x, y int) bool {
        y = (height + y) % height
        x = (width + x) % width
        return w[y][x]
}
Enter fullscreen mode Exit fullscreen mode

Do you have a good understanding of the modulo operator? The most complicated part of this method is how we are handling negative & out of bounds input. We want our game to wrap so if we send it (-1, -1), we really want to know the value of the end of our grid so (width, height).

Neighbors

func (w World) Neighbors(x, y int) int {
        var neighbors int

        for i := y - 1; i <= y+1; i++ {
                for j := x - 1; j <= x+1; j++ {
                        if i == y && j == x {
                                continue
                        }
                        if w.Alive(j, i) {
                                neighbors++
                        }
                }
        }
        return neighbors
}
Enter fullscreen mode Exit fullscreen mode

We want to check the life status of the 8 surrounding squares. We will check the 3 cells above, the 3 cells below, and the two cells horizontal to the given cell (we don't want to check the given cell itself; thus the first if statement).

It is so important to make sure that we are consistent in our x, y coordinates. I had some serious bugs when initially writing this program based on misuse of width vs height.

Next

func (w World) Next(x, y int) bool {
        n := w.Neighbors(x, y)
        alive := w.Alive(x, y)
        if n < 4 && n > 1 && alive {
                return true
        } else if n == 3 && !alive {
                return true
        } else {
                return false
        }
}
Enter fullscreen mode Exit fullscreen mode

In our Next method we want to get the given cell's life status and number of neighbors. Then using conditionals we implement the game's rules.

Step

func Step(a, b World) {
        for i := 0; i < height; i++ {
                for j := 0; j < width; j++ {
                        b[i][j] = a.Next(j, i)
                }
        }
}
Enter fullscreen mode Exit fullscreen mode

Step takes in two worlds and alters the second world based on each cell's Next status.

main

Now that we have all the methods and functions in place, lets write the game's sequence in main.

func main() {
        fmt.Println(ansiEscapeSeq)
        rand.Seed(time.Now().UTC().UnixNano())
        newWorld := MakeWorld()
        nextWorld := MakeWorld()
        newWorld.Seed()
        for {
                newWorld.Display()
                Step(newWorld, nextWorld)
                newWorld, nextWorld = nextWorld, newWorld
                time.Sleep(sleepIteration * time.Millisecond)
                fmt.Println(ansiEscapeSeq)
        }
}
Enter fullscreen mode Exit fullscreen mode

Let's walk through this one step-by-step.

  1. We clear the screen using our ansiEscapeSeq
  2. We seed our rand number generator (we will get different results every time based on the time of our system)
  3. We create our newWorld and our nextWorld
  4. We seed our newWorld
  5. Now we create an infinite loop (you don't have to but I could seriously watch this simulation for eternity)
  6. We display the newWorld's freshly seeded data
  7. We calculate the nextWorlds value by calling Step
  8. We swap the values of our nextWorld and the newWorld
  9. We wait for sleepIteration number of milliseconds
  10. We clear the screen
  11. Repeat

Conclusion

There you have it! Conway's game of life implemented in Go. Check it out in the Go playground or code it on your own machine! NOTE: The playground times out of the infinite loop.

Discussion (0)