swarm-0.4: src/Swarm/Game/Display.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- Orphan Hashable instances needed to derive Hashable Display
-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Utilities for describing how to display in-game entities in the TUI.
module Swarm.Game.Display (
-- * The display record
Priority,
Attribute (..),
Display,
-- ** Fields
defaultChar,
orientationMap,
curOrientation,
displayAttr,
displayPriority,
invisible,
-- ** Rendering
displayChar,
hidden,
-- ** Construction
defaultTerrainDisplay,
defaultEntityDisplay,
defaultRobotDisplay,
) where
import Control.Lens hiding (Const, from, (.=))
import Data.Hashable (Hashable)
import Data.Map (Map)
import Data.Map qualified as M
import Data.Maybe (fromMaybe, isJust)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Yaml
import GHC.Generics (Generic)
import Swarm.Language.Syntax (AbsoluteDir (..), Direction (..))
import Swarm.Util (maxOn)
import Swarm.Util.Lens (makeLensesNoSigs)
import Swarm.Util.Yaml (FromJSONE (..), With (runE), getE, liftE, withObjectE)
-- | Display priority. Entities with higher priority will be drawn on
-- top of entities with lower priority.
type Priority = Int
-- | An internal attribute name.
data Attribute = ADefault | ARobot | AEntity | AWorld Text | ATerrain Text
deriving (Eq, Ord, Show, Generic, Hashable)
terrainPrefix :: Text
terrainPrefix = "terrain_"
instance FromJSON Attribute where
parseJSON =
withText "attribute" $
pure . \case
"robot" -> ARobot
"entity" -> AEntity
"default" -> ADefault
t | terrainPrefix `T.isPrefixOf` t -> ATerrain $ T.drop (T.length terrainPrefix) t
w -> AWorld w
instance ToJSON Attribute where
toJSON = \case
ADefault -> String "default"
ARobot -> String "robot"
AEntity -> String "entity"
AWorld w -> String w
ATerrain t -> String $ terrainPrefix <> t
-- | A record explaining how to display an entity in the TUI.
data Display = Display
{ _defaultChar :: Char
, _orientationMap :: Map AbsoluteDir Char
, _curOrientation :: Maybe Direction
, _displayAttr :: Attribute
, _displayPriority :: Priority
, _invisible :: Bool
}
deriving (Eq, Ord, Show, Generic, Hashable)
instance Semigroup Display where
d1 <> d2
| _invisible d1 = d2
| _invisible d2 = d1
| otherwise = maxOn _displayPriority d1 d2
makeLensesNoSigs ''Display
-- | The default character to use for display.
defaultChar :: Lens' Display Char
-- | For robots or other entities that have an orientation, this map
-- optionally associates different display characters with
-- different orientations. If an orientation is not in the map,
-- the 'defaultChar' will be used.
orientationMap :: Lens' Display (Map AbsoluteDir Char)
-- | The display caches the current orientation of the entity, so we
-- know which character to use from the orientation map.
curOrientation :: Lens' Display (Maybe Direction)
-- | The attribute to use for display.
displayAttr :: Lens' Display Attribute
-- | This entity's display priority. Higher priorities are drawn
-- on top of lower.
displayPriority :: Lens' Display Priority
-- | Whether the entity is currently invisible.
invisible :: Lens' Display Bool
instance FromJSON Display where
parseJSON v = runE (parseJSONE v) (defaultEntityDisplay ' ')
instance FromJSONE Display Display where
parseJSONE = withObjectE "Display" $ \v -> do
defD <- getE
mc <- liftE $ v .:? "char"
let c = fromMaybe (defD ^. defaultChar) mc
let dOM = if isJust mc then mempty else defD ^. orientationMap
liftE $
Display c
<$> v .:? "orientationMap" .!= dOM
<*> v .:? "curOrientation" .!= (defD ^. curOrientation)
<*> (v .:? "attr") .!= (defD ^. displayAttr)
<*> v .:? "priority" .!= (defD ^. displayPriority)
<*> v .:? "invisible" .!= (defD ^. invisible)
instance ToJSON Display where
toJSON d =
object $
[ "char" .= (d ^. defaultChar)
, "attr" .= (d ^. displayAttr)
, "priority" .= (d ^. displayPriority)
]
++ ["orientationMap" .= (d ^. orientationMap) | not (M.null (d ^. orientationMap))]
++ ["invisible" .= (d ^. invisible) | d ^. invisible]
-- | Look up the character that should be used for a display.
displayChar :: Display -> Char
displayChar disp = fromMaybe (disp ^. defaultChar) $ do
DAbsolute d <- disp ^. curOrientation
M.lookup d (disp ^. orientationMap)
-- | Modify a display to use a @?@ character for entities that are
-- hidden/unknown.
hidden :: Display -> Display
hidden = (defaultChar .~ '?') . (curOrientation .~ Nothing)
-- | The default way to display some terrain using the given character
-- and attribute, with priority 0.
defaultTerrainDisplay :: Char -> Attribute -> Display
defaultTerrainDisplay c attr =
defaultEntityDisplay c
& displayPriority .~ 0
& displayAttr .~ attr
-- | Construct a default display for an entity that uses only a single
-- display character, the default entity attribute, and priority 1.
defaultEntityDisplay :: Char -> Display
defaultEntityDisplay c =
Display
{ _defaultChar = c
, _orientationMap = M.empty
, _curOrientation = Nothing
, _displayAttr = AEntity
, _displayPriority = 1
, _invisible = False
}
-- | Construct a default robot display for a given orientation, with
-- display characters @"X^>v<"@, the default robot attribute, and
-- priority 10.
--
-- Note that the 'defaultChar' is used for direction 'DDown'
-- and is overridden for the special base robot.
defaultRobotDisplay :: Display
defaultRobotDisplay =
Display
{ _defaultChar = 'X'
, _orientationMap =
M.fromList
[ (DEast, '>')
, (DWest, '<')
, (DSouth, 'v')
, (DNorth, '^')
]
, _curOrientation = Nothing
, _displayAttr = ARobot
, _displayPriority = 10
, _invisible = False
}
instance Monoid Display where
mempty = defaultEntityDisplay ' ' & invisible .~ True