- commit
- bba30fc732861cc900644f978c32036f7d1c66bb
- parent
- 1b4d049a157fc830880b4bc665e9f994163d9d36
- author
- Dami <dami.etoile@gmail.com>
| M | game/ | 1 | + |
| M | hashiwokakero/ | 4 | + |
| A | hashiwokakero/ | 45 | ++++++++++++++++ |
| M | hitori/ | 4 | + |
| A | hitori/ | 37 | ++++++++++++++ |
| M | lightsout/ | 4 | + |
| A | lightsout/ | 32 | ++++++++++++ |
| M | model.go | 82 | +++++++++++++++++++++++++++--- |
| M | nonogram/ | 4 | + |
| A | nonogram/ | 37 | ++++++++++++++ |
| M | sudoku/ | 4 | + |
| A | sudoku/ | 34 | ++++++++++++ |
| M | takuzu/ | 4 | + |
| A | takuzu/ | 35 | +++++++++++++ |
| M | wordsearch/ | 4 | + |
| A | wordsearch/ | 35 | +++++++++++++ |
@@ -1,12 +1,16 @@
11 package hashiwokakero22 33 import (4+ _ "embed"45 "math/rand/v2"56 67 "github.com/FelineStateMachine/puzzletea/game"78 "github.com/charmbracelet/bubbles/list"89 )910 11+//go:embed help.md12+var HelpContent string13+1014 func init() {1115 game.Register("Hashiwokakero", func(data []byte) (game.Gamer, error) {1216 return ImportModel(data)@@ -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.@@ -1,12 +1,16 @@
11 package hitori22 33 import (4+ _ "embed"45 "math/rand/v2"56 67 "github.com/FelineStateMachine/puzzletea/game"78 "github.com/charmbracelet/bubbles/list"89 )910 11+//go:embed help.md12+var HelpContent string13+1014 func init() {1115 game.Register("Hitori", func(data []byte) (game.Gamer, error) {1216 return ImportModel(data)@@ -0,0 +1,37 @@
1+# Hitori2+3+Shade cells to eliminate duplicate numbers in every row and column.4+5+## Rules6+7+- **No duplicates**: No number may appear more than once in any row or8+ column among the unshaded (white) cells.9+- **No adjacent shading**: No two shaded cells may share an edge.10+ Diagonal contact is fine.11+- **White connectivity**: All unshaded cells must form a single12+ orthogonally connected region.13+14+The puzzle is solved when all three rules hold simultaneously.15+16+## Controls17+18+| Key | Action |19+|-----|--------|20+| `Arrows` / `wasd` / `hjkl` | Move cursor |21+| `x` | Toggle shade on cell |22+| `z` | Toggle circle (confirmed white) |23+| `Backspace` | Clear cell to unmarked |24+| `Ctrl+H` | Toggle help bar |25+| `Ctrl+R` | Reset puzzle |26+| `Ctrl+N` | Return to main menu |27+28+## Tips29+30+- **Circle the unique numbers first.** If a number appears only once in31+ its row *and* column, it can never be shaded. Circling these cells32+ gives you safe anchors to reason from.33+- **Watch for forced shading.** When a number appears twice in a row or34+ column and one copy is already circled, the other copy must be shaded.35+- **Protect connectivity.** Before shading a cell, check whether it36+ would split the white region into disconnected pieces. Cells that act37+ as bridges between unshaded areas should almost always stay white.@@ -1,12 +1,16 @@
11 package lightsout22 33 import (4+ _ "embed"45 "math/rand/v2"56 67 "github.com/FelineStateMachine/puzzletea/game"78 "github.com/charmbracelet/bubbles/list"89 )910 11+//go:embed help.md12+var HelpContent string13+1014 // Register the import function for save games.1115 func init() {1216 game.Register("Lights Out", func(data []byte) (game.Gamer, error) {@@ -0,0 +1,32 @@
1+# Lights Out2+3+Toggle lights in a cross pattern to turn them all off.4+5+## Rules6+7+- The grid starts with some lights **on** and some **off**.8+- Toggling a cell flips that cell *and* its four orthogonal neighbors9+ (up, down, left, right).10+- Cells on edges and corners have fewer neighbors, so fewer lights flip.11+- The goal is to turn **every light off**.12+13+## Controls14+15+| Key | Action |16+|-----|--------|17+| `Arrows` / `wasd` / `hjkl` | Move cursor |18+| `Enter` / `Space` | Toggle light |19+| `Ctrl+H` | Toggle help bar |20+| `Ctrl+R` | Reset puzzle |21+| `Ctrl+N` | Return to main menu |22+23+## Tips24+25+- **Toggling the same cell twice cancels out.** Every move is its own26+ undo, so if you make a mistake, just press the same cell again.27+- **Work row by row from top to bottom.** Turn off all the lights in the28+ current row by toggling cells in the row below it, then move on. This29+ keeps solved rows from getting scrambled.30+- **Plan around edges and corners.** Corner cells only affect two31+ neighbors and edge cells only affect three, which limits your options.32+ Tackle these constrained areas first when you can.@@ -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)@@ -1,12 +1,16 @@
11 package nonogram22 33 import (4+ _ "embed"45 "math/rand/v2"56 67 "github.com/FelineStateMachine/puzzletea/game"78 "github.com/charmbracelet/bubbles/list"89 )910 11+//go:embed help.md12+var HelpContent string13+1014 func init() {1115 game.Register("Nonogram", func(data []byte) (game.Gamer, error) {1216 return ImportModel(data)@@ -0,0 +1,37 @@
1+# Nonogram2+3+Fill cells to match row and column hints, revealing a hidden pattern.4+5+## Rules6+7+- Each row and column has numeric **hints** that tell you how many8+ consecutive filled cells appear in that line, in order.9+- **Filled** cells count toward satisfying a hint.10+ **Marked** cells are visual reminders that a cell should stay empty.11+- Hints turn **green** when their row or column is correctly satisfied.12+- The puzzle is solved when every row and column matches its hints.13+14+## Controls15+16+| Key | Action |17+|-----|--------|18+| `Arrows` / `wasd` / `hjkl` | Move cursor |19+| `z` | Fill cell |20+| `x` | Mark cell |21+| `Backspace` | Clear cell |22+| `Ctrl+H` | Toggle help bar |23+| `Ctrl+R` | Reset puzzle |24+| `Ctrl+N` | Return to main menu |25+26+## Tips27+28+- **Start with the largest hints.** If a hint is close to the row or29+ column length, many cells are forced to be filled regardless of30+ alignment. Solve these first to unlock easier deductions elsewhere.31+- **Mark cells you know are empty.** Marks cost nothing and prevent32+ mistakes later. After filling a complete group, mark the cells on33+ either side so you don't accidentally extend it.34+- **Look for rows or columns whose hints nearly fill the line.** When35+ the sum of hints plus the required gaps between them equals (or36+ nearly equals) the line length, most cells can be determined37+ immediately.@@ -1,12 +1,16 @@
11 package sudoku22 33 import (4+ _ "embed"45 "math/rand/v2"56 67 "github.com/FelineStateMachine/puzzletea/game"78 "github.com/charmbracelet/bubbles/list"89 )910 11+//go:embed help.md12+var HelpContent string13+1014 func init() {1115 game.Register("Sudoku", func(data []byte) (game.Gamer, error) {1216 return ImportModel(data)@@ -0,0 +1,34 @@
1+# Sudoku2+3+Fill a 9x9 grid with digits so every row, column, and box is complete.4+5+## Rules6+7+- Each **row** must contain the digits 1 through 9 exactly once.8+- Each **column** must contain the digits 1 through 9 exactly once.9+- Each **3x3 box** must contain the digits 1 through 9 exactly once.10+- Pre-filled cells (shown in **bold**) cannot be changed. Conflicting11+ cells are highlighted in red.12+13+## Controls14+15+| Key | Action |16+|-----|--------|17+| `Arrows` / `wasd` / `hjkl` | Move cursor |18+| `1`-`9` | Fill cell with digit |19+| `Backspace` | Clear cell |20+| `Ctrl+H` | Toggle help bar |21+| `Ctrl+R` | Reset puzzle |22+| `Ctrl+N` | Return to main menu |23+24+## Tips25+26+- **Scan the most-filled groups first.** Rows, columns, or boxes that27+ already have seven or eight digits filled leave very few possibilities28+ for the remaining cells, making them the easiest to solve.29+- **Look for naked singles.** If a cell has only one digit that doesn't30+ conflict with its row, column, and box, that digit is the only option.31+ Check the most constrained cells before guessing anywhere.32+- **Use the conflict highlighting.** When you place a digit, conflicting33+ cells turn red immediately. Catch mistakes early so they don't cascade34+ across the board.@@ -1,12 +1,16 @@
11 package takuzu22 33 import (4+ _ "embed"45 "math/rand/v2"56 67 "github.com/FelineStateMachine/puzzletea/game"78 "github.com/charmbracelet/bubbles/list"89 )910 11+//go:embed help.md12+var HelpContent string13+1014 func init() {1115 game.Register("Takuzu", func(data []byte) (game.Gamer, error) {1216 return ImportModel(data)@@ -0,0 +1,35 @@
1+# Takuzu2+3+Fill the grid with two symbols so that every row and column obeys three simple rules.4+5+## Rules6+7+- **No triples**: Three consecutive identical symbols in a row or column are not allowed.8+- **Equal count**: Each row and column must contain exactly the same number of ● and ○.9+- **Unique lines**: No two rows may be identical, and no two columns may be identical.10+11+Pre-filled cells (shown in **bold**) are locked and cannot be changed. The puzzle is12+solved when every cell is filled and all three rules are satisfied.13+14+## Controls15+16+| Key | Action |17+|-----|--------|18+| `Arrows` / `wasd` / `hjkl` | Move cursor |19+| `z` / `0` | Place ● (filled) |20+| `x` / `1` | Place ○ (open) |21+| `Backspace` | Clear cell |22+| `Ctrl+H` | Toggle help bar |23+| `Ctrl+R` | Reset puzzle |24+| `Ctrl+N` | Return to main menu |25+26+## Tips27+28+- **Sandwich rule**: If two identical symbols are next to each other, the cells on29+ either side *must* be the opposite symbol. For example, ● ● means the neighbours30+ are both ○.31+- **Count what you have**: When a row or column already has its full quota of one32+ symbol, every remaining empty cell in that line must be the other symbol.33+- **Avoid the third**: Before placing a symbol, glance one and two cells ahead in34+ each direction. If placing it would create three in a row, it has to be the35+ other symbol.@@ -1,12 +1,16 @@
11 package wordsearch22 33 import (4+ _ "embed"45 "math/rand/v2"56 67 "github.com/FelineStateMachine/puzzletea/game"78 "github.com/charmbracelet/bubbles/list"89 )910 11+//go:embed help.md12+var HelpContent string13+1014 func init() {1115 game.Register("Word Search", func(data []byte) (game.Gamer, error) {1216 return ImportModel(data)@@ -0,0 +1,35 @@
1+# Word Search2+3+Find hidden words in a letter grid.4+5+## Rules6+7+- Words are hidden in the grid horizontally, vertically, and diagonally --8+ including **backwards** in all directions (8 total).9+- Found words are highlighted in green and crossed off the word list.10+- The puzzle is solved when every word has been found.11+12+To select a word, move to its first letter and press `Enter` or `Space`,13+navigate to the last letter, then press `Enter` or `Space` to confirm.14+Press `Escape` to cancel a selection.15+16+## Controls17+18+| Key | Action |19+|-----|--------|20+| `Arrows` / `wasd` / `hjkl` | Move cursor |21+| `Enter` / `Space` | Select start or confirm end |22+| `Escape` | Cancel current selection |23+| `Ctrl+H` | Toggle help bar |24+| `Ctrl+R` | Reset puzzle |25+| `Ctrl+N` | Return to main menu |26+27+## Tips28+29+- **Start with uncommon letters.** Letters like Q, Z, X, and J appear far30+ less often in the grid, so they are easy to spot and narrow down where a31+ word must be.32+- **Study the word list first.** Knowing exactly which words you need helps33+ you recognise patterns in the grid instead of scanning aimlessly.34+- **Check all 8 directions.** Words can run left, right, up, down, and along35+ all four diagonals -- don't forget to look backwards and diagonally.