Files
scrabble-solver/board/board.go
T
Ilia Denisov 15c7959d96 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.
2026-06-01 16:07:32 +02:00

106 lines
3.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package board holds the compact game board: a row-major grid of cell bytes encoded
// per internal/encoding (0 = empty, letter+1, with 0x80 marking a blank). It is
// otherwise alphabet-agnostic.
package board
import (
"fmt"
"unicode"
"github.com/iliadenisov/alphabet"
"scrabble-solver/internal/encoding"
)
// Board is a row-major grid of encoded cells.
type Board struct {
rows, cols int
cells []byte
}
// New returns an empty rows×cols board.
func New(rows, cols int) *Board {
return &Board{rows: rows, cols: cols, cells: make([]byte, rows*cols)}
}
// Rows returns the number of rows.
func (b *Board) Rows() int { return b.rows }
// Cols returns the number of columns.
func (b *Board) Cols() int { return b.cols }
// At returns the encoded cell at (r, c).
func (b *Board) At(r, c int) byte { return b.cells[r*b.cols+c] }
// Set stores the encoded cell v at (r, c).
func (b *Board) Set(r, c int, v byte) { b.cells[r*b.cols+c] = v }
// InBounds reports whether (r, c) is on the board.
func (b *Board) InBounds(r, c int) bool {
return r >= 0 && r < b.rows && c >= 0 && c < b.cols
}
// Empty reports whether (r, c) is an empty square.
func (b *Board) Empty(r, c int) bool { return encoding.IsEmpty(b.cells[r*b.cols+c]) }
// Filled reports whether (r, c) is on the board and occupied.
func (b *Board) Filled(r, c int) bool {
return b.InBounds(r, c) && !encoding.IsEmpty(b.cells[r*b.cols+c])
}
// IsEmpty reports whether the whole board is empty (used for the first move).
func (b *Board) IsEmpty() bool {
for _, c := range b.cells {
if !encoding.IsEmpty(c) {
return false
}
}
return true
}
// Clone returns a deep copy of the board.
func (b *Board) Clone() *Board {
cp := &Board{rows: b.rows, cols: b.cols, cells: make([]byte, len(b.cells))}
copy(cp.cells, b.cells)
return cp
}
// Transpose returns a new board with rows and columns swapped, turning vertical lines
// into horizontal ones. Down-play generation runs on the transpose.
func (b *Board) Transpose() *Board {
t := &Board{rows: b.cols, cols: b.rows, cells: make([]byte, len(b.cells))}
for r := range b.rows {
for c := range b.cols {
t.cells[c*t.cols+r] = b.cells[r*b.cols+c]
}
}
return t
}
// Parse builds a board from text rows: '.' (or space) is an empty square, a lowercase
// letter is a normal tile, and an uppercase letter is a blank standing for that letter.
// Letters are resolved through idx.
func Parse(rows []string, idx alphabet.Indexer) (*Board, error) {
if len(rows) == 0 {
return nil, fmt.Errorf("board: no rows")
}
cols := len([]rune(rows[0]))
b := New(len(rows), cols)
for r, line := range rows {
runes := []rune(line)
for c := 0; c < cols && c < len(runes); c++ {
ch := runes[c]
if ch == '.' || ch == ' ' {
continue
}
blank := unicode.IsUpper(ch)
li, err := idx.Index(string(unicode.ToLower(ch)))
if err != nil {
return nil, fmt.Errorf("board: row %d col %d %q: %w", r, c, string(ch), err)
}
b.Set(r, c, encoding.Cell(li, blank))
}
}
return b, nil
}