Skip to content

Commit

Permalink
navigable robots table (#2140)
Browse files Browse the repository at this point in the history
Towards #1341.  Should also be useful for #2133.

![Screenshot from 2024-09-09 22-04-10](https://github.com/user-attachments/assets/f2813cb0-be12-4299-8e4b-e745e930427e)

Uses the [`brick-tabular-list`](https://hackage.haskell.org/package/brick-tabular-list) package to render the <kbd>F2</kbd> robots dialog as a navigable list with tabular formatting.  Hitting <kbd>Tab</kbd> on a selected row shows details for that robot.

Also:
* Extracts some record definitions from `Swarm.TUI.Model.UI` into `Swarm.TUI.Model.UI.Gameplay`
* Removes re-export of the `Name` type from `Swarm.TUI.Model`
* Replace uses of `maximum` with the safer `maximum0`
* New `applyJust` combinator

## Testing
### Showing a large robots list
```
scripts/play.sh -i data/scenarios/Challenges/Ranching/beekeeping.yaml --debug all_robots --speed 2 --autoplay
```
and with extra column:
```
scripts/play.sh -i data/scenarios/Challenges/Ranching/beekeeping.yaml --debug all_robots,robot_id --speed 2 --autoplay
```

### Showing a small robots list with details view and logs

```
scripts/play.sh -i data/scenarios/Testing/562-lodestone.yaml --debug all_robots,robot_id --speed 2 --autoplay
```

Log view:
![Screenshot from 2024-09-13 17-25-23](https://github.com/user-attachments/assets/17e3fb6d-5d86-48b6-aeac-db28f37ae854)
  • Loading branch information
kostmo authored Sep 17, 2024
1 parent c2a3220 commit 671fd0f
Show file tree
Hide file tree
Showing 33 changed files with 975 additions and 386 deletions.
2 changes: 2 additions & 0 deletions .hlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
- {name: Data.List.head, within: []}
- {name: Prelude.head, within: [Swarm.Web.Tournament.Database.Query]}
- {name: Prelude.tail, within: []}
- {name: Prelude.maximum, within: [Swarm.Util]}
- {name: Prelude.minimum, within: []}
- {name: Prelude.!!, within: [Swarm.Util.indexWrapNonEmpty, TestEval]}
- {name: undefined, within: [Swarm.Language.Key, TestUtil]}
- {name: fromJust, within: []}
Expand Down
4 changes: 2 additions & 2 deletions src/swarm-doc/Swarm/Doc/Wiki/Cheatsheet.hs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import Swarm.Language.Syntax (Const (..))
import Swarm.Language.Syntax qualified as Syntax
import Swarm.Language.Text.Markdown as Markdown (docToMark)
import Swarm.Language.Typecheck (inferConst)
import Swarm.Util (showT)
import Swarm.Util (maximum0, showT)

-- * Types

Expand Down Expand Up @@ -99,7 +99,7 @@ listToRow mw xs = wrap '|' . T.intercalate "|" $ zipWith format mw xs
format w x = wrap ' ' x <> T.replicate (w - T.length x) " "

maxWidths :: [[Text]] -> [Int]
maxWidths = map (maximum . map T.length) . transpose
maxWidths = map (maximum0 . map T.length) . transpose

-- ** COMMANDS

Expand Down
14 changes: 7 additions & 7 deletions src/swarm-engine/Swarm/Game/State/Initialize.hs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import Swarm.Game.World.Gen (Seed)
import Swarm.Language.Capability (constCaps)
import Swarm.Language.Syntax (allConst, erase)
import Swarm.Language.Types
import Swarm.Util (binTuples, (?))
import Swarm.Util (applyWhen, binTuples, (?))
import System.Clock qualified as Clock
import System.Random (mkStdGen)

Expand Down Expand Up @@ -143,14 +143,14 @@ pureScenarioToGameState scenario theSeed now toRun gsc =
-- If we are in creative mode, give base all the things
& ix baseID
. robotInventory
%~ case scenario ^. scenarioOperation . scenarioCreative of
False -> id
True -> union (fromElems (map (0,) things))
%~ applyWhen
(scenario ^. scenarioOperation . scenarioCreative)
(union (fromElems (map (0,) things)))
& ix baseID
. equippedDevices
%~ case scenario ^. scenarioOperation . scenarioCreative of
False -> id
True -> const (fromList devices)
%~ applyWhen
(scenario ^. scenarioOperation . scenarioCreative)
(const (fromList devices))

running = case robotList of
[] -> False
Expand Down
15 changes: 15 additions & 0 deletions src/swarm-topography/Swarm/Game/Universe.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Control.Lens (makeLenses, view)
import Data.Function (on)
import Data.Int (Int32)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Yaml (FromJSON, ToJSON, Value (Object), parseJSON, withText, (.:))
import GHC.Generics (Generic)
import Linear (V2 (..))
Expand Down Expand Up @@ -82,3 +83,17 @@ defaultCosmicLocation = Cosmic DefaultRootSubworld origin

offsetBy :: Cosmic Location -> V2 Int32 -> Cosmic Location
offsetBy loc v = fmap (.+^ v) loc

-- ** Rendering

locationToString :: Location -> String
locationToString (Location x y) =
unwords $ map show [x, y]

renderCoordsString :: Cosmic Location -> String
renderCoordsString (Cosmic sw coords) =
unwords $ locationToString coords : suffix
where
suffix = case sw of
DefaultRootSubworld -> []
SubworldName swName -> ["in", T.unpack swName]
66 changes: 49 additions & 17 deletions src/swarm-tui/Swarm/TUI/Controller.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ module Swarm.TUI.Controller (
) where

-- See Note [liftA2 re-export from Prelude]
import Prelude hiding (Applicative (..))

import Brick hiding (Direction, Location)
import Brick.Focus
Expand All @@ -36,10 +35,11 @@ import Brick.Widgets.Dialog
import Brick.Widgets.Edit (Editor, applyEdit, handleEditorEvent)
import Brick.Widgets.List (handleListEvent)
import Brick.Widgets.List qualified as BL
import Brick.Widgets.TabularList.Mixed
import Control.Applicative (pure)
import Control.Category ((>>>))
import Control.Lens as Lens
import Control.Monad (unless, void, when)
import Control.Monad (forM_, unless, void, when)
import Control.Monad.Extra (whenJust)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.State (MonadState, execState)
Expand Down Expand Up @@ -87,7 +87,7 @@ import Swarm.Language.Value (Value (VKey), envTypes)
import Swarm.Log
import Swarm.TUI.Controller.EventHandlers
import Swarm.TUI.Controller.SaveScenario (saveScenarioInfoOnQuit)
import Swarm.TUI.Controller.UpdateUI (updateAndRedrawUI)
import Swarm.TUI.Controller.UpdateUI
import Swarm.TUI.Controller.Util
import Swarm.TUI.Editor.Controller qualified as EC
import Swarm.TUI.Editor.Model
Expand All @@ -101,7 +101,11 @@ import Swarm.TUI.Model.Name
import Swarm.TUI.Model.Repl
import Swarm.TUI.Model.StateUpdate
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay
import Swarm.TUI.View.Robot (getList)
import Swarm.TUI.View.Robot.Type
import Swarm.Util hiding (both, (<<.=))
import Prelude hiding (Applicative (..))

-- ~~~~ Note [liftA2 re-export from Prelude]
--
Expand Down Expand Up @@ -292,7 +296,11 @@ handleMainEvent forceRedraw ev = do
Web (RunWebCode e r) -> runBaseWebCode e r
UpstreamVersion _ -> error "version event should be handled by top-level handler"
VtyEvent (V.EvResize _ _) -> invalidateCache
EscapeKey | Just m <- s ^. uiState . uiGameplay . uiDialogs . uiModal -> closeModal m
EscapeKey
| Just m <- s ^. uiState . uiGameplay . uiDialogs . uiModal ->
if s ^. uiState . uiGameplay . uiDialogs . uiRobot . isDetailsOpened
then uiState . uiGameplay . uiDialogs . uiRobot . isDetailsOpened .= False
else closeModal m
-- Pass to key handler (allows users to configure bindings)
-- See Note [how Swarm event handlers work]
VtyEvent (V.EvKey k m)
Expand Down Expand Up @@ -375,19 +383,30 @@ closeModal m = do
handleModalEvent :: V.Event -> EventM Name AppState ()
handleModalEvent = \case
V.EvKey V.KEnter [] -> do
mdialog <- preuse $ uiState . uiGameplay . uiDialogs . uiModal . _Just . modalDialog
toggleModal QuitModal
case dialogSelection =<< mdialog of
Just (Button QuitButton, _) -> quitGame
Just (Button KeepPlayingButton, _) -> toggleModal KeepPlayingModal
Just (Button StartOverButton, StartOver currentSeed siPair) -> do
invalidateCache
restartGame currentSeed siPair
Just (Button NextButton, Next siPair) -> do
quitGame
invalidateCache
startGame siPair Nothing
_ -> return ()
modal <- preuse $ uiState . uiGameplay . uiDialogs . uiModal . _Just . modalType
case modal of
Just RobotsModal -> do
robotDialog <- use $ uiState . uiGameplay . uiDialogs . uiRobot
unless (robotDialog ^. isDetailsOpened) $ do
let widget = robotDialog ^. robotListContent . robotsListWidget
forM_ (BL.listSelectedElement $ getList widget) $ \x -> do
Brick.zoom (uiState . uiGameplay . uiDialogs . uiRobot) $ do
isDetailsOpened .= True
updateRobotDetailsPane $ snd x
_ -> do
mdialog <- preuse $ uiState . uiGameplay . uiDialogs . uiModal . _Just . modalDialog
toggleModal QuitModal
case dialogSelection =<< mdialog of
Just (Button QuitButton, _) -> quitGame
Just (Button KeepPlayingButton, _) -> toggleModal KeepPlayingModal
Just (Button StartOverButton, StartOver currentSeed siPair) -> do
invalidateCache
restartGame currentSeed siPair
Just (Button NextButton, Next siPair) -> do
quitGame
invalidateCache
startGame siPair Nothing
_ -> return ()
ev -> do
Brick.zoom (uiState . uiGameplay . uiDialogs . uiModal . _Just . modalDialog) (handleDialogEvent ev)
modal <- preuse $ uiState . uiGameplay . uiDialogs . uiModal . _Just . modalType
Expand Down Expand Up @@ -418,6 +437,19 @@ handleModalEvent = \case
refreshList $ uiState . uiGameplay . uiDialogs . uiStructure . structurePanelListWidget
StructureSummary -> handleInfoPanelEvent modalScroll (VtyEvent ev)
_ -> handleInfoPanelEvent modalScroll (VtyEvent ev)
Just RobotsModal -> Brick.zoom (uiState . uiGameplay . uiDialogs . uiRobot) $ case ev of
V.EvKey (V.KChar '\t') [] -> robotDetailsFocus %= focusNext
_ -> do
isInDetailsMode <- use isDetailsOpened
if isInDetailsMode
then Brick.zoom (robotListContent . robotDetailsPaneState . logsList) $ handleListEvent ev
else do
Brick.zoom (robotListContent . robotsListWidget) $
handleMixedListEvent ev

-- Ensure list widget content is updated immediately
widget <- use $ robotListContent . robotsListWidget
forM_ (BL.listSelectedElement $ getList widget) $ updateRobotDetailsPane . snd
_ -> handleInfoPanelEvent modalScroll (VtyEvent ev)
where
refreshGoalList lw = nestEventM' lw $ handleListEventWithSeparators ev shouldSkipSelection
Expand Down
1 change: 1 addition & 0 deletions src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Frame.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import Swarm.TUI.Controller.Util
import Swarm.TUI.Model
import Swarm.TUI.Model.Achievements (popupAchievement)
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay
import System.Clock

ticksPerFrameCap :: Int
Expand Down
1 change: 1 addition & 0 deletions src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import Swarm.TUI.Model.DebugOption (DebugOption (ToggleCreative, ToggleWorldEdit
import Swarm.TUI.Model.Dialog.Goal
import Swarm.TUI.Model.Event (MainEvent (..), SwarmEvent (..))
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay
import System.Clock (Clock (..), TimeSpec (..), getTime)

-- | Main keybindings event handler while running the game itself.
Expand Down
1 change: 1 addition & 0 deletions src/swarm-tui/Swarm/TUI/Controller/EventHandlers/REPL.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Swarm.TUI.Model
import Swarm.TUI.Model.Event
import Swarm.TUI.Model.Repl
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay

-- | Handle a user input key event for the REPL.
--
Expand Down
1 change: 1 addition & 0 deletions src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Robot.hs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import Swarm.TUI.List
import Swarm.TUI.Model
import Swarm.TUI.Model.Event
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay
import Swarm.TUI.View.Util (generateModal)

-- | Handle user input events in the robot panel.
Expand Down
1 change: 1 addition & 0 deletions src/swarm-tui/Swarm/TUI/Controller/EventHandlers/World.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Swarm.TUI.Controller.Util
import Swarm.TUI.Model
import Swarm.TUI.Model.Event
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay

-- | Handle a user input event in the world view panel.
worldEventHandlers :: [KeyEventHandler SwarmEvent (EventM Name AppState)]
Expand Down
1 change: 1 addition & 0 deletions src/swarm-tui/Swarm/TUI/Controller/SaveScenario.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Swarm.TUI.Model
import Swarm.TUI.Model.Achievements (attainAchievement')
import Swarm.TUI.Model.Repl
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay
import System.FilePath (splitDirectories)
import Prelude hiding (Applicative (..))

Expand Down
50 changes: 46 additions & 4 deletions src/swarm-tui/Swarm/TUI/Controller/UpdateUI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,28 @@
module Swarm.TUI.Controller.UpdateUI (
updateUI,
updateAndRedrawUI,
updateRobotDetailsPane,
) where

import Brick hiding (Direction, Location)
import Brick.Focus

-- See Note [liftA2 re-export from Prelude]
import Brick hiding (Direction, Location, on)
import Brick.Focus
import Brick.Widgets.List qualified as BL
import Control.Applicative (liftA2, pure)
import Control.Lens as Lens
import Control.Monad (unless, when)
import Control.Monad (forM_, unless, when)
import Control.Monad.IO.Class (liftIO)
import Data.Foldable (toList)
import Data.Function (on)
import Data.List.Extra (enumerate)
import Data.Map qualified as M
import Data.Maybe (isNothing)
import Data.String (fromString)
import Data.Text qualified as T
import Data.Vector qualified as V
import Swarm.Game.Entity hiding (empty)
import Swarm.Game.Robot
import Swarm.Game.Robot.Activity
import Swarm.Game.Robot.Concrete
import Swarm.Game.State
import Swarm.Game.State.Landscape
Expand All @@ -42,7 +46,11 @@ import Swarm.TUI.Model.Dialog.Popup (Popup (..), addPopup)
import Swarm.TUI.Model.Name
import Swarm.TUI.Model.Repl
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay
import Swarm.TUI.View.Objective qualified as GR
import Swarm.TUI.View.Robot
import Swarm.TUI.View.Robot.Type
import Swarm.Util (applyJust)
import Witch (into)
import Prelude hiding (Applicative (..))

Expand Down Expand Up @@ -165,6 +173,8 @@ updateUI = do

newPopups <- generateNotificationPopups

doRobotListUpdate g

let redraw =
g ^. needsRedraw
|| inventoryUpdated
Expand All @@ -174,6 +184,38 @@ updateUI = do
|| newPopups
pure redraw

doRobotListUpdate :: GameState -> EventM Name AppState ()
doRobotListUpdate g = do
gp <- use $ uiState . uiGameplay
dOps <- use $ uiState . uiDebugOptions

let rd =
mkRobotDisplay $
RobotRenderingContext
{ _mygs = g
, _gameplay = gp
, _timing = gp ^. uiTiming
, _uiDbg = dOps
}
oldList = getList $ gp ^. uiDialogs . uiRobot . robotListContent . robotsListWidget
maybeOldSelected = snd <$> BL.listSelectedElement oldList

-- Since we're replacing the entire contents of the list, we need to preserve the
-- selected row here.
maybeModificationFunc =
updateList . BL.listFindBy . ((==) `on` view (robot . robotID)) <$> maybeOldSelected

uiState . uiGameplay . uiDialogs . uiRobot . robotListContent . robotsListWidget .= applyJust maybeModificationFunc rd

Brick.zoom (uiState . uiGameplay . uiDialogs . uiRobot) $
forM_ maybeOldSelected updateRobotDetailsPane

updateRobotDetailsPane :: RobotWidgetRow -> EventM Name RobotDisplay ()
updateRobotDetailsPane robotPayload =
Brick.zoom robotListContent $ do
robotDetailsPaneState . cmdHistogramList . BL.listElementsL .= V.fromList (M.toList (robotPayload ^. robot . activityCounts . commandsHistogram))
robotDetailsPaneState . logsList . BL.listElementsL .= robotPayload ^. robot . robotLog

-- | Either pops up the updated Goals modal
-- or pops up the Congratulations (Win) modal, or pops
-- up the Condolences (Lose) modal.
Expand Down
6 changes: 4 additions & 2 deletions src/swarm-tui/Swarm/TUI/Controller/Util.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Keyboard key event patterns and drawing utilities
module Swarm.TUI.Controller.Util where

import Brick hiding (Direction)
Expand Down Expand Up @@ -35,15 +37,15 @@ import Swarm.Language.Capability (Capability (CDebug))
import Swarm.Language.Syntax hiding (Key)
import Swarm.TUI.Model (
AppState,
FocusablePanel,
ModalType (..),
Name (..),
gameState,
modalScroll,
uiState,
)
import Swarm.TUI.Model.Name
import Swarm.TUI.Model.Repl (REPLHistItem, REPLPrompt, REPLState, addREPLItem, replHistory, replPromptText, replPromptType)
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay
import Swarm.TUI.View.Util (generateModal)
import System.Clock (Clock (..), getTime)

Expand Down
1 change: 1 addition & 0 deletions src/swarm-tui/Swarm/TUI/Editor/Controller.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import Swarm.TUI.Editor.Util qualified as EU
import Swarm.TUI.Model
import Swarm.TUI.Model.Name
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay
import Swarm.Util (hoistMaybe)
import Swarm.Util.Erasable (maybeToErasable)
import System.Clock
Expand Down
2 changes: 1 addition & 1 deletion src/swarm-tui/Swarm/TUI/Editor/Masking.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Swarm.Game.Universe
import Swarm.Game.World.Coords
import Swarm.TUI.Editor.Model
import Swarm.TUI.Editor.Util qualified as EU
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay

shouldHideWorldCell :: UIGameplay -> Coords -> Bool
shouldHideWorldCell ui coords =
Expand Down
Loading

0 comments on commit 671fd0f

Please sign in to comment.