Skip to content

Commit

Permalink
Towards #358
Browse files Browse the repository at this point in the history
  • Loading branch information
kostmo committed Feb 1, 2023
1 parent e08f342 commit e5ace21
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 25 deletions.
14 changes: 14 additions & 0 deletions src/Swarm/Game/Scenario/Launch.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Swarm.Game.Scenario.Launch where

import Brick.Widgets.FileBrowser qualified as FB
import Brick.Forms qualified as BF
import Swarm.TUI.Model.Name

newtype SeedSelection = SeedSelection Int

-- | UI elements to configure scenario launch options
data LaunchOptions = LaunchOptions {
fileBrowser :: Maybe (FB.FileBrowser Name)
, seedSelectionForm :: BF.Form SeedSelection () Name
}

45 changes: 33 additions & 12 deletions src/Swarm/TUI/Controller.hs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import Control.Carrier.Lift qualified as Fused
import Control.Carrier.State.Lazy qualified as Fused
import Control.Lens
import Control.Lens.Extras (is)
import Swarm.Game.Scenario.Launch
import Control.Monad.Except
import Control.Monad.Extra (whenJust)
import Control.Monad.State
Expand Down Expand Up @@ -131,7 +132,7 @@ handleEvent = \case
-- quitGame function would have already halted the app).
NoMenu -> const halt
MainMenu l -> handleMainMenuEvent l
NewGameMenu l -> handleNewGameMenuEvent l
NewGameMenu l c -> handleNewGameMenuEvent l c
MessagesMenu -> handleMainMessagesEvent
AchievementsMenu l -> handleMainAchievementsEvent l
AboutMenu -> pressAnyKey (MainMenu (mainMenu About))
Expand All @@ -147,7 +148,7 @@ handleMainMenuEvent menu = \case
NewGame -> do
cheat <- use $ uiState . uiCheatMode
ss <- use $ gameState . scenarios
uiState . uiMenu .= NewGameMenu (NE.fromList [mkScenarioList cheat ss])
uiState . uiMenu .= NewGameMenu (NE.fromList [mkScenarioList cheat ss]) Nothing
Tutorial -> do
-- Set up the menu stack as if the user had chosen "New Game > Tutorials"
cheat <- use $ uiState . uiCheatMode
Expand All @@ -159,7 +160,7 @@ handleMainMenuEvent menu = \case
(mkScenarioList cheat ss)
tutorialMenu = mkScenarioList cheat tutorialCollection
menuStack = NE.fromList [tutorialMenu, topMenu]
uiState . uiMenu .= NewGameMenu menuStack
uiState . uiMenu .= NewGameMenu menuStack Nothing

-- Extract the first tutorial challenge and run it
let firstTutorial = case scOrder tutorialCollection of
Expand Down Expand Up @@ -194,7 +195,9 @@ getTutorials sc = case M.lookup tutorialsDirname (scMap sc) of
-- menu item is always the same as the currently played scenario! `quitGame`
-- is the only place this function should be called.
advanceMenu :: Menu -> Menu
advanceMenu = _NewGameMenu . ix 0 %~ BL.listMoveDown
advanceMenu m = case m of
NewGameMenu (z :| zs) x -> NewGameMenu (BL.listMoveDown z :| zs) x
_ -> m

handleMainAchievementsEvent ::
BL.List Name CategorizedAchievement ->
Expand All @@ -220,21 +223,33 @@ handleMainMessagesEvent = \case
where
returnToMainMenu = uiState . uiMenu .= MainMenu (mainMenu Messages)

handleNewGameMenuEvent :: NonEmpty (BL.List Name ScenarioItem) -> BrickEvent Name AppEvent -> EventM Name AppState ()
handleNewGameMenuEvent scenarioStack@(curMenu :| rest) = \case
Key V.KEnter ->
-- | TODO: Don't prompt if the scenario is a tutorial.
prepareGameStart :: ScenarioInfoPair -> EventM Name AppState ()
prepareGameStart siPair =
-- openModal ScenarioOptionsModal
startGame siPair Nothing

