clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

chore/regenerate-vhs-gifs
puzzletea / nurikabe / grid.go
grid.go 3.9K
1package nurikabe23import (4 "fmt"5 "strconv"6 "strings"7)89type cellState rune1011const (12 unknownCell cellState = '?'13 seaCell cellState = '~'14 islandCell cellState = 'o'15 legacyRequiredLand cellState = '!'16)1718type (19 grid [][]cellState20 clueGrid [][]int21)2223type Puzzle struct {24 Width, Height int25 Clues clueGrid26}2728func newGrid(width, height int, fill cellState) grid {29 g := make(grid, height)30 for y := range height {31 g[y] = make([]cellState, width)32 for x := range width {33 g[y][x] = fill34 }35 }36 return g37}3839func (g grid) clone() grid {40 c := make(grid, len(g))41 for y, row := range g {42 c[y] = make([]cellState, len(row))43 copy(c[y], row)44 }45 return c46}4748func (g grid) String() string {49 rows := make([]string, len(g))50 for y, row := range g {51 var b strings.Builder52 b.Grow(len(row))53 for _, c := range row {54 b.WriteRune(rune(c))55 }56 rows[y] = b.String()57 }58 return strings.Join(rows, "\n")59}6061func parseGrid(s string, width, height int) (grid, error) {62 rows := strings.Split(s, "\n")63 if len(rows) != height {64 return nil, fmt.Errorf("grid row count mismatch: got %d, want %d", len(rows), height)65 }6667 g := make(grid, height)68 for y := range height {69 runes := []rune(rows[y])70 if len(runes) != width {71 return nil, fmt.Errorf("grid width mismatch at row %d: got %d, want %d", y, len(runes), width)72 }73 g[y] = make([]cellState, width)74 for x, r := range runes {75 switch cellState(r) {76 case unknownCell, seaCell, islandCell:77 g[y][x] = cellState(r)78 case legacyRequiredLand:79 // Backward compatibility: old required marks now map to island.80 g[y][x] = islandCell81 default:82 return nil, fmt.Errorf("invalid grid symbol %q at (%d,%d)", r, x, y)83 }84 }85 }8687 return g, nil88}8990func serializeClues(clues clueGrid) string {91 rows := make([]string, len(clues))92 for y, row := range clues {93 parts := make([]string, len(row))94 for x, v := range row {95 if v < 0 {96 v = 097 }98 parts[x] = strconv.Itoa(v)99 }100 rows[y] = strings.Join(parts, ",")101 }102 return strings.Join(rows, "\n")103}104105func parseClues(s string, width, height int) (clueGrid, error) {106 rows := strings.Split(strings.TrimSpace(s), "\n")107 if height == 0 || width == 0 {108 return nil, fmt.Errorf("invalid clue dimensions: %dx%d", width, height)109 }110 if len(rows) != height {111 return nil, fmt.Errorf("clue row count mismatch: got %d, want %d", len(rows), height)112 }113114 clues := make(clueGrid, height)115 for y := range height {116 parts := strings.Split(rows[y], ",")117 if len(parts) != width {118 return nil, fmt.Errorf("clue width mismatch at row %d: got %d, want %d", y, len(parts), width)119 }120 clues[y] = make([]int, width)121 for x, p := range parts {122 v, err := strconv.Atoi(strings.TrimSpace(p))123 if err != nil {124 return nil, fmt.Errorf("invalid clue value %q at (%d,%d): %w", p, x, y, err)125 }126 if v < 0 {127 return nil, fmt.Errorf("negative clue value %d at (%d,%d)", v, x, y)128 }129 clues[y][x] = v130 }131 }132133 return clues, nil134}135136func cloneClues(c clueGrid) clueGrid {137 out := make(clueGrid, len(c))138 for y, row := range c {139 out[y] = make([]int, len(row))140 copy(out[y], row)141 }142 return out143}144145func validateClues(clues clueGrid, width, height int) error {146 if width <= 0 || height <= 0 {147 return fmt.Errorf("invalid dimensions: %dx%d", width, height)148 }149 if len(clues) != height {150 return fmt.Errorf("clue row count mismatch: got %d, want %d", len(clues), height)151 }152 for y, row := range clues {153 if len(row) != width {154 return fmt.Errorf("clue width mismatch at row %d: got %d, want %d", y, len(row), width)155 }156 for x, v := range row {157 if v < 0 {158 return fmt.Errorf("negative clue at (%d,%d)", x, y)159 }160 }161 }162 return nil163}164165func isClueCell(clues clueGrid, x, y int) bool {166 return y >= 0 && y < len(clues) && x >= 0 && x < len(clues[y]) && clues[y][x] > 0167}168169func countClues(clues clueGrid) int {170 total := 0171 for _, row := range clues {172 for _, v := range row {173 if v > 0 {174 total++175 }176 }177 }178 return total179}