fix(engine): single-word rule connects along the play line
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m11s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m11s
Bumps the engine to scrabble-solver v1.1.1, where a single-word-per-turn play must form its word along its own line through an existing tile: a multi-tile play that touches the board only perpendicular to itself (the contour РЮМ/КЕД/ОР cases) no longer connects. For a single tile that abuts the board on both axes the engine now plays the higher-scoring legal orientation instead of the geometrically longer one (playDirection), so a real word is never rejected in favour of a non-word. Reworks the single-word solver/engine tests for the corrected rule (no longer a superset of standard play) and updates ARCHITECTURE/FUNCTIONAL/PRERELEASE.
This commit is contained in:
@@ -62,7 +62,7 @@ func (g *Game) SubmitPlay(tiles []TileRecord) (MoveRecord, error) {
|
||||
if err != nil {
|
||||
return MoveRecord{}, err
|
||||
}
|
||||
return g.Play(resolveDirection(g.board, placements), placements)
|
||||
return g.Play(g.playDirection(placements), placements)
|
||||
}
|
||||
|
||||
// SubmitPlayDir is SubmitPlay with the orientation supplied rather than inferred.
|
||||
@@ -106,7 +106,7 @@ func (g *Game) EvaluatePlay(tiles []TileRecord) (MoveRecord, error) {
|
||||
if err != nil {
|
||||
return MoveRecord{}, err
|
||||
}
|
||||
move, err := g.solver.ValidatePlayOpts(g.board, resolveDirection(g.board, placements), placements, g.playOpts())
|
||||
move, err := g.solver.ValidatePlayOpts(g.board, g.playDirection(placements), placements, g.playOpts())
|
||||
if err != nil {
|
||||
return MoveRecord{}, fmt.Errorf("%w: %v", ErrIllegalPlay, err)
|
||||
}
|
||||
@@ -169,6 +169,33 @@ func (g *Game) placements(tiles []TileRecord) ([]scrabble.Placement, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// playDirection resolves the orientation for a live play. resolveDirection infers it from
|
||||
// geometry alone, preferring the longer word when a single tile abuts the board on both
|
||||
// axes; under the single-word rule that can pick an orientation whose word is not in the
|
||||
// dictionary while the other orientation's is. So for a single tile under that rule the
|
||||
// engine tries both orientations through the solver and keeps the higher-scoring legal one
|
||||
// (horizontal breaks a tie). Multi-tile plays, and every play under the standard rule, keep
|
||||
// the geometric resolution: a multi-tile play's orientation is fixed by the line its tiles
|
||||
// share, and under the standard rule every word the play forms must be valid regardless of
|
||||
// which one is named the main word.
|
||||
func (g *Game) playDirection(placements []scrabble.Placement) scrabble.Direction {
|
||||
geo := resolveDirection(g.board, placements)
|
||||
if len(placements) != 1 || g.multipleWords {
|
||||
return geo
|
||||
}
|
||||
best, found, bestScore := geo, false, 0
|
||||
for _, dir := range [...]scrabble.Direction{scrabble.Horizontal, scrabble.Vertical} {
|
||||
m, err := g.solver.ValidatePlayOpts(g.board, dir, placements, g.playOpts())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !found || m.Score > bestScore {
|
||||
best, found, bestScore = dir, true, m.Score
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// encodeTiles encodes decoded exchange tiles ("?" for a blank, otherwise a
|
||||
// concrete letter) into the internal byte form, wrapping a bad letter as
|
||||
// ErrTilesNotOnRack (the caller cannot hold a tile it cannot name).
|
||||
|
||||
Reference in New Issue
Block a user