clone

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

add help.md files and menu for guides

Dami committed bba30fc
commit
bba30fc732861cc900644f978c32036f7d1c66bb
parent
1b4d049a157fc830880b4bc665e9f994163d9d36
author
Dami <dami.etoile@gmail.com>
16 files changed, +359 -7
Mgame/gamer.go1+
Mhashiwokakero/Gamemode.go4+
Ahashiwokakero/help.md45++++++++++++++++
Mhitori/Gamemode.go4+
Ahitori/help.md37++++++++++++++
Mlightsout/Gamemode.go4+
Alightsout/help.md32++++++++++++
Mmodel.go82+++++++++++++++++++++++++++---
Mnonogram/Gamemode.go4+
Anonogram/help.md37++++++++++++++
Msudoku/Gamemode.go4+
Asudoku/help.md34++++++++++++
Mtakuzu/Gamemode.go4+
Atakuzu/help.md35+++++++++++++
Mwordsearch/Gamemode.go4+
Awordsearch/help.md35+++++++++++++
M game/gamer.go +1 -0
M hashiwokakero/Gamemode.go +4 -0
A hashiwokakero/help.md +45 -0
@@ -0,0 +1,45 @@
1+# Hashiwokakero2+3+Connect numbered islands with bridges to form a single connected group.4+5+## Rules6+7+- Each island displays a number indicating exactly how many bridges must connect to it.8+- Bridges run **horizontally** or **vertically** between adjacent islands.9+- Each pair of islands can be connected by a **single** or **double** bridge, but bridges may never cross.10+- The puzzle is solved when every island has the correct number of bridges and all islands form one connected group.11+12+## Controls13+14+### Navigation Mode15+16+| Key | Action |17+|-----|--------|18+| `Arrows` / `wasd` / `hjkl` | Jump to nearest island |19+| `Enter` / `Space` | Select island |20+21+### Bridge Mode22+23+After selecting an island, you enter Bridge Mode.24+25+| Key | Action |26+|-----|--------|27+| `Arrows` / `wasd` / `hjkl` | Build or cycle bridge in direction |28+| `Enter` / `Space` / `Escape` | Deselect island |29+30+Pressing a direction once builds a single bridge, twice upgrades to a31+double bridge, and a third time removes it.32+33+---34+35+| Key | Action |36+|-----|--------|37+| `Ctrl+R` | Reset puzzle |38+| `Ctrl+H` | Toggle help bar |39+| `Ctrl+N` | Return to main menu |40+41+## Tips42+43+- **Start with extremes.** Islands showing their maximum value (like an `8` in the interior or a `4` in a corner) have only one valid configuration -- fill those in first.44+- **Look for forced connections.** An island with only one neighboring island in each required direction has no choice; those bridges are guaranteed.45+- **Work from the edges inward.** Corner and border islands have fewer possible neighbors, making them easier to solve early and giving you constraints to propagate.
M hitori/Gamemode.go +4 -0
A hitori/help.md +37 -0
M lightsout/Gamemode.go +4 -0
A lightsout/help.md +32 -0
M model.go +75 -7
@@ -20,6 +20,7 @@ import (
2020 "github.com/charmbracelet/bubbles/list"2121 "github.com/charmbracelet/bubbles/spinner"2222 "github.com/charmbracelet/bubbles/table"23+ "github.com/charmbracelet/bubbles/viewport"2324 tea "github.com/charmbracelet/bubbletea"2425 "github.com/charmbracelet/glamour"2526 "github.com/charmbracelet/lipgloss"
@@ -38,13 +39,13 @@ var (
3839 debugStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).BorderForeground(lipgloss.Color("124"))3940 4041 GameCategories = []list.Item{41- game.Category{Name: "Hashiwokakero", Desc: "Connect islands with bridges.", Modes: hashiwokakero.Modes},42- game.Category{Name: "Hitori", Desc: "Shade cells to eliminate duplicates.", Modes: hitori.Modes},43- game.Category{Name: "Lights Out", Desc: "Turn off all the lights.", Modes: lightsout.Modes},44- game.Category{Name: "Nonogram", Desc: "Fill cells to match row and column hints.", Modes: nonogram.Modes},45- game.Category{Name: "Sudoku", Desc: "Fill the 9x9 grid following sudoku rules.", Modes: sudoku.Modes},46- game.Category{Name: "Takuzu", Desc: "Fill the grid with ● and ○.", Modes: takuzu.Modes},47- game.Category{Name: "Word Search", Desc: "Find hidden words in a letter grid.", Modes: wordsearch.Modes},42+ game.Category{Name: "Hashiwokakero", Desc: "Connect islands with bridges.", Modes: hashiwokakero.Modes, Help: hashiwokakero.HelpContent},43+ game.Category{Name: "Hitori", Desc: "Shade cells to eliminate duplicates.", Modes: hitori.Modes, Help: hitori.HelpContent},44+ game.Category{Name: "Lights Out", Desc: "Turn off all the lights.", Modes: lightsout.Modes, Help: lightsout.HelpContent},45+ game.Category{Name: "Nonogram", Desc: "Fill cells to match row and column hints.", Modes: nonogram.Modes, Help: nonogram.HelpContent},46+ game.Category{Name: "Sudoku", Desc: "Fill the 9x9 grid following sudoku rules.", Modes: sudoku.Modes, Help: sudoku.HelpContent},47+ game.Category{Name: "Takuzu", Desc: "Fill the grid with ● and ○.", Modes: takuzu.Modes, Help: takuzu.HelpContent},48+ game.Category{Name: "Word Search", Desc: "Find hidden words in a letter grid.", Modes: wordsearch.Modes, Help: wordsearch.HelpContent},4849 }4950 )5051
@@ -55,6 +56,8 @@ const (
5556 generatingView5657 gameView5758 continueView59+ helpSelectView60+ helpDetailView5861 )5962 6063 type menuItem struct {
@@ -70,6 +73,7 @@ var mainMenuItems = []list.Item{
7073 menuItem{title: "Daily Puzzle", desc: time.Now().Format("Jan _2 06")},7174 menuItem{title: "Generate", desc: "a new puzzle."},7275 menuItem{title: "Continue", desc: "a previous puzzle."},76+ menuItem{title: "Guides", desc: "to learn."},7377 menuItem{title: "Quit", desc: "the game."},7478 }7579
@@ -107,6 +111,11 @@ type model struct {
107111 dailyName string // pre-computed daily name108112 dailyGameType string // e.g. "Nonogram"109113 dailyModeTitle string // e.g. "Standard"114+115+ // Help page state116+ helpSelectList list.Model117+ helpCategory game.Category118+ helpViewport viewport.Model110119 }111120 112121 func initialModel(s *store.Store) model {
@@ -174,6 +183,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
174183 m.continueTable.SetWidth(w)175184 m.continueTable.SetHeight(ht)176185 }186+ if m.state == helpSelectView {187+ m.helpSelectList.SetSize(menuW, menuH)188+ }189+ if m.state == helpDetailView {190+ m.helpViewport.Width = w191+ m.helpViewport.Height = ht - 2192+ }177193 178194 case tea.KeyMsg:179195 // During generation, only allow Escape (to cancel) and Ctrl+C (to quit).
@@ -244,6 +260,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
244260 m.modeSelectList, cmd = m.modeSelectList.Update(msg)245261 case continueView:246262 m.continueTable, cmd = m.continueTable.Update(msg)263+ case helpSelectView:264+ m.helpSelectList, cmd = m.helpSelectList.Update(msg)265+ case helpDetailView:266+ m.helpViewport, cmd = m.helpViewport.Update(msg)247267 }248268 249269 return m, cmd
@@ -259,6 +279,8 @@ func (m model) handleEnter() (tea.Model, tea.Cmd) {
259279 return m.handleModeSelectEnter()260280 case continueView:261281 return m.handleContinueEnter()282+ case helpSelectView:283+ return m.handleHelpSelectEnter()262284 }263285 return m, nil264286 }
@@ -276,6 +298,10 @@ func (m model) handleMainMenuEnter() (tea.Model, tea.Cmd) {
276298 case "Continue":277299 m.initContinueTable()278300 m.state = continueView301+ case "Guides":302+ m.helpSelectList = initList(GameCategories, "How to Play")303+ m.helpSelectList.SetSize(min(m.width, 64), min(m.height, 24))304+ m.state = helpSelectView279305 case "Quit":280306 return m, tea.Quit281307 }
@@ -452,6 +478,33 @@ func (m model) handleContinueEnter() (tea.Model, tea.Cmd) {
452478 return m, nil453479 }454480 481+func (m model) handleHelpSelectEnter() (tea.Model, tea.Cmd) {482+ cat, ok := m.helpSelectList.SelectedItem().(game.Category)483+ if !ok {484+ return m, nil485+ }486+ m.helpCategory = cat487+488+ renderer, err := glamour.NewTermRenderer(489+ glamour.WithAutoStyle(),490+ glamour.WithWordWrap(m.width),491+ )492+ if err != nil {493+ log.Printf("failed to create help renderer: %v", err)494+ return m, nil495+ }496+ rendered, err := renderer.Render(cat.Help)497+ if err != nil {498+ log.Printf("failed to render help: %v", err)499+ return m, nil500+ }501+502+ m.helpViewport = viewport.New(m.width, m.height-2)503+ m.helpViewport.SetContent(rendered)504+ m.state = helpDetailView505+ return m, nil506+}507+455508 func (m model) handleEscape() (tea.Model, tea.Cmd) {456509 switch m.state {457510 case generatingView:
@@ -460,6 +513,10 @@ func (m model) handleEscape() (tea.Model, tea.Cmd) {
460513 m.state = gameSelectView461514 case gameSelectView, continueView:462515 m.state = mainMenuView516+ case helpDetailView:517+ m.state = helpSelectView518+ case helpSelectView:519+ m.state = mainMenuView463520 }464521 return m, nil465522 }
@@ -502,6 +559,17 @@ func (m model) View() string {
502559 debugInfo,503560 )504561 return rootStyle.Render(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, s))562+ case helpSelectView:563+ return rootStyle.Render(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, m.helpSelectList.View()))564+ case helpDetailView:565+ footer := lipgloss.NewStyle().566+ Foreground(menuTextDim).567+ Render("↑/↓ scroll • esc back")568+ s := lipgloss.JoinVertical(lipgloss.Left,569+ m.helpViewport.View(),570+ footer,571+ )572+ return rootStyle.Render(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, s))505573 default:506574 s := fmt.Sprintf("unknown state: %d", m.state)507575 panic(s)
M nonogram/Gamemode.go +4 -0
A nonogram/help.md +37 -0
M sudoku/Gamemode.go +4 -0
A sudoku/help.md +34 -0
M takuzu/Gamemode.go +4 -0
A takuzu/help.md +35 -0
M wordsearch/Gamemode.go +4 -0
A wordsearch/help.md +35 -0