DEV Community

Chig Beef
Chig Beef

Posted on

Make In A Day: Game Of Life

I don't think I've ever made Conway's Game of Life, so this will be a first for me. This game will be an interesting one for 2 reasons. The board is meant to be infinite. It's a 0-player game.

Either way, we have to make it in one day. I'll write out a full spec for us all to follow. If our application adheres to our concrete design list, we've made Conway's Game of Life!

Follow in your own programming language, but I'm using Golang.

From the User's Perspective

There's only one phase to this game. There should be a step over button, but also a play and pause. There is an infinite grid of on/off cells. A cell with less than 2 surrounding alive tiles dies. A cell with more than 3 surrounding alive tiles dies. A cell with 2 or 3 surrounding alive tiles stays alive. A cell with 3 neighbors that is dead becomes alive.

This is very short, let's make the list.

Shortlist

  1. The game will have a run mode where it will auto-step at a consistent pace
  2. This mode can be resumed and paused
  3. The game can be manually stepped
  4. The game is made up of an infinite grid of on/off cells
  5. The user should be able to move around the grid
  6. A cell with less than 2 surrounding alive tiles dies
  7. A cell with more than 3 surrounding alive tiles dies
  8. A cell with 2 or 3 surrounding alive tiles stays alive
  9. A cell with 3 neighbors that is dead becomes alive
  10. The cells for a new frame should update based on the last frame

This is a very short list, but there are a few parts that will make it difficult to get correct.

The Code

As per usual, we start off with our empty window.

Empty window

We're probably going to need some UI elements, so let's put them in. We're going to need a pause and play button, a step button, and a reset button. The pause/play buttons will simply switch an auto-stepping bool.

UI Elements

Now we have the hard part, how do we make an infinite board? My idea is to have a list that only contains the currently "on" tiles. To create the next step, this list must first be fully copied. Then, it would be "bulked", which would go through each tile, and create dummy tiles. These dummy tiles are the tiles around activated tiles, which may come alive. Once all the rules are done, we can then strip out the off tiles, finishing the step. Sounds like a good plan, so let's start by making tiles.

type Tile struct {
    row, col int
    on bool
}

func (t *Tile) draw(g *Game) {
    clr := tileOnColor
    }

    g.vis.DrawRect(
        float64(t.col*TILE_SIZE-g.offsetX),
        float64(t.row*TILE_SIZE-g.offsetY),
        TILE_SIZE,
        TILE_SIZE,
        clr,
    )
}
Enter fullscreen mode Exit fullscreen mode

You may remember my bulking idea, where we generate surrouning tiles.
Let's add this method to the tile.

func (t *Tile) getSurrounds() [8]Tile {
    return [8]Tile{
        {t.row-1, t.col-1, false},
        {t.row-1, t.col, false},
        {t.row-1, t.col+1, false},
        {t.row, t.col-1, false},
        {t.row, t.col+1, false},
        {t.row+1, t.col-1, false},
        {t.row+1, t.col, false},
        {t.row+1, t.col+1, false},
    }
}
Enter fullscreen mode Exit fullscreen mode

We have a finished tile, so now we need a virtual grid that can deal with them. Let's start with just adding a tile to the grid. When adding a tile, we don't want it to override a tile in the same position.

func (g *Grid) add(t Tile) {
    for _, o := range g.tiles {
        if o.row == t.row && o.col == t.col {
            return
        }
    }

    g.tiles = append(g.tiles, t)
}
Enter fullscreen mode Exit fullscreen mode

Pretty simple, now let's do stripping.

func (g *Grid) strip() {
    // Will give a little extra space,
    // but will be used later when bulking
    newTiles := make([]Tile, 0, len(g.tiles))

    for _, o := range g.tiles {
        if o.on {
            newTiles = append(newTiles, o)
        }
    }

    g.tiles = newTiles
}
Enter fullscreen mode Exit fullscreen mode

Bulking should also be very simple, just generate surrounding tiles, and add them one by one.

