fix(engine): EvaluatePlay honors the single-word rule
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m5s

The move preview (EvaluatePlay) validated under standard rules — it called
ValidatePlay without the game's play options — so under the single-word
rule it rejected a play whose only flaw was incidental invalid perpendicular
cross-words, even though SubmitPlay accepts it. The UI gates the submit
button on the preview, so such a play (e.g. КРАН bridging an existing Р on
the test contour) could not be made.

Pass g.playOpts() via ValidatePlayOpts, mirroring Play, so the preview's
legality and score match submission. Robots are unaffected — they search
via GenerateMovesOpts and submit via Play, both already opts-aware — and a
regression test asserts that too.
This commit is contained in:
Ilia Denisov
2026-06-12 11:14:20 +02:00
parent f1e77b5826
commit 5fa51d04d9
3 changed files with 128 additions and 8 deletions
+7 -5
View File
@@ -92,10 +92,12 @@ func (g *Game) SubmitExchange(tiles []string) (MoveRecord, error) {
// EvaluatePlay scores and validates a tentative play without committing it,
// backing the unlimited "what would my next move score, and is it legal?" tool.
// It infers the play's orientation from the tiles and the board exactly as
// SubmitPlay does, then returns the decoded move (placed tiles, the words it
// forms, its orientation and its score) or ErrIllegalPlay when the solver
// rejects it. The board, racks, bag and turn are left untouched.
// It infers the play's orientation from the tiles and the board and applies the
// game's play options exactly as SubmitPlay does, so under the single-word rule
// perpendicular cross-words are ignored: the preview's legality and score then
// match what submitting the play would yield. It returns the decoded move (placed
// tiles, the words it forms, its orientation and its score) or ErrIllegalPlay when
// the solver rejects it. The board, racks, bag and turn are left untouched.
func (g *Game) EvaluatePlay(tiles []TileRecord) (MoveRecord, error) {
if g.over {
return MoveRecord{}, ErrGameOver
@@ -104,7 +106,7 @@ func (g *Game) EvaluatePlay(tiles []TileRecord) (MoveRecord, error) {
if err != nil {
return MoveRecord{}, err
}
move, err := g.solver.ValidatePlay(g.board, resolveDirection(g.board, placements), placements)
move, err := g.solver.ValidatePlayOpts(g.board, resolveDirection(g.board, placements), placements, g.playOpts())
if err != nil {
return MoveRecord{}, fmt.Errorf("%w: %v", ErrIllegalPlay, err)
}