package moves import ( "math/rand" "os" "strings" "testing" "gitea.iliadenisov.ru/developer/scrabble-solver/board" "gitea.iliadenisov.ru/developer/scrabble-solver/rules" "gitea.iliadenisov.ru/developer/scrabble-solver/scrabble" "scrabble/loadtest/internal/edge" ) // TestReplayBoardMatchesParse checks that replaying decoded history reproduces the // exact board (positions, letters and blank flags) that board.Parse builds from the // equivalent text grid, and that non-play records are ignored. func TestReplayBoardMatchesParse(t *testing.T) { rs := rules.English() history := []edge.Move{ {Action: "pass"}, // must be ignored {Action: "play", Tiles: []edge.Tile{ {Row: 7, Col: 7, Letter: "c"}, {Row: 7, Col: 8, Letter: "a"}, {Row: 7, Col: 9, Letter: "t"}, }}, {Action: "play", Tiles: []edge.Tile{ {Row: 7, Col: 10, Letter: "s", Blank: true}, // a blank standing for s }}, } got, err := replayBoard(rs, history) if err != nil { t.Fatalf("replayBoard: %v", err) } rows := make([]string, rs.Rows) for i := range rows { rows[i] = strings.Repeat(".", rs.Cols) } // row 7: cols 0-6 empty, cat at 7-9, an uppercase S (blank) at 10. rows[7] = strings.Repeat(".", 7) + "cat" + "S" + strings.Repeat(".", rs.Cols-11) want, err := board.Parse(rows, rs.Alphabet) if err != nil { t.Fatalf("board.Parse: %v", err) } for r := 0; r < rs.Rows; r++ { for c := 0; c < rs.Cols; c++ { if got.At(r, c) != want.At(r, c) { t.Fatalf("cell (%d,%d): replay = %#x, parse = %#x", r, c, got.At(r, c), want.At(r, c)) } } } } // TestBuildRack checks the alphabet-index rack (255 a blank) is reconstructed faithfully. func TestBuildRack(t *testing.T) { rs := rules.English() rk := buildRack(rs, []byte{0, 0, 2, blankIndex}) // a a c blank if rk.Count(0) != 2 { t.Errorf("count(a) = %d, want 2", rk.Count(0)) } if rk.Count(2) != 1 { t.Errorf("count(c) = %d, want 1", rk.Count(2)) } if rk.Blanks() != 1 { t.Errorf("blanks = %d, want 1", rk.Blanks()) } if rk.Total() != 4 { t.Errorf("total = %d, want 4", rk.Total()) } } // TestMidRanked checks the pick always lands in the middle third of a ranked list and // that tiny lists yield their lowest-scoring move. func TestMidRanked(t *testing.T) { ms := make([]scrabble.Move, 9) // scores 100..92, index i has score 100-i for i := range ms { ms[i] = scrabble.Move{Score: 100 - i} } rng := rand.New(rand.NewSource(1)) for n := 0; n < 100; n++ { idx := 100 - midRanked(ms, rng).Score // recover the index from the score if idx < 3 || idx >= 6 { t.Fatalf("picked index %d outside middle third [3,6)", idx) } } if got := midRanked([]scrabble.Move{{Score: 5}}, rng).Score; got != 5 { t.Errorf("n=1 pick score = %d, want 5", got) } if got := midRanked([]scrabble.Move{{Score: 9}, {Score: 4}}, rng).Score; got != 4 { t.Errorf("n=2 pick score = %d, want 4 (lower-scoring)", got) } } // TestToPlayTiles checks the solver-placement to edge-tile mapping, including blanks. func TestToPlayTiles(t *testing.T) { tiles := toPlayTiles([]scrabble.Placement{ {Row: 1, Col: 2, Letter: 5}, {Row: 1, Col: 3, Letter: 255, Blank: true}, }) want := []edge.PlayTile{ {Row: 1, Col: 2, Letter: 5}, {Row: 1, Col: 3, Letter: 255, Blank: true}, } if len(tiles) != len(want) { t.Fatalf("len = %d, want %d", len(tiles), len(want)) } for i := range want { if tiles[i] != want[i] { t.Errorf("tile %d = %+v, want %+v", i, tiles[i], want[i]) } } } // TestPickUnknownVariant rejects a variant the registry does not hold. func TestPickUnknownVariant(t *testing.T) { reg := &Registry{engines: map[string]*engine{}} if _, err := reg.Pick("nope", nil, nil, 0, rand.New(rand.NewSource(1))); err == nil { t.Fatal("want error for an unknown variant") } } // TestPickWithDawg drives the full path against the committed DAWGs when they are // available (BACKEND_DICT_DIR, as the engine tests use); it generates a first-move // play from a productive rack. func TestPickWithDawg(t *testing.T) { dir := os.Getenv("BACKEND_DICT_DIR") if dir == "" { t.Skip("BACKEND_DICT_DIR not set; skipping DAWG-backed test") } reg, err := Open(dir) if err != nil { t.Fatalf("Open(%s): %v", dir, err) } defer reg.Close() rng := rand.New(rand.NewSource(1)) rack := []byte{2, 0, 19, 18, 4, 17, 13} // c a t s e r n — a productive English rack act, err := reg.Pick("scrabble_en", nil, rack, 90, rng) if err != nil { t.Fatalf("Pick: %v", err) } switch act.Kind { case "play": if len(act.Tiles) == 0 { t.Error("play action has no tiles") } if act.Dir != "H" && act.Dir != "V" { t.Errorf("dir = %q, want H or V", act.Dir) } case "exchange", "pass": // acceptable when the rack has no legal first move default: t.Errorf("unexpected action kind %q", act.Kind) } }