diff --git a/cmd/vita/main.go b/cmd/vita/main.go index 19851bc..22e7991 100644 --- a/cmd/vita/main.go +++ b/cmd/vita/main.go @@ -14,6 +14,7 @@ var ( generations = flag.Int("gens", 3, "how many generations to run the universe") population = flag.Int("pop", 45, "initial population percent of the universe") number = flag.Int("n", 1, "number of universes to run in parallel") + rules = flag.String("rules", "conway", "rules to use for the universe") ) func main() { @@ -32,6 +33,17 @@ func main() { func runSingleUniverse() { universe := game.NewUniverse(uint32(*height), uint32(*width)) + switch *rules { + case "dayandnight": + universe.Rules = game.DayAndNightRules + case "seeds": + universe.Rules = game.SeedsRules + case "wrap": + universe.Rules = game.ConwayRulesWrap + default: + // just use conway + } + universe.Randomize(*population) for i := 0; i < *generations; i++ { @@ -63,6 +75,15 @@ func createParallelUniverses() []*game.ParallelUniverse { for row := 0; row < *number; row++ { for col := 0; col < *number; col++ { u := game.NewParallelUniverse(uint32(*height), uint32(*width)) + switch *rules { + case "dayandnight": + u.Rules = game.DayAndNightRules + case "seeds": + u.Rules = game.SeedsRules + default: + // just use conway + } + u.Randomize(*population) multi = append(multi, u) } diff --git a/lib/game/conway.go b/lib/game/conway.go deleted file mode 100644 index df82546..0000000 --- a/lib/game/conway.go +++ /dev/null @@ -1,55 +0,0 @@ -package game - -// ConwayRules implements the classic Game of Life rules. -func (u *Universe) ConwayRules(cell uint8, row, column uint32) uint8 { - return RuleB3S23(cell, u.MooreNeighbors(row, column)) -} - -// RuleB3S23 implements the B3/S23 ruleset: -// https://www.conwaylife.com/wiki/Conway%27s_Game_of_Life#Rules -func RuleB3S23(cell uint8, liveNeighbors uint8) uint8 { - switch { - case cell == Alive && liveNeighbors < 2: - // 1. Any live cell with fewer than two live neighbours dies, as if by underpopulation. - return Dead - case cell == Alive && (liveNeighbors == 2 || liveNeighbors == 3): - // 2. Any live cell with two or three live neighbours lives on to the next generation. - return Alive - case cell == Alive && liveNeighbors > 3: - // 3. Any live cell with more than three live neighbours dies, as if by overpopulation. - return Dead - case cell == Dead && liveNeighbors == 3: - // 4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. - return Alive - default: - return cell - } -} - -// MooreNeighbors returns the number of alive neighbors for a given cell. -// It uses the Moore neighborhood, which includes the eight cells surrounding -// the given cell. -func (u *Universe) MooreNeighbors(row, column uint32) uint8 { - count := uint8(0) - - // We use height/width and modulos to avoid manually handling edge cases - // (literally: cases at the edge of the universe, e.g. cells at row/col 0). - for _, rowDiff := range []uint32{u.height - 1, 0, 1} { - for _, colDiff := range []uint32{u.width - 1, 0, 1} { - if rowDiff == 0 && colDiff == 0 { - // Skip checking the cell itself - continue - } - - neighborRow := (row + rowDiff) % u.height - neighborColumn := (column + colDiff) % u.width - neighborIdx := u.GetIndex(neighborRow, neighborColumn) - - if u.Cell(neighborIdx) == Alive { - count++ - } - } - } - - return count -} diff --git a/lib/game/conway_test.go b/lib/game/conway_test.go deleted file mode 100644 index 25d6982..0000000 --- a/lib/game/conway_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package game - -import ( - "testing" -) - -func TestConwayRule(t *testing.T) { - t.Run("MooreNeighbors", func(t *testing.T) { - u := NewUniverse(32, 32) - - idx := u.GetIndex(12, 12) - u.cells[idx] = Alive - if u.Cell(idx) != Alive { - t.Errorf("Expected cell %d to be alive, got %d", idx, u.Cell(idx)) - } - - if u.MooreNeighbors(0, 0) != 0 { - t.Errorf("Expected cell %d to have 0 alive neighbors, got %d", 0, u.MooreNeighbors(0, 0)) - } - - if u.MooreNeighbors(11, 12) != 1 { - t.Errorf("Expected cell %d to have 1 alive neighbors, got %d", idx, u.MooreNeighbors(11, 12)) - } - - idx = u.GetIndex(10, 12) - u.cells[idx] = Alive - - if u.MooreNeighbors(11, 12) != 2 { - t.Errorf("Expected cell %d to have 2 alive neighbors, got %d", idx, u.MooreNeighbors(11, 12)) - } - }) - - t.Run("RuleB3S23 when cell is Alive", func(t *testing.T) { - if RuleB3S23(Alive, 0) != Dead { - t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Alive, 0)) - } - - if RuleB3S23(Alive, 1) != Dead { - t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Alive, 1)) - } - - if RuleB3S23(Alive, 2) != Alive { - t.Errorf("Expected cell to be alive, got %d", RuleB3S23(Alive, 2)) - } - - if RuleB3S23(Alive, 3) != Alive { - t.Errorf("Expected cell to be alive, got %d", RuleB3S23(Alive, 3)) - } - - if RuleB3S23(Alive, 4) != Dead { - t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Alive, 4)) - } - }) - - t.Run("RuleB3S23 when cell is Dead", func(t *testing.T) { - if RuleB3S23(Dead, 0) != Dead { - t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Dead, 0)) - } - - if RuleB3S23(Dead, 1) != Dead { - t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Dead, 1)) - } - - if RuleB3S23(Dead, 2) != Dead { - t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Dead, 2)) - } - - if RuleB3S23(Dead, 3) != Alive { - t.Errorf("Expected cell to be alive, got %d", RuleB3S23(Dead, 3)) - } - - if RuleB3S23(Dead, 4) != Dead { - t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Dead, 4)) - } - }) -} diff --git a/lib/game/rules.go b/lib/game/rules.go new file mode 100644 index 0000000..07de8fc --- /dev/null +++ b/lib/game/rules.go @@ -0,0 +1,147 @@ +package game + +// ConwayRules implements the classic Game of Life rules. +func (u *Universe) ConwayRules(cell uint8, row, column uint32) uint8 { + return RuleB3S23(cell, u.MooreNeighbors(row, column)) +} + +// ConwayRulesWrap implements the classic Game of Life rules but wraps the grid. +func (u *Universe) ConwayRulesWrap(cell uint8, row, column uint32) uint8 { + return RuleB3S23(cell, u.MooreNeighborsWrap(row, column)) +} + +// RuleB3S23 implements the B3/S23 rules: +// https://www.conwaylife.com/wiki/Conway%27s_Game_of_Life#Rules +func RuleB3S23(cell uint8, liveNeighbors uint8) uint8 { + switch { + case cell == Dead && liveNeighbors == 3: + // Birth - any dead cell with exactly three live neighbours + // becomes a live cell, as if by reproduction. + return Alive + case cell == Alive && (liveNeighbors == 2 || liveNeighbors == 3): + // Survival - any live cell with two or three live neighbours + // lives on to the next generation. + return Alive + case cell == Alive && (liveNeighbors < 2 || liveNeighbors > 3): + // Death - any live cell with less than two or more than three + // live neighbours dies, as if by underpopulation/overpopulation. + return Dead + default: + return cell + } +} + +// SeedsRules implements the Seeds rules. +// See https://conwaylife.com/wiki/OCA:Seeds +func (u *Universe) SeedsRules(cell uint8, row, column uint32) uint8 { + return RuleB2S(cell, u.MooreNeighbors(row, column)) +} + +// RuleB2S implements the B2S rules: +// https://conwaylife.com/wiki/OCA:Seeds +func RuleB2S(cell uint8, liveNeighbors uint8) uint8 { + switch { + case cell == Dead && (liveNeighbors == 2): + // Birth - any dead cell with two live neighbours + // becomes a live cell as if by reproduction. + return Alive + case cell == Alive: + // Death - any live cell dies. + return Dead + default: + return cell + } +} + +// DayAndNightRules implements the Day And Night rules. +// See https://conwaylife.com/wiki/OCA:Day_%26_Night +func (u *Universe) DayAndNightRules(cell uint8, row, column uint32) uint8 { + return RuleB3678S34678(cell, u.MooreNeighbors(row, column)) +} + +// RuleB3678S34678 implements the B3678/S34678 rules: +// https://conwaylife.com/wiki/OCA:Day_%26_Night +func RuleB3678S34678(cell uint8, liveNeighbors uint8) uint8 { + switch { + case cell == Dead && (liveNeighbors == 3 || liveNeighbors == 6 || + liveNeighbors == 7 || liveNeighbors == 8): + // Birth - any dead cell with 3, 6, 7, or 8 live neighbours + // becomes a live cell as if by reproduction. + return Alive + case cell == Alive && (liveNeighbors == 3 || liveNeighbors == 4 || + liveNeighbors == 6 || liveNeighbors == 7 || liveNeighbors == 8): + // Survival - any live cell with 3, 4, 6, 7, or 8 live neighbours + // lives on to the next generation. + return Alive + case cell == Alive && (liveNeighbors < 3 || liveNeighbors == 5 || liveNeighbors > 8): + // Death - any live cell with less than three, five, or more than eight + // live neighbours dies, as if by underpopulation/overpopulation. + return Dead + default: + return cell + } +} + +// MooreNeighbors returns the number of alive neighbors for a given cell. +// It uses the Moore neighborhood, which includes the eight cells surrounding +// the given cell. +func (u *Universe) MooreNeighbors(row, column uint32) uint8 { + count := uint8(0) + + r, c := int32(row), int32(column) + for _, neighborRow := range []int32{r - 1, r, r + 1} { + for _, neighborColumn := range []int32{c - 1, c, c + 1} { + switch { + // Skip checking the cell itself + case neighborRow == r && neighborColumn == c: + // Ignore if pixel is out of bounds + case neighborRow < 0: + case neighborColumn < 0: + case neighborRow > int32(u.height)-1: + case neighborColumn > int32(u.width)-1: + // otherwise, check neighbor + default: + neighborIdx := u.GetIndex(uint32(neighborRow), uint32(neighborColumn)) + if u.Cell(neighborIdx) != Dead { + count++ + } + } + } + } + + return count +} + +// MooreNeighborsWrap returns the number of alive neighbors for a given cell. +// It uses the Moore neighborhood, which includes the eight cells surrounding +// the given cell, but wraps to the other side if it would be off the grid. +func (u *Universe) MooreNeighborsWrap(row, column uint32) uint8 { + count := uint8(0) + + r, c := int32(row), int32(column) + for _, neighborRow := range []int32{r - 1, r, r + 1} { + for _, neighborColumn := range []int32{c - 1, c, c + 1} { + switch { + // Skip checking the cell itself + case neighborRow == r && neighborColumn == c: + continue + // Wrap if pixel is out of bounds + case neighborRow < 0: + neighborRow = int32(u.height) - 1 + case neighborColumn < 0: + neighborColumn = int32(u.width) - 1 + case neighborRow > int32(u.height)-1: + neighborRow = 0 + case neighborColumn > int32(u.width)-1: + neighborColumn = 0 + } + // Now, check neighbor + neighborIdx := u.GetIndex(uint32(neighborRow), uint32(neighborColumn)) + if u.Cell(neighborIdx) != Dead { + count++ + } + } + } + + return count +} diff --git a/lib/game/rules_test.go b/lib/game/rules_test.go new file mode 100644 index 0000000..ab688bd --- /dev/null +++ b/lib/game/rules_test.go @@ -0,0 +1,185 @@ +package game + +import ( + "testing" +) + +func TestConwayRule(t *testing.T) { + t.Run("MooreNeighbors", func(t *testing.T) { + u := NewUniverse(32, 32) + + idx := u.GetIndex(12, 12) + u.cells[idx] = Alive + if u.Cell(idx) != Alive { + t.Errorf("Expected cell %d to be alive, got %d", idx, u.Cell(idx)) + } + + if u.MooreNeighbors(0, 0) != 0 { + t.Errorf("Expected cell %d to have 0 alive neighbors, got %d", 0, u.MooreNeighbors(0, 0)) + } + + if u.MooreNeighbors(11, 12) != 1 { + t.Errorf("Expected cell %d to have 1 alive neighbors, got %d", idx, u.MooreNeighbors(11, 12)) + } + + idx = u.GetIndex(10, 12) + u.cells[idx] = Alive + + if u.MooreNeighbors(11, 12) != 2 { + t.Errorf("Expected cell %d to have 2 alive neighbors, got %d", idx, u.MooreNeighbors(11, 12)) + } + }) + + t.Run("MooreNeighborsWrap", func(t *testing.T) { + u := NewUniverse(32, 32) + + idx := u.GetIndex(12, 12) + u.cells[idx] = Alive + if u.Cell(idx) != Alive { + t.Errorf("Expected cell %d to be alive, got %d", idx, u.Cell(idx)) + } + + if u.MooreNeighborsWrap(0, 0) != 0 { + t.Errorf("Expected cell %d to have 0 alive neighbors, got %d", 0, u.MooreNeighborsWrap(0, 0)) + } + + if u.MooreNeighborsWrap(11, 12) != 1 { + t.Errorf("Expected cell %d to have 1 alive neighbors, got %d", idx, u.MooreNeighborsWrap(11, 12)) + } + + idx = u.GetIndex(10, 12) + u.cells[idx] = Alive + + if u.MooreNeighborsWrap(11, 12) != 2 { + t.Errorf("Expected cell %d to have 2 alive neighbors, got %d", idx, u.MooreNeighborsWrap(11, 12)) + } + }) + + t.Run("RuleB3S23 when cell is Alive", func(t *testing.T) { + if RuleB3S23(Alive, 0) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Alive, 0)) + } + + if RuleB3S23(Alive, 1) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Alive, 1)) + } + + if RuleB3S23(Alive, 2) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3S23(Alive, 2)) + } + + if RuleB3S23(Alive, 3) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3S23(Alive, 3)) + } + + if RuleB3S23(Alive, 4) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Alive, 4)) + } + }) + + t.Run("RuleB3S23 when cell is Dead", func(t *testing.T) { + if RuleB3S23(Dead, 0) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Dead, 0)) + } + + if RuleB3S23(Dead, 1) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Dead, 1)) + } + + if RuleB3S23(Dead, 2) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Dead, 2)) + } + + if RuleB3S23(Dead, 3) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3S23(Dead, 3)) + } + + if RuleB3S23(Dead, 4) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3S23(Dead, 4)) + } + }) + + t.Run("RuleB3678S34678 when cell is Alive", func(t *testing.T) { + if RuleB3678S34678(Alive, 0) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3678S34678(Alive, 0)) + } + + if RuleB3678S34678(Alive, 1) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3678S34678(Alive, 1)) + } + + if RuleB3678S34678(Alive, 2) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3678S34678(Dead, 2)) + } + + if RuleB3678S34678(Alive, 3) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3678S34678(Alive, 3)) + } + + if RuleB3678S34678(Alive, 4) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3678S34678(Alive, 4)) + } + + if RuleB3678S34678(Alive, 5) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3678S34678(Alive, 5)) + } + + if RuleB3678S34678(Alive, 6) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3678S34678(Alive, 6)) + } + + if RuleB3678S34678(Alive, 7) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3678S34678(Alive, 7)) + } + + if RuleB3678S34678(Alive, 8) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3678S34678(Alive, 8)) + } + + if RuleB3678S34678(Alive, 9) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3678S34678(Alive, 9)) + } + }) + + t.Run("RuleB3678S34678 when cell is Dead", func(t *testing.T) { + if RuleB3678S34678(Dead, 0) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3678S34678(Dead, 0)) + } + + if RuleB3678S34678(Dead, 1) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3678S34678(Dead, 1)) + } + + if RuleB3678S34678(Dead, 2) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3678S34678(Dead, 2)) + } + + if RuleB3678S34678(Dead, 3) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3678S34678(Dead, 3)) + } + + if RuleB3678S34678(Dead, 4) != Dead { + t.Errorf("Expected cell to be alive, got %d", RuleB3678S34678(Dead, 4)) + } + + if RuleB3678S34678(Dead, 5) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3678S34678(Dead, 5)) + } + + if RuleB3678S34678(Dead, 6) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3678S34678(Dead, 6)) + } + + if RuleB3678S34678(Dead, 7) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3678S34678(Dead, 7)) + } + + if RuleB3678S34678(Dead, 8) != Alive { + t.Errorf("Expected cell to be alive, got %d", RuleB3678S34678(Dead, 8)) + } + + if RuleB3678S34678(Dead, 9) != Dead { + t.Errorf("Expected cell to be dead, got %d", RuleB3678S34678(Dead, 9)) + } + }) +}