package scrabble import ( "testing" "github.com/iliadenisov/alphabet" "gitea.iliadenisov.ru/developer/scrabble-solver/board" "gitea.iliadenisov.ru/developer/scrabble-solver/dictdawg" "gitea.iliadenisov.ru/developer/scrabble-solver/internal/encoding" "gitea.iliadenisov.ru/developer/scrabble-solver/rack" "gitea.iliadenisov.ru/developer/scrabble-solver/rules" "gitea.iliadenisov.ru/developer/scrabble-solver/wordlist" ) func makeRack(letters string, blanks int) rack.Rack { r := rack.New(26) for i := range len(letters) { r.Add(letters[i] - 'a') } for range blanks { r.AddBlank() } return r } func placeWord(b *board.Board, r, c int, dir Direction, word string) { for i := range len(word) { rr, cc := r, c+i if dir == Vertical { rr, cc = r+i, c } b.Set(rr, cc, encoding.Cell(word[i]-'a', false)) } } func genMoves(moves []Move) map[string]Move { out := make(map[string]Move, len(moves)) for _, m := range moves { out[moveKey(m.Dir, m.Tiles)] = m } return out } // testWords is a small lexicon with enough overlaps to form across and cross plays. var testWords = []string{ "aa", "ace", "act", "arc", "are", "art", "as", "at", "ate", "cab", "cap", "car", "care", "cars", "cart", "cat", "cats", "cot", "oat", "oats", "ta", "tar", "tare", "tat", "tea", "teat", } func compareToBrute(t *testing.T, name string, gen Generator, b *board.Board, d dict, rk rack.Rack, mode Mode) { t.Helper() want := bruteForce(b, plainRulesShared, d, rk, mode) got := genMoves(gen.GenerateMoves(b, rk, mode)) for k, wm := range want { gm, ok := got[k] if !ok { t.Errorf("%s [%s]: %s missing %s (score %d)", name, gen.Name(), gen.Name(), k, wm.Score) continue } if gm.Score != wm.Score { t.Errorf("%s [%s]: %s score %d, want %d", name, gen.Name(), k, gm.Score, wm.Score) } } for k := range got { if _, ok := want[k]; !ok { t.Errorf("%s [%s]: extra move %s", name, gen.Name(), k) } } if len(got) != len(want) { t.Errorf("%s [%s]: %d moves, oracle has %d", name, gen.Name(), len(got), len(want)) } } func mustPlainRules() *rules.Ruleset { eng := rules.English() rs, err := rules.FromTemplate("plain7", eng.Alphabet, eng.Values, eng.Counts, 2, 7, 50, plain7) if err != nil { panic(err) } return rs } var plainRulesShared = mustPlainRules() type scenario struct { name string setup func(*board.Board) rack rack.Rack mode Mode } func genScenarios() []scenario { return []scenario{ {"first move", func(*board.Board) {}, makeRack("cat", 0), Both}, {"first move blank", func(*board.Board) {}, makeRack("ca", 1), Both}, {"extend cat", func(b *board.Board) { placeWord(b, 3, 1, Horizontal, "cat") }, makeRack("srs", 0), Both}, {"cross cat", func(b *board.Board) { placeWord(b, 1, 3, Horizontal, "cat") }, makeRack("aort", 0), Both}, {"only horizontal", func(b *board.Board) { placeWord(b, 3, 1, Horizontal, "cat") }, makeRack("aser", 0), OnlyHorizontal}, {"only vertical", func(b *board.Board) { placeWord(b, 1, 3, Vertical, "cat") }, makeRack("aser", 0), OnlyVertical}, } } func TestDAWGGeneratorVsBruteForce(t *testing.T) { rs := plainRulesShared words := wordlist.Encode(testWords, alphabet.Latin(), 2, 15) f, err := dictdawg.Build(alphabet.Latin(), words) if err != nil { t.Fatal(err) } gen := NewDAWGGenerator(rs, f) d := makeDict(words) for _, c := range genScenarios() { b := board.New(rs.Rows, rs.Cols) c.setup(b) compareToBrute(t, c.name, gen, b, d, c.rack, c.mode) } }