From a0ab77f2e8f21a7096dc9b0828907d6e4e34fc2d Mon Sep 17 00:00:00 2001 From: JP Appel Date: Wed, 11 Sep 2024 22:27:00 -0400 Subject: Start work on bingo functionality Created a simple struct for game state. Started work on board generators and their seeds. --- bingo/board.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++ bingo/board_test.go | 60 +++++++++++++++++++++++++++++++ bingo/generators.go | 41 +++++++++++++++++++++ bingo/tiles.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 303 insertions(+) create mode 100644 bingo/board.go create mode 100644 bingo/board_test.go create mode 100644 bingo/generators.go create mode 100644 bingo/tiles.go (limited to 'bingo') diff --git a/bingo/board.go b/bingo/board.go new file mode 100644 index 0000000..19264a1 --- /dev/null +++ b/bingo/board.go @@ -0,0 +1,101 @@ +package bingo + +import "iter" + +type WinDirection int + +const ( + Row WinDirection = iota + Column + Diagonal +) + +type Game struct { + Board []string + Checked []bool + Length int + FreeSquare bool + Seed GameSeed +} + +func all(group []bool) bool { + allTrue := true + + for _, v := range group { + allTrue = allTrue && v + } + + return allTrue +} + +// Return if a game has been won +func (g Game) Win() bool { + length := g.Length + + for row := range g.Rows(length) { + if all(row) { + return true + } + } + + for col := range g.Cols(length) { + if all(col) { + return true + } + } + + for diag := range g.Diags(length) { + if all(diag) { + return true + } + } + + return false +} + +// Iterator for rows of a board +func (g Game) Rows(length int) iter.Seq[[]bool] { + return func(yield func([]bool) bool) { + for row := 0; (row+1)*length > len(g.Checked); row++ { + if !yield(g.Checked[row*length : (row+1)*length]) { + return + } + } + } +} + +// Iterator for columns of a board +func (g Game) Cols(length int) iter.Seq[[]bool] { + return func(yield func([]bool) bool) { + for col := 0; col*length+1 > len(g.Checked); col++ { + column := make([]bool, length) + for i := range length { + column[i] = g.Checked[i*length+col] + } + if !yield(column) { + return + } + } + } +} + +// Iterator for diagonals of square boards +func (g Game) Diags(length int) iter.Seq[[]bool] { + return func(yield func([]bool) bool) { + if length*length != len(g.Checked) { + return + } + + diagonal := make([]bool, length) + for i := 0; i < length; i++ { + diagonal[i] = g.Checked[i*length+i] + } + if !yield(diagonal) { + return + } + + for i := 0; i < length; i++ { + diagonal[i] = g.Checked[i*length+(length-1-i)] + } + } +} diff --git a/bingo/board_test.go b/bingo/board_test.go new file mode 100644 index 0000000..5df20ee --- /dev/null +++ b/bingo/board_test.go @@ -0,0 +1,60 @@ +package bingo_test + +import ( + "iter" + "testing" + + "github.com/jpappel/bingo-factory/bingo" +) + +func TestRows(t *testing.T) { + g := bingo.Game{} + + testGame := func(size int, length int) { + + g.Checked = make([]bool, size) + + testGroup := func(name string, iter iter.Seq[[]bool]) { + for i := range size { + g.Checked[i] = false + } + for group := range iter { + if len(group) != length { + t.Logf("Mismatching %s length: %d != %d", name, length, len(group)) + t.FailNow() + } + + for i := range length { + if group[i] != false { + t.Errorf("Incorrect value in %s!\n", name) + } + } + } + for i := range size { + g.Checked[i] = true + } + for group := range iter { + if len(group) != length { + t.Logf("Mismatching %s length: %d != %d\n", name, length, len(group)) + } + + for i := range length { + if group[i] != true { + t.Errorf("Incorrect value in %s!\n", name) + } + } + } + } + + testGroup("row", g.Rows(length)) + testGroup("col", g.Cols(length)) + testGroup("diag", g.Diags(length)) + + } + + t.Log("Testing Square Games") + testGame(9, 3) + testGame(25, 5) + t.Log("Testing Non-Square Games") + testGame(22, 2) +} diff --git a/bingo/generators.go b/bingo/generators.go new file mode 100644 index 0000000..1365f2b --- /dev/null +++ b/bingo/generators.go @@ -0,0 +1,41 @@ +package bingo + +type GameSeed string + +type Generator interface { + New(int, int) *Game + SetSeed(int64) + Seed() int64 +} + +type RandomGenerator struct { + tiles TilePool + picker TilePicker + seed int64 +} + +func (g RandomGenerator) New(size int, length int) *Game { + g.picker.Reset() + + board := make([]string, 0, size) + checked := make([]bool, size) + + for _, tile := range g.picker.Iter(size) { + board = append(board, tile) + } + + game := new(Game) + game.Board = board + game.Checked = checked + game.Length = length + + return game +} + +func (g RandomGenerator) Seed() int64 { + return g.seed +} + +func (g RandomGenerator) SetSeed(seed int64) { + g.seed = seed +} diff --git a/bingo/tiles.go b/bingo/tiles.go new file mode 100644 index 0000000..81e945f --- /dev/null +++ b/bingo/tiles.go @@ -0,0 +1,101 @@ +package bingo + +import ( + "iter" + "math/rand" + "slices" +) + +type TilePool map[string][]string + +func (pool TilePool) All() iter.Seq[string] { + return func(yield func(string) bool) { + for _, list := range pool { + for _, tile := range list { + if !yield(tile) { + return + } + } + } + } +} + +type TilePicker interface { + All() iter.Seq[string] // provides an iterator over an entire TilePool + Iter(int) iter.Seq2[string, string] // provides an iterator over n elements of a TilePool + Reset() // reset the internal state of a TilePicker +} + +type RandomTilePicker struct { + ChosenTags []string + tilePool TilePool + rand rand.Rand +} + +func NewRandomTilePicker(tiles TilePool, r rand.Rand) *RandomTilePicker { + tp := new(RandomTilePicker) + tp.tilePool = tiles + tp.rand = r + + return tp +} + +// Iterate over all elements of a TilePool in a random order +func (tp RandomTilePicker) All() iter.Seq[string] { + return func(yield func(string) bool) { + tiles := slices.Collect(tp.tilePool.All()) + if len(tiles) == 0 { + return + } + + tp.rand.Shuffle(len(tiles), func(i int, j int) { + tiles[i], tiles[j] = tiles[j], tiles[i] + }) + + for _, tile := range tiles { + if !yield(tile) { + return + } + } + } +} + +// Iterator over a TilePool by choosing one tile per tag until pool is exhausted or size tiles have been yielded +func (tp RandomTilePicker) Iter(size int) iter.Seq2[string, string] { + return func(yield func(string, string) bool) { + if len(tp.tilePool) == 0 { + return + } + + tags := make([]string, len(tp.tilePool)) + for tag := range tp.tilePool { + tags = append(tags, tag) + } + tp.rand.Shuffle(len(tags), func(i int, j int) { + tags[i], tags[j] = tags[j], tags[i] + }) + + yielded := 0 + for _, tag := range tags { + if yielded == size { + return + } + + list := tp.tilePool[tag] + if len(list) == 0 { + continue + } + + tile := list[tp.rand.Intn(len(list))] + if !yield(tag, tile) { + return + } + yielded++ + } + + } +} + +func (tp RandomTilePicker) Reset() { + tp.ChosenTags = make([]string, len(tp.ChosenTags)) +} -- cgit v1.2.3