- commit
- a53abecafb98b5ef02abe5260cf40d111d0abe6c
- parent
- e3f4b9470ff02a37547a20e16ee0cd0727232857
- author
- Dami <dami@Damis-MacBook-Air.local>
| D | core.go | 31 | --------- |
| M | game/ | 38 | ++++++++++++ |
| M | hashiwokakero/ | 43 | +++++++------ |
| M | hashiwokakero/ | 2 | +- |
| M | model.go | 99 | +++++++++++++++++++----------- |
| M | nonogram/ | 30 | ++++++--- |
| M | nonogram/ | 2 | +- |
| M | sudoku/ | 23 | ++++--- |
| M | sudoku/ | 2 | +- |
| M | wordsearch/ | 30 | ++++++--- |
| M | wordsearch/ | 11 | +-- |
@@ -1,31 +0,0 @@
1-package main2-3-import (4- "fmt"5-6- "github.com/FelineStateMachine/puzzletea/game"7- "github.com/FelineStateMachine/puzzletea/hashiwokakero"8- "github.com/FelineStateMachine/puzzletea/sudoku"9-10- // Implementations of the Gamer interface should interface here.11- nonogram "github.com/FelineStateMachine/puzzletea/nonogram"12- "github.com/FelineStateMachine/puzzletea/wordsearch"13-)14-15-func (m model) SpawnGame(mode game.Mode) (game.Gamer, error) {16- switch mode := mode.(type) {17- case nonogram.NonogramMode:18- hints := nonogram.GenerateRandomTomography(mode)19- return nonogram.New(mode, hints)20- case sudoku.SudokuMode:21- return sudoku.New(mode, sudoku.GenerateProvidedCells(mode))22- case wordsearch.WordSearchMode:23- grid, words := wordsearch.GenerateWordSearch(mode.Width, mode.Height, mode.WordCount, mode.MinWordLen, mode.MaxWordLen, mode.AllowedDirs)24- return wordsearch.New(mode, grid, words), nil25- case hashiwokakero.HashiMode:26- puzzle := hashiwokakero.GeneratePuzzle(mode)27- return hashiwokakero.New(mode, puzzle), nil28- default:29- return nil, fmt.Errorf("Faled to create game.\nUnimplemented game type `%v`.", mode)30- }31-}@@ -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 }@@ -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+}@@ -26,7 +26,7 @@ func New(mode HashiMode, puzzle Puzzle) game.Gamer {
2626 puzzle: puzzle,2727 cursorIsland: cursorID,2828 keys: DefaultKeyMap,29- modeName: mode.title,29+ modeName: mode.Title(),3030 }3131 }3232 @@ -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 {@@ -2,25 +2,33 @@ package nonogram
22 33 import (44 "github.com/FelineStateMachine/puzzletea/game"5+ "github.com/charmbracelet/bubbles/list"56 )67 78 type NonogramMode struct {8- title string9- description string9+ game.BaseMode1010 Height, Width int1111 }1212 13-var _ game.Mode = NonogramMode{} // Verify that T implements I.14-15-func (n NonogramMode) Title() string { return "nonogram\t" + n.title }16-func (n NonogramMode) Description() string { return n.description }17-func (n NonogramMode) FilterValue() string { return "nonogram " + n.title + " " + n.description }13+var _ game.Mode = NonogramMode{} // compile-time interface check14+var _ game.Spawner = NonogramMode{} // compile-time interface check1815 1916 func NewMode(title, description string, Height, Width int) NonogramMode {2017 return NonogramMode{21- title: title,22- description: description,23- Height: Height,24- Width: Width,18+ BaseMode: game.NewBaseMode(title, description),19+ Height: Height,20+ Width: Width,2521 }2622 }23+24+func (n NonogramMode) Spawn() (game.Gamer, error) {25+ hints := GenerateRandomTomography(n)26+ return New(n, hints)27+}28+29+var Modes = []list.Item{30+ NewMode("Easy - 5x5", "A random nonogram on a five by five board.", 5, 5),31+ NewMode("Medium - 10x10", "A random nonogram on a ten by ten board.", 10, 10),32+ NewMode("Hard - 15x15", "A random nonogram on a fifteen by fifteen board.", 15, 15),33+ NewMode("Extra - 5x10", "A random nonogram on a five by ten board.", 5, 10),34+}@@ -46,7 +46,7 @@ func New(mode NonogramMode, hints Hints, save ...string) (game.Gamer, error) {
4646 colHints: c,4747 grid: newGrid(s),4848 keys: DefaultKeyMap,49- modeName: mode.title,49+ modeName: mode.Title(),5050 }5151 5252 return m, nil@@ -2,24 +2,29 @@ package sudoku
22 33 import (44 "github.com/FelineStateMachine/puzzletea/game"5+ "github.com/charmbracelet/bubbles/list"56 )67 78 type SudokuMode struct {8- title string9- description string9+ game.BaseMode1010 ProvidedCount int1111 }1212 13-var _ game.Mode = SudokuMode{} // Verify that T implements I.14-15-func (n SudokuMode) Title() string { return "sudoku\t" + n.title }16-func (n SudokuMode) Description() string { return n.description }17-func (n SudokuMode) FilterValue() string { return "sudoku " + n.title + " " + n.description }13+var _ game.Mode = SudokuMode{} // compile-time interface check14+var _ game.Spawner = SudokuMode{} // compile-time interface check1815 1916 func NewMode(title, description string, providedCount int) SudokuMode {2017 return SudokuMode{21- title: title,22- description: description,18+ BaseMode: game.NewBaseMode(title, description),2319 ProvidedCount: providedCount,2420 }2521 }22+23+func (s SudokuMode) Spawn() (game.Gamer, error) {24+ return New(s, GenerateProvidedCells(s))25+}26+27+var Modes = []list.Item{28+ NewMode("Easy - 38 Provided Cells", "A random sudoku with at least 38 cells provided to start.", 38),29+ NewMode("Hard - 26 Provided Cells", "A random sudoku with at least 26 cells provided to start.", 26),30+}@@ -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 }@@ -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+}@@ -32,15 +32,10 @@ type Model struct {
3232 }3333 3434 // New creates a new word search game35-func New(mode game.Mode, g grid, words []Word) *Model {36- wsMode, ok := mode.(WordSearchMode)37- if !ok {38- return nil39- }40-35+func New(mode WordSearchMode, g grid, words []Word) *Model {4136 return &Model{42- width: wsMode.Width,43- height: wsMode.Height,37+ width: mode.Width,38+ height: mode.Height,4439 grid: g,4540 words: words,4641 cursor: cursor{x: 0, y: 0},