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

asked Dec 13, 2018 at 12:37
\$\endgroup\$
2
  • \$\begingroup\$ Cross posteed on reddit: reddit.com/r/golang/comments/a6278n/… \$\endgroup\$ Commented Dec 14, 2018 at 13:58
  • \$\begingroup\$ great to see you used termbox. have had a look at tcell ? \$\endgroup\$ Commented Nov 18, 2019 at 21:16

1 Answer 1

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.

answered Nov 19, 2019 at 12:47
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.