1package app23import (4 "context"5 "math/rand/v2"67 "github.com/FelineStateMachine/puzzletea/difficulty"8 "github.com/FelineStateMachine/puzzletea/game"9 sessionflow "github.com/FelineStateMachine/puzzletea/session"10 "github.com/FelineStateMachine/puzzletea/store"1112 tea "charm.land/bubbletea/v2"13)1415type sessionController struct {16 model *model17}1819func newSessionController(m *model) sessionController {20 return sessionController{model: m}21}2223func (c sessionController) beginSpawnContext() (context.Context, int64) {24 c.cancelActiveSpawn()25 c.model.session.spawnJobID++26 jobID := c.model.session.spawnJobID27 ctx, cancel := context.WithTimeout(context.Background(), spawnTimeout)28 c.model.session.spawnCancel = cancel29 c.model.session.generating = true30 return ctx, jobID31}3233func (c sessionController) cancelActiveSpawn() {34 if c.model.session.spawnCancel != nil {35 c.model.session.spawnCancel()36 c.model.session.spawnCancel = nil37 }38 if c.model.session.generating {39 c.model.session.spawnJobID++40 }41 c.model.session.generating = false42 c.model.session.spawn = nil43}4445func (c sessionController) startSpawn(spawner game.Spawner, request spawnRequest) tea.Cmd {46 ctx, jobID := c.beginSpawnContext()47 c.model.session.spawn = &request48 c.model.state = generatingView49 *c.model = c.model.clearNotice()50 *c.model = c.model.initScreen(generatingView)51 return tea.Batch(c.model.spinner.Tick, spawnCmd(spawner, ctx, jobID))52}5354func (c sessionController) startEloSpawn(spawner game.EloSpawner, seed string, elo difficulty.Elo, request spawnRequest) tea.Cmd {55 ctx, jobID := c.beginSpawnContext()56 c.model.session.spawn = &request57 c.model.state = generatingView58 *c.model = c.model.clearNotice()59 *c.model = c.model.initScreen(generatingView)60 return tea.Batch(c.model.spinner.Tick, spawnEloCmd(spawner, seed, elo, ctx, jobID))61}6263func (c sessionController) startSeededSpawn(spawner game.SeededSpawner, rng *rand.Rand, request spawnRequest) tea.Cmd {64 ctx, jobID := c.beginSpawnContext()65 c.model.session.spawn = &request66 c.model.state = generatingView67 *c.model = c.model.clearNotice()68 *c.model = c.model.initScreen(generatingView)69 return tea.Batch(c.model.spinner.Tick, spawnSeededCmd(spawner, rng, ctx, jobID))70}7172func (c sessionController) handleSpawnComplete(jobID int64, msg game.SpawnCompleteMsg) tea.Cmd {73 if jobID != c.model.session.spawnJobID {74 return nil75 }7677 c.model.session.generating = false78 if c.model.session.spawnCancel != nil {79 c.model.session.spawnCancel()80 c.model.session.spawnCancel = nil81 }82 request := c.model.session.spawn83 c.model.session.spawn = nil8485 if c.model.state != generatingView {86 return nil87 }8889 if msg.Err != nil {90 *c.model = c.model.setErrorf("Could not generate puzzle: %v", msg.Err)91 if request != nil {92 c.model.state = request.returnState93 }94 return nil95 }9697 if request == nil {98 *c.model = c.model.setErrorf("Internal error: missing spawn request metadata")99 return nil100 }101102 c.activateGame(msg.Game.SetTitle(request.name), 0, false, gameOpenOptions{103 returnState: request.exitState,104 weeklyInfo: request.weeklyInfo,105 })106 *c.model = c.model.clearNotice()107108 rec, err := sessionflow.CreateRecordWithDifficulty(109 c.model.store,110 c.model.session.game,111 request.name,112 request.gameType,113 request.modeTitle,114 request.run,115 difficultyMetadataFromSpawnReport(msg.Report),116 )117 if err != nil {118 *c.model = c.model.setErrorf("Started puzzle, but could not create a save record: %v", err)119 } else {120 c.model.session.activeGameID = rec.ID121 }122 return nil123}124125func difficultyMetadataFromSpawnReport(report difficulty.Report) sessionflow.DifficultyMetadata {126 if report.Confidence == "" {127 return sessionflow.DifficultyMetadata{}128 }129 target := report.TargetElo130 actual := report.ActualElo131 return sessionflow.DifficultyMetadata{132 TargetElo: &target,133 ActualElo: &actual,134 Confidence: report.Confidence,135 }136}137138func (c sessionController) activateGame(g game.Gamer, activeGameID int64, completionSaved bool, options gameOpenOptions) {139 g, _ = g.Update(game.HelpToggleMsg{Show: c.model.help.showFull})140 g, _ = g.Update(tea.WindowSizeMsg{Width: c.model.width, Height: c.model.height})141142 c.model.session.game = g143 c.model.session.activeGameID = activeGameID144 c.model.session.completionSaved = completionSaved145 c.model.session.returnState = options.returnState146 c.model.session.weeklyAdvance = options.weeklyInfo147 c.model.state = gameView148}149150func (c sessionController) importRecord(rec store.GameRecord, options gameOpenOptions) bool {151 g, err := sessionflow.ImportRecord(&rec)152 if err != nil {153 *c.model = c.model.setErrorf("Could not load saved puzzle %q: %v", rec.Name, err)154 return false155 }156157 activeGameID := rec.ID158 completionSaved := rec.Status == store.StatusCompleted159 if options.readOnly {160 activeGameID = 0161 completionSaved = true162 }163164 c.activateGame(g, activeGameID, completionSaved, options)165 *c.model = c.model.clearNotice()166 return true167}168169func (c sessionController) updateActiveGame(msg tea.Msg) tea.Cmd {170 var cmd tea.Cmd171 c.model.session.game, cmd = c.model.session.game.Update(msg)172 if c.model.debug.enabled {173 c.model.debug.info = c.model.renderDebugInfo()174 }175 c.persistCompletionIfSolved()176 return cmd177}178179func (c sessionController) persistCompletionIfSolved() {180 if c.model.session.game == nil || c.model.session.activeGameID == 0 ||181 c.model.session.completionSaved || !c.model.session.game.IsSolved() {182 return183 }184185 c.model.session.completionSaved = true186 saveData, err := c.model.session.game.GetSave()187 if err == nil {188 if err := c.model.store.UpdateSaveState(c.model.session.activeGameID, string(saveData)); err != nil {189 *c.model = c.model.setErrorf("Puzzle completed, but saving the final state failed: %v", err)190 }191 } else {192 *c.model = c.model.setErrorf("Puzzle completed, but reading the final state failed: %v", err)193 }194 if err := c.model.store.UpdateStatus(c.model.session.activeGameID, store.StatusCompleted); err != nil {195 *c.model = c.model.setErrorf("Puzzle completed, but updating completion status failed: %v", err)196 }197}198199func (c sessionController) saveCurrentGame(status store.GameStatus) {200 if c.model.session.game == nil {201 return202 }203 if c.model.session.activeGameID == 0 {204 c.clearActiveGame()205 return206 }207208 saveData, err := c.model.session.game.GetSave()209 if err != nil {210 *c.model = c.model.setErrorf("Could not save puzzle progress: %v", err)211 return212 }213 if err := c.model.store.UpdateSaveState(c.model.session.activeGameID, string(saveData)); err != nil {214 *c.model = c.model.setErrorf("Could not save puzzle progress: %v", err)215 }216 if !(c.model.session.completionSaved && status != store.StatusCompleted) {217 if err := c.model.store.UpdateStatus(c.model.session.activeGameID, status); err != nil {218 *c.model = c.model.setErrorf("Could not update puzzle status: %v", err)219 }220 }221 c.clearActiveGame()222}223224func (c sessionController) clearActiveGame() {225 c.model.session.activeGameID = 0226 c.model.session.game = nil227 c.model.session.completionSaved = false228 c.model.session.returnState = mainMenuView229 c.model.session.weeklyAdvance = nil230}