swarm-0.7.0.0: src/swarm-scenario/Swarm/Game/Device.hs
-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- A device is an entity that provides capabilities.
--
-- Some capabilities have a cost to exercise.
-- Items will be consumed from the inventory for
-- invoking a command that utilizes a given capability.
module Swarm.Game.Device (
SingleEntityCapabilities,
MultiEntityCapabilities,
Capabilities (..),
DeviceUseCost (..),
ExerciseCost (..),
CommandsAndCost (..),
getCapabilitySet,
zeroCostCapabilities,
commandsForDeviceCaps,
)
where
import Control.Applicative ((<|>))
import Data.Function (on)
import Data.Hashable
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map (Map)
import Data.Map qualified as M
import Data.Set (Set)
import Data.Vector qualified as V
import Data.Yaml
import GHC.Generics (Generic)
import Swarm.Game.Ingredients
import Swarm.Language.Capability (Capability, constByCaps)
import Swarm.Language.Syntax (Const)
-- | The 'Capabilities e' wrapper type stores information of type @e@ for each
-- of some set of capabilities.
-- For example, @e@ could be a list of ingredients needed to exercise a
-- capability, or a set of devices capable of providing a capability.
newtype Capabilities e = Capabilities
{ getMap :: Map Capability e
}
deriving (Show, Eq, Generic, ToJSON, Hashable, Functor, Foldable, Traversable)
-- | Get the set of capabilities about which we are storing information.
getCapabilitySet :: Capabilities e -> Set Capability
getCapabilitySet (Capabilities m) = M.keysSet m
-- | Records an 'ExerciseCost', i.e. list of consumed ingredients, per capability that can be exercised. This represents information about a single entity/device, which can provide multiple capabilities (with a different exercise cost for each).
type SingleEntityCapabilities e = Capabilities (ExerciseCost e)
-- | Records a list of devices capable of providing each capability;
-- along with each device is recorded the 'ExerciseCost' needed to use
-- that device to achieve the given capability.
--
-- See 'DeviceUseCost' for explanation of type parameters.
type MultiEntityCapabilities e en = Capabilities (NonEmpty (DeviceUseCost e en))
-- | Create a default 'SingleEntityCapabilities' map for a device which provides capabilities with no associated costs.
zeroCostCapabilities :: Set Capability -> SingleEntityCapabilities e
zeroCostCapabilities = Capabilities . M.fromSet (const $ ExerciseCost [])
-- | Package together a capability and exercise cost; only used temporarily for parsing this information from JSON format.
data CapabilityCost e = CapabilityCost
{ capability :: Capability
, cost :: IngredientList e
}
deriving (Generic, FromJSON)
-- | First, attempt to parse capabilities as a list, interpreted as a set of capabilities with no exercise cost.
-- Otherwise, parse as a Map from capabilities to ingredients.
instance (FromJSON e) => FromJSON (SingleEntityCapabilities e) where
parseJSON x =
(Capabilities <$> costMap) <|> simpleList
where
simpleList = zeroCostCapabilities <$> parseJSON x
costMap = withArray "Capabilities" (fmap (M.fromList . map toMapEntry) . mapM parseJSON . V.toList) x
toMapEntry (CapabilityCost a b) = (a, ExerciseCost b)
instance (Ord e, Semigroup e) => Semigroup (Capabilities e) where
Capabilities c1 <> Capabilities c2 =
Capabilities $ M.unionWith (<>) c1 c2
instance (Ord e, Semigroup e) => Monoid (Capabilities e) where
mempty = Capabilities mempty
-- | Exercising a capability may have a cost, in the form of entities that must be consumed each time it is used.
newtype ExerciseCost e = ExerciseCost
{ ingredients :: IngredientList e
}
deriving (Eq, Show, Generic, FromJSON, ToJSON, Hashable, Functor, Foldable, Traversable)
-- | Sort 'ExerciseCost's by the total count of ingredients consumed.
instance (Eq e) => Ord (ExerciseCost e) where
compare = compare `on` (getCost . ingredients)
-- | A device paired with a cost to use it.
--
-- At scenario parse time, the type parameters @e@ and @en@ will stand for
-- 'Entity' and 'EntityName'.
-- This is because `ExerciseCost` is a member of the 'Entity' datatype, and
-- therefore can only refer to another 'Entity' by name before all 'Entity's
-- are parsed.
--
-- However, after parse time, we are able to look up actual 'Entity' objects
-- by name, and therefore can instantiate 'ExerciseCost' with 'Entity' as
-- the type parameter.
-- Then the two type parameters of 'DeviceUseCost' are both of 'Entity' type.
data DeviceUseCost e en = DeviceUseCost
{ device :: e
, useCost :: ExerciseCost en
}
deriving (Eq, Ord, Show, Generic, FromJSON, ToJSON, Functor, Foldable, Traversable)
-- * Utils
-- | A nonempty list of commands together with an exercise cost for using any of them (typically these will be a list of commands all requiring the same capability).
data CommandsAndCost e = CommandsAndCost
{ commandCost :: ExerciseCost e
, enabledCommands :: NonEmpty Const
}
-- | Given mapping from capabilities to their exercise costs provided by a single device, turn it into an mapping from capabilities to their exercise cost and enabled commands.
--
-- NOTE: Because each 'Const' is mapped to at most one
-- 'Capability' by the 'constCaps' function, we know that
-- a given 'Const' will not appear more than once as a value in the 'Map' produced by
-- this function, i.e. for the capabilities provided by a single 'Entity`
-- ('SingleEntityCapabilities').
commandsForDeviceCaps :: SingleEntityCapabilities e -> Capabilities (CommandsAndCost e)
commandsForDeviceCaps = Capabilities . M.mapMaybeWithKey f . getMap
where
f cap xc =
CommandsAndCost xc <$> M.lookup cap constByCaps