swarm-0.5.0.0: src/Swarm/Game/Step/Util.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- SPDX-License-Identifier: BSD-3-Clause
module Swarm.Game.Step.Util where
import Control.Applicative (Applicative (..))
import Control.Carrier.State.Lazy
import Control.Effect.Error
import Control.Effect.Lens
import Control.Effect.Lift
import Control.Lens as Lens hiding (Const, distrib, from, parts, use, uses, view, (%=), (+=), (.=), (<+=), (<>=))
import Control.Monad (forM, forM_, guard, join, when)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)
import Data.Array (bounds, (!))
import Data.IntMap qualified as IM
import Data.List (find)
import Data.Map qualified as M
import Data.Set qualified as S
import Data.Text (Text)
import Data.Text qualified as T
import Linear (zero)
import Swarm.Game.Entity hiding (empty, lookup, singleton, union)
import Swarm.Game.Exception
import Swarm.Game.Location
import Swarm.Game.ResourceLoading (NameGenerator (..))
import Swarm.Game.Robot
import Swarm.Game.State
import Swarm.Game.Universe
import Swarm.Game.World qualified as W
import Swarm.Game.World.Modify qualified as WM
import Swarm.Language.Capability
import Swarm.Language.Requirement qualified as R
import Swarm.Language.Syntax
import Swarm.Util hiding (both)
import System.Clock (TimeSpec)
import System.Clock qualified
import System.Random (UniformRange, uniformR)
import Prelude hiding (Applicative (..), lookup)
-- | All functions that are used for robot step can access 'GameState' and the current 'Robot'.
--
-- They can also throw exception of our custom type, which is handled elsewhere.
-- Because of that the constraint is only 'Throw', but not 'Catch'/'Error'.
type HasRobotStepState sig m = (Has (State GameState) sig m, Has (State Robot) sig m, Has (Throw Exn) sig m)
deriveHeading :: HasRobotStepState sig m => Direction -> m Heading
deriveHeading d = do
orient <- use robotOrientation
when (isCardinal d) $ hasCapabilityFor COrient $ TDir d
return $ applyTurn d $ orient ? zero
lookInDirection :: HasRobotStepState sig m => Direction -> m (Cosmic Location, Maybe Entity)
lookInDirection d = do
newHeading <- deriveHeading d
loc <- use robotLocation
let nextLoc = loc `offsetBy` newHeading
(nextLoc,) <$> entityAt nextLoc
-- | Modify the entity (if any) at a given location.
updateEntityAt ::
(Has (State GameState) sig m) =>
Cosmic Location ->
(Maybe Entity -> Maybe Entity) ->
m ()
updateEntityAt cLoc@(Cosmic subworldName loc) upd = do
someChange <-
zoomWorld subworldName $
W.updateM @Int (W.locToCoords loc) upd
forM_ (WM.getModification =<< someChange) $ \_modType -> do
wakeWatchingRobots cLoc
-- * Capabilities
-- | Exempts the robot from various command constraints
-- when it is either a system robot or playing in creative mode
isPrivilegedBot :: (Has (State GameState) sig m, Has (State Robot) sig m) => m Bool
isPrivilegedBot = (||) <$> use systemRobot <*> use creativeMode
-- | Test whether the current robot has a given capability (either
-- because it has a device which gives it that capability, or it is a
-- system robot, or we are in creative mode).
hasCapability :: (Has (State Robot) sig m, Has (State GameState) sig m) => Capability -> m Bool
hasCapability cap = do
isPrivileged <- isPrivilegedBot
caps <- use robotCapabilities
return (isPrivileged || cap `S.member` caps)
-- | Ensure that either a robot has a given capability, OR we are in creative
-- mode.
hasCapabilityFor ::
(Has (State Robot) sig m, Has (State GameState) sig m, Has (Throw Exn) sig m) => Capability -> Term -> m ()
hasCapabilityFor cap term = do
h <- hasCapability cap
h `holdsOr` Incapable FixByEquip (R.singletonCap cap) term
-- * Exceptions
holdsOrFail' :: (Has (Throw Exn) sig m) => Const -> Bool -> [Text] -> m ()
holdsOrFail' c a ts = a `holdsOr` cmdExn c ts
isJustOrFail' :: (Has (Throw Exn) sig m) => Const -> Maybe a -> [Text] -> m a
isJustOrFail' c a ts = a `isJustOr` cmdExn c ts
-- | Create an exception about a command failing.
cmdExn :: Const -> [Text] -> Exn
cmdExn c parts = CmdFailed c (T.unwords parts) Nothing
-- * Some utility functions
getNow :: Has (Lift IO) sig m => m TimeSpec
getNow = sendIO $ System.Clock.getTime System.Clock.Monotonic
-- | Set a flag telling the UI that the world needs to be redrawn.
flagRedraw :: (Has (State GameState) sig m) => m ()
flagRedraw = needsRedraw .= True
-- * World queries
getNeighborLocs :: Cosmic Location -> [Cosmic Location]
getNeighborLocs loc = map (offsetBy loc . flip applyTurn north . DRelative . DPlanar) listEnums
-- | Perform an action requiring a 'W.World' state component in a
-- larger context with a 'GameState'.
zoomWorld ::
(Has (State GameState) sig m) =>
SubworldName ->
StateC (W.World Int Entity) Identity b ->
m (Maybe b)
zoomWorld swName n = do
mw <- use $ landscape . multiWorld
forM (M.lookup swName mw) $ \w -> do
let (w', a) = run (runState w n)
landscape . multiWorld %= M.insert swName w'
return a
-- | Get the entity (if any) at a given location.
entityAt :: (Has (State GameState) sig m) => Cosmic Location -> m (Maybe Entity)
entityAt (Cosmic subworldName loc) =
join <$> zoomWorld subworldName (W.lookupEntityM @Int (W.locToCoords loc))
-- | Get the robot with a given ID.
robotWithID :: (Has (State GameState) sig m) => RID -> m (Maybe Robot)
robotWithID rid = use (robotMap . at rid)
-- | Get the robot with a given name.
robotWithName :: (Has (State GameState) sig m) => Text -> m (Maybe Robot)
robotWithName rname = use (robotMap . to IM.elems . to (find $ \r -> r ^. robotName == rname))
-- * Randomness
-- | Generate a uniformly random number using the random generator in
-- the game state.
uniform :: (Has (State GameState) sig m, UniformRange a) => (a, a) -> m a
uniform bnds = do
rand <- use randGen
let (n, g) = uniformR bnds rand
randGen .= g
return n
-- | Given a weighting function and a list of values, choose one of
-- the values randomly (using the random generator in the game
-- state), with the probability of each being proportional to its
-- weight. Return @Nothing@ if the list is empty.
weightedChoice :: Has (State GameState) sig m => (a -> Integer) -> [a] -> m (Maybe a)
weightedChoice weight as = do
r <- uniform (0, total - 1)
return $ go r as
where
total = sum (map weight as)
go _ [] = Nothing
go !k (x : xs)
| k < w = Just x
| otherwise = go (k - w) xs
where
w = weight x
-- | Generate a random robot name in the form @adjective_name@.
randomName :: Has (State GameState) sig m => m Text
randomName = do
NameGenerator adjs names <- use $ robotNaming . nameGenerator
i <- uniform (bounds adjs)
j <- uniform (bounds names)
return $ T.concat [adjs ! i, "_", names ! j]
-- * Moving
data MoveFailureMode = PathBlocked | PathLiquid
data MoveFailureDetails = MoveFailureDetails Entity MoveFailureMode
-- | Make sure nothing is in the way.
-- No exception for system robots
checkMoveFailureUnprivileged :: HasRobotStepState sig m => Cosmic Location -> m (Maybe MoveFailureDetails)
checkMoveFailureUnprivileged nextLoc = do
me <- entityAt nextLoc
caps <- use robotCapabilities
unwalkables <- use unwalkableEntities
return $ do
e <- me
go caps unwalkables e
where
go caps unwalkables e
-- robots can not walk through walls
| e `hasProperty` Unwalkable || (e ^. entityName) `S.member` unwalkables = Just $ MoveFailureDetails e PathBlocked
-- robots drown if they walk over liquid without boat
| e `hasProperty` Liquid && CFloat `S.notMember` caps =
Just $ MoveFailureDetails e PathLiquid
| otherwise = Nothing
-- | Make sure nothing is in the way. Note that system robots implicitly ignore
-- and base throws on failure.
checkMoveFailure :: HasRobotStepState sig m => Cosmic Location -> m (Maybe MoveFailureDetails)
checkMoveFailure nextLoc = do
systemRob <- use systemRobot
runMaybeT $ do
guard $ not systemRob
maybeMoveFailure <- lift $ checkMoveFailureUnprivileged nextLoc
hoistMaybe maybeMoveFailure