clone

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

add async generation

Dami committed 5471c93
commit
5471c93fd9cf231e97c2f8c6a18be77276741841
parent
1cf6c5442f759dba7da96692c16d0bffc63d0536
author
Dami <dami.etoile@gmail.com>
6 files changed, +93 -23
Mgame/gamer.go6+++
Mhashiwokakero/Gamemode.go5++-
Mhashiwokakero/generator.go10+++-
Mhashiwokakero/hashiwokakero_test.go15+++++-
Mmodel.go67+++++++++++++++++++++++++++++-
Mtakuzu/generator.go13------
M game/gamer.go +6 -0
M hashiwokakero/Gamemode.go +4 -1
M hashiwokakero/generator.go +7 -3
M hashiwokakero/hashiwokakero_test.go +12 -3
M model.go +64 -3
@@ -16,6 +16,7 @@ import (
1616 "github.com/FelineStateMachine/puzzletea/takuzu"1717 "github.com/FelineStateMachine/puzzletea/wordsearch"1818 "github.com/charmbracelet/bubbles/list"19+ "github.com/charmbracelet/bubbles/spinner"1920 "github.com/charmbracelet/bubbles/table"2021 tea "github.com/charmbracelet/bubbletea"2122 "github.com/charmbracelet/glamour"
@@ -48,6 +49,7 @@ const (
4849 mainMenuView = iota4950 gameSelectView5051 modeSelectView52+ generatingView5153 gameView5254 continueView5355 )
@@ -82,6 +84,9 @@ type model struct {
8284 mode game.Mode8385 game game.Gamer8486 87+ spinner spinner.Model88+ generating bool // true while an async Spawn is in flight89+8590 width int // available content width (terminal - rootStyle frame)8691 height int // available content height (terminal - rootStyle frame)8792
@@ -99,11 +104,15 @@ func initialModel(s *store.Store) model {
99104 r := initDebugRenderer()100105 l := initGameSelectList()101106 ml := initMainMenuList()107+ sp := spinner.New()108+ sp.Spinner = spinner.Dot109+ sp.Style = lipgloss.NewStyle().Foreground(menuAccent)102110 return model{103111 state: mainMenuView,104112 debugRenderer: r,105113 gameSelectList: l,106114 mainMenuList: ml,115+ spinner: sp,107116 store: s,108117 }109118 }
@@ -114,11 +123,15 @@ func initialModelWithGame(s *store.Store, g game.Gamer, activeGameID int64, comp
114123 r := initDebugRenderer()115124 l := initGameSelectList()116125 ml := initMainMenuList()126+ sp := spinner.New()127+ sp.Spinner = spinner.Dot128+ sp.Style = lipgloss.NewStyle().Foreground(menuAccent)117129 return model{118130 state: gameView,119131 debugRenderer: r,120132 gameSelectList: l,121133 mainMenuList: ml,134+ spinner: sp,122135 store: s,123136 game: g,124137 activeGameID: activeGameID,
@@ -134,6 +147,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
134147 var cmd tea.Cmd135148 136149 switch msg := msg.(type) {150+ case game.SpawnCompleteMsg:151+ return m.handleSpawnComplete(msg)152+137153 case tea.WindowSizeMsg:138154 h, v := rootStyle.GetFrameSize()139155 w, ht := msg.Width-h, msg.Height-v
@@ -151,6 +167,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
151167 }152168 153169 case tea.KeyMsg:170+ // During generation, only allow Escape (to cancel) and Ctrl+C (to quit).171+ if m.state == generatingView {172+ switch msg.Type {173+ case tea.KeyEscape:174+ m.state = modeSelectView175+ return m, nil176+ case tea.KeyCtrlC:177+ return m, tea.Quit178+ }179+ return m, nil180+ }154181 switch msg.Type {155182 case tea.KeyCtrlN:156183 m = saveCurrentGame(m, store.StatusInProgress)
@@ -182,6 +209,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
182209 switch m.state {183210 case mainMenuView:184211 m.mainMenuList, cmd = m.mainMenuList.Update(msg)212+ case generatingView:213+ m.spinner, cmd = m.spinner.Update(msg)185214 case gameView:186215 m.game, cmd = m.game.Update(msg)187216 if m.debug {
@@ -251,12 +280,39 @@ func (m model) handleGameSelectEnter() (tea.Model, tea.Cmd) {
251280 252281 func (m model) handleModeSelectEnter() (tea.Model, tea.Cmd) {253282 m.mode, _ = m.modeSelectList.SelectedItem().(game.Mode)254- g, err := m.mode.(game.Spawner).Spawn()255- if err != nil {283+ spawner, ok := m.mode.(game.Spawner)284+ if !ok {256285 return m, nil257286 }287+ m.state = generatingView288+ m.generating = true289+ return m, tea.Batch(m.spinner.Tick, spawnCmd(spawner))290+}291+292+// spawnCmd returns a tea.Cmd that runs Spawn() off the main goroutine.293+func spawnCmd(spawner game.Spawner) tea.Cmd {294+ return func() tea.Msg {295+ g, err := spawner.Spawn()296+ return game.SpawnCompleteMsg{Game: g, Err: err}297+ }298+}299+300+func (m model) handleSpawnComplete(msg game.SpawnCompleteMsg) (tea.Model, tea.Cmd) {301+ m.generating = false302+303+ // If the user navigated away while generating, discard the result.304+ if m.state != generatingView {305+ return m, nil306+ }307+308+ if msg.Err != nil {309+ log.Printf("failed to spawn game: %v", msg.Err)310+ m.state = modeSelectView311+ return m, nil312+ }313+258314 name := generateUniqueName(m.store)259- m.game = g.SetTitle(name)315+ m.game = msg.Game.SetTitle(name)260316 m.game, _ = m.game.Update(game.HelpToggleMsg{Show: m.showFullHelp})261317 m.state = gameView262318 m.completionSaved = false
@@ -308,6 +364,8 @@ func (m model) handleContinueEnter() (tea.Model, tea.Cmd) {
308364 309365 func (m model) handleEscape() (tea.Model, tea.Cmd) {310366 switch m.state {367+ case generatingView:368+ m.state = modeSelectView311369 case modeSelectView:312370 m.state = gameSelectView313371 case gameSelectView, continueView:
@@ -324,6 +382,9 @@ func (m model) View() string {
324382 return rootStyle.Render(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, m.gameSelectList.View()))325383 case modeSelectView:326384 return rootStyle.Render(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, m.modeSelectList.View()))385+ case generatingView:386+ s := m.spinner.View() + " Generating puzzle..."387+ return rootStyle.Render(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, s))327388 case continueView:328389 var s string329390 if len(m.continueGames) == 0 {
M takuzu/generator.go +0 -13