clone

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

feat/app-decomposition
puzzletea / app / session_controller.go
session_controller.go 6.0K
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 *c.model = c.model.initScreen(generatingView)50 return tea.Batch(c.model.spinner.Tick, spawnCmd(spawner, ctx, jobID))51}5253func (c sessionController) startSeededSpawn(spawner game.SeededSpawner, rng *rand.Rand, request spawnRequest) tea.Cmd {54 ctx, jobID := c.beginSpawnContext()55 c.model.session.spawn = &request56 c.model.state = generatingView57 *c.model = c.model.clearNotice()58 *c.model = c.model.initScreen(generatingView)59 return tea.Batch(c.model.spinner.Tick, spawnSeededCmd(spawner, rng, ctx, jobID))60}6162func (c sessionController) handleSpawnComplete(jobID int64, msg game.SpawnCompleteMsg) tea.Cmd {63 if jobID != c.model.session.spawnJobID {64 return nil65 }6667 c.model.session.generating = false68 if c.model.session.spawnCancel != nil {69 c.model.session.spawnCancel()70 c.model.session.spawnCancel = nil71 }72 request := c.model.session.spawn73 c.model.session.spawn = nil7475 if c.model.state != generatingView {76 return nil77 }7879 if msg.Err != nil {80 *c.model = c.model.setErrorf("Could not generate puzzle: %v", msg.Err)81 if request != nil {82 c.model.state = request.returnState83 }84 return nil85 }8687 if request == nil {88 *c.model = c.model.setErrorf("Internal error: missing spawn request metadata")89 return nil90 }9192 c.activateGame(msg.Game.SetTitle(request.name), 0, false, gameOpenOptions{93 returnState: request.exitState,94 weeklyInfo: request.weeklyInfo,95 })96 *c.model = c.model.clearNotice()9798 rec, err := sessionflow.CreateRecord(99 c.model.store,100 c.model.session.game,101 request.name,102 request.gameType,103 request.modeTitle,104 request.run,105 )106 if err != nil {107 *c.model = c.model.setErrorf("Started puzzle, but could not create a save record: %v", err)108 } else {109 c.model.session.activeGameID = rec.ID110 }111 return nil112}113114func (c sessionController) activateGame(g game.Gamer, activeGameID int64, completionSaved bool, options gameOpenOptions) {115 g, _ = g.Update(game.HelpToggleMsg{Show: c.model.help.showFull})116 g, _ = g.Update(tea.WindowSizeMsg{Width: c.model.width, Height: c.model.height})117118 c.model.session.game = g119 c.model.session.activeGameID = activeGameID120 c.model.session.completionSaved = completionSaved121 c.model.session.returnState = options.returnState122 c.model.session.weeklyAdvance = options.weeklyInfo123 c.model.state = gameView124}125126func (c sessionController) importRecord(rec store.GameRecord, options gameOpenOptions) bool {127 g, err := sessionflow.ImportRecord(&rec)128 if err != nil {129 *c.model = c.model.setErrorf("Could not load saved puzzle %q: %v", rec.Name, err)130 return false131 }132133 activeGameID := rec.ID134 completionSaved := rec.Status == store.StatusCompleted135 if options.readOnly {136 activeGameID = 0137 completionSaved = true138 }139140 c.activateGame(g, activeGameID, completionSaved, options)141 *c.model = c.model.clearNotice()142 return true143}144145func (c sessionController) updateActiveGame(msg tea.Msg) tea.Cmd {146 var cmd tea.Cmd147 c.model.session.game, cmd = c.model.session.game.Update(msg)148 if c.model.debug.enabled {149 c.model.debug.info = c.model.renderDebugInfo()150 }151 c.persistCompletionIfSolved()152 return cmd153}154155func (c sessionController) persistCompletionIfSolved() {156 if c.model.session.game == nil || c.model.session.activeGameID == 0 ||157 c.model.session.completionSaved || !c.model.session.game.IsSolved() {158 return159 }160161 c.model.session.completionSaved = true162 saveData, err := c.model.session.game.GetSave()163 if err == nil {164 if err := c.model.store.UpdateSaveState(c.model.session.activeGameID, string(saveData)); err != nil {165 *c.model = c.model.setErrorf("Puzzle completed, but saving the final state failed: %v", err)166 }167 } else {168 *c.model = c.model.setErrorf("Puzzle completed, but reading the final state failed: %v", err)169 }170 if err := c.model.store.UpdateStatus(c.model.session.activeGameID, store.StatusCompleted); err != nil {171 *c.model = c.model.setErrorf("Puzzle completed, but updating completion status failed: %v", err)172 }173}174175func (c sessionController) saveCurrentGame(status store.GameStatus) {176 if c.model.session.game == nil {177 return178 }179 if c.model.session.activeGameID == 0 {180 c.clearActiveGame()181 return182 }183184 saveData, err := c.model.session.game.GetSave()185 if err != nil {186 *c.model = c.model.setErrorf("Could not save puzzle progress: %v", err)187 return188 }189 if err := c.model.store.UpdateSaveState(c.model.session.activeGameID, string(saveData)); err != nil {190 *c.model = c.model.setErrorf("Could not save puzzle progress: %v", err)191 }192 if !(c.model.session.completionSaved && status != store.StatusCompleted) {193 if err := c.model.store.UpdateStatus(c.model.session.activeGameID, status); err != nil {194 *c.model = c.model.setErrorf("Could not update puzzle status: %v", err)195 }196 }197 c.clearActiveGame()198}199200func (c sessionController) clearActiveGame() {201 c.model.session.activeGameID = 0202 c.model.session.game = nil203 c.model.session.completionSaved = false204 c.model.session.returnState = mainMenuView205 c.model.session.weeklyAdvance = nil206}