Implement Scrabble move generator (DAWG) with English and Russian rules
A Go library that returns every legal play ranked by score and scores or validates plays, using the Appel-Jacobson DAWG algorithm over github.com/iliadenisov/dafsa v1.1.0. - DAWG move generation (across / down / both), full tournament scoring with a per-tile breakdown; public Solver: GenerateMoves (ranked), ScorePlay, ValidatePlay. - Rulesets: English Scrabble, Russian Scrabble, Эрудит (parameterizable Ruleset). - cmd/builddict (build the DAWG from the dictionaries submodule), cmd/stress (self-play benchmark), selfplay engine; brute-force test oracle. - A GADDAG was implemented, benchmarked and removed (the DAWG was smaller and faster for a scoring solver); see RESULTS.md and ALGORITHM.md.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package scrabble
|
||||
|
||||
import (
|
||||
dawg "github.com/iliadenisov/dafsa"
|
||||
|
||||
"scrabble-solver/board"
|
||||
"scrabble-solver/internal/encoding"
|
||||
)
|
||||
|
||||
// letterSet is a bit set over alphabet letter indexes (alphabets are at most 63
|
||||
// letters, so a uint64 suffices). It encodes a square's cross-set: the letters that,
|
||||
// placed on the square, form a legal perpendicular word.
|
||||
type letterSet uint64
|
||||
|
||||
func (s letterSet) has(l byte) bool { return s&(letterSet(1)<<l) != 0 }
|
||||
|
||||
// fullSet is the cross-set of a square with no perpendicular neighbours: every letter
|
||||
// is allowed.
|
||||
func fullSet(size int) letterSet { return letterSet(uint64(1)<<uint(size)) - 1 }
|
||||
|
||||
// columnContext returns the contiguous run of filled cells immediately above and below
|
||||
// the empty square (r, c), each read top to bottom, as alphabet letter indexes. These
|
||||
// are the tiles a perpendicular (vertical) word through (r, c) would include.
|
||||
func columnContext(b *board.Board, r, c int) (above, below []byte) {
|
||||
start := r
|
||||
for start-1 >= 0 && b.Filled(start-1, c) {
|
||||
start--
|
||||
}
|
||||
for rr := start; rr < r; rr++ {
|
||||
above = append(above, encoding.Letter(b.At(rr, c)))
|
||||
}
|
||||
|
||||
end := r
|
||||
for end+1 < b.Rows() && b.Filled(end+1, c) {
|
||||
end++
|
||||
}
|
||||
for rr := r + 1; rr <= end; rr++ {
|
||||
below = append(below, encoding.Letter(b.At(rr, c)))
|
||||
}
|
||||
return above, below
|
||||
}
|
||||
|
||||
// completers returns the letters X (< size) that complete a word when followed from
|
||||
// state: those whose arc leads directly to an accepting node. It is a single arc
|
||||
// enumeration — the deterministic cross-set primitive.
|
||||
func completers(cur *dawg.Cursor, state dawg.Node, size int) letterSet {
|
||||
var set letterSet
|
||||
lim := byte(size)
|
||||
cur.Arcs(state, func(a dawg.Arc) bool {
|
||||
if a.Final && a.Label < lim {
|
||||
set |= letterSet(1) << a.Label
|
||||
}
|
||||
return true
|
||||
})
|
||||
return set
|
||||
}
|
||||
|
||||
// walk follows word left to right from the cursor's root.
|
||||
func walk(cur *dawg.Cursor, word []byte) (dawg.Node, bool) {
|
||||
n := cur.Root()
|
||||
for _, l := range word {
|
||||
var ok bool
|
||||
if n, _, ok = cur.Next(n, l); !ok {
|
||||
return n, false
|
||||
}
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
// dawgCrossSet returns the letters X for which above·X·below is a stored word. A right
|
||||
// extension (no tiles below) is deterministic — X just completes the prefix above. A
|
||||
// left extension (tiles below) is non-deterministic and must probe each X.
|
||||
func dawgCrossSet(cur *dawg.Cursor, above, below []byte, size int) letterSet {
|
||||
switch {
|
||||
case len(above) == 0 && len(below) == 0:
|
||||
return fullSet(size)
|
||||
case len(below) == 0:
|
||||
node, ok := walk(cur, above)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return completers(cur, node, size)
|
||||
default:
|
||||
node := cur.Root()
|
||||
if len(above) > 0 {
|
||||
var ok bool
|
||||
if node, ok = walk(cur, above); !ok {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
var set letterSet
|
||||
for x := range size {
|
||||
m, final, ok := cur.Next(node, byte(x))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, l := range below {
|
||||
if m, final, ok = cur.Next(m, l); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if ok && final {
|
||||
set |= letterSet(1) << uint(x)
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user