- commit
- df60dd8244eeec9335b75c7db610d3b9e84d964b
- parent
- dcef8c16f7fb425d77c8d3a9159cf65cf07a676f
- author
- Dami <dami@Damis-MacBook-Air.local>
| M | game/ | 5 | + |
| M | hashiwokakero/ | 15 | ++- |
| M | hashiwokakero/ | 16 | ++- |
| M | hashiwokakero/ | 163 | ++++++++++++++++++++++-------- |
| M | model.go | 14 | ++- |
| M | nonogram/ | 28 | +++-- |
| M | nonogram/ | 39 | ++++--- |
| M | sudoku/ | 30 | +++-- |
| M | sudoku/ | 11 | +- |
| M | wordsearch/ | 12 | ++- |
| M | wordsearch/ | 11 | +- |
@@ -13,6 +13,7 @@ type Gamer interface {
1313 1414 GetSave() ([]byte, error)1515 IsSolved() bool16+ SetTitle(string) Gamer1617 1718 Init() tea.Cmd1819 View() string@@ -61,6 +62,10 @@ func (c Category) Title() string { return c.Name }
6162 func (c Category) Description() string { return c.Desc }6263 func (c Category) FilterValue() string { return c.Name }6364 65+// HelpToggleMsg is sent from the root model to games when the user toggles66+// the full help display with Ctrl+H.67+type HelpToggleMsg struct{ Show bool }68+6469 // Registry maps game type names to their import functions.6570 var Registry = map[string]func([]byte) (Gamer, error){}6671 @@ -39,7 +39,16 @@ func (h HashiMode) Spawn() (game.Gamer, error) {
3939 }4040 4141 var Modes = []list.Item{42- NewMode("Easy - 7x7", "Connect islands with bridges on a 7x7 grid.", 7, 7, 8, 12),43- NewMode("Medium - 9x9", "Connect islands with bridges on a 9x9 grid.", 9, 9, 12, 16),44- NewMode("Hard - 13x13", "Connect islands with bridges on a 13x13 grid.", 13, 13, 20, 26),42+ NewMode("Easy 7x7", "7x7 grid with 8-10 islands.", 7, 7, 8, 10),43+ NewMode("Medium 7x7", "7x7 grid with 12-15 islands.", 7, 7, 12, 15),44+ NewMode("Hard 7x7", "7x7 grid with 17-20 islands.", 7, 7, 17, 20),45+ NewMode("Easy 9x9", "9x9 grid with 12-16 islands.", 9, 9, 12, 16),46+ NewMode("Medium 9x9", "9x9 grid with 20-24 islands.", 9, 9, 20, 24),47+ NewMode("Hard 9x9", "9x9 grid with 28-32 islands.", 9, 9, 28, 32),48+ NewMode("Easy 11x11", "11x11 grid with 18-24 islands.", 11, 11, 18, 24),49+ NewMode("Medium 11x11", "11x11 grid with 30-36 islands.", 11, 11, 30, 36),50+ NewMode("Hard 11x11", "11x11 grid with 42-48 islands.", 11, 11, 42, 48),51+ NewMode("Easy 13x13", "13x13 grid with 25-34 islands.", 13, 13, 25, 34),52+ NewMode("Medium 13x13", "13x13 grid with 42-51 islands.", 13, 13, 42, 51),53+ NewMode("Hard 13x13", "13x13 grid with 59-68 islands.", 13, 13, 59, 68),4554 }@@ -14,7 +14,8 @@ type Model struct {
1414 cursorIsland int // ID of island the cursor is on1515 selectedIsland *int // ID of island selected for bridge building, nil if none1616 keys KeyMap17- modeName string17+ modeTitle string18+ showFullHelp bool1819 }1920 2021 func New(mode HashiMode, puzzle Puzzle) game.Gamer {@@ -26,7 +27,7 @@ func New(mode HashiMode, puzzle Puzzle) game.Gamer {
2627 puzzle: puzzle,2728 cursorIsland: cursorID,2829 keys: DefaultKeyMap,29- modeName: mode.Title(),30+ modeTitle: mode.Title(),3031 }3132 }3233 @@ -36,6 +37,8 @@ func (m Model) Init() tea.Cmd {
3637 3738 func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {3839 switch msg := msg.(type) {40+ case game.HelpToggleMsg:41+ m.showFullHelp = msg.Show3942 case tea.KeyMsg:4043 if m.selectedIsland != nil {4144 m = m.handleBridgeMode(msg)@@ -121,14 +124,19 @@ func (m Model) cycleBridge(dx, dy int) Model {
121124 func (m Model) View() string {122125 solved := m.puzzle.IsSolved()123126 124- title := titleBarView(m.modeName, solved)127+ title := titleBarView(m.modeTitle, solved)125128 grid := gridView(m)126129 info := infoView(&m.puzzle)127- status := statusBarView(m.selectedIsland != nil)130+ status := statusBarView(m.selectedIsland != nil, m.showFullHelp)128131 129132 return lipgloss.JoinVertical(lipgloss.Left, title, grid, info, status)130133 }131134 135+func (m Model) SetTitle(t string) game.Gamer {136+ m.modeTitle = t137+ return m138+}139+132140 func (m Model) IsSolved() bool {133141 return m.puzzle.IsSolved()134142 }@@ -10,48 +10,108 @@ import (
1010 1111 const cellWidth = 31212 13+// Color palette — cool ocean/slate theme14+const (15+ colorIslandFg = "#e0e6ed"16+ colorIslandBg = "#3a4555"17+ colorSatisfiedFg = "#7dcea0"18+ colorSatisfiedBg = "#1a3330"19+ colorOverFg = "#e07070"20+ colorOverBg = "#3a1a1e"21+ colorCursorFg = "#1a1e24"22+ colorCursorBg = "#5dade2"23+ colorSelectedFg = "#1a1e24"24+ colorSelectedBg = "#f5b041"25+ colorAdjacentBg = "#2e3a48"26+ colorBridge = "#5a7a8a"27+ colorBridgeSolved = "#7dcea0"28+ colorEmptyDot = "#2e3640"29+ colorEmptySolved = "#4a6a5a"30+ colorInfoSatisfied = "#7dcea0"31+ colorInfoText = "#8a9aaa"32+)33+1334 var (1435 baseStyle = lipgloss.NewStyle()1536 1637 islandDefaultStyle = baseStyle.17- Foreground(lipgloss.Color("#ffffff")).18- Background(lipgloss.Color("#4a4a4a")).38+ Foreground(lipgloss.Color(colorIslandFg)).39+ Background(lipgloss.Color(colorIslandBg)).1940 Bold(true)2041 2142 islandSatisfiedStyle = baseStyle.22- Foreground(lipgloss.Color("#00ff00")).23- Background(lipgloss.Color("#1a3a1a")).43+ Foreground(lipgloss.Color(colorSatisfiedFg)).44+ Background(lipgloss.Color(colorSatisfiedBg)).2445 Bold(true)2546 2647 islandOverStyle = baseStyle.27- Foreground(lipgloss.Color("#ff0000")).28- Background(lipgloss.Color("#3a1a1a")).48+ Foreground(lipgloss.Color(colorOverFg)).49+ Background(lipgloss.Color(colorOverBg)).2950 Bold(true)3051 3152 islandCursorStyle = baseStyle.32- Foreground(lipgloss.Color("#ffffff")).33- Background(lipgloss.Color("#ff00ff")).53+ Foreground(lipgloss.Color(colorCursorFg)).54+ Background(lipgloss.Color(colorCursorBg)).3455 Bold(true)3556 3657 islandSelectedStyle = baseStyle.37- Foreground(lipgloss.Color("#ffffff")).38- Background(lipgloss.Color("#ff8800")).58+ Foreground(lipgloss.Color(colorSelectedFg)).59+ Background(lipgloss.Color(colorSelectedBg)).60+ Bold(true)61+62+ islandAdjacentStyle = baseStyle.63+ Foreground(lipgloss.Color(colorIslandFg)).64+ Background(lipgloss.Color(colorAdjacentBg)).3965 Bold(true)4066 4167 bridgeHStyle = baseStyle.42- Foreground(lipgloss.Color("#666666"))68+ Foreground(lipgloss.Color(colorBridge))4369 4470 bridgeVStyle = baseStyle.45- Foreground(lipgloss.Color("#666666"))71+ Foreground(lipgloss.Color(colorBridge))72+73+ bridgeHSolvedStyle = baseStyle.74+ Foreground(lipgloss.Color(colorBridgeSolved))75+76+ bridgeVSolvedStyle = baseStyle.77+ Foreground(lipgloss.Color(colorBridgeSolved))4678 4779 emptyStyle = baseStyle.48- Foreground(lipgloss.Color("#333333"))80+ Foreground(lipgloss.Color(colorEmptyDot))4981 5082 statusBarStyle = lipgloss.NewStyle().51- Foreground(lipgloss.Color("#888888")).83+ Foreground(lipgloss.Color(colorInfoText)).5284 MarginTop(1)5385 )5486 87+// isHighlightedNeighbor returns true if islandID is directly connectable88+// to the cursor (or selected) island in any cardinal direction.89+func isHighlightedNeighbor(m Model, islandID int) bool {90+ sourceID := m.cursorIsland91+ if m.selectedIsland != nil {92+ sourceID = *m.selectedIsland93+ }94+ if sourceID == islandID {95+ return false96+ }97+ for _, dir := range [][2]int{{0, -1}, {0, 1}, {-1, 0}, {1, 0}} {98+ adj := m.puzzle.FindAdjacentIsland(sourceID, dir[0], dir[1])99+ if adj != nil && adj.ID == islandID {100+ return true101+ }102+ }103+ return false104+}105+106+// emptyCellView renders an empty grid cell as a subtle dot.107+func emptyCellView(solved bool) string {108+ s := emptyStyle109+ if solved {110+ s = s.Foreground(lipgloss.Color(colorEmptySolved))111+ }112+ return s.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render("·")113+}114+55115 // gridView renders the puzzle using a display grid with gap cells between56116 // logical cells so that bridges between directly adjacent islands are visible.57117 // Display grid is (2*width-1) x (2*height-1): even coords are logical cells,@@ -86,72 +146,72 @@ func displayCellView(m Model, dx, dy int, solved bool) string {
86146 case cellIsland:87147 return islandView(m, ci.IslandID, solved)88148 case cellBridgeH:89- return bridgeHView(ci.BridgeCount)149+ return bridgeHView(ci.BridgeCount, solved)90150 case cellBridgeV:91- return bridgeVView(ci.BridgeCount)151+ return bridgeVView(ci.BridgeCount, solved)92152 default:93- return emptyStyle.Width(cellWidth).Render(" ")153+ return emptyCellView(solved)94154 }95155 }96156 97157 if !evenX && evenY {98158 // Horizontal gap between (dx/2, dy/2) and (dx/2+1, dy/2)99159 lx, ly := dx/2, dy/2100- return hGapView(m, lx, ly)160+ return hGapView(m, lx, ly, solved)101161 }102162 103163 if evenX && !evenY {104164 // Vertical gap between (dx/2, dy/2) and (dx/2, dy/2+1)105165 lx, ly := dx/2, dy/2106- return vGapView(m, lx, ly)166+ return vGapView(m, lx, ly, solved)107167 }108168 109169 // Odd x and odd y — intersection of gaps, always empty110- return emptyStyle.Width(cellWidth).Render(" ")170+ return baseStyle.Width(cellWidth).Render(" ")111171 }112172 113173 // hGapView renders the horizontal gap between logical cell (lx,ly) and (lx+1,ly).114174 // Shows a bridge connector if both neighbors are islands connected by a bridge.115-func hGapView(m Model, lx, ly int) string {175+func hGapView(m Model, lx, ly int, solved bool) string {116176 left := m.puzzle.FindIslandAt(lx, ly)117177 right := m.puzzle.FindIslandAt(lx+1, ly)118178 if left != nil && right != nil {119179 if b := m.puzzle.GetBridge(left.ID, right.ID); b != nil {120- return bridgeHView(b.Count)180+ return bridgeHView(b.Count, solved)121181 }122182 }123183 // Also check if a bridge passes through this gap (non-adjacent islands)124184 ci := m.puzzle.CellContent(lx, ly)125185 if ci.Kind == cellBridgeH {126- return bridgeHView(ci.BridgeCount)186+ return bridgeHView(ci.BridgeCount, solved)127187 }128188 ciR := m.puzzle.CellContent(lx+1, ly)129189 if ciR.Kind == cellBridgeH {130- return bridgeHView(ciR.BridgeCount)190+ return bridgeHView(ciR.BridgeCount, solved)131191 }132- return emptyStyle.Width(cellWidth).Render(" ")192+ return baseStyle.Width(cellWidth).Render(" ")133193 }134194 135195 // vGapView renders the vertical gap between logical cell (lx,ly) and (lx,ly+1).136196 // Shows a bridge connector if both neighbors are islands connected by a bridge.137-func vGapView(m Model, lx, ly int) string {197+func vGapView(m Model, lx, ly int, solved bool) string {138198 top := m.puzzle.FindIslandAt(lx, ly)139199 bottom := m.puzzle.FindIslandAt(lx, ly+1)140200 if top != nil && bottom != nil {141201 if b := m.puzzle.GetBridge(top.ID, bottom.ID); b != nil {142- return bridgeVView(b.Count)202+ return bridgeVView(b.Count, solved)143203 }144204 }145205 // Also check if a bridge passes through this gap (non-adjacent islands)146206 ci := m.puzzle.CellContent(lx, ly)147207 if ci.Kind == cellBridgeV {148- return bridgeVView(ci.BridgeCount)208+ return bridgeVView(ci.BridgeCount, solved)149209 }150210 ciB := m.puzzle.CellContent(lx, ly+1)151211 if ciB.Kind == cellBridgeV {152- return bridgeVView(ciB.BridgeCount)212+ return bridgeVView(ciB.BridgeCount, solved)153213 }154- return emptyStyle.Width(cellWidth).Render(" ")214+ return baseStyle.Width(cellWidth).Render(" ")155215 }156216 157217 func islandView(m Model, islandID int, solved bool) string {@@ -173,37 +233,53 @@ func islandView(m Model, islandID int, solved bool) string {
173233 style = islandSatisfiedStyle174234 } else if current > isl.Required {175235 style = islandOverStyle236+ } else if isHighlightedNeighbor(m, islandID) {237+ style = islandAdjacentStyle176238 }177239 178240 label := fmt.Sprintf("%d", isl.Required)179241 return style.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(label)180242 }181243 182-func bridgeHView(count int) string {244+func bridgeHView(count int, solved bool) string {183245 r := "───"184246 if count == 2 {185247 r = "═══"186248 }187- return bridgeHStyle.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(r)249+ s := bridgeHStyle250+ if solved {251+ s = bridgeHSolvedStyle252+ }253+ return s.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(r)188254 }189255 190-func bridgeVView(count int) string {256+func bridgeVView(count int, solved bool) string {191257 r := "│"192258 if count == 2 {193259 r = "║"194260 }195- return bridgeVStyle.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(r)261+ s := bridgeVStyle262+ if solved {263+ s = bridgeVSolvedStyle264+ }265+ return s.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(r)196266 }197267 198-func titleBarView(modeName string, solved bool) string {199- return game.TitleBarView("Hashiwokakero", modeName, solved)268+func titleBarView(modeTitle string, solved bool) string {269+ return game.TitleBarView("Hashiwokakero", modeTitle, solved)200270 }201271 202-func statusBarView(selected bool) string {272+func statusBarView(selected, showFullHelp bool) string {203273 if selected {204- return statusBarStyle.Render("arrows/wasd: build bridge enter/space/esc: deselect ctrl+n: menu ctrl+e: debug")274+ if showFullHelp {275+ return statusBarStyle.Render("arrows/wasd: build bridge enter/space/esc: deselect ctrl+n: menu ctrl+h: help")276+ }277+ return statusBarStyle.Render("arrows/wasd: build bridge enter/space/esc: deselect")205278 }206- return statusBarStyle.Render("arrows/wasd: move enter/space: select island ctrl+n: menu ctrl+e: debug")279+ if showFullHelp {280+ return statusBarStyle.Render("arrows/wasd: move enter/space: select island ctrl+n: menu ctrl+h: help")281+ }282+ return statusBarStyle.Render("enter/space: select island")207283 }208284 209285 func infoView(p *Puzzle) string {@@ -215,9 +291,12 @@ func infoView(p *Puzzle) string {
215291 }216292 }217293 294+ satisfiedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(colorInfoSatisfied))295+ infoStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(colorInfoText))296+218297 var sb strings.Builder219- sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render(220- fmt.Sprintf("Islands: %d/%d satisfied Bridges: %d", satisfied, total, len(p.Bridges)),221- ))298+ sb.WriteString(infoStyle.Render("Islands: "))299+ sb.WriteString(satisfiedStyle.Render(fmt.Sprintf("%d", satisfied)))300+ sb.WriteString(infoStyle.Render(fmt.Sprintf("/%d satisfied Bridges: %d", total, len(p.Bridges))))222301 return sb.String()223302 }@@ -72,6 +72,7 @@ type model struct {
7272 debug bool7373 debugRenderer *glamour.TermRenderer7474 debuginfo string75+ showFullHelp bool7576 7677 store *store.Store7778 activeGameID int64@@ -150,7 +151,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
150151 if err != nil {151152 return m, nil152153 }153- m.game = g154+ name := generateUniqueName(m.store)155+ m.game = g.SetTitle(name)156+ m.game, _ = m.game.Update(game.HelpToggleMsg{Show: m.showFullHelp})154157 m.state = gameView155158 m.completionSaved = false156159 @@ -160,7 +163,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
160163 log.Printf("failed to get initial save: %v", err)161164 return m, nil162165 }163- name := generateUniqueName(m.store)164166 rec := &store.GameRecord{165167 Name: name,166168 GameType: m.selectedCategory.Name,@@ -191,7 +193,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
191193 log.Printf("failed to import game: %v", err)192194 return m, nil193195 }194- m.game = g196+ m.game = g.SetTitle(rec.Name)197+ m.game, _ = m.game.Update(game.HelpToggleMsg{Show: m.showFullHelp})195198 m.activeGameID = rec.ID196199 m.state = gameView197200 m.completionSaved = rec.Status == store.StatusCompleted@@ -211,6 +214,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
211214 return m, tea.Quit212215 case tea.KeyCtrlE:213216 m.debug = !m.debug217+ case tea.KeyCtrlH:218+ m.showFullHelp = !m.showFullHelp219+ if m.state == gameView && m.game != nil {220+ m.game, _ = m.game.Update(game.HelpToggleMsg{Show: m.showFullHelp})221+ }214222 }215223 }216224 @@ -23,7 +23,8 @@ type Model struct {
2323 cursor game.Cursor2424 grid grid2525 keys KeyMap26- modeName string26+ modeTitle string27+ showFullHelp bool2728 }2829 2930 func New(mode NonogramMode, hints Hints, save ...string) (game.Gamer, error) {@@ -37,13 +38,13 @@ func New(mode NonogramMode, hints Hints, save ...string) (game.Gamer, error) {
3738 }3839 s := loadSave(createEmptyState(h, w), save...)3940 m := Model{40- width: w,41- height: h,42- rowHints: r,43- colHints: c,44- grid: newGrid(s),45- keys: DefaultKeyMap,46- modeName: mode.Title(),41+ width: w,42+ height: h,43+ rowHints: r,44+ colHints: c,45+ grid: newGrid(s),46+ keys: DefaultKeyMap,47+ modeTitle: mode.Title(),4748 }4849 4950 return m, nil@@ -55,6 +56,8 @@ func (m Model) Init() tea.Cmd {
5556 5657 func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {5758 switch msg := msg.(type) {59+ case game.HelpToggleMsg:60+ m.showFullHelp = msg.Show5861 case tea.KeyMsg:5962 switch {6063 case key.Matches(msg, m.keys.FillTile):@@ -77,12 +80,12 @@ func (m Model) View() string {
7780 7881 maxWidth, maxHeight := m.rowHints.RequiredLen()*cellWidth, m.colHints.RequiredLen()7982 80- title := nonoTitleBarView(m.modeName, solved)83+ title := nonoTitleBarView(m.modeTitle, solved)8184 g := gridView(m.grid, m.cursor, solved)8285 r := rowHintView(m.rowHints, maxWidth, curr.rows)8386 c := colHintView(m.colHints, maxHeight, curr.cols)8487 spacer := baseStyle.Width(maxWidth).Height(maxHeight).Render("")85- status := nonoStatusBarView(m.keys)88+ status := nonoStatusBarView(m.showFullHelp)8689 8790 s1 := lipgloss.JoinHorizontal(lipgloss.Bottom, spacer, c)8891 s2 := lipgloss.JoinHorizontal(lipgloss.Top, r, g)@@ -167,6 +170,11 @@ func intSliceStr(s []int) string {
167170 return result168171 }169172 173+func (m Model) SetTitle(t string) game.Gamer {174+ m.modeTitle = t175+ return m176+}177+170178 func (m Model) IsSolved() bool {171179 curr := generateTomography(m.grid)172180 return curr.rows.equal(m.rowHints) && curr.cols.equal(m.colHints)@@ -12,29 +12,29 @@ var (
1212 baseStyle = lipgloss.NewStyle()1313 1414 filledStyle = baseStyle.15- Foreground(lipgloss.Color("#ffffff")).16- Background(lipgloss.Color("#4a4a4a"))15+ Foreground(lipgloss.Color("#ffd599")).16+ Background(lipgloss.Color("#3d2e1a"))1717 1818 markedStyle = baseStyle.19- Foreground(lipgloss.Color("#ff6b6b")).20- Background(lipgloss.Color("#2a1a1a"))19+ Foreground(lipgloss.Color("#c97b5a")).20+ Background(lipgloss.Color("#2a1f17"))2121 2222 emptyStyle = baseStyle.23- Foreground(lipgloss.Color("#333333")).24- Background(lipgloss.Color("#1a1a1a"))23+ Foreground(lipgloss.Color("#4a3f35")).24+ Background(lipgloss.Color("#1e1a16"))2525 2626 cursorStyle = baseStyle.2727 Bold(true).28- Foreground(lipgloss.Color("#ffffff")).29- Background(lipgloss.Color("#ff00ff"))28+ Foreground(lipgloss.Color("#1e1a16")).29+ Background(lipgloss.Color("#f0a84a"))3030 31- crosshairBG = lipgloss.Color("#252525")31+ crosshairBG = lipgloss.Color("#2a2520")3232 3333 hintStyle = baseStyle.34- Foreground(lipgloss.Color("#888888"))34+ Foreground(lipgloss.Color("#8a7e72"))3535 3636 hintSatisfiedStyle = baseStyle.37- Foreground(lipgloss.Color("#00ff00"))37+ Foreground(lipgloss.Color("#b8d46a"))3838 3939 nonoStatusBarStyle = lipgloss.NewStyle().4040 Foreground(lipgloss.Color("#888888")).@@ -51,9 +51,9 @@ var (
5151 }5252 5353 renderRuneMap = map[rune]string{54- filledTile: "▐█",55- markedTile: "✕",56- emptyTile: " ",54+ filledTile: " ■ ",55+ markedTile: " ✕ ",56+ emptyTile: " · ",5757 }5858 )5959 @@ -165,12 +165,15 @@ func tileView(val rune, isCursor, inCursorRow, inCursorCol, solved bool) string
165165 return s.Width(cellWidth).AlignHorizontal(lipgloss.Center).Render(r)166166 }167167 168-func nonoTitleBarView(modeName string, solved bool) string {169- return game.TitleBarView("Nonogram", modeName, solved)168+func nonoTitleBarView(modeTitle string, solved bool) string {169+ return game.TitleBarView("Nonogram", modeTitle, solved)170170 }171171 172-func nonoStatusBarView(_ KeyMap) string {173- return nonoStatusBarStyle.Render("arrows/wasd: move z: fill x: mark bkspc: clear ctrl+n: menu ctrl+e: debug")172+func nonoStatusBarView(showFullHelp bool) string {173+ if showFullHelp {174+ return nonoStatusBarStyle.Render("arrows/wasd: move z: fill x: mark bkspc: clear ctrl+n: menu ctrl+h: help")175+ }176+ return nonoStatusBarStyle.Render("z: fill x: mark bkspc: clear")174177 }175178 176179 func intSliceEqual(a, b []int) bool {@@ -14,21 +14,22 @@ import (
1414 const gridSize = 91515 1616 type Model struct {17- cursor game.Cursor18- grid grid19- provided []cell20- keys KeyMap21- modeName string17+ cursor game.Cursor18+ grid grid19+ provided []cell20+ keys KeyMap21+ modeTitle string22+ showFullHelp bool2223 }2324 2425 // New creates a new sudoku game using the provided cell values.2526 func New(mode SudokuMode, provided []cell, save ...string) (game.Gamer, error) {2627 g := loadSave(newGrid(provided), save...)2728 m := Model{28- grid: g,29- provided: provided,30- keys: DefaultKeyMap,31- modeName: mode.Title(),29+ grid: g,30+ provided: provided,31+ keys: DefaultKeyMap,32+ modeTitle: mode.Title(),3233 }3334 return m, nil3435 }@@ -41,6 +42,8 @@ func (m Model) Init() tea.Cmd {
4142 // Update implements game.Gamer.4243 func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {4344 switch msg := msg.(type) {45+ case game.HelpToggleMsg:46+ m.showFullHelp = msg.Show4447 case tea.KeyMsg:4548 switch {4649 case key.Matches(msg, m.keys.FillValue):@@ -56,6 +59,11 @@ func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {
5659 return m, nil5760 }5861 62+func (m Model) SetTitle(t string) game.Gamer {63+ m.modeTitle = t64+ return m65+}66+5967 // IsSolved implements game.Gamer.6068 func (m Model) IsSolved() bool {6169 return m.isSolved()@@ -71,9 +79,9 @@ func (m *Model) updateCell(v int) {
7179 7280 // View implements game.Gamer.7381 func (m Model) View() string {74- title := titleBarView(m.modeName, m.isSolved())82+ title := titleBarView(m.modeTitle, m.isSolved())7583 grid := renderGrid(m)76- status := statusBarView()84+ status := statusBarView(m.showFullHelp)7785 7886 return lipgloss.JoinVertical(lipgloss.Left, title, grid, status)7987 }@@ -174,10 +174,13 @@ func hasConflict(m Model, c cell, x, y int) bool {
174174 return false175175 }176176 177-func titleBarView(modeName string, solved bool) string {178- return game.TitleBarView("Sudoku", modeName, solved)177+func titleBarView(modeTitle string, solved bool) string {178+ return game.TitleBarView("Sudoku", modeTitle, solved)179179 }180180 181-func statusBarView() string {182- return statusBarStyle.Render("arrows/wasd: move 1-9: fill bkspc: clear ctrl+n: menu ctrl+e: debug")181+func statusBarView(showFullHelp bool) string {182+ if showFullHelp {183+ return statusBarStyle.Render("arrows/wasd: move 1-9: fill bkspc: clear ctrl+n: menu ctrl+h: help")184+ }185+ return statusBarStyle.Render("1-9: fill bkspc: clear")183186 }@@ -25,8 +25,9 @@ type Model struct {
2525 selection selectionState2626 selectionStart game.Cursor2727 keys KeyMap28- modeName string28+ modeTitle string2929 solved bool30+ showFullHelp bool3031 }3132 3233 // New creates a new word search game@@ -38,7 +39,7 @@ func New(mode WordSearchMode, g grid, words []Word) *Model {
3839 words: words,3940 selection: noSelection,4041 keys: DefaultKeyMap,41- modeName: mode.Title(),42+ modeTitle: mode.Title(),4243 solved: false,4344 }4445 }@@ -49,6 +50,8 @@ func (m Model) Init() tea.Cmd {
4950 5051 func (m Model) Update(msg tea.Msg) (game.Gamer, tea.Cmd) {5152 switch msg := msg.(type) {53+ case game.HelpToggleMsg:54+ m.showFullHelp = msg.Show5255 case tea.KeyMsg:5356 return m.handleKeyPress(msg)5457 }@@ -215,6 +218,11 @@ func (m Model) GetSave() ([]byte, error) {
215218 return json.Marshal(data)216219 }217220 221+func (m Model) SetTitle(t string) game.Gamer {222+ m.modeTitle = t223+ return m224+}225+218226 func (m Model) IsSolved() bool {219227 return m.solved220228 }@@ -43,7 +43,7 @@ var (
4343 )4444 4545 func renderView(m Model) string {46- title := game.TitleBarView("Word Search", m.modeName, m.solved)46+ title := game.TitleBarView("Word Search", m.modeTitle, m.solved)4747 gridView := renderGrid(m)4848 wordListView := renderWordList(m)4949 @@ -51,13 +51,16 @@ func renderView(m Model) string {
5151 spacer := strings.Repeat(" ", 4)5252 mainView := lipgloss.JoinHorizontal(lipgloss.Top, gridView, spacer, wordListView)5353 54- status := statusBarView()54+ status := statusBarView(m.showFullHelp)5555 5656 return lipgloss.JoinVertical(lipgloss.Left, title, mainView, status)5757 }5858 59-func statusBarView() string {60- return statusBarStyle.Render("arrows/wasd: move enter/space: select esc: cancel ctrl+n: menu ctrl+e: debug")59+func statusBarView(showFullHelp bool) string {60+ if showFullHelp {61+ return statusBarStyle.Render("arrows/wasd: move enter/space: select esc: cancel ctrl+n: menu ctrl+h: help")62+ }63+ return statusBarStyle.Render("enter/space: select esc: cancel")6164 }6265 6366 func renderGrid(m Model) string {