clone

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

removed core spawner to respect open/close

Dami committed a53abec
commit
a53abecafb98b5ef02abe5260cf40d111d0abe6c
parent
e3f4b9470ff02a37547a20e16ee0cd0727232857
author
Dami <dami@Damis-MacBook-Air.local>
11 files changed, +184 -127
Dcore.go31---------
Mgame/gamer.go38++++++++++++
Mhashiwokakero/Gamemode.go43+++++++------
Mhashiwokakero/Model.go2+-
Mmodel.go99+++++++++++++++++++-----------
Mnonogram/Gamemode.go30++++++---
Mnonogram/Model.go2+-
Msudoku/Gamemode.go23++++---
Msudoku/Model.go2+-
Mwordsearch/Gamemode.go30++++++---
Mwordsearch/Model.go11+--
D core.go +0 -31
M game/gamer.go +38 -0
@@ -2,9 +2,11 @@ package game
22 33 import (44 "github.com/charmbracelet/bubbles/key"5+ "github.com/charmbracelet/bubbles/list"56 tea "github.com/charmbracelet/bubbletea"67 )78 9+// Gamer is the interface that an active game instance must implement.810 type Gamer interface {911 GetDebugInfo() string1012 GetFullHelp() [][]key.Binding
@@ -16,8 +18,44 @@ type Gamer interface {
1618 Update(msg tea.Msg) (Gamer, tea.Cmd)1719 }1820 21+// Mode describes a game mode for menu display.1922 type Mode interface {2023 Title() string2124 Description() string2225 FilterValue() string2326 }27+28+// Spawner creates a new game instance. Implemented by concrete mode types29+// but kept separate from Mode so that Category (which also satisfies Mode)30+// does not need a Spawn method.31+type Spawner interface {32+ Spawn() (Gamer, error)33+}34+35+// BaseMode provides the common title/description fields and the three36+// Mode-interface methods so that concrete mode structs can embed it37+// instead of duplicating boilerplate.38+type BaseMode struct {39+ title string40+ description string41+}42+43+// NewBaseMode creates a BaseMode with the given title and description.44+func NewBaseMode(title, description string) BaseMode {45+ return BaseMode{title: title, description: description}46+}47+48+func (b BaseMode) Title() string { return b.title }49+func (b BaseMode) Description() string { return b.description }50+func (b BaseMode) FilterValue() string { return b.title + " " + b.description }51+52+// Category groups related game modes under a heading in the menu.53+type Category struct {54+ Name string55+ Desc string56+ Modes []list.Item57+}58+59+func (c Category) Title() string { return c.Name }60+func (c Category) Description() string { return c.Desc }61+func (c Category) FilterValue() string { return c.Name }
M hashiwokakero/Gamemode.go +24 -19
@@ -2,33 +2,38 @@ package hashiwokakero
22 33 import (44 "github.com/FelineStateMachine/puzzletea/game"5+ "github.com/charmbracelet/bubbles/list"56 )67 78 // HashiMode defines a hashiwokakero difficulty/configuration.89 type HashiMode struct {9- title string10- description string11- Width int12- Height int13- MinIslands int14- MaxIslands int10+ game.BaseMode11+ Width int12+ Height int13+ MinIslands int14+ MaxIslands int1515 }1616 17-var _ game.Mode = HashiMode{} // compile-time interface check18-19-func (h HashiMode) Title() string { return "hashiwokakero\t" + h.title }20-func (h HashiMode) Description() string { return h.description }21-func (h HashiMode) FilterValue() string {22- return "hashiwokakero bridges hashi " + h.title + " " + h.description23-}17+var _ game.Mode = HashiMode{} // compile-time interface check18+var _ game.Spawner = HashiMode{} // compile-time interface check2419 2520 func NewMode(title, description string, width, height, minIslands, maxIslands int) HashiMode {2621 return HashiMode{27- title: title,28- description: description,29- Width: width,30- Height: height,31- MinIslands: minIslands,32- MaxIslands: maxIslands,22+ BaseMode: game.NewBaseMode(title, description),23+ Width: width,24+ Height: height,25+ MinIslands: minIslands,26+ MaxIslands: maxIslands,3327 }3428 }29+30+func (h HashiMode) Spawn() (game.Gamer, error) {31+ puzzle := GeneratePuzzle(h)32+ return New(h, puzzle), nil33+}34+35+var Modes = []list.Item{36+ NewMode("Easy - 7x7", "Connect islands with bridges on a 7x7 grid.", 7, 7, 6, 8),37+ NewMode("Medium - 9x9", "Connect islands with bridges on a 9x9 grid.", 9, 9, 10, 14),38+ NewMode("Hard - 13x13", "Connect islands with bridges on a 13x13 grid.", 13, 13, 18, 24),39+}
M hashiwokakero/Model.go +1 -1
M model.go +62 -37
@@ -18,34 +18,26 @@ var (
1818 rootStyle = lipgloss.NewStyle().Margin(1, 2)1919 debugStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).BorderForeground(lipgloss.Color("124"))2020 21- GameModes = []list.Item{22- nonogram.NewMode("Easy - 5x5", "A random nonogram on a five by five board.", 5, 5),23- nonogram.NewMode("Medium - 10x10", "A random nonogram on a ten by ten board.", 10, 10),24- nonogram.NewMode("Hard - 15x15", "A random nonogram on a fifteen by fifteen board.", 15, 15),25- nonogram.NewMode("Extra - 5x10", "A random nonogram on a five by ten board.", 5, 10),26-27- wordsearch.NewMode("Easy - 10x10", "Find 6 words in a 10x10 grid.", 10, 10, 6, 3, 5, []wordsearch.Direction{wordsearch.Right, wordsearch.Down, wordsearch.DownRight}),28- wordsearch.NewMode("Medium - 15x15", "Find 10 words in a 15x15 grid.", 15, 15, 10, 4, 7, []wordsearch.Direction{wordsearch.Right, wordsearch.Down, wordsearch.DownRight, wordsearch.DownLeft, wordsearch.Left, wordsearch.Up}),29- wordsearch.NewMode("Hard - 20x20", "Find 15 words in a 20x20 grid.", 20, 20, 15, 5, 10, []wordsearch.Direction{wordsearch.Right, wordsearch.Down, wordsearch.DownRight, wordsearch.DownLeft, wordsearch.Left, wordsearch.Up, wordsearch.UpRight, wordsearch.UpLeft}),30-31- sudoku.NewMode("Easy - 38 Provided Cells", "A random sudoku with at least 38 cells provided to start.", 38),32- sudoku.NewMode("Hard - 26 Provided Cells", "A random sudoku with at least 26 cells provided to start.", 26),33-34- hashiwokakero.NewMode("Easy - 7x7", "Connect islands with bridges on a 7x7 grid.", 7, 7, 6, 8),35- hashiwokakero.NewMode("Medium - 9x9", "Connect islands with bridges on a 9x9 grid.", 9, 9, 10, 14),36- hashiwokakero.NewMode("Hard - 13x13", "Connect islands with bridges on a 13x13 grid.", 13, 13, 18, 24),21+ GameCategories = []list.Item{22+ game.Category{Name: "Nonogram", Desc: "Fill cells to match row and column hints.", Modes: nonogram.Modes},23+ game.Category{Name: "Word Search", Desc: "Find hidden words in a letter grid.", Modes: wordsearch.Modes},24+ game.Category{Name: "Sudoku", Desc: "Fill the 9x9 grid following sudoku rules.", Modes: sudoku.Modes},25+ game.Category{Name: "Hashiwokakero", Desc: "Connect islands with bridges.", Modes: hashiwokakero.Modes},3726 }3827 )3928 4029 const (41- menuView = iota30+ gameSelectView = iota31+ modeSelectView4232 gameView4333 )4434 4535 type model struct {4636 state int4737 48- gamemodesList list.Model38+ gameSelectList list.Model39+ modeSelectList list.Model40+ selectedCategory game.Category4941 5042 mode game.Mode5143 game game.Gamer
@@ -57,10 +49,10 @@ type model struct {
5749 5850 func initialModel() model {5951 r := initDebugRenderer()60- l := initGameModeList()52+ l := initGameSelectList()6153 return model{62- debugRenderer: r,63- gamemodesList: l,54+ debugRenderer: r,55+ gameSelectList: l,6456 }6557 }6658
@@ -74,20 +66,40 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7466 switch msg := msg.(type) {7567 case tea.WindowSizeMsg:7668 h, v := rootStyle.GetFrameSize()77- m.gamemodesList.SetSize(msg.Width-h, msg.Height-v)69+ w, ht := msg.Width-h, msg.Height-v70+ m.gameSelectList.SetSize(w, ht)71+ if m.state == modeSelectView {72+ m.modeSelectList.SetSize(w, ht)73+ }7874 7975 case tea.KeyMsg:8076 switch msg.Type {8177 case tea.KeyCtrlN:82- m.state = menuView78+ m.state = gameSelectView8379 m.debug = false8480 case tea.KeyEnter:85- if m.state == menuView {86- m.mode, _ = m.gamemodesList.SelectedItem().(game.Mode)87- m.game, _ = m.SpawnGame(m.mode)81+ if m.state == gameSelectView {82+ cat, ok := m.gameSelectList.SelectedItem().(game.Category)83+ if !ok {84+ return m, nil85+ }86+ m.selectedCategory = cat87+ m.modeSelectList = initModeSelectList(cat)88+ m.modeSelectList.SetSize(m.gameSelectList.Width(), m.gameSelectList.Height())89+ m.state = modeSelectView90+ return m, nil91+ }92+ if m.state == modeSelectView {93+ m.mode, _ = m.modeSelectList.SelectedItem().(game.Mode)94+ m.game, _ = m.mode.(game.Spawner).Spawn()8895 m.state = gameView8996 return m, nil9097 }98+ case tea.KeyEscape:99+ if m.state == modeSelectView {100+ m.state = gameSelectView101+ return m, nil102+ }91103 case tea.KeyCtrlC:92104 return m, tea.Quit93105 case tea.KeyCtrlE:
@@ -101,8 +113,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
101113 if m.debug {102114 m.debuginfo = m.renderDebugInfo()103115 }104- case menuView:105- m.gamemodesList, cmd = m.gamemodesList.Update(msg)116+ case gameSelectView:117+ m.gameSelectList, cmd = m.gameSelectList.Update(msg)118+ case modeSelectView:119+ m.modeSelectList, cmd = m.modeSelectList.Update(msg)106120 }107121 108122 return m, cmd
@@ -112,8 +126,11 @@ func (m model) View() string {
112126 var s string113127 114128 switch m.state {115- case menuView:116- s = rootStyle.Render(m.gamemodesList.View())129+ case gameSelectView:130+ s = rootStyle.Render(m.gameSelectList.View())131+ return s132+ case modeSelectView:133+ s = rootStyle.Render(m.modeSelectList.View())117134 return s118135 case gameView:119136 var debugInfo string
@@ -132,13 +149,21 @@ func (m model) View() string {
132149 return rootStyle.Render(s)133150 }134151 135-func initGameModeList() list.Model {136- gamemodes := list.New(GameModes, list.NewDefaultDelegate(), 64, 64)137- gamemodes.Title = "Select Gamemode"138- gamemodes.DisableQuitKeybindings()139- gamemodes.SetFilteringEnabled(false)140- gamemodes.SetShowHelp(false)141- return gamemodes152+func initList(items []list.Item, title string) list.Model {153+ l := list.New(items, list.NewDefaultDelegate(), 64, 64)154+ l.Title = title155+ l.DisableQuitKeybindings()156+ l.SetFilteringEnabled(false)157+ l.SetShowHelp(false)158+ return l159+}160+161+func initGameSelectList() list.Model {162+ return initList(GameCategories, "Select Game")163+}164+165+func initModeSelectList(cat game.Category) list.Model {166+ return initList(cat.Modes, cat.Name+" - Select Mode")142167 }143168 144169 func initDebugRenderer() *glamour.TermRenderer {
M nonogram/Gamemode.go +19 -11
M nonogram/Model.go +1 -1
M sudoku/Gamemode.go +14 -9
M sudoku/Model.go +1 -1
@@ -32,7 +32,7 @@ func New(mode SudokuMode, provided []cell, save ...string) (game.Gamer, error) {
3232 grid: g,3333 provided: provided,3434 keys: DefaultKeyMap,35- modeName: mode.title,35+ modeName: mode.Title(),3636 }3737 return m, nil3838 }
M wordsearch/Gamemode.go +21 -9
@@ -1,11 +1,13 @@
11 package wordsearch22 3-import "github.com/FelineStateMachine/puzzletea/game"3+import (4+ "github.com/FelineStateMachine/puzzletea/game"5+ "github.com/charmbracelet/bubbles/list"6+)47 58 // WordSearchMode implements game.Mode for word search puzzles69 type WordSearchMode struct {7- title string8- description string10+ game.BaseMode911 Width int1012 Height int1113 WordCount int
@@ -14,11 +16,13 @@ type WordSearchMode struct {
1416 AllowedDirs []Direction1517 }1618 19+var _ game.Mode = WordSearchMode{} // compile-time interface check20+var _ game.Spawner = WordSearchMode{} // compile-time interface check21+1722 // NewMode creates a new WordSearchMode with the given parameters18-func NewMode(title, description string, width, height, wordCount, minLen, maxLen int, allowedDirs []Direction) game.Mode {23+func NewMode(title, description string, width, height, wordCount, minLen, maxLen int, allowedDirs []Direction) WordSearchMode {1924 return WordSearchMode{20- title: title,21- description: description,25+ BaseMode: game.NewBaseMode(title, description),2226 Width: width,2327 Height: height,2428 WordCount: wordCount,
@@ -27,6 +31,14 @@ func NewMode(title, description string, width, height, wordCount, minLen, maxLen
2731 AllowedDirs: allowedDirs,2832 }2933 }30-func (n WordSearchMode) Title() string { return "word search\t" + n.title }31-func (n WordSearchMode) Description() string { return n.description }32-func (n WordSearchMode) FilterValue() string { return "word search " + n.title + " " + n.description }34+35+func (w WordSearchMode) Spawn() (game.Gamer, error) {36+ grid, words := GenerateWordSearch(w.Width, w.Height, w.WordCount, w.MinWordLen, w.MaxWordLen, w.AllowedDirs)37+ return New(w, grid, words), nil38+}39+40+var Modes = []list.Item{41+ NewMode("Easy - 10x10", "Find 6 words in a 10x10 grid.", 10, 10, 6, 3, 5, []Direction{Right, Down, DownRight}),42+ NewMode("Medium - 15x15", "Find 10 words in a 15x15 grid.", 15, 15, 10, 4, 7, []Direction{Right, Down, DownRight, DownLeft, Left, Up}),43+ NewMode("Hard - 20x20", "Find 15 words in a 20x20 grid.", 20, 20, 15, 5, 10, []Direction{Right, Down, DownRight, DownLeft, Left, Up, UpRight, UpLeft}),44+}
M wordsearch/Model.go +3 -8