Implementing Conway's Game of Life in Go⚡

Thu Mar 17 2022

demo

I recently started learning Go and I absolutely love it. I think implementing Conway's Game of Life in a language is like a more sophisticated 'Hello World' program. Its pretty easy to do it and you learn a lot of stuff about that language. So that's what I did.

Walk-through

So first let's initialize a Go project:

go mod init github.com/nexxeln/go-gol

Now make a gol.go file. This is where all our code will go.

package main

import (
	"image/color"
	"log"
	"math/rand"

	"github.com/hajimehoshi/ebiten"
)

So we start by declaring this as the main package and importing the tools we need. The last one called ebiten is a very simple 2-D engine written in Go, and we import it straight from GitHub which is pretty cool.

const scale int = 2
const width = 300
const height = 300

var blue color.Color = color.RGBA{69, 145, 196, 255}
var yellow color.Color = color.RGBA{255, 230, 120, 255}
var grid [width][height]uint8 = [width][height]uint8{}
var buffer [width][height]uint8 = [width][height]uint8{}
var count int = 0

Here we have declared some global variables. The scale is the density of the pixels. width and height are the width and height of the window. Next we declare some colors namely blue and yellow. The grid is where everything is going to be displayed and the buffer is the next iteration of the grid.

func update() error {
	for x := 1; x < width-1; x++ {
		for y := 1; y < height-1; y++ {
			buffer[x][y] = 0
			n := grid[x-1][y-1] + grid[x-1][y+0] + grid[x-1][y+1] + grid[x+0][y-1] + grid[x+0][y+1] + grid[x+1][y-1] + grid[x+1][y+0] + grid[x+1][y+1]

			if grid[x][y] == 0 && n == 3 {
				buffer[x][y] = 1
			} else if n < 2 || n > 3 {
				buffer[x][y] = 0
			} else {
				buffer[x][y] = grid[x][y]
			}
		}
	}

	temp := buffer
	buffer = grid
	grid = temp
	return nil
}

This is the update function. Here we first get all the neighbors of the cell and store it in n. Next we check if the cells around it are dead, if yes we make it alive. If there's overpopulation or under population we make it dead, otherwise we just copy it to the next iteration. After that we just return nil.

func display(window *ebiten.Image) {
	window.Fill(blue)

	for x := 0; x < width; x++ {
		for y := 0; y < height; y++ {
			for i := 0; i < scale; i++ {
				for j := 0; j < scale; j++ {
					if grid[x][y] == 1 {
						window.Set(x*scale+i, y*scale+j, yellow)
					}
				}
			}
		}
	}
}

This is the display function which is going to render it on the screen. This is where ebiten comes in. We start by defining window as an ebiten.Image and fill it with a nice blue color. Next for every pixel we're going to set that pixel as yellow.

func frame(window *ebiten.Image) error {
	count++
	var err error = nil
	if count == 20 {
		err = update()
		count = 0
	}
	if !ebiten.IsDrawingSkipped() {
		display(window)
	}

	return err
}

This is the frame function. First we increment the count variable, and once it reaches 20 we're going to call the update function and reset the count variable. We also check if ebiten is not going to skip the rendering phase, then we're just going to call the display function and render it to the screen.

We're almost finished, let's set up our main function.

func main() {
	for x := 1; x < width-1; x++ {
		for y := 1; y < height-1; y++ {
			if rand.Float32() < 0.5 {
				grid[x][y] = 1
			}
		}
	}

	if err := ebiten.Run(frame, width, height, 2, "Game of Life"); err != nil {
		log.Fatal(err)
	}
}

So we start off by setting up the grid and randomly populating it with cells using rand.Float32(). Next we use ebiten.Run to run the program with the name "Game of Life". If ebiten.Run returns an error we're just going to log it to the terminal. This is reminiscent of pattern matching in Rust.

Well that's pretty much it! Now just open up the terminal and run the program:

go run ./main.go

All the code can be found here.

This was my first blog ever, so please tell me if I did something wrong. If you read till here you're very cool. Okay bye!