15c7959d96
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.
109 lines
2.9 KiB
Go
109 lines
2.9 KiB
Go
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
|
|
}
|
|
}
|