clone

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

feat/elo-difficulty
puzzletea / app / screen_core.go
screen_core.go 8.4K
1package app23import (4 "time"56 "github.com/FelineStateMachine/puzzletea/config"7 "github.com/FelineStateMachine/puzzletea/registry"8 sessionflow "github.com/FelineStateMachine/puzzletea/session"9 "github.com/FelineStateMachine/puzzletea/store"10 "github.com/FelineStateMachine/puzzletea/theme"11 "github.com/FelineStateMachine/puzzletea/ui"1213 tea "charm.land/bubbletea/v2"14)1516type screenAction interface {17 applyToModel(m model) (model, tea.Cmd)18}1920type openPlayMenuAction struct{}2122func (a openPlayMenuAction) applyToModel(m model) (model, tea.Cmd) {23 m.nav.playMenu = ui.NewMainMenu(buildPlayMenuItems(time.Now(), m.currentWeeklyMenuIndex()))24 m.state = playMenuView25 m = m.clearNotice()26 return m.initScreen(playMenuView), nil27}2829type openStatsAction struct{}3031func (a openStatsAction) applyToModel(m model) (model, tea.Cmd) {32 return m.handleStatsEnter()33}3435type openOptionsMenuAction struct{}3637func (a openOptionsMenuAction) applyToModel(m model) (model, tea.Cmd) {38 m.nav.optionsMenu = ui.NewMainMenu(optionsMenuItems)39 m.state = optionsMenuView40 m = m.clearNotice()41 return m.initScreen(optionsMenuView), nil42}4344type quitAction struct{}4546func (a quitAction) applyToModel(m model) (model, tea.Cmd) {47 return m, tea.Quit48}4950type openCreateAction struct{}5152func (a openCreateAction) applyToModel(m model) (model, tea.Cmd) {53 createCfg := config.CreateConfig{}54 if m.cfg != nil {55 createCfg = m.cfg.Create56 }57 m.create = newCreateState(createCfg, m.width)58 m.state = createView59 m = m.clearNotice()60 return m.initScreen(createView), nil61}6263type openContinueAction struct{}6465func (a openContinueAction) applyToModel(m model) (model, tea.Cmd) {66 m.cont.table, m.cont.games = ui.InitContinueTable(m.store, m.height)67 m.state = continueView68 m = m.clearNotice()69 return m.initScreen(continueView), nil70}7172type openDailyAction struct{}7374func (a openDailyAction) applyToModel(m model) (model, tea.Cmd) {75 return m.handleDailyPuzzle()76}7778type openExportAction struct{}7980func (a openExportAction) applyToModel(m model) (model, tea.Cmd) {81 return m.handleExportEnter()82}8384type openWeeklyAction struct{}8586func (a openWeeklyAction) applyToModel(m model) (model, tea.Cmd) {87 return m.enterWeeklyView()88}8990type backAction struct {91 target viewState92}9394func (a backAction) applyToModel(m model) (model, tea.Cmd) {95 if m.state == themeSelectView {96 previous := m.theme.previous97 if ts, ok := m.screens[themeSelectView].(themeSelectScreen); ok {98 previous = ts.theme.previous99 }100 _ = theme.Apply(previous)101 }102 m.state = a.target103 return m.resizeActiveScreen(), nil104}105106type gameSelectEnterAction struct {107 entry registry.Entry108}109110func (a gameSelectEnterAction) applyToModel(m model) (model, tea.Cmd) {111 m.nav.selectedCategory = a.entry112 m.nav.modeSelectList = ui.InitList(buildModeDisplayItems(a.entry), a.entry.Definition.Name+" - Select Variant")113 m.nav.modeSelectList.SetSize(min(m.width, 64), min(m.height, ui.ListHeight(m.nav.modeSelectList)))114 m.state = modeSelectView115 m = m.initScreen(modeSelectView)116 return m, nil117}118119type modeSelectEnterAction struct {120 entry registry.Entry121 mode registry.VariantEntry122}123124func (a modeSelectEnterAction) applyToModel(m model) (model, tea.Cmd) {125 m.nav.selectedCategory = a.entry126 m.nav.selectedModeTitle = a.mode.Definition.Title127 name := sessionflow.GenerateUniqueName(m.store)128 request := spawnRequest{129 source: spawnSourceNormal,130 name: name,131 gameType: a.entry.Definition.Name,132 modeTitle: a.mode.Definition.Title,133 run: store.NormalRunMetadata(),134 returnState: modeSelectView,135 exitState: mainMenuView,136 }137 if a.mode.Elo != nil {138 cmd := newSessionController(&m).startEloSpawn(a.mode.Elo, name, a.mode.Definition.DefaultElo, request)139 return m, cmd140 }141 cmd := newSessionController(&m).startSpawn(a.mode.Seeded, request)142 return m, cmd143}144145type continueEnterAction struct {146 record store.GameRecord147}148149func (a continueEnterAction) applyToModel(m model) (model, tea.Cmd) {150 m, _ = m.importAndActivateRecord(a.record)151 return m, nil152}153154type weeklyShiftAction struct {155 delta int156 weekly weeklyState157}158159func (a weeklyShiftAction) applyToModel(m model) (model, tea.Cmd) {160 m.weekly = a.weekly161 m = m.moveWeeklyWeek(a.delta)162 m = m.initScreen(weeklyView)163 return m, nil164}165166type weeklyEnterAction struct {167 weekly weeklyState168}169170func (a weeklyEnterAction) applyToModel(m model) (model, tea.Cmd) {171 m.weekly = a.weekly172 return m.handleWeeklyEnter()173}174175type helpSelectEnterAction struct {176 entry registry.Entry177}178179func (a helpSelectEnterAction) applyToModel(m model) (model, tea.Cmd) {180 m.help.category = a.entry181 return m.handleHelpSelectEnter()182}183184type openThemeSelectAction struct{}185186func (a openThemeSelectAction) applyToModel(m model) (model, tea.Cmd) {187 return m.handleThemeEnter()188}189190type openHelpSelectAction struct{}191192func (a openHelpSelectAction) applyToModel(m model) (model, tea.Cmd) {193 m.help.selectList = ui.InitList(gameCategoryItems, "How to Play")194 m.state = helpSelectView195 m = m.clearNotice()196 return m.initScreen(helpSelectView), nil197}198199type previewThemeAction struct {200 name string201}202203func (a previewThemeAction) applyToModel(m model) (model, tea.Cmd) {204 _ = theme.Apply(a.name)205 if ts, ok := m.screens[themeSelectView].(themeSelectScreen); ok {206 ui.UpdateThemeListStyles(&ts.theme.list)207 m.screens[themeSelectView] = ts208 }209 return m, nil210}211212type confirmThemeAction struct {213 theme themeState214}215216func (a confirmThemeAction) applyToModel(m model) (model, tea.Cmd) {217 m.theme = a.theme218 return m.handleThemeConfirm()219}220221type seedConfirmAction struct {222 seed seedState223}224225func (a seedConfirmAction) applyToModel(m model) (model, tea.Cmd) {226 m.seed = a.seed227 return m.handleSeedConfirm()228}229230// exportSubmitAction is a pure tea.Msg emitted by exportSubmitCmd.231// It is handled in the root Update() and does not implement screenAction.232type exportSubmitAction struct{}233234type screenModel interface {235 Resize(width, height int) screenModel236 Update(msg tea.Msg) (screenModel, tea.Cmd, screenAction)237 View(notice noticeState) string238}239240type screenFactory func(m model) screenModel241242var screenRegistry = map[viewState]screenFactory{243 mainMenuView: func(m model) screenModel {244 return mainMenuScreen{width: m.width, height: m.height, menu: m.nav.mainMenu}245 },246 playMenuView: func(m model) screenModel {247 return playMenuScreen{width: m.width, height: m.height, menu: m.nav.playMenu}248 },249 optionsMenuView: func(m model) screenModel {250 return optionsMenuScreen{width: m.width, height: m.height, menu: m.nav.optionsMenu}251 },252 seedInputView: func(m model) screenModel {253 return seedInputScreen{width: m.width, height: m.height, seed: m.seed}254 },255 createView: func(m model) screenModel {256 return createScreen{width: m.width, height: m.height, create: m.create}257 },258 gameSelectView: func(m model) screenModel {259 return gameSelectScreen{width: m.width, height: m.height, list: m.nav.gameSelectList, detail: m.nav.categoryDetail}260 },261 modeSelectView: func(m model) screenModel {262 return modeSelectScreen{width: m.width, height: m.height, entry: m.nav.selectedCategory, list: m.nav.modeSelectList}263 },264 continueView: func(m model) screenModel {265 return continueScreen{width: m.width, height: m.height, cont: m.cont}266 },267 weeklyView: func(m model) screenModel {268 return weeklyScreen{width: m.width, height: m.height, weekly: m.weekly}269 },270 helpSelectView: func(m model) screenModel {271 return helpSelectScreen{width: m.width, height: m.height, help: m.help}272 },273 helpDetailView: func(m model) screenModel {274 return helpDetailScreen{width: m.width, height: m.height, help: m.help}275 },276 statsView: func(m model) screenModel {277 return statsScreen{width: m.width, height: m.height, stats: m.stats}278 },279 themeSelectView: func(m model) screenModel {280 return themeSelectScreen{width: m.width, height: m.height, theme: m.theme}281 },282 generatingView: func(m model) screenModel {283 return generatingScreen{width: m.width, height: m.height, spinner: m.spinner}284 },285 exportRunningView: func(m model) screenModel {286 return exportRunningScreen{width: m.width, height: m.height, spinner: m.spinner}287 },288}289290func (m model) activeScreen() screenModel {291 return m.screens[m.state]292}293294// initScreen creates a fresh screen for the given state (using the registry295// factory) and stores it in m.screens. It also calls Resize so dimensions296// are set correctly.297func (m model) initScreen(state viewState) model {298 factory, ok := screenRegistry[state]299 if !ok {300 return m301 }302 if m.screens == nil {303 m.screens = make(map[viewState]screenModel)304 }305 m.screens[state] = factory(m).Resize(m.width, m.height)306 return m307}