// 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 }