handleNewGameMenuEvent
:: NonEmpty (BL.List Name ScenarioItem)
-> Maybe LaunchOptions
-> BrickEvent Name AppEvent
-> EventM Name AppState ()
handleNewGameMenuEvent scenarioStack@(curMenu :| rest) maybeLaunchOptions = \case
VtyEvent (V.EvKey V.KEnter modifiers) ->
case snd <$> BL.listSelectedElement curMenu of
Nothing -> continueWithoutRedraw
Just (SISingle siPair) -> startGame siPair Nothing
Just (SISingle siPair) -> case modifiers of
[V.MShift] -> prepareGameStart siPair
_ -> startGame siPair Nothing
Just (SICollection _ c) -> do
cheat <- use $ uiState . uiCheatMode
uiState . uiMenu .= NewGameMenu (NE.cons (mkScenarioList cheat c) scenarioStack)
uiState . uiMenu .= NewGameMenu (NE.cons (mkScenarioList cheat c) scenarioStack) maybeLaunchOptions
Key V.KEsc -> exitNewGameMenu scenarioStack
CharKey 'q' -> exitNewGameMenu scenarioStack
ControlChar 'q' -> halt
VtyEvent ev -> do
menu' <- nestEventM' curMenu (handleListEvent ev)
uiState . uiMenu .= NewGameMenu (menu' :| rest)
uiState . uiMenu .= NewGameMenu (menu' :| rest) maybeLaunchOptions
_ -> continueWithoutRedraw

exitNewGameMenu :: NonEmpty (BL.List Name ScenarioItem) -> EventM Name AppState ()
Expand All @@ -243,7 +258,7 @@ exitNewGameMenu stk = do
. uiMenu
.= case snd (NE.uncons stk) of
Nothing -> MainMenu (mainMenu NewGame)
Just stk' -> NewGameMenu stk'
Just stk' -> NewGameMenu stk' Nothing

pressAnyKey :: Menu -> BrickEvent Name AppEvent -> EventM Name AppState ()
pressAnyKey m (VtyEvent (V.EvKey _ _)) = uiState . uiMenu .= m
Expand Down Expand Up @@ -464,7 +479,13 @@ saveScenarioInfoOnQuit = do
-- See what scenario is currently focused in the menu. Depending on how the
-- previous scenario ended (via quit vs. via win), it might be the same as
-- currentScenarioPath or it might be different.
curPath <- preuse $ uiState . uiMenu . _NewGameMenu . ix 0 . BL.listSelectedElementL . _SISingle . _2 . scenarioPath
uim <- preuse $ uiState . uiMenu
let curPath = case uim of
Just (NewGameMenu (z :| _) _) ->
case BL.listSelectedElement z of
Just (_, SISingle (_, sInfo)) -> Just $ _scenarioPath sInfo
_ -> Nothing
_ -> Nothing
-- Now rebuild the NewGameMenu so it gets the updated ScenarioInfo,
-- being sure to preserve the same focused scenario.
sc <- use $ gameState . scenarios
Expand Down
2 changes: 1 addition & 1 deletion src/Swarm/TUI/Model.hs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ data AppOpts = AppOpts
-- is the last among its siblings.
nextScenario :: Menu -> Maybe ScenarioInfoPair
nextScenario = \case
NewGameMenu (curMenu :| _) ->
NewGameMenu (curMenu :| _) _ ->
let nextMenuList = BL.listMoveDown curMenu
isLastScenario = BL.listSelected curMenu == Just (length (BL.listElements curMenu) - 1)
in if isLastScenario
Expand Down
19 changes: 14 additions & 5 deletions src/Swarm/TUI/Model/Menu.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
module Swarm.TUI.Model.Menu where

import Brick.Widgets.Dialog (Dialog)
import Brick.Widgets.List qualified as BL
import Control.Lens hiding (from, (<.>))
import Swarm.Game.Scenario.Launch
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Map qualified as M
Expand All @@ -26,6 +26,7 @@ import Swarm.Game.ScenarioInfo (
import Swarm.Game.State
import Swarm.TUI.Model.Achievement.Definitions
import Swarm.TUI.Model.Name
import Brick.Widgets.List qualified as BL
import Swarm.Util
import System.FilePath (dropTrailingPathSeparator, splitPath, takeFileName)
import Witch (into)
Expand Down Expand Up @@ -72,9 +73,10 @@ data MainMenuEntry
deriving (Eq, Ord, Show, Read, Bounded, Enum)

data Menu
= NoMenu -- We started playing directly from command line, no menu to show
= NoMenu
-- ^ We started playing directly from command line, no menu to show
| MainMenu (BL.List Name MainMenuEntry)
| -- Stack of scenario item lists. INVARIANT: the currently selected
| -- | Stack of scenario item lists. INVARIANT: the currently selected
-- menu item is ALWAYS the same as the scenario currently being played.
-- See https://github.com/swarm-game/swarm/issues/1064 and
-- https://github.com/swarm-game/swarm/pull/1065.
Expand All @@ -98,9 +100,16 @@ mkScenarioList cheat = flip (BL.list ScenarioList) 1 . V.fromList . filterTest .
-- path to some folder or scenario, construct a 'NewGameMenu' stack
-- focused on the given item, if possible.
mkNewGameMenu :: Bool -> ScenarioCollection -> FilePath -> Maybe Menu
mkNewGameMenu cheat sc path = NewGameMenu . NE.fromList <$> go (Just sc) (splitPath path) []
mkNewGameMenu cheat sc path = do
theList <- NE.fromList <$> go (Just sc) (splitPath path) []
return $ NewGameMenu theList Nothing

where
go :: Maybe ScenarioCollection -> [FilePath] -> [BL.List Name ScenarioItem] -> Maybe [BL.List Name ScenarioItem]
go
:: Maybe ScenarioCollection
-> [FilePath]
-> [BL.List Name ScenarioItem]
-> Maybe [BL.List Name ScenarioItem]
go _ [] stk = Just stk
go Nothing _ _ = Nothing
go (Just curSC) (thing : rest) stk = go nextSC rest (lst : stk)
Expand Down
12 changes: 12 additions & 0 deletions src/Swarm/TUI/Model/Name.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ data FocusablePanel
InfoPanel
deriving (Eq, Ord, Show, Read, Bounded, Enum)

data ScenarioConfigFocusable =
ScenarioConfigFileSelector
| ScenarioConfigPanelControl ScenarioConfigPanelFocusable
deriving (Eq, Ord, Show, Read)

data ScenarioConfigPanelFocusable
= SeedSelector
| ScriptSelector
deriving (Eq, Ord, Show, Read, Bounded, Enum)

data GoalWidget
= ObjectivesList
| GoalSummary
Expand Down Expand Up @@ -44,6 +54,8 @@ data Name
MenuList
| -- | The list of achievements.
AchievementList
| -- | An individual control within the scenario launch config panel
ScenarioConfigControl ScenarioConfigFocusable
| -- | The list of goals/objectives.
GoalWidgets GoalWidget
| -- | The list of scenario choices.
Expand Down
50 changes: 45 additions & 5 deletions src/Swarm/TUI/View.hs
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,23 @@ import Brick.Focus
import Brick.Forms
import Brick.Widgets.Border (hBorder, hBorderWithLabel, joinableBorder, vBorder)
import Brick.Widgets.Center (center, centerLayer, hCenter)
import Swarm.Game.Scenario.Launch
import Brick.Forms qualified as BF
import Brick.Widgets.Dialog
import Brick.Widgets.Edit (getEditContents, renderEditor)
import Brick.Widgets.List qualified as BL
import Brick.Widgets.Table qualified as BT
import Control.Lens hiding (Const, from)
import Control.Monad (guard)
import Control.Monad.Reader (withReaderT)
import Brick.Widgets.FileBrowser qualified as FB
import Data.Array (range)
import Data.Bits (shiftL, shiftR, (.&.))
import Data.Foldable qualified as F
import Data.Functor (($>))
import Data.IntMap qualified as IM
import Data.List (intersperse)
import Swarm.TUI.Model.Menu
import Data.List qualified as L
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
Expand Down Expand Up @@ -87,6 +92,9 @@ import Swarm.Game.ScenarioInfo (
scenarioStatus,
)
import Swarm.Game.State
import Brick.Widgets.Border
( borderWithLabel
)
import Swarm.Game.World qualified as W
import Swarm.Language.Capability (constCaps)
import Swarm.Language.Pretty (prettyText)
Expand All @@ -109,6 +117,7 @@ import System.Clock (TimeSpec (..))
import Text.Printf
import Text.Wrap
import Witch (from, into)
import Control.Exception qualified as E

-- | The main entry point for drawing the entire UI. Figures out
-- which menu screen we should show (if any), or just the game itself.
Expand All @@ -120,7 +129,7 @@ drawUI s
-- quit the app instead. But just in case, we display the main menu anyway.
NoMenu -> [drawMainMenuUI s (mainMenu NewGame)]
MainMenu l -> [drawMainMenuUI s l]
NewGameMenu stk -> [drawNewGameMenuUI stk]
NewGameMenu stk scenarioCfg -> drawNewGameMenuUI stk scenarioCfg
AchievementsMenu l -> [drawAchievementsMenuUI s l]
MessagesMenu -> [drawMainMessages s]
AboutMenu -> [drawAboutMenuUI (s ^. uiState . appData . at "about")]
Expand Down Expand Up @@ -168,9 +177,40 @@ drawLogo = centerLayer . vBox . map (hBox . T.foldr (\c ws -> drawThing c : ws)
attrFor '' = dirtAttr
attrFor _ = defAttr

drawNewGameMenuUI :: NonEmpty (BL.List Name ScenarioItem) -> Widget Name
drawNewGameMenuUI (l :| ls) =
padLeftRight 20
drawFileBrowser :: FB.FileBrowser Name -> Widget Name
drawFileBrowser b =
center $ ui <=> help
where
ui = hCenter $
vLimit 15 $
hLimit 50 $
borderWithLabel (txt "Choose a file") $
FB.renderFileBrowser True b
help = padTop (Pad 1) $
vBox [ case FB.fileBrowserException b of
Nothing -> emptyWidget
Just e -> hCenter $ withDefAttr BF.invalidFormInputAttr $
txt $ T.pack $ E.displayException e
, hCenter $ txt "Up/Down: select"
, hCenter $ txt "/: search, Ctrl-C or Esc: cancel search"
, hCenter $ txt "Enter: change directory or select file"
, hCenter $ txt "Esc: quit"
]

-- | When launching a game, a modal prompt may appear on another layer
-- to input seed and/or a script to run.
drawNewGameMenuUI
:: NonEmpty (BL.List Name ScenarioItem)
-> Maybe LaunchOptions
-> [Widget Name]
drawNewGameMenuUI (l :| ls) maybeLaunchOptions = case maybeLaunchOptions of
Nothing -> pure mainWidget
Just (LaunchOptions maybeFileBrowser seedSelector) -> case maybeFileBrowser of
Nothing -> pure mainWidget
Just fb -> [drawFileBrowser fb, mainWidget]

where
mainWidget = padLeftRight 20
. centerLayer
$ hBox
[ vBox
Expand All @@ -183,7 +223,7 @@ drawNewGameMenuUI (l :| ls) =
]
, padLeft (Pad 5) (maybe (txt "") (drawDescription . snd) (BL.listSelectedElement l))
]
where

drawScenarioItem (SISingle (s, si)) = padRight (Pad 1) (drawStatusInfo s si) <+> txt (s ^. scenarioName)
drawScenarioItem (SICollection nm _) = padRight (Pad 1) (withAttr boldAttr $ txt " > ") <+> txt nm
drawStatusInfo s si = case si ^. scenarioBestTime of
Expand Down
4 changes: 2 additions & 2 deletions src/Swarm/TUI/View/Util.hs
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ maxModalWindowWidth = 500
-- | Get the name of the current New Game menu.
curMenuName :: AppState -> Maybe Text
curMenuName s = case s ^. uiState . uiMenu of
NewGameMenu (_ :| (parentMenu : _)) ->
NewGameMenu (_ :| (parentMenu : _)) _ ->
Just (parentMenu ^. BL.listSelectedElementL . to scenarioItemName)
NewGameMenu _ -> Just "Scenarios"
NewGameMenu _ _ -> Just "Scenarios"
_ -> Nothing

quitMsg :: Menu -> Text
Expand Down
1 change: 1 addition & 0 deletions swarm.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ library
Swarm.Game.Robot
Swarm.Game.Scenario
Swarm.Game.Scenario.Cell
Swarm.Game.Scenario.Launch
Swarm.Game.Scenario.Objective.Logic
Swarm.Game.Scenario.Objective.Graph
Swarm.Game.Scenario.Objective.Presentation.Model
Expand Down

0 comments on commit e5ace21

Please sign in to comment.