scrabble: add single-word-per-turn rule via PlayOptions
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.
This commit is contained in:
+29
-13
@@ -24,9 +24,18 @@ func NewDAWGGenerator(rs *rules.Ruleset, finder dawg.Finder) *DAWGGenerator {
|
||||
// Name identifies the generator.
|
||||
func (g *DAWGGenerator) Name() string { return "dawg" }
|
||||
|
||||
// GenerateMoves returns every legal play for rk on b in the modes' orientations.
|
||||
// GenerateMoves returns every legal play for rk on b in the modes' orientations under
|
||||
// standard rules. It satisfies the Generator interface; GenerateMovesOpts adds optional
|
||||
// rule variations.
|
||||
func (g *DAWGGenerator) GenerateMoves(b *board.Board, rk rack.Rack, mode Mode) []Move {
|
||||
return generateBoth(b, g.rules, rk, mode, g.runAcross)
|
||||
return g.GenerateMovesOpts(b, rk, mode, PlayOptions{})
|
||||
}
|
||||
|
||||
// GenerateMovesOpts is GenerateMoves with optional rule variations (PlayOptions). With
|
||||
// opts.IgnoreCrossWords the cross-sets are unconstrained (every letter that extends the
|
||||
// main word is allowed) and each move is scored by its main word only.
|
||||
func (g *DAWGGenerator) GenerateMovesOpts(b *board.Board, rk rack.Rack, mode Mode, opts PlayOptions) []Move {
|
||||
return generateBoth(b, g.rules, rk, mode, opts, g.runAcross)
|
||||
}
|
||||
|
||||
// tileInfo is a tentatively placed left-part tile (its column is fixed only once the
|
||||
@@ -52,24 +61,31 @@ type acrossGen struct {
|
||||
}
|
||||
|
||||
// runAcross generates all across plays on bd (cross-sets are computed as vertical words
|
||||
// on bd) and reports each via emit in bd's coordinates.
|
||||
func (g *DAWGGenerator) runAcross(bd *board.Board, rk rack.Rack, emit func([]Placement)) {
|
||||
// on bd) and reports each via emit in bd's coordinates. With opts.IgnoreCrossWords the
|
||||
// cross-sets are unconstrained, so any letter that extends the main word may be placed.
|
||||
func (g *DAWGGenerator) runAcross(bd *board.Board, rk rack.Rack, opts PlayOptions, emit func([]Placement)) {
|
||||
cur, err := dawg.NewCursor(g.finder)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
size := g.rules.Size()
|
||||
|
||||
cross := make([]letterSet, bd.Rows()*bd.Cols())
|
||||
known := make([]bool, bd.Rows()*bd.Cols())
|
||||
crossFn := func(r, c int) letterSet {
|
||||
i := r*bd.Cols() + c
|
||||
if !known[i] {
|
||||
above, below := columnContext(bd, r, c)
|
||||
cross[i] = dawgCrossSet(cur, above, below, size)
|
||||
known[i] = true
|
||||
var crossFn func(r, c int) letterSet
|
||||
if opts.IgnoreCrossWords {
|
||||
full := fullSet(size)
|
||||
crossFn = func(int, int) letterSet { return full }
|
||||
} else {
|
||||
cross := make([]letterSet, bd.Rows()*bd.Cols())
|
||||
known := make([]bool, bd.Rows()*bd.Cols())
|
||||
crossFn = func(r, c int) letterSet {
|
||||
i := r*bd.Cols() + c
|
||||
if !known[i] {
|
||||
above, below := columnContext(bd, r, c)
|
||||
cross[i] = dawgCrossSet(cur, above, below, size)
|
||||
known[i] = true
|
||||
}
|
||||
return cross[i]
|
||||
}
|
||||
return cross[i]
|
||||
}
|
||||
|
||||
ag := &acrossGen{bd: bd, cur: cur, rs: g.rules, rk: rk, size: size, cross: crossFn, emit: emit}
|
||||
|
||||
Reference in New Issue
Block a user