Files
scrabble-solver/scrabble/move.go
T
Ilia Denisov d70bd35470 scrabble: single-word rule requires in-line connection
Under PlayOptions.IgnoreCrossWords a play connected to the board through any
perpendicular contact, even one forming a non-word, because connected() reused
the standard rule's touchesPerpendicular test while cross-words were suppressed.
That let an all-new word merely abut the board sideways and be accepted, and the
generator, with relaxed cross-sets, offered such plays.

Require the main word to run through an existing tile along the play direction
when cross-words are ignored, and filter generated moves by the same predicate so
generation agrees with ValidatePlayOpts. Standard rules are unchanged.
2026-06-14 14:38:49 +02:00

87 lines
3.1 KiB
Go

// Package scrabble is the public library: it builds a move generator over a dictionary
// and a ruleset, generates every legal play for a position ranked by score, and scores
// or validates arbitrary plays. The generator is the DAWG algorithm (Appel-Jacobson).
package scrabble
// Direction is the orientation of a play's main word.
type Direction uint8
const (
// Horizontal is an across play (left to right along a row).
Horizontal Direction = iota
// Vertical is a down play (top to bottom along a column).
Vertical
)
// String renders the direction for diagnostics.
func (d Direction) String() string {
if d == Vertical {
return "vertical"
}
return "horizontal"
}
// Mode selects which orientations GenerateMoves produces. Russian "Эрудит" requires a
// single orientation per turn, which OnlyHorizontal / OnlyVertical express.
type Mode uint8
const (
// Both generates across plays (on the board) and down plays (on its transpose).
Both Mode = iota
// OnlyHorizontal generates across plays only.
OnlyHorizontal
// OnlyVertical generates down plays only.
OnlyVertical
)
// Includes reports whether the mode produces plays in direction d.
func (m Mode) Includes(d Direction) bool {
switch m {
case Both:
return true
case OnlyHorizontal:
return d == Horizontal
case OnlyVertical:
return d == Vertical
}
return false
}
// Placement is a single newly-placed tile.
type Placement struct {
Row, Col int
Letter byte // alphabet letter index
Blank bool // placed from a blank tile, so it scores 0
}
// Word is a word formed by a play, with its location and score.
type Word struct {
Row, Col int // square of the word's first letter
Dir Direction // orientation of the word
Letters []byte // alphabet indices of the whole word (existing + new tiles)
Blanks []bool // per letter: true if that tile is a blank (scores 0)
Score int // the word's score, with premiums from newly-placed tiles
}
// Move is a complete legal play with a full scoring breakdown.
type Move struct {
Dir Direction // orientation of the main word
Tiles []Placement // the newly-placed tiles, in main-word order
Main Word // the main word formed along Dir
Cross []Word // perpendicular words formed by the new tiles
Bonus int // all-tiles (bingo) bonus included in Score, or 0
Score int // total: Main.Score + Σ Cross.Score + Bonus
}
// PlayOptions tunes optional rule variations applied when evaluating, validating or
// generating plays. The zero value is standard Scrabble.
type PlayOptions struct {
// IgnoreCrossWords makes a play's perpendicular cross-words irrelevant: they are not
// formed, validated, scored or used to constrain move generation. It expresses the
// "single word per turn" house rule, under which only the main word along the play
// direction must be a valid word. That main word must still connect by running through
// an existing tile along its line — a tile that only abuts the board perpendicular to
// that line does not connect — and the first-move centre rule still applies.
IgnoreCrossWords bool
}