ec435c0e7f
- 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.
69 lines
2.1 KiB
Go
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] })
|
|
}
|