aboutsummaryrefslogtreecommitdiffstats
path: root/bingo
diff options
context:
space:
mode:
Diffstat (limited to 'bingo')
-rw-r--r--bingo/board.go101
-rw-r--r--bingo/board_test.go60
-rw-r--r--bingo/generators.go41
-rw-r--r--bingo/tiles.go101
4 files changed, 303 insertions, 0 deletions
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))
+}