clone

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

refactor: extract shared styles, remove dead code, fix panics and caching

Dami committed 6de7d24
- Extract StatusBarStyle, CursorStyle, CursorWarmStyle to game/style.go
  (eliminates 8 identical statusBarStyle and 6 cursorStyle definitions)
- Extract shared BorderChar/HBorderRow helpers to game/border.go
  (deduplicates nearly identical functions between takuzu and hitori)
- Remove unused game.ResetMsg and duplicate handlers in hitori/shikaku
  (root model already calls Reset() directly)
- Cache sudoku conflicts in Model instead of recomputing on every View()
- Replace panic in View() default case with error string
- Replace panics in debug renderer with graceful fallback
- Fix non-adaptive debug border color to use AdaptiveColor
commit
6de7d24fbbdd9bf764352fd260e7593bb6dd60cd
parent
16f7659b677d80ab65d40869f995baee317b83b8
author
Dami <dami.etoile@gmail.com>
18 files changed, +132 -174
Mdebug.go7+++-
Agame/border.go47++++++++++++++++++++++++
Mgame/gamer.go4--
Mgame/style.go19++++++++++
Mhashiwokakero/style.go20+++--------
Mhitori/Model.go5---
Mhitori/style.go58++++++++----------------------
Mlightsout/style.go14++------
Mnonogram/style.go14++------
Mshikaku/Model.go4--
Mshikaku/style.go12++----
Msudoku/Export.go1+
Msudoku/Model.go14++++---
Msudoku/style.go14++-----
Mtakuzu/style.go56+++++++----------------------
Mui/style.go2+-
Mview.go3+-
Mwordsearch/style.go12++-----
M debug.go +5 -2
A game/border.go +47 -0
@@ -0,0 +1,47 @@
1+package game2+3+import "github.com/charmbracelet/lipgloss"4+5+// GridBorderColors holds the color configuration for a grid border with6+// crosshair and solved-state highlighting.7+type GridBorderColors struct {8+ BorderFG lipgloss.AdaptiveColor9+ BackgroundBG lipgloss.AdaptiveColor10+ CrosshairBG lipgloss.AdaptiveColor11+ SolvedBorderFG lipgloss.AdaptiveColor12+ SolvedBG lipgloss.AdaptiveColor13+}14+15+// BorderChar renders a single border character with optional crosshair highlighting.16+func BorderChar(ch string, colors GridBorderColors, solved, highlight bool) string {17+ s := lipgloss.NewStyle().Foreground(colors.BorderFG).Background(colors.BackgroundBG)18+ if solved {19+ s = lipgloss.NewStyle().Foreground(colors.SolvedBorderFG).Background(colors.SolvedBG)20+ } else if highlight {21+ s = s.Background(colors.CrosshairBG)22+ }23+ return s.Render(ch)24+}25+26+// HBorderRow builds a horizontal border row (top or bottom) with per-column27+// crosshair highlighting. cellWidth is the visual width of each cell.28+func HBorderRow(w, cursorX, cellWidth int, left, right string, colors GridBorderColors, solved bool) string {29+ var parts []string30+ parts = append(parts, BorderChar(left, colors, solved, false))31+ segment := ""32+ for range cellWidth {33+ segment += "─"34+ }35+ for x := range w {36+ highlight := !solved && x == cursorX37+ s := lipgloss.NewStyle().Foreground(colors.BorderFG).Background(colors.BackgroundBG)38+ if solved {39+ s = lipgloss.NewStyle().Foreground(colors.SolvedBorderFG).Background(colors.SolvedBG)40+ } else if highlight {41+ s = s.Background(colors.CrosshairBG)42+ }43+ parts = append(parts, s.Render(segment))44+ }45+ parts = append(parts, BorderChar(right, colors, solved, false))46+ return lipgloss.JoinHorizontal(lipgloss.Top, parts...)47+}
M game/gamer.go +0 -4
M game/style.go +19 -0
M hashiwokakero/style.go +5 -15
@@ -18,8 +18,6 @@ var (
1818 colorSatisfiedBg = lipgloss.AdaptiveColor{Dark: "194", Light: "236"}1919 colorOverFg = lipgloss.AdaptiveColor{Dark: "160", Light: "167"}2020 colorOverBg = lipgloss.AdaptiveColor{Dark: "224", Light: "52"}21- colorCursorFg = lipgloss.AdaptiveColor{Dark: "255", Light: "235"}22- colorCursorBg = lipgloss.AdaptiveColor{Dark: "130", Light: "173"}2321 colorSelectedFg = lipgloss.AdaptiveColor{Dark: "255", Light: "235"}2422 colorSelectedBg = lipgloss.AdaptiveColor{Dark: "172", Light: "179"}2523 colorAdjacentBg = lipgloss.AdaptiveColor{Dark: "223", Light: "58"}
@@ -49,11 +47,7 @@ var (
4947 Background(colorOverBg).5048 Bold(true)5149 52- islandCursorStyle = baseStyle.53- Foreground(colorCursorFg).54- Background(colorCursorBg).55- Bold(true)56-50+ islandCursorStyle = game.CursorStyle5751 islandCursorSolvedStyle = game.CursorSolvedStyle5852 5953 islandSelectedStyle = baseStyle.
@@ -81,10 +75,6 @@ var (
8175 emptyStyle = baseStyle.8276 Foreground(colorEmptyDot)8377 84- statusBarStyle = lipgloss.NewStyle().85- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).86- MarginTop(1)87-8878 gridBorderStyle = lipgloss.NewStyle().8979 Border(lipgloss.RoundedBorder()).9080 Padding(1).
@@ -285,14 +275,14 @@ func bridgeVView(count int, solved bool) string {
285275 func statusBarView(selected, showFullHelp bool) string {286276 if selected {287277 if showFullHelp {288- return statusBarStyle.Render("arrows/wasd: build bridge enter/space/esc: cancel ctrl+n: menu ctrl+r: reset ctrl+h: help")278+ return game.StatusBarStyle.Render("arrows/wasd: build bridge enter/space/esc: cancel ctrl+n: menu ctrl+r: reset ctrl+h: help")289279 }290- return statusBarStyle.Render("arrows/wasd: build bridge enter/space/esc: cancel")280+ return game.StatusBarStyle.Render("arrows/wasd: build bridge enter/space/esc: cancel")291281 }292282 if showFullHelp {293- return statusBarStyle.Render("arrows/wasd: move enter/space: select island ctrl+n: menu ctrl+r: reset ctrl+h: help")283+ return game.StatusBarStyle.Render("arrows/wasd: move enter/space: select island ctrl+n: menu ctrl+r: reset ctrl+h: help")294284 }295- return statusBarStyle.Render("enter/space: select island")285+ return game.StatusBarStyle.Render("enter/space: select island")296286 }297287 298288 func infoView(p *Puzzle) string {
M hitori/Model.go +0 -5
M hitori/style.go +15 -43
@@ -24,11 +24,7 @@ var (
2424 Foreground(lipgloss.AdaptiveColor{Light: "25", Dark: "75"}).2525 Background(backgroundColor)2626 27- cursorStyle = baseStyle.28- Bold(true).29- Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).30- Background(lipgloss.AdaptiveColor{Light: "130", Dark: "214"})31-27+ cursorStyle = game.CursorWarmStyle3228 cursorSolvedStyle = game.CursorSolvedStyle3329 3430 conflictStyle = baseStyle.
@@ -39,10 +35,6 @@ var (
3935 solvedBG = lipgloss.AdaptiveColor{Light: "151", Dark: "22"}4036 4137 borderFG = lipgloss.AdaptiveColor{Light: "250", Dark: "240"}42-43- statusBarStyle = lipgloss.NewStyle().44- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).45- MarginTop(1)4638 )4739 4840 const cellWidth = 3
@@ -82,6 +74,14 @@ func cellView(num rune, mark cellMark, isCursor, inCursorRow, inCursorCol, solve
8274 return s.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(display)8375 }8476 77+var gridBorderColors = game.GridBorderColors{78+ BorderFG: borderFG,79+ BackgroundBG: backgroundColor,80+ CrosshairBG: crosshairBG,81+ SolvedBorderFG: lipgloss.AdaptiveColor{Light: "22", Dark: "149"},82+ SolvedBG: solvedBG,83+}84+8585 func gridView(numbers grid, marks [][]cellMark, c game.Cursor, solved bool, conflicts [][]bool) string {8686 w := 08787 if len(numbers) > 0 {
@@ -93,7 +93,7 @@ func gridView(numbers grid, marks [][]cellMark, c game.Cursor, solved bool, conf
9393 var rowCells []string9494 inCursorRow := y == c.Y9595 96- rowCells = append(rowCells, borderChar("\u2502", solved, !solved && inCursorRow))96+ rowCells = append(rowCells, game.BorderChar("│", gridBorderColors, solved, !solved && inCursorRow))9797 9898 for x, num := range row {9999 isCursor := x == c.X && y == c.Y
@@ -102,47 +102,19 @@ func gridView(numbers grid, marks [][]cellMark, c game.Cursor, solved bool, conf
102102 rowCells = append(rowCells, cell)103103 }104104 105- rowCells = append(rowCells, borderChar("\u2502", solved, !solved && inCursorRow))105+ rowCells = append(rowCells, game.BorderChar("│", gridBorderColors, solved, !solved && inCursorRow))106106 rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, rowCells...))107107 }108108 109- topRow := hBorderRow(w, c.X, "\u256D", "\u256E", solved)110- botRow := hBorderRow(w, c.X, "\u2570", "\u256F", solved)109+ topRow := game.HBorderRow(w, c.X, cellWidth, "╭", "╮", gridBorderColors, solved)110+ botRow := game.HBorderRow(w, c.X, cellWidth, "╰", "╯", gridBorderColors, solved)111111 112112 return lipgloss.JoinVertical(lipgloss.Left, topRow, lipgloss.JoinVertical(lipgloss.Left, rows...), botRow)113113 }114114 115-func borderChar(ch string, solved, highlight bool) string {116- s := baseStyle.Foreground(borderFG).Background(backgroundColor)117- if solved {118- s = baseStyle.Foreground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"}).Background(solvedBG)119- } else if highlight {120- s = s.Background(crosshairBG)121- }122- return s.Render(ch)123-}124-125-func hBorderRow(w, cursorX int, left, right string, solved bool) string {126- var parts []string127- parts = append(parts, borderChar(left, solved, false))128- segment := "\u2500\u2500\u2500" // ───129- for x := range w {130- highlight := !solved && x == cursorX131- s := baseStyle.Foreground(borderFG).Background(backgroundColor)132- if solved {133- s = baseStyle.Foreground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"}).Background(solvedBG)134- } else if highlight {135- s = s.Background(crosshairBG)136- }137- parts = append(parts, s.Render(segment))138- }139- parts = append(parts, borderChar(right, solved, false))140- return lipgloss.JoinHorizontal(lipgloss.Top, parts...)141-}142-143115 func statusBarView(showFullHelp bool) string {144116 if showFullHelp {145- return statusBarStyle.Render("arrows/wasd: move x: shade z: circle bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")117+ return game.StatusBarStyle.Render("arrows/wasd: move x: shade z: circle bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")146118 }147- return statusBarStyle.Render("x: shade z: circle bkspc: clear")119+ return game.StatusBarStyle.Render("x: shade z: circle bkspc: clear")148120 }
M lightsout/style.go +3 -11
M nonogram/style.go +3 -11
M shikaku/Model.go +0 -4
M shikaku/style.go +4 -8
@@ -52,10 +52,6 @@ var (
5252 infoSatisfied = lipgloss.AdaptiveColor{Dark: "22", Light: "149"}5353 infoText = lipgloss.AdaptiveColor{Dark: "137", Light: "137"}5454 55- statusBarStyle = lipgloss.NewStyle().56- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).57- MarginTop(1)58-5955 gridBorderStyle = lipgloss.NewStyle().6056 Border(lipgloss.RoundedBorder()).6157 BorderForeground(borderColor)
@@ -180,12 +176,12 @@ func infoView(p *Puzzle) string {
180176 func statusBarView(selected, showFullHelp bool) string {181177 if selected {182178 if showFullHelp {183- return statusBarStyle.Render("arrows: expand shift+arrows: shrink enter: confirm esc: cancel bkspc: delete ctrl+n: menu ctrl+r: reset ctrl+h: help")179+ return game.StatusBarStyle.Render("arrows: expand shift+arrows: shrink enter: confirm esc: cancel bkspc: delete ctrl+n: menu ctrl+r: reset ctrl+h: help")184180 }185- return statusBarStyle.Render("arrows: expand shift+arrows: shrink enter: confirm esc: cancel")181+ return game.StatusBarStyle.Render("arrows: expand shift+arrows: shrink enter: confirm esc: cancel")186182 }187183 if showFullHelp {188- return statusBarStyle.Render("arrows/wasd: move enter/space: select clue bkspc: delete ctrl+n: menu ctrl+r: reset ctrl+h: help")184+ return game.StatusBarStyle.Render("arrows/wasd: move enter/space: select clue bkspc: delete ctrl+n: menu ctrl+r: reset ctrl+h: help")189185 }190- return statusBarStyle.Render("enter/space: select clue bkspc: delete")186+ return game.StatusBarStyle.Render("enter/space: select clue bkspc: delete")191187 }
M sudoku/Export.go +1 -0
M sudoku/Model.go +8 -6
@@ -20,6 +20,7 @@ type Model struct {
2020 grid grid2121 provided []cell2222 providedGrid [gridSize][gridSize]bool23+ conflicts [gridSize][gridSize]bool2324 keys KeyMap2425 modeTitle string2526 showFullHelp bool
@@ -39,6 +40,7 @@ func New(mode SudokuMode, provided []cell) (game.Gamer, error) {
3940 grid: g,4041 provided: provided,4142 providedGrid: buildProvidedGrid(provided),43+ conflicts: computeConflicts(g),4244 keys: DefaultKeyMap,4345 modeTitle: mode.Title(),4446 }
@@ -83,6 +85,7 @@ func (m Model) IsSolved() bool {
8385 // Reset implements game.Gamer.8486 func (m Model) Reset() game.Gamer {8587 m.grid = newGrid(m.provided)88+ m.conflicts = computeConflicts(m.grid)8689 return m8790 }8891
@@ -91,14 +94,14 @@ func (m *Model) updateCell(v int) {
9194 return9295 }9396 m.grid[m.cursor.Y][m.cursor.X].v = v97+ m.conflicts = computeConflicts(m.grid)9498 }9599 96100 // View implements game.Gamer.97101 func (m Model) View() string {98- conflicts := computeConflicts(m.grid)99- solved := isSolvedWith(m.grid, conflicts)102+ solved := isSolvedWith(m.grid, m.conflicts)100103 title := game.TitleBarView("Sudoku", m.modeTitle, solved)101- grid := renderGrid(m, solved, conflicts)104+ grid := renderGrid(m, solved, m.conflicts)102105 status := statusBarView(m.showFullHelp)103106 104107 return lipgloss.JoinVertical(lipgloss.Center, title, grid, status)
@@ -106,10 +109,9 @@ func (m Model) View() string {
106109 107110 // GetDebugInfo implements game.Gamer.108111 func (m Model) GetDebugInfo() string {109- conflicts := computeConflicts(m.grid)110112 cursorCell := m.grid[m.cursor.Y][m.cursor.X]111113 isProvided := m.providedGrid[m.cursor.Y][m.cursor.X]112- conflict := conflicts[m.cursor.Y][m.cursor.X]114+ conflict := m.conflicts[m.cursor.Y][m.cursor.X]113115 solved := m.isSolved()114116 115117 filledCount := 0
@@ -119,7 +121,7 @@ func (m Model) GetDebugInfo() string {
119121 if m.grid[y][x].v != 0 {120122 filledCount++121123 }122- if conflicts[y][x] {124+ if m.conflicts[y][x] {123125 conflictCount++124126 }125127 }
M sudoku/style.go +4 -10
M takuzu/style.go +13 -43
@@ -20,21 +20,13 @@ var (
2020 Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "240"}).2121 Background(backgroundColor)2222 23- cursorStyle = baseStyle.24- Bold(true).25- Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).26- Background(lipgloss.AdaptiveColor{Light: "130", Dark: "214"})27-23+ cursorStyle = game.CursorWarmStyle2824 cursorSolvedStyle = game.CursorSolvedStyle2925 3026 crosshairBG = lipgloss.AdaptiveColor{Light: "254", Dark: "237"}3127 solvedBG = lipgloss.AdaptiveColor{Light: "151", Dark: "22"}3228 3329 borderFG = lipgloss.AdaptiveColor{Light: "250", Dark: "240"}34-35- statusBarStyle = lipgloss.NewStyle().36- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).37- MarginTop(1)3830 )3931 4032 const cellWidth = 3
@@ -92,7 +84,7 @@ func gridView(g grid, provided [][]bool, c game.Cursor, solved bool) string {
9284 inCursorRow := y == c.Y9385 9486 // Left border segment for this row.95- rowCells = append(rowCells, borderChar("│", solved, !solved && inCursorRow))87+ rowCells = append(rowCells, game.BorderChar("│", gridBorderColors, solved, !solved && inCursorRow))9688 9789 for x, val := range row {9890 isCursor := x == c.X && y == c.Y
@@ -102,51 +94,29 @@ func gridView(g grid, provided [][]bool, c game.Cursor, solved bool) string {
10294 }10395 10496 // Right border segment for this row.105- rowCells = append(rowCells, borderChar("│", solved, !solved && inCursorRow))97+ rowCells = append(rowCells, game.BorderChar("│", gridBorderColors, solved, !solved && inCursorRow))10698 10799 rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, rowCells...))108100 }109101 110102 // Build top and bottom border rows with per-column crosshair highlighting.111- topRow := hBorderRow(w, c.X, "╭", "╮", solved)112- botRow := hBorderRow(w, c.X, "╰", "╯", solved)103+ topRow := game.HBorderRow(w, c.X, cellWidth, "╭", "╮", gridBorderColors, solved)104+ botRow := game.HBorderRow(w, c.X, cellWidth, "╰", "╯", gridBorderColors, solved)113105 114106 return lipgloss.JoinVertical(lipgloss.Left, topRow, lipgloss.JoinVertical(lipgloss.Left, rows...), botRow)115107 }116108 117-// borderChar renders a single border character with optional crosshair highlighting.118-func borderChar(ch string, solved, highlight bool) string {119- s := baseStyle.Foreground(borderFG).Background(backgroundColor)120- if solved {121- s = baseStyle.Foreground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"}).Background(solvedBG)122- } else if highlight {123- s = s.Background(crosshairBG)124- }125- return s.Render(ch)126-}127-128-// hBorderRow builds a horizontal border row (top or bottom) with per-column crosshair.129-func hBorderRow(w, cursorX int, left, right string, solved bool) string {130- var parts []string131- parts = append(parts, borderChar(left, solved, false))132- segment := "───" // cellWidth = 3133- for x := range w {134- highlight := !solved && x == cursorX135- s := baseStyle.Foreground(borderFG).Background(backgroundColor)136- if solved {137- s = baseStyle.Foreground(lipgloss.AdaptiveColor{Light: "22", Dark: "149"}).Background(solvedBG)138- } else if highlight {139- s = s.Background(crosshairBG)140- }141- parts = append(parts, s.Render(segment))142- }143- parts = append(parts, borderChar(right, solved, false))144- return lipgloss.JoinHorizontal(lipgloss.Top, parts...)109+var gridBorderColors = game.GridBorderColors{110+ BorderFG: borderFG,111+ BackgroundBG: backgroundColor,112+ CrosshairBG: crosshairBG,113+ SolvedBorderFG: lipgloss.AdaptiveColor{Light: "22", Dark: "149"},114+ SolvedBG: solvedBG,145115 }146116 147117 func statusBarView(showFullHelp bool) string {148118 if showFullHelp {149- return statusBarStyle.Render("arrows/wasd: move z: ● x: ○ bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")119+ return game.StatusBarStyle.Render("arrows/wasd: move z: ● x: ○ bkspc: clear ctrl+n: menu ctrl+r: reset ctrl+h: help")150120 }151- return statusBarStyle.Render("z: ● x: ○ bkspc: clear")121+ return game.StatusBarStyle.Render("z: ● x: ○ bkspc: clear")152122 }
M ui/style.go +1 -1
M view.go +1 -2
M wordsearch/style.go +3 -9
@@ -13,9 +13,7 @@ var (
1313 Foreground(lipgloss.AdaptiveColor{Light: "236", Dark: "252"}).1414 Background(lipgloss.AdaptiveColor{Light: "254", Dark: "236"})1515 16- cursorStyle = lipgloss.NewStyle().17- Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "235"}).18- Background(lipgloss.AdaptiveColor{Light: "130", Dark: "173"})16+ cursorStyle = game.CursorStyle1917 2018 selectionStyle = lipgloss.NewStyle().2119 Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "235"}).
@@ -37,10 +35,6 @@ var (
3735 unfoundWordStyle = lipgloss.NewStyle().3836 Foreground(lipgloss.AdaptiveColor{Light: "137", Dark: "137"})3937 40- statusBarStyle = lipgloss.NewStyle().41- Foreground(lipgloss.AdaptiveColor{Light: "244", Dark: "244"}).42- MarginTop(1)43-4438 borderFG = lipgloss.AdaptiveColor{Light: "250", Dark: "240"}4539 gridBG = lipgloss.AdaptiveColor{Light: "254", Dark: "236"}4640 solvedBdrFG = lipgloss.AdaptiveColor{Light: "22", Dark: "149"}
@@ -94,9 +88,9 @@ func renderView(m Model) string {
9488 9589 func statusBarView(showFullHelp bool) string {9690 if showFullHelp {97- return statusBarStyle.Render("arrows/wasd: move enter/space: select esc: cancel ctrl+n: menu ctrl+r: reset ctrl+h: help")91+ return game.StatusBarStyle.Render("arrows/wasd: move enter/space: select esc: cancel ctrl+n: menu ctrl+r: reset ctrl+h: help")9892 }99- return statusBarStyle.Render("enter/space: select esc: cancel")93+ return game.StatusBarStyle.Render("enter/space: select esc: cancel")10094 }10195 10296 func renderGrid(m Model) string {