Files
scrabble-game/backend/internal/engine/bag.go
T
Ilia Denisov ec435c0e7f
Tests · Go / test (push) Successful in 8s
Tests · Integration / integration (push) Successful in 11s
Tests · Go / test (pull_request) Successful in 8s
Tests · Integration / integration (pull_request) Successful in 11s
Stage 14: solver & dictionary split — consume published module + DAWG artifact (TODO-1/TODO-2)
- backend/go.mod pins gitea.iliadenisov.ru/developer/scrabble-solver v1.0.0; the engine's
  imports use the published module path; go.work drops the solver replace (GOPRIVATE fetches
  it directly from Gitea). The solver's wordlist/dictdawg are now public packages.
- CI (go-unit, integration): drop the solver sibling-clone, set GOPRIVATE, and download the
  dictionary DAWG release artifact (scrabble-dawg-<DICT_VERSION>.tar.gz from the new
  scrabble-dictionary repo) for BACKEND_DICT_DIR.
- Docs: ARCHITECTURE §5/§11/§13/§14 + backend/README updated to the published-module +
  release-artifact model. PLAN.md re-scoped Stage 14 to the split and added Stages 15 (deploy
  infra & test contour), 16 (prod contour), 17 (dual Telegram bots); TODO-1/TODO-2 marked done.
2026-06-04 20:00:36 +02:00

69 lines
2.1 KiB
Go

package engine
import (
"math/rand"
"gitea.iliadenisov.ru/developer/scrabble-solver/rules"
)
// blankTile marks a blank tile in a hand or in the bag, matching the
// scrabble-solver convention (selfplay) so a hand of these bytes interoperates
// with the solver's rack helpers.
const blankTile byte = 0xff
// Bag is the shuffled draw pile for one game. Unlike the solver's self-play bag
// it supports returning tiles, which an exchange needs. It is seeded once, so a
// game's draws are reproducible from its seed and the sequence of operations.
// Bag is not safe for concurrent use; the owning Game serialises access.
type Bag struct {
tiles []byte
rng *rand.Rand
}
// NewBag fills a bag from the ruleset's tile counts and blanks and shuffles it
// with seed. Letters are stored as alphabet-index bytes and blanks as blankTile.
func NewBag(rs *rules.Ruleset, seed int64) *Bag {
var tiles []byte
for i, n := range rs.Counts {
for range n {
tiles = append(tiles, byte(i))
}
}
for range rs.Blanks {
tiles = append(tiles, blankTile)
}
b := &Bag{tiles: tiles, rng: rand.New(rand.NewSource(seed))}
b.shuffle()
return b
}
// Len returns the number of tiles left in the bag.
func (b *Bag) Len() int { return len(b.tiles) }
// Draw removes up to n tiles from the bag and returns them in a fresh slice.
// Drawing more than remain returns all of them; drawing from an empty bag
// returns an empty slice.
func (b *Bag) Draw(n int) []byte {
if n > len(b.tiles) {
n = len(b.tiles)
}
out := make([]byte, n)
copy(out, b.tiles[len(b.tiles)-n:])
b.tiles = b.tiles[:len(b.tiles)-n]
return out
}
// Return puts tiles back into the bag and reshuffles, as when a player exchanges
// tiles. The tiles must use the same encoding as Draw (alphabet indices and
// blankTile).
func (b *Bag) Return(tiles []byte) {
b.tiles = append(b.tiles, tiles...)
b.shuffle()
}
// shuffle randomises the remaining tiles with the bag's own RNG, keeping draws
// deterministic for a given seed and sequence of operations.
func (b *Bag) shuffle() {
b.rng.Shuffle(len(b.tiles), func(i, j int) { b.tiles[i], b.tiles[j] = b.tiles[j], b.tiles[i] })
}