Skip to content

Commit

Permalink
Convert Abrash's algorithm to a 1d array
Browse files Browse the repository at this point in the history
  • Loading branch information
makyo committed Apr 2, 2024
1 parent 152bf79 commit 739dd3e
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 7 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ goos: linux
goarch: amd64
pkg: github.com/makyo/gogol
cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
BenchmarkEvolveNaive2d-8 1458 8926277 ns/op 530798 B/op 257 allocs/op
BenchmarkEvolveNaive1d-8 2181 5333053 ns/op 524529 B/op 1 allocs/op
BenchmarkEvolveScholes-8 6126 2015931 ns/op 5767270 B/op 11 allocs/op
BenchmarkEvolveAbrash-8 19974 588416 ns/op 1054775 B/op 257 allocs/op
BenchmarkEvolveAbrashBitwise-8 70332 165391 ns/op 71681 B/op 257 allocs/op
BenchmarkEvolveNaive2d-8 1436 10173303 ns/op 531187 B/op 257 allocs/op
BenchmarkEvolveNaive1d-8 2203 5615695 ns/op 524527 B/op 1 allocs/op
BenchmarkEvolveScholes-8 7864 1926182 ns/op 5767254 B/op 11 allocs/op
BenchmarkEvolveAbrashStruct-8 21536 538795 ns/op 1055156 B/op 257 allocs/op
BenchmarkEvolveAbrash-8 72214 164118 ns/op 72065 B/op 257 allocs/op
BenchmarkEvolveAbrash1d-8 94778 110808 ns/op 65536 B/op 1 allocs/op
PASS
ok github.com/makyo/gogol 69.763s
ok github.com/makyo/gogol 86.224s
```
236 changes: 236 additions & 0 deletions abrash1d/life.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package abrash1d

import (
"math"
"math/rand"

"github.com/makyo/gogol/rle"
)

// Bitwise operations are not my strong suit, so lots of comments ahead to help me understand.

// A cell is just a byte, which holds both the neighbor count and the state.
type cell byte

const (
// Store the current state of the cell in the fourth bit (so 0000x000). This is mostly syntactic sugar, meaning...
state = 4

// ...if we shift 1 over 4 spots, we get the fourth bit (16).
statebit = 1 << state

// We can thus use everything less than that (15) for the count of neighbors.
countbit = 0xf
)

// state gets the state of the cell by AND-ing the fourth bit. The result will be 0 (so, dead) for anything where 00001000 is not true.
func (c cell) state() bool {
return (c & statebit) != 0
}

// neighbors gets the count of neighbors by AND-ing everything below the fourth bit (00000111). The result will be the remainder. Essentually mod statebit.
func (c cell) neighbors() cell {
return c & countbit
}

// vivify makes the cell alive by OR-ing the fourth bit (so, noop if it's already alive).
func (c cell) vivify() cell {
return c | statebit
}

// kill makes the cell dead by AND-ing the bit with NOT the fourth bit (so, AND-ing with false).
func (c cell) kill() cell {
return c &^ statebit
}

type model struct {
width int
height int
field []cell
}

// wrapPos wraps a cell position that would otherwise be outside of a rectangular grid.
func (m model) wrapPos(pos int) int {
return int(math.Abs(float64(pos))) % len(m.field)
}

// addToNeighbors to all neighboring cells.
func (m model) addToNeighbors(pos int) {
// West
newPos := m.wrapPos(pos - 1)
m.field[newPos] += 0x1

// Northwest
newPos = m.wrapPos(pos - m.width - 1)
m.field[newPos] += 0x1

// North
newPos = m.wrapPos(pos - m.width)
m.field[newPos] += 0x1

// Northeast
newPos = m.wrapPos(pos - m.width + 1)
m.field[newPos] += 0x1

// East
newPos = m.wrapPos(pos + 1)
m.field[newPos] += 0x1

// Southeast
newPos = m.wrapPos(pos + m.width + 1)
m.field[newPos] += 0x1

// South
newPos = m.wrapPos(pos + m.width)
m.field[newPos] += 0x1

// Southwest
newPos = m.wrapPos(pos + m.width - 1)
m.field[newPos] += 0x1
}

// subtractFromNeighbors subtracts from all neighboring cells.
func (m model) subtractFromNeighbors(pos int) {
// West
newPos := m.wrapPos(pos - 1)
m.field[newPos] -= 0x1

// Northwest
newPos = m.wrapPos(pos - m.width - 1)
m.field[newPos] -= 0x1

// North
newPos = m.wrapPos(pos - m.width)
m.field[newPos] -= 0x1

// Northeast
newPos = m.wrapPos(pos - m.width + 1)
m.field[newPos] -= 0x1

// East
newPos = m.wrapPos(pos + 1)
m.field[newPos] -= 0x1

// Southeast
newPos = m.wrapPos(pos + m.width + 1)
m.field[newPos] -= 0x1

// South
newPos = m.wrapPos(pos + m.width)
m.field[newPos] -= 0x1

// Southwest
newPos = m.wrapPos(pos + m.width - 1)
m.field[newPos] -= 0x1
}

// calculateNeighbors calculates alive neighbors for every cell in the model.
func (m model) calculateAllNeighbors() {
for i, c := range m.field {
if c.state() {
m.addToNeighbors(i)
}
}
}

// makeAlive sets the cell state to alive and increments the neighbor count. It assumes the cell is dead.
func (m model) makeAlive(pos int) {
m.addToNeighbors(pos)
m.field[pos] = m.field[pos].vivify()
}

// makeDead sets the cell state to dead and decrements the neighbor count. It assumes the cell is alive.
func (m model) makeDead(pos int) {
m.subtractFromNeighbors(pos)
m.field[pos] = m.field[pos].kill()
}

// nextGeneration evolves the field of automata one generation based on the rules of Conway's Game of Life.
func (m model) Next() {
// Deep copy the field
next := make([]cell, m.width*m.height)
for i, _ := range m.field {
next[i] = m.field[i]
}

// Loop over the field.
for i, c := range next {

// If a cell has zero alive neighbors and is already dead, it's just going to stay dead.
if c == 0 {
continue
}
if c.state() {
if c.neighbors() != 0x2 && c.neighbors() != 0x3 {
m.makeDead(i)
}
} else if c.neighbors() == 0x3 {
m.makeAlive(i)
}
}
}

// Populate generates a random field of automata, where each cell has a 1 in 5 chance of being alive.
func (m model) Populate() {
for i, _ := range m.field {
m.field[i] = 0x0
if rand.Intn(5) == 0 {
m.field[i] = m.field[i].vivify()
}
}
m.calculateAllNeighbors()
}

// Ingest sets the field to the given value.
func (m model) Ingest(f *rle.RLEField) {
startX := (m.width - f.Width) / 2
startY := (m.height - f.Height) / 2
for y, row := range f.Field {
for x, col := range row {
if col {
pos := (y+startY)*m.width + x + startX
m.field[pos] = m.field[pos].vivify()
}
}
}
m.calculateAllNeighbors()
}

func (m model) ToggleCell(x, y int) {
pos := y*m.width + x
if m.field[pos].state() {
m.field[pos] = m.field[pos].vivify()
} else {
m.field[pos] = m.field[pos].kill()
}
}

// View builds the entire screen's worth of cells to be printed by returning a • for a living cell or a space for a dead cell.
func (m model) String() string {
var frame string

// Loop the field.
for i, c := range m.field {

// Set the cell contents
if c.state() {
frame += "•"
} else {
frame += " "
}
if i%m.width == 0 {
frame += "\n"
}

}
return frame
}

func New(width, height int) model {
m := model{
width: width,
height: height,
field: make([]cell, width*height),
}
return m
}
5 changes: 4 additions & 1 deletion life.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

tea "github.com/charmbracelet/bubbletea"
"github.com/makyo/gogol/abrash"
"github.com/makyo/gogol/abrash1d"
"github.com/makyo/gogol/abrashstruct"
"github.com/makyo/gogol/base"
"github.com/makyo/gogol/naive1d"
Expand All @@ -21,7 +22,7 @@ type model struct {
}

var (
algoFlag = flag.String("algo", "naive1d", "Which algorithm to use (naive1d, naive2d, scholes, abrashstruct, abrash)")
algoFlag = flag.String("algo", "naive1d", "Which algorithm to use (naive1d, naive2d, scholes, abrashstruct, abrash, abrash1d)")
width = 10
height = 10
)
Expand All @@ -39,6 +40,8 @@ func getModel(width, height int) model {
m.base = abrashstruct.New(width, height)
case "abrash":
m.base = abrash.New(width, height)
case "abrash1d":
m.base = abrash1d.New(width, height)
}
return m
}
Expand Down
10 changes: 10 additions & 0 deletions life_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/makyo/gogol/abrash"
"github.com/makyo/gogol/abrash1d"
"github.com/makyo/gogol/abrashstruct"
"github.com/makyo/gogol/base"
"github.com/makyo/gogol/naive1d"
Expand Down Expand Up @@ -73,3 +74,12 @@ func BenchmarkEvolveAbrash(b *testing.B) {
m.Next()
}
}

func BenchmarkEvolveAbrash1d(b *testing.B) {
var m base.Model
m = abrash1d.New(256, 256)
m.Ingest(acorn())
for i := 0; i < b.N; i++ {
m.Next()
}
}

0 comments on commit 739dd3e

Please sign in to comment.