256999b42c
- Rename module to gitea.iliadenisov.ru/developer/scrabble-solver so it can be consumed as a versioned dependency (no go.work replace / CI clone). - De-internalize wordlist and dictdawg as public packages. - Remove cmd/builddict, dictprep/, the dictionaries submodule and the dawg Makefile: the word-list parsing and DAWG build now live in the separate scrabble-dictionary repository, which publishes the DAWG set as a release artifact. - internal/dict loads the committed dawg/en_sowpods.dawg fixture for cmd/stress. - Update README/CLAUDE docs accordingly.
106 lines
3.0 KiB
Go
106 lines
3.0 KiB
Go
// 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"
|
||
|
||
"gitea.iliadenisov.ru/developer/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
|
||
}
|