a8a559993d
Add an optional "single word per turn" rule: only the main word along the
play direction is validated and scored; perpendicular cross-words are ignored
— not formed, validated, scored, or used to constrain move generation. The
zero PlayOptions stays standard Scrabble, so existing callers are unchanged.
- PlayOptions{IgnoreCrossWords} threaded through new EvaluateOpts,
Solver.{ScorePlay,ValidatePlay,GenerateMoves}Opts and the DAWG generator
(relaxed cross-sets via fullSet, main-word-only scoring).
- connected() tests perpendicular adjacency directly instead of via
Move.Cross, so an all-new main word touching the board only sideways still
connects when cross-words are suppressed (behaviour-preserving for standard
play).
- Tests: focused corner-case suite (solver_opts_test.go) and a single-word
GCG fixture; the 17 real-game GCG fixtures stay green as the standard-rules
regression guard.
86 lines
2.9 KiB
Go
86 lines
2.9 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; board connectivity and the first-move centre rule
|
|
// still apply.
|
|
IgnoreCrossWords bool
|
|
}
|