Documentation
¶
Overview ¶
Package movegen contains all the move-generating functions. It makes heavy use of the GADDAG. Implementation notes: - Is the specification in the paper a bit buggy? Basically, if I assume an anchor is the leftmost tile of a word, the way the algorithm works, it will create words blindly. For example, if I have a word FIRE on the board, and I have the letter E on my rack, and I specify F as the anchor, it will create the word EF! (Ignoring the fact that IRE is on the board) You can see this by just stepping through the algorithm. It seems that anchors can only be on the rightmost tile of a word
Package movegen - shadow.go implements the shadow play algorithm for best-first move finding. The algorithm was originally developed in wolges (https://github.com/andy-k/wolges/blob/main/details.txt) and ported to macondo from magpie's implementation.
Shadow computes an upper bound on the score achievable from each anchor square, then uses a max-heap to process anchors in descending order. This allows early termination once we can prove no remaining anchor can beat the best move found so far.
wordmap_gen.go: WMP-based word retrieval for shadow-best-first move generation. Mirrors MAGPIE's wordmap_gen + record_wmp_plays_for_word + record_wmp_play.
Given a WMP-recorded anchor (one with TilesToPlay, PlaythroughBlocks, WordLength, LeftmostStartCol, RightmostStartCol populated), wordmapGen asks the WMP for every blankless word that fits the anchor's (rack subset, word length) tuple, then for each word and start column it:
- Checks the play against the board (cross-sets and playthrough match), filling gen.strip with playthrough markers and unblanked-letter placements.
- Recursively decides which non-playthrough positions are designated blanks vs real tiles, given how many blanks the subrack has.
- Scores each fully-specified play and emits it via gen.playRecorder.
This replaces recursive_gen for WMP anchors. The advantage over recursive_gen is that the WMP gives us the entire word list directly — no GADDAG traversal — and the per-(blocks, tiles) anchor structure avoids the redundant work that the previous integration suffered from.
Index ¶
- Constants
- func AllPlaysRecorder(gen MoveGenerator, rack *tilemapping.Rack, leftstrip, rightstrip int, ...)
- func AllPlaysSmallRecorder(gen MoveGenerator, rack *tilemapping.Rack, leftstrip, rightstrip int, ...)
- func NullPlayRecorder(gen MoveGenerator, a *tilemapping.Rack, leftstrip, rightstrip int, ...)
- func TopNPlayRecorder(gen MoveGenerator, rack *tilemapping.Rack, leftstrip, rightstrip int, ...)
- func TopPlayOnlyRecorder(gen MoveGenerator, rack *tilemapping.Rack, leftstrip, rightstrip int, ...)
- type Anchor
- type AnchorHeap
- type GordonGenerator
- func (gen *GordonGenerator) AtLeastOneTileMove(rack *tilemapping.Rack) bool
- func (gen *GordonGenerator) GADDAG() *kwg.KWG
- func (gen *GordonGenerator) GenAll(rack *tilemapping.Rack, addExchange bool) []*move.Move
- func (gen *GordonGenerator) GenAllWithShadow(rack *tilemapping.Rack, addExchange bool) []*move.Move
- func (gen *GordonGenerator) Plays() []*move.Move
- func (gen *GordonGenerator) RunShadowOnly(rack *tilemapping.Rack) []Anchor
- func (gen *GordonGenerator) RunShadowOnlyWMP(rack *tilemapping.Rack, tilesInBag, oppRackScore int) []Anchor
- func (gen *GordonGenerator) SetEquityCalculators(calcs []equity.EquityCalculator)
- func (gen *GordonGenerator) SetGADDAG(g *kwg.KWG)
- func (gen *GordonGenerator) SetGame(g *game.Game)
- func (gen *GordonGenerator) SetGenPass(p bool)
- func (gen *GordonGenerator) SetMaxCanExchange(m int)
- func (gen *GordonGenerator) SetMaxTileUsage(t int)
- func (gen *GordonGenerator) SetPlayRecorder(pr PlayRecorderFunc)
- func (gen *GordonGenerator) SetPlayRecorderTopPlay()
- func (gen *GordonGenerator) SetRecordNTopPlays(n int)
- func (gen *GordonGenerator) SetSortingParameter(s SortBy)
- func (gen *GordonGenerator) SetWMP(w *wmp.WMP)
- func (gen *GordonGenerator) SetWMPRecordSubAnchors(b bool)
- func (gen *GordonGenerator) SmallPlays() []tinymove.SmallMove
- func (gen *GordonGenerator) WMP() *wmp.WMP
- type MoveGenerator
- type PlayRecorderFunc
- type SortBy
Constants ¶
const DefaultSmallPlayCapacity = 250_000
DefaultSmallPlayCapacity is the pre-allocated capacity for the smallPlays slice on each GordonGenerator. It matches magpie's DEFAULT_SMALL_MOVE_LIST_CAPACITY (config_defs.h:10). Two-blank racks can produce very large move counts, so we allocate generously to avoid any reallocation during move generation.
Variables ¶
This section is empty.
Functions ¶
func AllPlaysRecorder ¶ added in v0.4.9
func AllPlaysRecorder(gen MoveGenerator, rack *tilemapping.Rack, leftstrip, rightstrip int, t move.MoveType, score int)
func AllPlaysSmallRecorder ¶ added in v0.8.7
func AllPlaysSmallRecorder(gen MoveGenerator, rack *tilemapping.Rack, leftstrip, rightstrip int, t move.MoveType, score int)
AllPlaysSmallRecorder is a recorder that records all plays, but as "SmallMove"s, which allocate much less and are smaller overall than a regular move.Move
func NullPlayRecorder ¶ added in v0.4.9
func NullPlayRecorder(gen MoveGenerator, a *tilemapping.Rack, leftstrip, rightstrip int, t move.MoveType, score int)
func TopNPlayRecorder ¶ added in v0.10.2
func TopNPlayRecorder(gen MoveGenerator, rack *tilemapping.Rack, leftstrip, rightstrip int, t move.MoveType, score int)
func TopPlayOnlyRecorder ¶ added in v0.4.9
func TopPlayOnlyRecorder(gen MoveGenerator, rack *tilemapping.Rack, leftstrip, rightstrip int, t move.MoveType, score int)
TopPlayOnlyRecorder is a heavily optimized, ugly function to avoid allocating a lot of moves just to throw them out. It only records the very top move.
Types ¶
type Anchor ¶ added in v0.12.4
type Anchor struct {
HighestPossibleEquity float64
HighestPossibleScore int32
Row uint8
Col uint8
LastAnchorCol uint8
Dir board.BoardDirection
// WMP-only fields. Zero when not produced by the WMP path.
TilesToPlay uint8
PlaythroughBlocks uint8
WordLength uint8
LeftmostStartCol uint8
RightmostStartCol uint8
}
Anchor represents a board anchor with its shadow-computed upper bound.
The WMP-specific fields (TilesToPlay, PlaythroughBlocks, WordLength, LeftmostStartCol, RightmostStartCol) are populated only when WMP is active and the anchor came out of the per-(blocks, tiles) WMP anchor table. They mirror MAGPIE's Anchor extension fields used by wmp_move_gen_add_anchors / wordmap_gen.
Layout-packed: 8 (equity) + 4 (score) + 9 × 1 (uint8 fields) = 21 bytes, padded to 24 with 8-byte alignment. Down from 80 bytes when every field was a Go `int`. Smaller anchors mean cheaper heap operations: heapifyDown swaps cost ~3× less and the heap fits more entries per cache line. The sentinel value for LastAnchorCol is math.MaxUint8 (255) instead of the 100 used previously.
All "small int" fields use uint8 directly (not a typed alias) so callers don't need ad-hoc conversions every time they read one. Score uses int32 because a 7-tile bingo on triple-triples can exceed 255 but is bounded well below int16's max.
type AnchorHeap ¶ added in v0.12.4
type AnchorHeap struct {
// contains filtered or unexported fields
}
AnchorHeap is a max-heap of anchors ordered by highestPossibleEquity.
type GordonGenerator ¶ added in v0.2.0
type GordonGenerator struct {
// contains filtered or unexported fields
}
GordonGenerator is the main move generation struct. It implements Steven A. Gordon's algorithm from his paper "A faster Scrabble Move Generation Algorithm"
func NewGordonGenerator ¶ added in v0.2.0
func NewGordonGenerator(gd gaddag.WordGraph, board *board.GameBoard, ld *tilemapping.LetterDistribution) *GordonGenerator
NewGordonGenerator returns a Gordon move generator.
func (*GordonGenerator) AtLeastOneTileMove ¶ added in v0.7.0
func (gen *GordonGenerator) AtLeastOneTileMove(rack *tilemapping.Rack) bool
AtLeastOneTileMove generates moves. We don't care what they are; return true if there is at least one move that plays tiles, false otherwise.
func (*GordonGenerator) GADDAG ¶ added in v0.7.0
func (gen *GordonGenerator) GADDAG() *kwg.KWG
func (*GordonGenerator) GenAll ¶ added in v0.2.0
func (gen *GordonGenerator) GenAll(rack *tilemapping.Rack, addExchange bool) []*move.Move
GenAll generates all moves on the board. It assumes anchors have already been updated, as well as cross-sets / cross-scores. When shadow is enabled (via SetPlayRecorderTopPlay or SetRecordNTopPlays), GenAll uses the shadow algorithm for best-first move finding, which allows early termination when looking for only the top move(s). Shadow can be disabled for endgame where all moves must be found without the overhead of computing upper bounds.
func (*GordonGenerator) GenAllWithShadow ¶ added in v0.12.4
func (gen *GordonGenerator) GenAllWithShadow(rack *tilemapping.Rack, addExchange bool) []*move.Move
GenAllWithShadow generates all moves using shadow for best-first ordering. Only useful when we need the best move (not all moves).
func (*GordonGenerator) Plays ¶ added in v0.2.0
func (gen *GordonGenerator) Plays() []*move.Move
Plays returns the generator's generated plays.
func (*GordonGenerator) RunShadowOnly ¶ added in v0.12.4
func (gen *GordonGenerator) RunShadowOnly(rack *tilemapping.Rack) []Anchor
RunShadowOnly runs the shadow pass and returns anchors sorted by descending highest possible score. For testing.
func (*GordonGenerator) RunShadowOnlyWMP ¶ added in v0.13.0
func (gen *GordonGenerator) RunShadowOnlyWMP(rack *tilemapping.Rack, tilesInBag, oppRackScore int) []Anchor
RunShadowOnlyWMP runs the shadow pass with full WMP (and leave map) initialization, mirroring the slice of GenAllWithShadow that comes before recursiveGen. Returns the anchors in descending equity order (the same order MAGPIE's extract_sorted_anchors_for_test produces).
tilesInBag and oppRackScore are stamped onto the generator so shadow_record's equity branch matches the MAGPIE shadow tests. For score-only assertions (sortingParameter == SortByScore) the exact tilesInBag value doesn't matter as long as it is > 0, since shadow.bestLeaves remains zero with no equity calculator set.
Intended for tests that want to inspect anchor output without running the full recursive_gen pipeline.
func (*GordonGenerator) SetEquityCalculators ¶ added in v0.5.0
func (gen *GordonGenerator) SetEquityCalculators(calcs []equity.EquityCalculator)
func (*GordonGenerator) SetGADDAG ¶ added in v0.13.0
func (gen *GordonGenerator) SetGADDAG(g *kwg.KWG)
SetGADDAG replaces the underlying word graph used for move generation. Used by the endgame solver to swap in a pruned KWG for the duration of a solve and restore the original afterwards.
func (*GordonGenerator) SetGame ¶ added in v0.4.9
func (gen *GordonGenerator) SetGame(g *game.Game)
func (*GordonGenerator) SetGenPass ¶ added in v0.7.0
func (gen *GordonGenerator) SetGenPass(p bool)
func (*GordonGenerator) SetMaxCanExchange ¶ added in v0.9.9
func (gen *GordonGenerator) SetMaxCanExchange(m int)
func (*GordonGenerator) SetMaxTileUsage ¶ added in v0.8.0
func (gen *GordonGenerator) SetMaxTileUsage(t int)
func (*GordonGenerator) SetPlayRecorder ¶ added in v0.4.9
func (gen *GordonGenerator) SetPlayRecorder(pr PlayRecorderFunc)
func (*GordonGenerator) SetPlayRecorderTopPlay ¶ added in v0.12.4
func (gen *GordonGenerator) SetPlayRecorderTopPlay()
SetPlayRecorderTopPlay sets the play recorder to TopPlayOnlyRecorder with shadow enabled for best-first move finding.
func (*GordonGenerator) SetRecordNTopPlays ¶ added in v0.10.2
func (gen *GordonGenerator) SetRecordNTopPlays(n int)
func (*GordonGenerator) SetSortingParameter ¶ added in v0.2.0
func (gen *GordonGenerator) SetSortingParameter(s SortBy)
SetSortingParameter tells the play sorter to sort by score, equity, or perhaps other things. This is useful for the endgame solver, which does not care about equity.
func (*GordonGenerator) SetWMP ¶ added in v0.13.0
func (gen *GordonGenerator) SetWMP(w *wmp.WMP)
SetWMP enables WMP-aware move generation. Pass nil to disable. When set, shadow play uses the WMP existence checks to tighten the highestShadowEquity bound for each anchor square AND records per-(playthrough_blocks, tiles_to_play) sub-anchors that are processed by wordmapGen (instead of recursive_gen) during the best-first scoring pass. Mirrors MAGPIE's player_set_wmp + the wmp_move_gen_init call inside gen_load_position.
func (*GordonGenerator) SetWMPRecordSubAnchors ¶ added in v0.13.0
func (gen *GordonGenerator) SetWMPRecordSubAnchors(b bool)
SetWMPRecordSubAnchors lets callers override the default sub-anchor recording behavior. SetWMP turns this on automatically; the override is mostly for benchmarks that want to compare the shadow-only-gating variant against the full WMP integration.
func (*GordonGenerator) SmallPlays ¶ added in v0.8.7
func (gen *GordonGenerator) SmallPlays() []tinymove.SmallMove
SmallPlays returns the generator's generated SmallPlays
func (*GordonGenerator) WMP ¶ added in v0.13.0
func (gen *GordonGenerator) WMP() *wmp.WMP
WMP returns the WMP currently in use, or nil if WMP is disabled.
type MoveGenerator ¶ added in v0.2.11
type MoveGenerator interface {
GenAll(rack *tilemapping.Rack, addExchange bool) []*move.Move
SetMaxCanExchange(int)
SetSortingParameter(s SortBy)
Plays() []*move.Move
SmallPlays() []tinymove.SmallMove
SetPlayRecorder(pf PlayRecorderFunc)
SetEquityCalculators([]equity.EquityCalculator)
AtLeastOneTileMove(rack *tilemapping.Rack) bool
SetMaxTileUsage(int)
SetGenPass(bool)
}
MoveGenerator is a generic interface for generating moves.
type PlayRecorderFunc ¶ added in v0.4.9
type PlayRecorderFunc func(MoveGenerator, *tilemapping.Rack, int, int, move.MoveType, int)