clone

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

chore/regenerate-vhs-gifs
puzzletea / app / session_controller.go
session_controller.go 5.9K
1package app23import (4 "context"5 "math/rand/v2"67 "github.com/FelineStateMachine/puzzletea/game"8 sessionflow "github.com/FelineStateMachine/puzzletea/session"9 "github.com/FelineStateMachine/puzzletea/store"1011 tea "charm.land/bubbletea/v2"12)1314type sessionController struct {15 model *model16}1718func newSessionController(m *model) sessionController {19 return sessionController{model: m}20}2122func (c sessionController) beginSpawnContext() (context.Context, int64) {23 c.cancelActiveSpawn()24 c.model.session.spawnJobID++25 jobID := c.model.session.spawnJobID26 ctx, cancel := context.WithTimeout(context.Background(), spawnTimeout)27 c.model.session.spawnCancel = cancel28 c.model.session.generating = true29 return ctx, jobID30}3132func (c sessionController) cancelActiveSpawn() {33 if c.model.session.spawnCancel != nil {34 c.model.session.spawnCancel()35 c.model.session.spawnCancel = nil36 }37 if c.model.session.generating {38 c.model.session.spawnJobID++39 }40 c.model.session.generating = false41 c.model.session.spawn = nil42}4344func (c sessionController) startSpawn(spawner game.Spawner, request spawnRequest) tea.Cmd {45 ctx, jobID := c.beginSpawnContext()46 c.model.session.spawn = &request47 c.model.state = generatingView48 *c.model = c.model.clearNotice()49 return tea.Batch(c.model.spinner.Tick, spawnCmd(spawner, ctx, jobID))50}5152func (c sessionController) startSeededSpawn(spawner game.SeededSpawner, rng *rand.Rand, request spawnRequest) tea.Cmd {53 ctx, jobID := c.beginSpawnContext()54 c.model.session.spawn = &request55 c.model.state = generatingView56 *c.model = c.model.clearNotice()57 return tea.Batch(c.model.spinner.Tick, spawnSeededCmd(spawner, rng, ctx, jobID))58}5960func (c sessionController) handleSpawnComplete(jobID int64, msg game.SpawnCompleteMsg) tea.Cmd {61 if jobID != c.model.session.spawnJobID {62 return nil63 }6465 c.model.session.generating = false66 if c.model.session.spawnCancel != nil {67 c.model.session.spawnCancel()68 c.model.session.spawnCancel = nil69 }70 request := c.model.session.spawn71 c.model.session.spawn = nil7273 if c.model.state != generatingView {74 return nil75 }7677 if msg.Err != nil {78 *c.model = c.model.setErrorf("Could not generate puzzle: %v", msg.Err)79 if request != nil {80 c.model.state = request.returnState81 }82 return nil83 }8485 if request == nil {86 *c.model = c.model.setErrorf("Internal error: missing spawn request metadata")87 return nil88 }8990 c.activateGame(msg.Game.SetTitle(request.name), 0, false, gameOpenOptions{91 returnState: request.exitState,92 weeklyInfo: request.weeklyInfo,93 })94 *c.model = c.model.clearNotice()9596 rec, err := sessionflow.CreateRecord(97 c.model.store,98 c.model.session.game,99 request.name,100 request.gameType,101 request.modeTitle,102 request.run,103 )104 if err != nil {105 *c.model = c.model.setErrorf("Started puzzle, but could not create a save record: %v", err)106 } else {107 c.model.session.activeGameID = rec.ID108 }109 return nil110}111112func (c sessionController) activateGame(g game.Gamer, activeGameID int64, completionSaved bool, options gameOpenOptions) {113 g, _ = g.Update(game.HelpToggleMsg{Show: c.model.help.showFull})114 g, _ = g.Update(tea.WindowSizeMsg{Width: c.model.width, Height: c.model.height})115116 c.model.session.game = g117 c.model.session.activeGameID = activeGameID118 c.model.session.completionSaved = completionSaved119 c.model.session.returnState = options.returnState120 c.model.session.weeklyAdvance = options.weeklyInfo121 c.model.state = gameView122}123124func (c sessionController) importRecord(rec store.GameRecord, options gameOpenOptions) bool {125 g, err := sessionflow.ImportRecord(&rec)126 if err != nil {127 *c.model = c.model.setErrorf("Could not load saved puzzle %q: %v", rec.Name, err)128 return false129 }130131 activeGameID := rec.ID132 completionSaved := rec.Status == store.StatusCompleted133 if options.readOnly {134 activeGameID = 0135 completionSaved = true136 }137138 c.activateGame(g, activeGameID, completionSaved, options)139 *c.model = c.model.clearNotice()140 return true141}142143func (c sessionController) updateActiveGame(msg tea.Msg) tea.Cmd {144 var cmd tea.Cmd145 c.model.session.game, cmd = c.model.session.game.Update(msg)146 if c.model.debug.enabled {147 c.model.debug.info = c.model.renderDebugInfo()148 }149 c.persistCompletionIfSolved()150 return cmd151}152153func (c sessionController) persistCompletionIfSolved() {154 if c.model.session.game == nil || c.model.session.activeGameID == 0 ||155 c.model.session.completionSaved || !c.model.session.game.IsSolved() {156 return157 }158159 c.model.session.completionSaved = true160 saveData, err := c.model.session.game.GetSave()161 if err == nil {162 if err := c.model.store.UpdateSaveState(c.model.session.activeGameID, string(saveData)); err != nil {163 *c.model = c.model.setErrorf("Puzzle completed, but saving the final state failed: %v", err)164 }165 } else {166 *c.model = c.model.setErrorf("Puzzle completed, but reading the final state failed: %v", err)167 }168 if err := c.model.store.UpdateStatus(c.model.session.activeGameID, store.StatusCompleted); err != nil {169 *c.model = c.model.setErrorf("Puzzle completed, but updating completion status failed: %v", err)170 }171}172173func (c sessionController) saveCurrentGame(status store.GameStatus) {174 if c.model.session.game == nil {175 return176 }177 if c.model.session.activeGameID == 0 {178 c.clearActiveGame()179 return180 }181182 saveData, err := c.model.session.game.GetSave()183 if err != nil {184 *c.model = c.model.setErrorf("Could not save puzzle progress: %v", err)185 return186 }187 if err := c.model.store.UpdateSaveState(c.model.session.activeGameID, string(saveData)); err != nil {188 *c.model = c.model.setErrorf("Could not save puzzle progress: %v", err)189 }190 if !(c.model.session.completionSaved && status != store.StatusCompleted) {191 if err := c.model.store.UpdateStatus(c.model.session.activeGameID, status); err != nil {192 *c.model = c.model.setErrorf("Could not update puzzle status: %v", err)193 }194 }195 c.clearActiveGame()196}197198func (c sessionController) clearActiveGame() {199 c.model.session.activeGameID = 0200 c.model.session.game = nil201 c.model.session.completionSaved = false202 c.model.session.returnState = mainMenuView203 c.model.session.weeklyAdvance = nil204}