swarm-0.3.0.0: src/Swarm/TUI/Model/StateUpdate.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Swarm.TUI.Model.StateUpdate (
initAppState,
startGame,
restartGame,
attainAchievement,
attainAchievement',
scenarioToAppState,
) where
import Brick.AttrMap (applyAttrMappings)
import Control.Applicative ((<|>))
import Control.Lens hiding (from, (<.>))
import Control.Monad.Except
import Control.Monad.State
import Data.List qualified as List
import Data.Map qualified as M
import Data.Maybe (fromMaybe, isJust)
import Data.Text (Text)
import Data.Time (ZonedTime, getZonedTime)
import Swarm.Game.Log (ErrorLevel (..), LogSource (ErrorTrace))
import Swarm.Game.Scenario (loadScenario, scenarioAttrs)
import Swarm.Game.Scenario.Objective.Presentation.Model (emptyGoalDisplay)
import Swarm.Game.ScenarioInfo (
ScenarioInfo (..),
ScenarioInfoPair,
ScenarioStatus (..),
normalizeScenarioPath,
scenarioItemByPath,
scenarioPath,
scenarioSolution,
scenarioStatus,
_SISingle,
)
import Swarm.Game.State
import Swarm.TUI.Attr (swarmAttrMap)
import Swarm.TUI.Inventory.Sorting
import Swarm.TUI.Model
import Swarm.TUI.Model.Achievement.Attainment
import Swarm.TUI.Model.Achievement.Definitions
import Swarm.TUI.Model.Achievement.Persistence
import Swarm.TUI.Model.Failure (prettyFailure)
import Swarm.TUI.Model.Repl
import Swarm.TUI.Model.UI
import Swarm.TUI.View.CustomStyling (toAttrPair)
import System.Clock
-- | Initialize the 'AppState'.
initAppState :: AppOpts -> ExceptT Text IO AppState
initAppState AppOpts {..} = do
let isRunningInitialProgram = isJust scriptToRun || autoPlay
skipMenu = isJust userScenario || isRunningInitialProgram || isJust userSeed
gs <- initGameState
(warnings, ui) <- initUIState (not skipMenu) cheatMode
let logWarning rs w = rs & eventLog %~ logEvent (ErrorTrace Error) ("UI Loading", -8) (prettyFailure w)
let rs = List.foldl' logWarning initRuntimeState warnings
case skipMenu of
False -> return $ AppState gs ui rs
True -> do
(scenario, path) <- loadScenario (fromMaybe "classic" userScenario) (gs ^. entityMap)
let maybeAutoplay = do
guard autoPlay
soln <- scenario ^. scenarioSolution
return $ SuggestedSolution soln
let realToRun = maybeAutoplay <|> (ScriptPath <$> scriptToRun)
execStateT
(startGameWithSeed userSeed (scenario, ScenarioInfo path NotStarted NotStarted NotStarted) realToRun)
(AppState gs ui rs)
-- | Load a 'Scenario' and start playing the game.
startGame :: (MonadIO m, MonadState AppState m) => ScenarioInfoPair -> Maybe CodeToRun -> m ()
startGame = startGameWithSeed Nothing
-- | Re-initialize the game from the stored reference to the current scenario.
--
-- Note that "restarting" is intended only for "scenarios";
-- with some scenarios, it may be possible to get stuck so that it is
-- either impossible or very annoying to win, so being offered an
-- option to restart is more user-friendly.
--
-- Since scenarios are stored as a Maybe in the UI state, we handle the Nothing
-- case upstream so that the Scenario passed to this function definitely exists.
restartGame :: (MonadIO m, MonadState AppState m) => Seed -> ScenarioInfoPair -> m ()
restartGame currentSeed siPair = startGameWithSeed (Just currentSeed) siPair Nothing
-- | Load a 'Scenario' and start playing the game, with the
-- possibility for the user to override the seed.
startGameWithSeed :: (MonadIO m, MonadState AppState m) => Maybe Seed -> ScenarioInfoPair -> Maybe CodeToRun -> m ()
startGameWithSeed userSeed siPair@(_scene, si) toRun = do
t <- liftIO getZonedTime
ss <- use $ gameState . scenarios
p <- liftIO $ normalizeScenarioPath ss (si ^. scenarioPath)
gameState . currentScenarioPath .= Just p
gameState . scenarios . scenarioItemByPath p . _SISingle . _2 . scenarioStatus .= InProgress t 0 0
scenarioToAppState siPair userSeed toRun
-- TODO: #516 do we need to keep an old entity map around???
-- | Modify the 'AppState' appropriately when starting a new scenario.
scenarioToAppState ::
(MonadIO m, MonadState AppState m) =>
ScenarioInfoPair ->
Maybe Seed ->
Maybe CodeToRun ->
m ()
scenarioToAppState siPair@(scene, _) userSeed toRun = do
withLensIO gameState $ scenarioToGameState scene userSeed toRun
withLensIO uiState $ scenarioToUIState siPair
where
withLensIO :: (MonadIO m, MonadState AppState m) => Lens' AppState x -> (x -> IO x) -> m ()
withLensIO l a = do
x <- use l
x' <- liftIO $ a x
l .= x'
attainAchievement :: (MonadIO m, MonadState AppState m) => CategorizedAchievement -> m ()
attainAchievement a = do
currentTime <- liftIO getZonedTime
attainAchievement' currentTime Nothing a
attainAchievement' ::
(MonadIO m, MonadState AppState m) =>
ZonedTime ->
Maybe FilePath ->
CategorizedAchievement ->
m ()
attainAchievement' t p a = do
(uiState . uiAchievements)
%= M.insertWith
(<>)
a
(Attainment a p t)
newAchievements <- use $ uiState . uiAchievements
liftIO $ saveAchievementsInfo $ M.elems newAchievements
-- | Modify the UI state appropriately when starting a new scenario.
scenarioToUIState :: ScenarioInfoPair -> UIState -> IO UIState
scenarioToUIState siPair u = do
curTime <- getTime Monotonic
return $
u
& uiPlaying .~ True
& uiGoal .~ emptyGoalDisplay
& uiFocusRing .~ initFocusRing
& uiInventory .~ Nothing
& uiInventorySort .~ defaultSortOptions
& uiShowFPS .~ False
& uiShowZero .~ True
& lgTicksPerSecond .~ initLgTicksPerSecond
& uiREPL .~ initREPLState (u ^. uiREPL . replHistory)
& uiREPL . replHistory %~ restartREPLHistory
& uiAttrMap .~ applyAttrMappings (map toAttrPair $ fst siPair ^. scenarioAttrs) swarmAttrMap
& scenarioRef ?~ siPair
& lastFrameTime .~ curTime