func (g *Grid) bulk() {
    // Only use current length, don't create new slice
    curLength := len(g.tiles)

    for i := range curLength {

        // Paranoia, shouldn't be possible because of strip
        if g.tiles[i].on {
            surTiles := g.tiles[i].getSurrounds()
            for n := range 8 {
                g.add(surTiles[n])
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The way we've done it allows us to use the same slice to read and write to. This makes can avoid a reallocation of our slice. We're really pushing through this well and quick, now let's do the copy method.

func (g *Grid) clone() Grid {
    newGrid := Grid{}

    // Allocate the whole slice
    newGrid.tiles = make([]Tile, len(g.tiles))

    // Copy over each tile
    for i, t := range g.tiles {
        newGrid.tiles[i] = t
    }

    return newGrid
}

func (g *Grid) step() {
    lastFrame := g.clone()
    g.bulk()
    g.applyRules(lastFrame)
    g.strip()
}
Enter fullscreen mode Exit fullscreen mode

The step method we defined above is exactly how our plan should be executed. We'll write the applyRules method later. There's no point in writing it if we can't, place and see it in action! So let's work on placing tiles and moving the camera.

func (g *Game) leftClick() {
    // Get actual position
    x := int(g.mousePos[0]) + g.offsetX
    y := int(g.mousePos[1]) + g.offsetY

    // Get row and col
    row := y / TILE_SIZE
    col := x / TILE_SIZE

    // Place the tile
    g.grid.add(Tile{row, col, true})
}

func (g *Game) cameraMove() {
    if ebiten.IsKeyPressed(ebiten.KeyW) {
        g.offsetY--
    }

    if ebiten.IsKeyPressed(ebiten.KeyS) {
        g.offsetY++
    }

    if ebiten.IsKeyPressed(ebiten.KeyA) {
        g.offsetX--
    }

    if ebiten.IsKeyPressed(ebiten.KeyD) {
        g.offsetX++
    }
}
Enter fullscreen mode Exit fullscreen mode

Some placed tiles

Perfect, let's see how much we've done so far.

  • [x] The game will have a run mode where it will auto-step at a consistent pace
  • [x] This mode can be resumed and paused
  • [x] The game can be manually stepped
  • [x] The game is made up of an infinite grid of on/off cells
  • [x] The user should be able to move around the grid
  • [ ] A cell with less than 2 surrounding alive tiles dies
  • [ ] A cell with more than 3 surrounding alive tiles dies
  • [ ] A cell with 2 or 3 surrounding alive tiles stays alive
  • [ ] A cell with 3 neighbours that is dead becomes alive
  • [x] The cells for a new frame should update based on the last frame

We've done everything except the actual game logic! Let's implement that real quick. But first, we're going to need a few more methods. For example, it would useful to be able to count the number of alive surrounds a tile has.

func (g *Grid) isOn(row, col int) {
    for i, o := range g.tiles {
        if o.row == row && o.col == col {
            return o.on
        }
    }
    return false
}

func (t *Tile) countSurrounds(lastGrid Grid) int {
    var n int

    if lastGrid.isOn(t.row-1, t.col-1) {
        n++
    }

    if lastGrid.isOn(t.row-1, t.col) {
        n++
    }

    if lastGrid.isOn(t.row-1, t.col+1) {
        n++
    }

    if lastGrid.isOn(t.row, t.col-1) {
        n++
    }

    if lastGrid.isOn(t.row, t.col+1) {
        n++
    }

    if lastGrid.isOn(t.row+1, t.col-1) {
        n++
    }

    if lastGrid.isOn(t.row+1, t.col) {
        n++
    }

    if lastGrid.isOn(t.row+1, t.col+1) {
        n++
    }

    return n
}
Enter fullscreen mode Exit fullscreen mode

Now let's write that logic.

func (t *Tile) step(lastGrid Grid) {
    n := t.countSurrounds(lastGrid)

    // Die (alone)
    if n < 2 {
        t.on = false
        return
    }

    // Die (popular)
    if n > 3 {
        t.on = false
        return
    }

    // Exist
    if n == 2 {
        return
    }

    // Born
    if n == 3 {
        t.on = true
        return
    }
}
Enter fullscreen mode Exit fullscreen mode

How simple is that! And it works perfectly!

Done

Nicely done, that was a simple yet fun one. An infinite grid can be a daunting idea, but we managed to implement a working solution. You may want to look at other possible ways to describe an infinite grid, to see what works best for you. The code for this project as well as the others is here as always. The way we've implemented our code would make it relatively simple to alter the rules.

Your Next Steps

Now that you have Conway's Game of Life, here are some ways you could possibly extend it.

  1. Make hexagonal tiles instead
  2. Alter the game's rules
  3. Allow the change of animation speed
  4. Give cells health that can increase
  5. Make a playable character that can kill cells
  6. Create a live shared Game of Life instance between devices
  7. Colour the cells and have blending mechanics

If you have any more ideas for games I should explain and make, put them in the comments.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more