Files
scrabble-game/backend/internal/engine/bag_test.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

79 lines
2.1 KiB
Go

package engine
import (
"maps"
"slices"
"testing"
"gitea.iliadenisov.ru/developer/scrabble-solver/rules"
)
// allTiles returns the full multiset of tiles a bag is filled from, in ruleset
// order (letters then blanks).
func allTiles(rs *rules.Ruleset) []byte {
var ts []byte
for i, n := range rs.Counts {
for range n {
ts = append(ts, byte(i))
}
}
for range rs.Blanks {
ts = append(ts, blankTile)
}
return ts
}
// TestBagDeterministic checks that two bags with the same seed draw identically.
func TestBagDeterministic(t *testing.T) {
rs := rules.English()
a, b := NewBag(rs, 42), NewBag(rs, 42)
if a.Len() != b.Len() {
t.Fatalf("len mismatch: %d vs %d", a.Len(), b.Len())
}
for a.Len() > 0 {
if da, db := a.Draw(3), b.Draw(3); !slices.Equal(da, db) {
t.Fatalf("same seed drew differently: %v vs %v", da, db)
}
}
}
// TestBagReturnConservesMultiset checks Len accounting and that Return puts the
// exact tiles back, leaving the bag's multiset unchanged.
func TestBagReturnConservesMultiset(t *testing.T) {
rs := rules.English()
want := tileCounts(allTiles(rs))
total := len(allTiles(rs))
b := NewBag(rs, 7)
if b.Len() != total {
t.Fatalf("new bag len = %d, want %d", b.Len(), total)
}
drawn := b.Draw(rs.RackSize)
if b.Len() != total-rs.RackSize {
t.Fatalf("after draw len = %d, want %d", b.Len(), total-rs.RackSize)
}
b.Return(drawn)
if b.Len() != total {
t.Fatalf("after return len = %d, want %d", b.Len(), total)
}
if got := tileCounts(b.Draw(b.Len())); !maps.Equal(got, want) {
t.Fatalf("multiset changed across draw/return")
}
}
// TestBagDrawAll returns everything once the bag is exhausted and never panics.
func TestBagDrawAll(t *testing.T) {
rs := rules.English()
b := NewBag(rs, 1)
all := b.Draw(b.Len() + 10) // asking for more than present returns all
if len(all) != len(allTiles(rs)) {
t.Fatalf("drew %d, want %d", len(all), len(allTiles(rs)))
}
if b.Len() != 0 {
t.Fatalf("bag len = %d, want 0", b.Len())
}
if got := b.Draw(1); len(got) != 0 {
t.Fatalf("draw from empty bag returned %d tiles", len(got))
}
}