7
\$\begingroup\$

I wrote the Game Of Life algorithm with Go. There are a million ways that we can implement the algorithm, but I want to know is it implemented in good-way or not?

I wonder how can I improve the performance and the code quality?

package main

import (
    "math"
    "math/rand"
    "strconv"
    "time"

    "github.com/nsf/termbox-go"
)

type Cell struct {
    X    int
    Y    int
    Dead bool
}

type GameOfLife struct {
    cells      [][]Cell
    Generation int
    Alives     int
}

var (
    maxRow, maxCol int
    pause          bool
    speed          time.Duration
    random         bool
)

func randInt(min int, max int) int {
    return min + rand.Intn(max-min)
}

func main() {
    pause = true
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    speed = 100
    random = false
    defer termbox.Close()
    termbox.SetInputMode(termbox.InputMouse)
    maxRow, maxCol = termbox.Size()
    maxCol -= 3
    rand.Seed(time.Now().UTC().UnixNano())
    b := initiate(false)
    eventQueue := make(chan termbox.Event)
    go func() {
        for {
            eventQueue <- termbox.PollEvent()
        }
    }()
loop:
    for {
        select {
        case ev := <-eventQueue:
            if ev.Type == termbox.EventKey {
                if ev.Key == termbox.KeyCtrlC {
                    break loop
                }

                if ev.Key == termbox.KeyCtrlS {
                    pause = !pause
                }

                if ev.Key == termbox.KeyCtrlN {
                    b = initiate(false)
                    pause = true
                }

                if ev.Key == termbox.KeyCtrlI {
                    speed++
                }

                if ev.Key == termbox.KeyCtrlD {
                    speed--
                }

                if ev.Key == termbox.KeyCtrlR {
                    b = initiate(true)
                }
            }
            if ev.Type == termbox.EventMouse {
                if ev.MouseX <= maxRow && ev.MouseY <= maxCol {
                    b.cells[ev.MouseY][ev.MouseX].Dead = false
                }
            }
            b.Print()
            termbox.Flush()

        default:
            if !pause {
                b.NextGen()
                time.Sleep(speed * time.Millisecond)
            }
            b.Print()
            termbox.Flush()
        }
    }
}

func initiate(random bool) *GameOfLife {
    b := &GameOfLife{
        cells: make([][]Cell, int(math.Max(float64(maxCol), float64(maxRow)))),
    }
    for i := 0; i <= maxCol; i++ {
        for j := 0; j <= maxRow; j++ {
            dead := true
            if random {
                if randInt(0, 100) < 50 {
                    dead = false
                }
            }
            c := Cell{
                X:    i,
                Y:    j,
                Dead: dead,
            }
            b.cells[i] = append(b.cells[i], c)
        }
    }

    return b
}

func (b *GameOfLife) NextGen() {
    duplicate := make([][]Cell, len(b.cells))
    for i := range b.cells {
        duplicate[i] = make([]Cell, len(b.cells[i]))
        copy(duplicate[i], b.cells[i])
    }
    b.Alives = 0
    for i := 0; i <= maxCol; i++ {
        for j := 0; j <= maxRow; j++ {
            ncnt := b.Nighbors(b.cells[i][j])
            if ncnt < 2 || ncnt > 3 {
                duplicate[i][j].Dead = true
            }
            if b.cells[i][j].Dead && ncnt == 3 {
                duplicate[i][j].Dead = false
            }
            if ncnt == 2 {
                duplicate[i][j] = b.cells[i][j]
            }

            if !duplicate[i][j].Dead {
                b.Alives++
            }
        }
    }
    b.Generation++
    b.cells = duplicate
}

func (b *GameOfLife) Nighbors(cell Cell) int {
    cnt := 0
    for x1 := cell.X - 1; x1 <= cell.X+1; x1++ {
        for y1 := cell.Y - 1; y1 <= cell.Y+1; y1++ {
            if x1 == cell.X && y1 == cell.Y {
                continue
            }
            if x1 < 0 || x1 >= maxCol {
                continue
            }
            if y1 < 0 || y1 >= maxRow {
                continue
            }
            if !b.cells[x1][y1].Dead {
                cnt++
            }
        }
    }
    return cnt
}

func (b *GameOfLife) Print() {
    termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)

    for i := 0; i <= maxCol; i++ {
        for j := 0; j <= maxRow; j++ {
            ch := '█'
            if b.cells[i][j].Dead {
                ch = '░'
            }
            termbox.SetCell(j, i, ch, termbox.ColorDefault, termbox.ColorDefault)
        }
    }
    b.PrintStatus()
}

func (b *GameOfLife) PrintStatus() {
    statuses := []string{
        "Generation: " + strconv.Itoa(b.Generation) + " | Alives: " + strconv.Itoa(b.Alives) + " | Speed: " + (speed * time.Microsecond).String(),
        "Shortcuts: New (Ctrl+N) | Start/Pause(Ctrl+S) | Random (Ctrl+R) | Set Cell Alive (Mouse Click) | Speed Up (Ctrl+D) | Speed Down (Ctrl+I)",
    }
    for c, status := range statuses {
        for x, ch := range status {
            termbox.SetCell(x+1, maxCol+c+1, ch, termbox.ColorCyan, termbox.ColorDefault)
        }
    }
}

Edit: source code updated

github repository

\$\endgroup\$
6
\$\begingroup\$

I really liked the way you used Termbox; you should try Tcell as well for fun.

Review

The function initiate is doing 2 jobs: generate the world and randomize.

They could be 2 functions as below:

func initiate(maxCol, maxRow) *GameOfLife {

    b := &GameOfLife{
        cells: make([][]Cell, int(math.Max(float64(maxCol), float64(maxRow)))),
    }
}

and Randomization could be a separate function.

Also, randInt could be better achieved with a seed:

seed := rand.New(rand.NewSource(time.Now().Unix()))

and then you may call seed.Intn(30) 30 here being the upper limit

Neighbors function Nighbors is bit messy; you could add more readability here. Which will just return Count of Neighbors and pass it to Cell.NextState

The Cell could be a struct and have the method NextState e.g.

func (c *Cell) NextState(neighbours int) {
    if c.Alive && (neighbours < 2 || neighbours > 3) {
      c.Alive = false
  }

  if c.Alive && (neighbours == 2 || neighbours == 3) {
     c.Alive = true
  }

  if !c.Alive && neighbours == 3 {
     c.Alive = true
  }
}

You could have a separate directions struct and loop through each Direction, and have another method to get the Direction Cell e.g.

func (b GameOfLife) getCell(x, y int) Cell {
    return b.cells[x+(y*wl.width)]
}

func(b GameOfLife) Plus(x, y int, direction Vector) Cell {
    return b.getCell(x+direction.x, y+direction.y)
}

var DirectionNames = strings.Split("n ne e se s sw w nw", " ")
var Directions = map[string]Vector{
    "n":  {0, -1},
    "ne": {1, -1},
    "e":  {1, 0},
    "se": {1, 1},
    "s":  {0, 1},
    "sw": {-1, 1},
    "w":  {-1, 0},
    "nw": {-1, -1},
}

In the same way, I see NextGen could be refactored into more functions to have better readability here.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.