Files
scrabble-solver/board/board.go
T
Ilia Denisov 256999b42c Publish as versioned Gitea module; move dictionary pipeline out
- 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.
2026-06-04 19:11:46 +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"
"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
}