26aa154547
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
Squash the 12 goose migrations into one 00001_baseline.sql (there is no prod data; verified schema-identical to the chain via a pg_dump diff + the green integration suite) and rename the game-variant labels english/russian_scrabble/erudit -> scrabble_en/scrabble_ru/erudit_ru across the backend, the FlatBuffers wire values and the UI. dawg filenames and the Go enum identifiers are unchanged; the i18n display keys are kept. Adds PRERELEASE.md (the R1-R7 pre-release tracker), linked from CLAUDE.md. Contour DB wipe and the scrabble-dictionary tidy are follow-ups.
111 lines
4.0 KiB
Go
111 lines
4.0 KiB
Go
package engine
|
||
|
||
import (
|
||
"errors"
|
||
"slices"
|
||
"testing"
|
||
)
|
||
|
||
// TestAlphabetTableEnglish pins the English table against the solver ruleset: 26 letters,
|
||
// contiguous indices, the concrete lower-case characters the solver emits and the standard
|
||
// tile values. This is the real parity check the UI no longer carries (Stage 13).
|
||
func TestAlphabetTableEnglish(t *testing.T) {
|
||
tab, err := AlphabetTable(VariantEnglish)
|
||
if err != nil {
|
||
t.Fatalf("AlphabetTable(scrabble_en): %v", err)
|
||
}
|
||
if len(tab) != 26 {
|
||
t.Fatalf("size = %d, want 26", len(tab))
|
||
}
|
||
for i, e := range tab {
|
||
if int(e.Index) != i {
|
||
t.Errorf("entry %d has Index %d, want %d (index must equal position)", i, e.Index, i)
|
||
}
|
||
}
|
||
// a=index0/value1, q=index16/value10, z=index25/value10.
|
||
if tab[0].Letter != "a" || tab[0].Value != 1 {
|
||
t.Errorf("entry 0 = %q/%d, want a/1", tab[0].Letter, tab[0].Value)
|
||
}
|
||
if tab[16].Letter != "q" || tab[16].Value != 10 {
|
||
t.Errorf("entry 16 = %q/%d, want q/10", tab[16].Letter, tab[16].Value)
|
||
}
|
||
if tab[25].Letter != "z" || tab[25].Value != 10 {
|
||
t.Errorf("entry 25 = %q/%d, want z/10", tab[25].Letter, tab[25].Value)
|
||
}
|
||
}
|
||
|
||
// TestAlphabetTableRussianVariants pins both Russian variants: they share the 33-letter
|
||
// alphabet but differ in tile values — most visibly ё (index 6), worth 3 in Russian
|
||
// Scrabble and 0 in Эрудит.
|
||
func TestAlphabetTableRussianVariants(t *testing.T) {
|
||
ru, err := AlphabetTable(VariantRussianScrabble)
|
||
if err != nil {
|
||
t.Fatalf("AlphabetTable(scrabble_ru): %v", err)
|
||
}
|
||
er, err := AlphabetTable(VariantErudit)
|
||
if err != nil {
|
||
t.Fatalf("AlphabetTable(erudit_ru): %v", err)
|
||
}
|
||
if len(ru) != 33 || len(er) != 33 {
|
||
t.Fatalf("sizes = %d/%d, want 33/33", len(ru), len(er))
|
||
}
|
||
if ru[0].Letter != "а" || ru[0].Value != 1 {
|
||
t.Errorf("scrabble_ru entry 0 = %q/%d, want а/1", ru[0].Letter, ru[0].Value)
|
||
}
|
||
if ru[6].Letter != "ё" || ru[6].Value != 3 {
|
||
t.Errorf("scrabble_ru ё (entry 6) = %q/%d, want ё/3", ru[6].Letter, ru[6].Value)
|
||
}
|
||
if er[6].Letter != "ё" || er[6].Value != 0 {
|
||
t.Errorf("erudit_ru ё (entry 6) = %q/%d, want ё/0", er[6].Letter, er[6].Value)
|
||
}
|
||
if ru[32].Letter != "я" || er[32].Letter != "я" {
|
||
t.Errorf("last letter = %q/%q, want я/я", ru[32].Letter, er[32].Letter)
|
||
}
|
||
}
|
||
|
||
// TestAlphabetTableUnknownVariant rejects a variant outside the catalogue.
|
||
func TestAlphabetTableUnknownVariant(t *testing.T) {
|
||
if _, err := AlphabetTable(Variant(99)); !errors.Is(err, ErrUnknownVariant) {
|
||
t.Fatalf("got %v, want ErrUnknownVariant", err)
|
||
}
|
||
}
|
||
|
||
// TestRackCodecRoundTrip pins the rack/exchange index codec the edge uses: EncodeRack maps
|
||
// concrete letters (with "?" for a blank) to indices (BlankIndex for the blank) and
|
||
// DecodeTiles inverts it. EncodeRack is case-insensitive so it accepts the lower-case
|
||
// Hand form and an upper-case letter alike.
|
||
func TestRackCodecRoundTrip(t *testing.T) {
|
||
letters := []string{"c", "a", "t", "?"}
|
||
idx, err := EncodeRack(VariantEnglish, letters)
|
||
if err != nil {
|
||
t.Fatalf("EncodeRack: %v", err)
|
||
}
|
||
if want := []int{2, 0, 19, BlankIndex}; !slices.Equal(idx, want) {
|
||
t.Fatalf("EncodeRack = %v, want %v", idx, want)
|
||
}
|
||
back, err := DecodeTiles(VariantEnglish, idx)
|
||
if err != nil {
|
||
t.Fatalf("DecodeTiles: %v", err)
|
||
}
|
||
if !slices.Equal(back, letters) {
|
||
t.Fatalf("DecodeTiles = %v, want %v", back, letters)
|
||
}
|
||
if up, err := EncodeRack(VariantEnglish, []string{"C"}); err != nil || !slices.Equal(up, []int{2}) {
|
||
t.Errorf("EncodeRack upper-case = %v,%v; want [2],nil", up, err)
|
||
}
|
||
}
|
||
|
||
// TestDecodeWordAndBounds covers the word-check decode and the out-of-range guard.
|
||
func TestDecodeWordAndBounds(t *testing.T) {
|
||
w, err := DecodeWord(VariantEnglish, []int{2, 0, 19})
|
||
if err != nil || w != "cat" {
|
||
t.Fatalf("DecodeWord = %q,%v; want cat,nil", w, err)
|
||
}
|
||
if _, err := LetterForIndex(VariantEnglish, 26); !errors.Is(err, ErrIllegalPlay) {
|
||
t.Errorf("out-of-range index: got %v, want ErrIllegalPlay", err)
|
||
}
|
||
if _, err := DecodeWord(VariantEnglish, []int{BlankIndex}); !errors.Is(err, ErrIllegalPlay) {
|
||
t.Errorf("blank in word: got %v, want ErrIllegalPlay", err)
|
||
}
|
||
}
|