- Bug Fix: Fixed cell.String() in sudoku/cell.go swapping row/column labels (c.x, c.y → c.y, c.x).
- Dead Code Removal: Removed unused GetTomography() from nonogram/Export.go and dead isl1/isl2 lookups with _ = isl1; _ = isl2 suppressions in hashiwokakero/generator.go.
- Removed Solved fields all 6 Export.go files.
- math/rand/v2 Migration: Updated 4 files from deprecated
math/rand to math/rand/v2, changing Intn → IntN.
- Error Wrapping: Changed %v → %w in hashiwokakero/Export.go
and sudoku/Export.go.
- Compile-time Assertions: Added var _ game.Gamer = Model{} to
nonogram, wordsearch, sudoku, and hashiwokakero Model.go.
- var → const: Extracted cellWidth/cellHeight integer values
from var blocks to const declarations in lightsout, takuzu, and nonogram style.go.
- Package-level Colors: Extracted inline
lipgloss.AdaptiveColor allocations to package-level solvedBG vars in nonogram and takuzu style.go.
- Sudoku O(1) Lookup: Added providedGrid [9][9]bool field,
replaced all slices.Contains(m.provided, c) with O(1) array lookups, simplified IsProvidedCell().
- Single IsSolved per Frame: Sudoku View() now computes
conflicts and solved state once and passes them to renderGrid. Hashiwokakero gridView now accepts solved bool
parameter.
- Hashiwokakero Optimizations: FindIslandByID uses direct
slice index, FindIslandAt uses lazy posIndex map, BridgeCount uses lazy bridgeCounts array, IsConnected builds
adjacency list for BFS.
- Wordsearch Render: Added foundCells [][]bool for O(1)
found-word cell lookup instead of per-cell O(words * wordLen) scan.- commit
- 43c1aae5392a4dfeef357878bfd053c83d4c6326
- parent
- df4f76118ee1caab11189a8fba0046b472117b70
- parent
- 4ee50dedbb9e93ab24efdb0192e806587640b1dd
- author
- FelineStateMachine <dami.etoile@gmail.com>
| M | hashiwokakero/ | 4 | +- |
| M | hashiwokakero/ | 6 | ++- |
| M | hashiwokakero/ | 14 | ++--- |
| M | hashiwokakero/ | 80 | +++++++++++++++++++++--------- |
| M | hashiwokakero/ | 3 | +- |
| M | lightsout/ | 2 | - |
| M | lightsout/ | 2 | +- |
| M | lightsout/ | 6 | +- |
| M | lightsout/ | 2 | + |
| M | model.go | 2 | +- |
| M | nonogram/ | 8 | +--- |
| M | nonogram/ | 6 | ++- |
| M | nonogram/ | 7 | ++- |
| M | sudoku/ | 11 | ++-- |
| M | sudoku/ | 34 | +++++++++---- |
| M | sudoku/ | 8 | ++- |
| M | sudoku/ | 8 | +--- |
| M | sudoku/ | 8 | +-- |
| M | takuzu/ | 2 | - |
| M | takuzu/ | 2 | +- |
| M | takuzu/ | 7 | ++- |
| M | wordsearch/ | 1 | + |
| M | wordsearch/ | 38 | +++++++++++--- |
| M | wordsearch/ | 10 | ++-- |
| M | wordsearch/ | 8 | +-- |
@@ -6,7 +6,6 @@ import (
66 )77 88 type Save struct {9- Solved bool `json:"solved"`109 Width int `json:"width"`1110 Height int `json:"height"`1211 Islands []Island `json:"islands"`@@ -38,7 +37,6 @@ func ImportModel(data []byte) (*Model, error) {
3837 3938 func (m Model) GetSave() ([]byte, error) {4039 save := Save{41- Solved: m.puzzle.IsSolved(),4240 Width: m.puzzle.Width,4341 Height: m.puzzle.Height,4442 Islands: m.puzzle.Islands,@@ -46,7 +44,7 @@ func (m Model) GetSave() ([]byte, error) {
4644 }4745 jsonData, err := json.Marshal(save)4846 if err != nil {49- return nil, fmt.Errorf("unable to marshal save data: %v", err)47+ return nil, fmt.Errorf("unable to marshal save data: %w", err)5048 }5149 return jsonData, nil5250 }@@ -10,6 +10,8 @@ import (
1010 "github.com/charmbracelet/lipgloss"1111 )1212 13+var _ game.Gamer = Model{}14+1315 type Model struct {1416 puzzle Puzzle1517 cursorIsland int // ID of island the cursor is on@@ -126,11 +128,11 @@ func (m Model) View() string {
126128 solved := m.puzzle.IsSolved()127129 128130 title := game.TitleBarView("Hashiwokakero", m.modeTitle, solved)129- grid := gridView(m)131+ grid := gridView(m, solved)130132 info := infoView(&m.puzzle)131133 status := statusBarView(m.selectedIsland != nil, m.showFullHelp)132134 133- return lipgloss.JoinVertical(lipgloss.Left, title, grid, info, status)135+ return lipgloss.JoinVertical(lipgloss.Center, title, grid, info, status)134136 }135137 136138 func (m Model) SetTitle(t string) game.Gamer {@@ -1,12 +1,12 @@
11 package hashiwokakero22 33 import (4- "math/rand"4+ "math/rand/v2"55 )66 77 // GeneratePuzzle creates a solvable hashiwokakero puzzle for the given mode.88 func GeneratePuzzle(mode HashiMode) Puzzle {9- islandCount := mode.MinIslands + rand.Intn(mode.MaxIslands-mode.MinIslands+1)9+ islandCount := mode.MinIslands + rand.IntN(mode.MaxIslands-mode.MinIslands+1)1010 1111 for {1212 p := tryGenerate(mode.Width, mode.Height, islandCount)@@ -69,8 +69,8 @@ func placeIslands(width, height, count int) []Island {
6969 for i := range count {7070 placed := false7171 for range 200 {72- x := rand.Intn(width)73- y := rand.Intn(height)72+ x := rand.IntN(width)73+ y := rand.IntN(height)7474 p := pos{x, y}7575 7676 if occupied[p] {@@ -177,7 +177,7 @@ func buildSpanningTree(p *Puzzle) bool {
177177 if p.WouldCross(pr.id1, pr.id2) {178178 continue179179 }180- count := 1 + rand.Intn(2) // 1 or 2 bridges180+ count := 1 + rand.IntN(2) // 1 or 2 bridges181181 p.SetBridge(pr.id1, pr.id2, count)182182 union(pr.id1, pr.id2)183183 edgesAdded++@@ -242,8 +242,6 @@ func addExtraBridges(p *Puzzle) {
242242 }243243 244244 // Don't let any island exceed 8 bridges245- isl1 := p.FindIslandByID(pr.id1)246- isl2 := p.FindIslandByID(pr.id2)247245 addAmount := newCount248246 if existing != nil {249247 addAmount = newCount - existing.Count@@ -251,8 +249,6 @@ func addExtraBridges(p *Puzzle) {
251249 if p.BridgeCount(pr.id1)+addAmount > 8 || p.BridgeCount(pr.id2)+addAmount > 8 {252250 continue253251 }254- _ = isl1255- _ = isl2256252 257253 p.SetBridge(pr.id1, pr.id2, newCount)258254 added++@@ -36,21 +36,35 @@ type Puzzle struct {
3636 Width, Height int3737 Islands []Island3838 Bridges []Bridge39- cellCache [][]cellInfo // lazily built, invalidated on bridge changes39+ cellCache [][]cellInfo // lazily built, invalidated on bridge changes40+ posIndex map[[2]int]int // (x,y) → island slice index; lazily built41+ bridgeCounts []int // per-island bridge count; lazily built4042 }4143 4244 // FindIslandAt returns the island at (x,y), or nil.4345 func (p *Puzzle) FindIslandAt(x, y int) *Island {44- for i := range p.Islands {45- if p.Islands[i].X == x && p.Islands[i].Y == y {46- return &p.Islands[i]47- }46+ if p.posIndex == nil {47+ p.buildPosIndex()48+ }49+ if idx, ok := p.posIndex[[2]int{x, y}]; ok {50+ return &p.Islands[idx]4851 }4952 return nil5053 }5154 55+func (p *Puzzle) buildPosIndex() {56+ p.posIndex = make(map[[2]int]int, len(p.Islands))57+ for i, isl := range p.Islands {58+ p.posIndex[[2]int{isl.X, isl.Y}] = i59+ }60+}61+5262 // FindIslandByID returns the island with the given ID, or nil.5363 func (p *Puzzle) FindIslandByID(id int) *Island {64+ if id >= 0 && id < len(p.Islands) && p.Islands[id].ID == id {65+ return &p.Islands[id]66+ }67+ // Fallback for non-sequential IDs5468 for i := range p.Islands {5569 if p.Islands[i].ID == id {5670 return &p.Islands[i]@@ -75,13 +89,25 @@ func (p *Puzzle) GetBridge(id1, id2 int) *Bridge {
7589 7690 // BridgeCount returns the total number of bridges connected to an island.7791 func (p *Puzzle) BridgeCount(islandID int) int {78- count := 092+ if p.bridgeCounts == nil {93+ p.buildBridgeCounts()94+ }95+ if islandID >= 0 && islandID < len(p.bridgeCounts) {96+ return p.bridgeCounts[islandID]97+ }98+ return 099+}100+101+func (p *Puzzle) buildBridgeCounts() {102+ p.bridgeCounts = make([]int, len(p.Islands))79103 for _, b := range p.Bridges {80- if b.Island1 == islandID || b.Island2 == islandID {81- count += b.Count104+ if b.Island1 >= 0 && b.Island1 < len(p.bridgeCounts) {105+ p.bridgeCounts[b.Island1] += b.Count106+ }107+ if b.Island2 >= 0 && b.Island2 < len(p.bridgeCounts) {108+ p.bridgeCounts[b.Island2] += b.Count82109 }83110 }84- return count85111 }86112 87113 // FindAdjacentIsland casts a ray from the island at fromID in direction (dx,dy)@@ -236,42 +262,48 @@ func (p *Puzzle) rebuildCellCache() {
236262 p.cellCache = cache237263 }238264 239-// invalidateCache marks the cell cache as stale.265+// invalidateCache marks all lazy caches as stale.240266 func (p *Puzzle) invalidateCache() {241267 p.cellCache = nil268+ p.posIndex = nil269+ p.bridgeCounts = nil242270 }243271 244272 // IsConnected checks if all islands form a single connected component via bridges.245273 func (p *Puzzle) IsConnected() bool {246- if len(p.Islands) <= 1 {274+ n := len(p.Islands)275+ if n <= 1 {247276 return true248277 }249278 if len(p.Bridges) == 0 {250279 return false251280 }252281 253- visited := make(map[int]bool)282+ // Build adjacency list from bridges283+ adj := make(map[int][]int, n)284+ for _, b := range p.Bridges {285+ adj[b.Island1] = append(adj[b.Island1], b.Island2)286+ adj[b.Island2] = append(adj[b.Island2], b.Island1)287+ }288+289+ visited := make([]bool, n)254290 queue := []int{p.Islands[0].ID}291+ visited[p.Islands[0].ID] = true292+ count := 1255293 256294 for len(queue) > 0 {257295 cur := queue[0]258296 queue = queue[1:]259- if visited[cur] {260- continue261- }262- visited[cur] = true263-264- for _, b := range p.Bridges {265- if b.Island1 == cur && !visited[b.Island2] {266- queue = append(queue, b.Island2)267- }268- if b.Island2 == cur && !visited[b.Island1] {269- queue = append(queue, b.Island1)297+ for _, nb := range adj[cur] {298+ if !visited[nb] {299+ visited[nb] = true300+ count++301+ queue = append(queue, nb)270302 }271303 }272304 }273305 274- return len(visited) == len(p.Islands)306+ return count == n275307 }276308 277309 // IsSolved returns true if the puzzle is completely and correctly solved.@@ -115,8 +115,7 @@ func emptyCellView(solved bool) string {
115115 // logical cells so that bridges between directly adjacent islands are visible.116116 // Display grid is (2*width-1) x (2*height-1): even coords are logical cells,117117 // odd coords are gap cells showing bridge connectors.118-func gridView(m Model) string {119- solved := m.puzzle.IsSolved()118+func gridView(m Model, solved bool) string {120119 dispW := 2*m.puzzle.Width - 1121120 dispH := 2*m.puzzle.Height - 1122121 @@ -12,7 +12,6 @@ type Save struct {
1212 CursorX int `json:"cx"`1313 CursorY int `json:"cy"`1414 ModeTitle string `json:"mode_title"`15- Solved bool `json:"solved"`1615 }1716 1817 func (m Model) GetSave() ([]byte, error) {@@ -21,7 +20,6 @@ func (m Model) GetSave() ([]byte, error) {
2120 CursorX: m.cursor.X,2221 CursorY: m.cursor.Y,2322 ModeTitle: m.modeTitle,24- Solved: m.IsSolved(),2523 }2624 jsonData, err := json.Marshal(save)2725 if err != nil {@@ -62,7 +62,7 @@ func (m Model) View() string {
6262 grid := gridView(m.grid, m.cursor, solved)6363 status := statusBarView(m.showFullHelp)6464 65- return lipgloss.JoinVertical(lipgloss.Left, title, grid, status)65+ return lipgloss.JoinVertical(lipgloss.Center, title, grid, status)6666 }6767 6868 func (m Model) SetTitle(t string) game.Gamer {@@ -1,7 +1,7 @@
11 package lightsout22 33 import (4- "math/rand"4+ "math/rand/v2"55 )66 77 // Generate creates a new w x h grid with a solvable puzzle.@@ -17,7 +17,7 @@ func Generate(w, h int) [][]bool {
1717 // iterating once through all cells covers all reachable states from "all off".1818 for y := 0; y < h; y++ {1919 for x := 0; x < w; x++ {20- if rand.Intn(2) == 1 {20+ if rand.IntN(2) == 1 {2121 Toggle(grid, x, y)2222 }2323 }@@ -26,7 +26,7 @@ func Generate(w, h int) [][]bool {
2626 // Ensure the puzzle isn't already solved (all off).2727 if IsSolved(grid) {2828 // Toggle one random cell to ensure at least some lights are on.29- Toggle(grid, rand.Intn(w), rand.Intn(h))29+ Toggle(grid, rand.IntN(w), rand.IntN(h))3030 }3131 3232 return grid@@ -3,12 +3,11 @@ package nonogram
33 import (44 "encoding/json"55 "fmt"6- "math/rand"6+ "math/rand/v2"77 "strings"88 )99 1010 type Save struct {11- Solved bool `json:"solved"`1211 State string `json:"state"`1312 Width int `json:"width"`1413 Height int `json:"height"`@@ -20,7 +19,6 @@ func (m Model) GetSave() ([]byte, error) {
2019 save := Save{2120 RowHints: m.rowHints,2221 ColHints: m.colHints,23- Solved: m.solved,2422 State: m.grid.String(),2523 Width: m.width,2624 Height: m.height,@@ -51,10 +49,6 @@ func ImportModel(data []byte) (*Model, error) {
5149 }, nil5250 }5351 54-func (m Model) GetTomography() Hints {55- return generateTomography(m.grid)56-}57-5852 func GenerateRandomTomography(mode NonogramMode) Hints {5953 for {6054 s := generateRandomState(mode.Height, mode.Width, mode.Density)@@ -17,6 +17,8 @@ const (
1717 emptyTile = ' '1818 )1919 20+var _ game.Gamer = Model{}21+2022 type Model struct {2123 width, height int2224 rowHints TomographyDefinition@@ -93,9 +95,9 @@ func (m Model) View() string {
9395 s1 := lipgloss.JoinHorizontal(lipgloss.Bottom, spacer, c)9496 s2 := lipgloss.JoinHorizontal(lipgloss.Top, r, g)9597 96- grid := lipgloss.JoinVertical(lipgloss.Left, s1, s2)98+ grid := lipgloss.JoinVertical(lipgloss.Center, s1, s2)9799 98- return lipgloss.JoinVertical(lipgloss.Left, title, grid, status)100+ return lipgloss.JoinVertical(lipgloss.Center, title, grid, status)99101 }100102 101103 func (m Model) GetDebugInfo() string {@@ -29,6 +29,7 @@ var (
2929 Background(lipgloss.AdaptiveColor{Light: "130", Dark: "214"})3030 3131 crosshairBG = lipgloss.AdaptiveColor{Light: "254", Dark: "237"}32+ solvedBG = lipgloss.AdaptiveColor{Light: "151", Dark: "22"}3233 3334 hintStyle = baseStyle.3435 Foreground(lipgloss.AdaptiveColor{Light: "137", Dark: "137"})@@ -39,9 +40,11 @@ var (
3940 nonoStatusBarStyle = lipgloss.NewStyle().4041 Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).4142 MarginTop(1)43+)4244 43- cellWidth = 345+const cellWidth = 34446 47+var (4548 hintCellStyle = baseStyle.Width(cellWidth)4649 4750 renderStyleMap = map[rune]lipgloss.Style{@@ -155,7 +158,7 @@ func tileView(val rune, isCursor, inCursorRow, inCursorCol, solved bool) string
155158 156159 if solved {157160 // Brighten filled tiles when solved158- s = s.Background(lipgloss.AdaptiveColor{Light: "151", Dark: "22"})161+ s = s.Background(solvedBG)159162 }160163 161164 r, ok := renderRuneMap[val]@@ -13,7 +13,6 @@ type exportCell struct {
1313 }1414 1515 type Save struct {16- Solved bool `json:"solved"`1716 Grid string `json:"grid"`1817 Provided []exportCell `json:"provided"`1918 }@@ -56,9 +55,10 @@ func ImportModel(data []byte) (*Model, error) {
5655 }5756 5857 return &Model{59- grid: g,60- provided: provided,61- keys: DefaultKeyMap,58+ grid: g,59+ provided: provided,60+ providedGrid: buildProvidedGrid(provided),61+ keys: DefaultKeyMap,6262 }, nil6363 }6464 @@ -68,13 +68,12 @@ func (m Model) GetSave() ([]byte, error) {
6868 provided[i] = exportCell{X: c.x, Y: c.y, V: c.v}6969 }7070 save := Save{71- Solved: m.isSolved(),7271 Grid: gridToString(m.grid),7372 Provided: provided,7473 }7574 jsonData, err := json.Marshal(save)7675 if err != nil {77- return nil, fmt.Errorf("unable to marshal save data: %v", err)76+ return nil, fmt.Errorf("unable to marshal save data: %w", err)7877 }7978 return jsonData, nil8079 }@@ -3,7 +3,6 @@ package sudoku
33 44 import (55 "fmt"6- "slices"76 "strconv"87 98 "github.com/FelineStateMachine/puzzletea/game"@@ -14,22 +13,34 @@ import (
1413 1514 const gridSize = 91615 16+var _ game.Gamer = Model{}17+1718 type Model struct {1819 cursor game.Cursor1920 grid grid2021 provided []cell22+ providedGrid [gridSize][gridSize]bool2123 keys KeyMap2224 modeTitle string2325 showFullHelp bool2426 }2527 28+func buildProvidedGrid(provided []cell) [gridSize][gridSize]bool {29+ var pg [gridSize][gridSize]bool30+ for _, c := range provided {31+ pg[c.y][c.x] = true32+ }33+ return pg34+}35+2636 func New(mode SudokuMode, provided []cell) (game.Gamer, error) {2737 g := newGrid(provided)2838 m := Model{29- grid: g,30- provided: provided,31- keys: DefaultKeyMap,32- modeTitle: mode.Title(),39+ grid: g,40+ provided: provided,41+ providedGrid: buildProvidedGrid(provided),42+ keys: DefaultKeyMap,43+ modeTitle: mode.Title(),3344 }3445 return m, nil3546 }@@ -70,17 +81,18 @@ func (m Model) IsSolved() bool {
7081 }7182 7283 func (m *Model) updateCell(v int) {73- c := &m.grid[m.cursor.Y][m.cursor.X]74- if slices.Contains(m.provided, *c) {84+ if m.providedGrid[m.cursor.Y][m.cursor.X] {7585 return7686 }77- c.v = v87+ m.grid[m.cursor.Y][m.cursor.X].v = v7888 }7989 8090 // View implements game.Gamer.8191 func (m Model) View() string {82- title := game.TitleBarView("Sudoku", m.modeTitle, m.isSolved())83- grid := renderGrid(m)92+ conflicts := computeConflicts(m.grid)93+ solved := isSolvedWith(m.grid, conflicts)94+ title := game.TitleBarView("Sudoku", m.modeTitle, solved)95+ grid := renderGrid(m, solved, conflicts)8496 status := statusBarView(m.showFullHelp)8597 8698 return lipgloss.JoinVertical(lipgloss.Left, title, grid, status)@@ -90,7 +102,7 @@ func (m Model) View() string {
90102 func (m Model) GetDebugInfo() string {91103 conflicts := computeConflicts(m.grid)92104 cursorCell := m.grid[m.cursor.Y][m.cursor.X]93- isProvided := slices.Contains(m.provided, cursorCell)105+ isProvided := m.providedGrid[m.cursor.Y][m.cursor.X]94106 conflict := conflicts[m.cursor.Y][m.cursor.X]95107 solved := m.isSolved()96108 @@ -26,14 +26,18 @@ func newGrid(provided []cell) grid {
2626 }2727 2828 func (c cell) String() string {29- return fmt.Sprintf("r%dc%d=%d", c.x, c.y, c.v)29+ return fmt.Sprintf("r%dc%d=%d", c.y, c.x, c.v)3030 }3131 3232 func (m Model) isSolved() bool {3333 conflicts := computeConflicts(m.grid)34+ return isSolvedWith(m.grid, conflicts)35+}36+37+func isSolvedWith(g grid, conflicts [gridSize][gridSize]bool) bool {3438 for y := range gridSize {3539 for x := range gridSize {36- if m.grid[y][x].v == 0 || conflicts[y][x] {40+ if g[y][x].v == 0 || conflicts[y][x] {3741 return false3842 }3943 }@@ -34,13 +34,7 @@ func (m *Model) updateKeyBindings() {
3434 }3535 3636 func (m Model) IsProvidedCell() bool {37- v := false38- for _, hint := range m.provided {39- if m.cursor.X == hint.x && m.cursor.Y == hint.y {40- v = true41- }42- }43- return v37+ return m.providedGrid[m.cursor.Y][m.cursor.X]4438 }4539 4640 // GetFullHelp implements game.Gamer.@@ -2,7 +2,6 @@ package sudoku
22 33 import (44 "fmt"5- "slices"65 "strings"76 87 "github.com/charmbracelet/lipgloss"@@ -50,8 +49,7 @@ var (
5049 sudokuCellWidth = 25150 )5251 53-func renderGrid(m Model) string {54- conflicts := computeConflicts(m.grid)52+func renderGrid(m Model, solved bool, conflicts [gridSize][gridSize]bool) string {5553 var rows []string5654 5755 for y := range gridSize {@@ -89,7 +87,7 @@ func renderGrid(m Model) string {
8987 9088 grid := lipgloss.JoinVertical(lipgloss.Left, rows...)9189 92- if m.isSolved() {90+ if solved {9391 return gridBorderSolvedStyle.Render(grid)9492 }9593 return gridBorderStyle.Render(grid)@@ -105,7 +103,7 @@ func getCellStyle(m Model, c cell, x, y int, conflict bool) lipgloss.Style {
105103 return conflictCellStyle106104 }107105 108- isProvided := slices.Contains(m.provided, c)106+ isProvided := m.providedGrid[y][x]109107 inCursorRow := m.cursor.Y == y110108 inCursorCol := m.cursor.X == x111109 inCursorBox := (x/3 == m.cursor.X/3) && (y/3 == m.cursor.Y/3)@@ -14,7 +14,6 @@ type Save struct {
1414 State string `json:"state"`1515 Provided string `json:"provided"`1616 ModeTitle string `json:"mode_title"`17- Solved bool `json:"solved"`1817 }1918 2019 func (m Model) GetSave() ([]byte, error) {@@ -23,7 +22,6 @@ func (m Model) GetSave() ([]byte, error) {
2322 State: m.grid.String(),2423 Provided: serializeProvided(m.provided),2524 ModeTitle: m.modeTitle,26- Solved: m.solved,2725 }2826 data, err := json.Marshal(save)2927 if err != nil {@@ -75,7 +75,7 @@ func (m Model) View() string {
7575 title := game.TitleBarView("Takuzu", m.modeTitle, m.solved)7676 grid := gridView(m.grid, m.provided, m.cursor, m.solved)7777 status := statusBarView(m.showFullHelp)78- return lipgloss.JoinVertical(lipgloss.Left, title, grid, status)78+ return lipgloss.JoinVertical(lipgloss.Center, title, grid, status)7979 }8080 8181 func (m Model) SetTitle(t string) game.Gamer {@@ -26,6 +26,7 @@ var (
2626 Background(lipgloss.AdaptiveColor{Light: "130", Dark: "214"})2727 2828 crosshairBG = lipgloss.AdaptiveColor{Light: "254", Dark: "237"}29+ solvedBG = lipgloss.AdaptiveColor{Light: "151", Dark: "22"}2930 3031 gridBorderStyle = lipgloss.NewStyle().3132 Border(lipgloss.RoundedBorder()).@@ -38,9 +39,11 @@ var (
3839 statusBarStyle = lipgloss.NewStyle().3940 Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).4041 MarginTop(1)42+)4143 42- cellWidth = 344+const cellWidth = 34345 46+var (4447 renderStyleMap = map[rune]lipgloss.Style{4548 zeroCell: zeroStyle,4649 oneCell: oneStyle,@@ -71,7 +74,7 @@ func cellView(val rune, isProvided, isCursor, inCursorRow, inCursorCol, solved b
7174 }7275 7376 if solved {74- s = s.Background(lipgloss.AdaptiveColor{Light: "151", Dark: "22"})77+ s = s.Background(solvedBG)7578 }7679 7780 r, ok := renderRuneMap[val]@@ -35,6 +35,7 @@ func ImportModel(data []byte) (*Model, error) {
3535 selectionStart: game.Cursor{X: exported.SelectionX, Y: exported.SelectionY},3636 keys: DefaultKeyMap,3737 solved: exported.Solved,38+ foundCells: buildFoundCells(exported.Width, exported.Height, exported.Words),3839 }, nil3940 }4041 @@ -17,6 +17,8 @@ const (
1717 startSelected1818 )1919 20+var _ game.Gamer = Model{}21+2022 type Model struct {2123 width, height int2224 grid grid@@ -28,19 +30,36 @@ type Model struct {
2830 modeTitle string2931 solved bool3032 showFullHelp bool33+ foundCells [][]bool34+}35+36+func buildFoundCells(width, height int, words []Word) [][]bool {37+ fc := make([][]bool, height)38+ for y := range fc {39+ fc[y] = make([]bool, width)40+ }41+ for i := range words {42+ if words[i].Found {43+ for _, pos := range words[i].Positions() {44+ fc[pos.Y][pos.X] = true45+ }46+ }47+ }48+ return fc3149 }3250 3351 // New creates a new word search game3452 func New(mode WordSearchMode, g grid, words []Word) (game.Gamer, error) {3553 return &Model{36- width: mode.Width,37- height: mode.Height,38- grid: g,39- words: words,40- selection: noSelection,41- keys: DefaultKeyMap,42- modeTitle: mode.Title(),43- solved: false,54+ width: mode.Width,55+ height: mode.Height,56+ grid: g,57+ words: words,58+ selection: noSelection,59+ keys: DefaultKeyMap,60+ modeTitle: mode.Title(),61+ solved: false,62+ foundCells: buildFoundCells(mode.Width, mode.Height, words),4463 }, nil4564 }4665 @@ -101,6 +120,9 @@ func (m *Model) validateSelection() {
101120 }102121 if m.words[i].Text == word || m.words[i].Text == wordReverse {103122 m.words[i].Found = true123+ for _, pos := range m.words[i].Positions() {124+ m.foundCells[pos.Y][pos.X] = true125+ }104126 m.checkWin()105127 return106128 }@@ -1,7 +1,7 @@
11 package wordsearch22 33 import (4- "math/rand"4+ "math/rand/v2"55 "strings"66 )77 @@ -55,11 +55,11 @@ func tryPlaceWord(g grid, text string, allowedDirs []Direction, maxAttempts int)
5555 5656 for range maxAttempts {5757 // Random starting position58- x := rand.Intn(width)59- y := rand.Intn(height)58+ x := rand.IntN(width)59+ y := rand.IntN(height)6060 6161 // Random direction62- dir := allowedDirs[rand.Intn(len(allowedDirs))]62+ dir := allowedDirs[rand.IntN(len(allowedDirs))]6363 dx, dy := dir.Vector()6464 6565 // Check if word fits in this direction@@ -113,7 +113,7 @@ func fillEmptyCells(g grid) {
113113 for y := range g {114114 for x := range g[y] {115115 if g[y][x] == ' ' {116- g[y][x] = rune(letters[rand.Intn(len(letters))])116+ g[y][x] = rune(letters[rand.IntN(len(letters))])117117 }118118 }119119 }@@ -53,7 +53,7 @@ func renderView(m Model) string {
5353 5454 status := statusBarView(m.showFullHelp)5555 56- return lipgloss.JoinVertical(lipgloss.Left, title, mainView, status)56+ return lipgloss.JoinVertical(lipgloss.Center, title, mainView, status)5757 }5858 5959 func statusBarView(showFullHelp bool) string {@@ -92,10 +92,8 @@ func getCellStyle(m Model, x, y int) lipgloss.Style {
9292 }9393 9494 // Check if part of found word95- for _, word := range m.words {96- if word.Found && word.Contains(Position{X: x, Y: y}) {97- return foundStyle98- }95+ if m.foundCells[y][x] {96+ return foundStyle9997 }10098 10199 return normalStyle