combustion (#1432)

Closes #1355

# Demo

    scripts/play.sh -i data/scenarios/Testing/1355-combustion.yaml --autoplay

![image](https://github.com/swarm-game/swarm/assets/261693/eda5d1c7-35fa-4fce-865d-a87c83923c61)
This commit is contained in:
Karl Ostmo 2023-08-20 19:23:52 -07:00 committed by GitHub
parent 0bed202e82
commit da6ad0c874
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 705 additions and 154 deletions

View File

@ -7,8 +7,21 @@
A tall, living entity made of a tough cellular material called "wood".
They regrow after being harvested and are an important raw ingredient used
in making many different devices.
properties: [portable, growable, opaque]
properties: [portable, growable, opaque, combustible]
growth: [500, 600]
combustion:
ignition: 0.01
duration: [80, 120]
product: ash
- name: ash
display:
attr: rock
char: '#'
description:
- |
Burned-out remnants of combustion.
properties: [portable]
- name: branch
display:
@ -25,7 +38,11 @@
char: 'l'
description:
- A wooden log, obtained by harvesting a tree and cutting off its branches.
properties: [portable]
properties: [portable, combustible]
combustion:
ignition: 0.05
duration: [40, 80]
product: ash
- name: board
display:
@ -33,7 +50,11 @@
char: 'w'
description:
- A wooden board, made by cutting a log into pieces.
properties: [portable]
properties: [portable, combustible]
combustion:
ignition: 0.2
duration: [20, 40]
product: ash
- name: workbench
display:
@ -57,7 +78,11 @@
description:
- A flat material made of pressed and dried wood fibers,
used as a surface on which to inscribe symbols.
properties: [portable]
properties: [portable, combustible]
combustion:
ignition: 0.5
duration: [10, 20]
product: ash
- name: PhD thesis
display:
@ -336,8 +361,12 @@
- A plant with tufts of soft fibers that can be harvested and used
to make things, including sheets of material that the local
aliens like to drape over their bodies.
properties: [portable, growable]
properties: [portable, growable, combustible]
growth: [100, 800]
combustion:
ignition: 0.1
duration: [20, 40]
product: ash
- name: linotype
display:

View File

@ -149,6 +149,7 @@ table.
| `description` | | `string list` | A description of the entity, as a list of paragraphs. |
| `orientation` | `null` | `int × int` | A 2-tuple of integers specifying an orientation vector for the entity. Currently unused. |
| `growth` | `null` | `int × int` | For growable entities, a 2-tuple of integers specifying the minimum and maximum amount of time taken for one growth stage. The actual time for one growth stage will be chosen uniformly at random from this range; it takes two growth stages for an entity to be fully grown. |
| `combustion` | | `object` | [Combustion](#combustion) information for the entity. |
| `yields` | `null` | `string` | The name of the entity which will be added to a robot's inventory when it executes `grab` or `harvest` on this entity. If omitted, the entity will simply yield itself. |
| `properties` | `[]` | `string list` | A list of properties of this entity. See [Entity properties](#entity-properties). |
| `capabilities` | `[]` | `string list` | A list of capabilities provided by entity, when it is equipped as a device. See [Capabilities](#capabilities). |
@ -182,6 +183,16 @@ capabilities here, which would be annoying to keep up-to-date, see the
sheet](https://github.com/swarm-game/swarm/wiki/Commands-Cheat-Sheet)
on the Swarm wiki.
### Combustion
The *combustion* property specifies whether and how an entity may combust, described by the following table.
| Key | Default? | Type | Description |
|------------------|----------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `ignition` | `0.5` | `number` | The rate of ignition by a neighbor, per tick. |
| `duration` | `null` | `int × int` | For combustible entities, a 2-tuple of integers specifying the minimum and maximum amount of time taken for combustion. |
| `product` | `ash` | `string` | What entity, if any, is left over after combustion |
### Display
A *display* specifies how an entity or a robot (robots are essentially

View File

@ -40,5 +40,6 @@
1320-world-DSL
1356-portals
144-subworlds
1355-combustion.yaml
1379-single-world-portal-reorientation.yaml
1399-backup-command.yaml

View File

@ -0,0 +1,133 @@
version: 1
name: Combustion
creative: false
seed: 0
description: Demo of spreading fire
objectives:
- goal:
- Blow up the dynamite
condition: |
j <- robotnamed "judge";
as j {
ishere "crater";
};
solution: |
def doN = \n. \f. if (n > 0) {f; doN (n - 1) f} {}; end;
move;
ignite forward;
turn right;
move;
ignite forward;
turn right;
move; move; move;
turn right;
move;
ignite left;
move;
turn right;
doN 2 (move; ignite left);
doN 14 move;
turn left;
doN 5 move;
turn left;
ignite right;
doN 8 move;
ignite right;
doN 10 move;
ignite right;
doN 8 move;
ignite right;
robots:
- name: base
dir: [1, 0]
devices:
- branch predictor
- calculator
- dictionary
- comparator
- logger
- treads
- torch
- name: judge
dir: [1, 0]
system: true
display:
invisible: true
char: J
entities:
- name: torch
display:
attr: wood
char: 't'
description:
- Can set things on fire
properties: [known, portable]
capabilities: [ignite]
- name: fuse
display:
attr: wood
char: '~'
description:
- Reliably combustible
combustion:
ignition: 1
duration: [8, 8]
product: null
properties: [known, portable, combustible]
- name: dynamite
display:
attr: red
char: '!'
description:
- Explosive material
combustion:
ignition: 1
duration: [2, 2]
product: crater
properties: [known, portable, combustible]
- name: crater
display:
attr: rock
char: '@'
description:
- Result of explosive excavation
properties: [known]
known: [ash, tree, log, board, paper, cotton]
world:
palette:
'Ω': [grass, null, base]
'T': [grass, tree]
'q': [grass, paper]
'l': [grass, log]
'b': [grass, board]
'i': [grass, cotton]
'F': [grass, fuse]
'd': [grass, dynamite, judge]
'.': [grass]
upperleft: [0, 0]
map: |
..iiii....bbbb..TT..llll....iiii.....
..iiii....bbbb..TT..llll....iiii.....
..iiii....bbbb..TT..llll....iiii.....
..iiii....bbbb..TT..llll....iiii.....
..iiii....bbbb..TT..llll....iiii.....
..iiii....bbbb..TT..llll....iiii.....
..TTTT....TTTT..TT..TTTT....TTTT.....
..TTTT....TTTT..TT..TTTT....TTTT.....
................TT...................
................TT...................
iiiiiiiiiii.....TT....FFFFF...FFFFF..
......iiiiii....TT....F...F...F...F..
iiiiiiiiiiiii...TT....F...F...F...F..
......iiiiiiii........F...F...F...F..
iiiiiiiiiiiiiii..Ω.FFFF...FFFFF...d..
......iiiiiiii.......................
iiiiiiiiiiiii.....qqqqqqqqqqqqqqqq...
......iiiiii......qqqqqqqqqqqqqqqq...
iiiiiiiiiii.......qqqqqqqqqqqqqqqq...
..................qqqqqqqqqqqqqqqq...
..................qqqqqqqqqqqqqqqq...
..................qqqqqqqqqqqqqqqq...
..................qqqqqqqqqqqqqqqq...
..................qqqqqqqqqqqqqqqq...

View File

@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/combustion.json",
"title": "Swarm entity combustion",
"description": "Properties of combustion",
"type": "object",
"properties": {
"ignition": {
"default": 0.5,
"type": "number",
"description": "Rate of ignition by a neighbor, per tick."
},
"duration": {
"type": "array",
"items": [
{
"name": "minimum",
"type": "number"
},
{
"name": "maximum",
"type": "number"
}
],
"description": "For combustible entities, a 2-tuple of integers specifying the minimum and maximum amount of time that the combustion shall persist."
},
"product": {
"default": "ash",
"type": "string",
"description": "What entity, if any, is left over after combustion"
}
}
}

View File

@ -61,6 +61,11 @@
],
"description": "For growable entities, a 2-tuple of integers specifying the minimum and maximum amount of time taken for one growth stage. The actual time for one growth stage will be chosen uniformly at random from this range; it takes two growth stages for an entity to be fully grown."
},
"combustion": {
"type": "object",
"$ref": "https://raw.githubusercontent.com/swarm-game/swarm/main/data/schema/combustion.json",
"description": "Properties of combustion."
},
"yields": {
"default": "null",
"type": "string",

View File

@ -60,6 +60,7 @@
"turn"
"grab"
"harvest"
"ignite"
"place"
"give"
"equip"

View File

@ -58,7 +58,7 @@
},
{
"name": "keyword.other",
"match": "\\b(?i)(self|parent|base|if|inl|inr|case|fst|snd|force|undefined|fail|not|format|chars|split|charat|tochar|key|noop|wait|selfdestruct|move|backup|push|stride|turn|grab|harvest|place|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|waypoint|detect|resonate|density|sniff|chirp|watch|surveil|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|instant|installkeyhandler|teleport|as|robotnamed|robotnumbered|knows)\\b"
"match": "\\b(?i)(self|parent|base|if|inl|inr|case|fst|snd|force|undefined|fail|not|format|chars|split|charat|tochar|key|noop|wait|selfdestruct|move|backup|push|stride|turn|grab|harvest|ignite|place|give|equip|unequip|make|has|equipped|count|drill|use|build|salvage|reprogram|say|listen|log|view|appear|create|halt|time|scout|whereami|waypoint|detect|resonate|density|sniff|chirp|watch|surveil|heading|blocked|scan|upload|ishere|isempty|meet|meetall|whoami|setname|random|run|return|try|swap|atomic|instant|installkeyhandler|teleport|as|robotnamed|robotnumbered|knows)\\b"
}
]
},

View File

@ -13,10 +13,14 @@
-- are mutually recursive (an inventory contains entities, which can
-- have inventories).
module Swarm.Game.Entity (
EntityName,
-- * Properties
EntityProperty (..),
GrowthTime (..),
defaultGrowthTime,
Combustibility (..),
defaultCombustibility,
-- * Entities
Entity,
@ -31,6 +35,7 @@ module Swarm.Game.Entity (
entityDescription,
entityOrientation,
entityGrowth,
entityCombustion,
entityYields,
entityProperties,
hasProperty,
@ -118,6 +123,8 @@ import Text.Read (readMaybe)
import Witch
import Prelude hiding (lookup)
type EntityName = Text
------------------------------------------------------------
-- Properties
------------------------------------------------------------
@ -133,6 +140,8 @@ data EntityProperty
Opaque
| -- | Regrows from a seed after it is harvested.
Growable
| -- | Can use the Ignite command on it
Combustible
| -- | Regenerates infinitely when grabbed or harvested.
Infinite
| -- | Robots drown if they walk on this without a boat.
@ -162,6 +171,24 @@ newtype GrowthTime = GrowthTime (Integer, Integer)
defaultGrowthTime :: GrowthTime
defaultGrowthTime = GrowthTime (100, 200)
-- | Properties of combustion
data Combustibility = Combustibility
{ ignition :: Double
-- ^ Rate of ignition by a neighbor, per tick.
-- When denoted as "lambda",
-- probability of ignition over a period "t" is:
-- 1 - e^(-(lambda * t))
-- See: https://math.stackexchange.com/a/1243629
, duration :: (Integer, Integer)
-- ^ min and max tick counts for combustion to persist
, product :: Maybe EntityName
-- ^ what entity, if any, is left over after combustion
}
deriving (Eq, Ord, Show, Read, Generic, Hashable, FromJSON, ToJSON)
defaultCombustibility :: Combustibility
defaultCombustibility = Combustibility 0.5 (100, 200) (Just "ash")
------------------------------------------------------------
-- Entity
------------------------------------------------------------
@ -224,6 +251,8 @@ data Entity = Entity
-- a robot moves, it moves in the direction of its orientation.
, _entityGrowth :: Maybe GrowthTime
-- ^ If this entity grows, how long does it take?
, _entityCombustion :: Maybe Combustibility
-- ^ If this entity is combustible, how spreadable is it?
, _entityYields :: Maybe Text
-- ^ The name of a different entity obtained when this entity is
-- grabbed.
@ -243,7 +272,7 @@ data Entity = Entity
-- | The @Hashable@ instance for @Entity@ ignores the cached hash
-- value and simply combines the other fields.
instance Hashable Entity where
hashWithSalt s (Entity _ disp nm pl descr orient grow yld props caps inv) =
hashWithSalt s (Entity _ disp nm pl descr orient grow combust yld props caps inv) =
s
`hashWithSalt` disp
`hashWithSalt` nm
@ -251,6 +280,7 @@ instance Hashable Entity where
`hashWithSalt` docToText descr
`hashWithSalt` orient
`hashWithSalt` grow
`hashWithSalt` combust
`hashWithSalt` yld
`hashWithSalt` props
`hashWithSalt` caps
@ -284,7 +314,20 @@ mkEntity ::
[Capability] ->
Entity
mkEntity disp nm descr props caps =
rehashEntity $ Entity 0 disp nm Nothing descr Nothing Nothing Nothing (Set.fromList props) (Set.fromList caps) empty
rehashEntity $
Entity
0
disp
nm
Nothing
descr
Nothing
Nothing
Nothing
Nothing
(Set.fromList props)
(Set.fromList caps)
empty
------------------------------------------------------------
-- Entity map
@ -345,6 +388,7 @@ instance FromJSON Entity where
<*> (v .: "description")
<*> v .:? "orientation"
<*> v .:? "growth"
<*> v .:? "combustion"
<*> v .:? "yields"
<*> v .:? "properties" .!= mempty
<*> v .:? "capabilities" .!= mempty
@ -445,6 +489,10 @@ entityOrientation = hashedLens _entityOrientation (\e x -> e {_entityOrientation
entityGrowth :: Lens' Entity (Maybe GrowthTime)
entityGrowth = hashedLens _entityGrowth (\e x -> e {_entityGrowth = x})
-- | Susceptibility to and duration of combustion
entityCombustion :: Lens' Entity (Maybe Combustibility)
entityCombustion = hashedLens _entityCombustion (\e x -> e {_entityCombustion = x})
-- | The name of a different entity yielded when this entity is
-- grabbed, if any.
entityYields :: Lens' Entity (Maybe Text)

View File

@ -9,19 +9,16 @@
module Swarm.Game.Scenario.Topography.EntityFacade where
import Control.Lens ((^.))
import Data.Text (Text)
import Data.Yaml as Y (ToJSON (toJSON))
import Swarm.Game.Display (Display)
import Swarm.Game.Entity qualified as E
type EntityName = Text
-- | This datatype is a lightweight stand-in for the
-- full-fledged "Entity" type without the baggage of all
-- of its other fields.
-- It contains the bare minimum display information
-- for rendering.
data EntityFacade = EntityFacade EntityName Display
data EntityFacade = EntityFacade E.EntityName Display
deriving (Eq)
-- Note: This instance is used only for the purpose of WorldPalette

View File

@ -30,8 +30,7 @@ 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 (foldM, forM, forM_, guard, join, msum, unless, when, zipWithM)
import Data.Array (bounds, (!))
import Control.Monad (foldM, forM, forM_, guard, msum, unless, when, zipWithM)
import Data.Bifunctor (second)
import Data.Bool (bool)
import Data.Char (chr, ord)
@ -76,6 +75,8 @@ import Swarm.Game.Scenario.Objective.WinCheck qualified as WC
import Swarm.Game.Scenario.Topography.Navigation.Portal (Navigation (..), destination, reorientation)
import Swarm.Game.Scenario.Topography.Navigation.Waypoint (WaypointName (..))
import Swarm.Game.State
import Swarm.Game.Step.Combustion qualified as Combustion
import Swarm.Game.Step.Util
import Swarm.Game.Universe
import Swarm.Game.Value
import Swarm.Game.World qualified as W
@ -94,8 +95,6 @@ import Swarm.Language.Value
import Swarm.Util hiding (both)
import Swarm.Util.Effect (throwToMaybe)
import System.Clock (TimeSpec)
import System.Clock qualified
import System.Random (UniformRange, uniformR)
import Witch (From (from), into)
import Prelude hiding (Applicative (..), lookup)
@ -372,9 +371,6 @@ evalPT ::
m Value
evalPT t = evaluateCESK (initMachine t empty emptyStore)
getNow :: Has (Lift IO) sig m => m TimeSpec
getNow = sendIO $ System.Clock.getTime System.Clock.Monotonic
-- | Create a special robot to check some hypothetical, for example the win condition.
--
-- Use ID (-1) so it won't conflict with any robots currently in the robot map.
@ -417,91 +413,6 @@ runCESK cesk = case finalValue cesk of
Just (v, _) -> return v
Nothing -> stepCESK cesk >>= runCESK
------------------------------------------------------------
-- Some utility functions
------------------------------------------------------------
-- | Set a flag telling the UI that the world needs to be redrawn.
flagRedraw :: (Has (State GameState) sig m) => m ()
flagRedraw = needsRedraw .= True
-- | 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 multiWorld
forM (M.lookup swName mw) $ \w -> do
let (w', a) = run (runState w n)
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))
-- | 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
didChange <-
fmap (fromMaybe False) $
zoomWorld subworldName $
W.updateM @Int (W.locToCoords loc) upd
when didChange $
wakeWatchingRobots cLoc
-- | 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))
-- | 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
adjs <- use @GameState adjList
names <- use @GameState nameList
i <- uniform (bounds adjs)
j <- uniform (bounds names)
return $ T.concat [adjs ! i, "_", names ! j]
------------------------------------------------------------
-- Debugging
------------------------------------------------------------
@ -565,27 +476,6 @@ ensureCanExecute c =
(isPrivileged || hasCaps)
`holdsOr` Incapable FixByEquip (R.singletonCap cap) (TConst c)
-- | 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
-- | Create an exception about a command failing.
cmdExn :: Const -> [Text] -> Exn
cmdExn c parts = CmdFailed c (T.unwords parts) Nothing
-- | Create an exception about a command failing, with an achievement
cmdExnWithAchievement :: Const -> [Text] -> GameplayAchievement -> Exn
cmdExnWithAchievement c parts a = CmdFailed c (T.unwords parts) $ Just a
@ -1082,7 +972,7 @@ addSeedBot e (minT, maxT) loc ts =
()
Nothing
"seed"
"A growing seed."
(Markdown.fromText $ T.unwords ["A growing", e ^. entityName, "seed."])
(Just loc)
zero
( defaultEntityDisplay '.'
@ -1096,12 +986,6 @@ addSeedBot e (minT, maxT) loc ts =
False
ts
-- | 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)
-- | Interpret the execution (or evaluation) of a constant application
-- to some values.
execConst ::
@ -1230,6 +1114,11 @@ execConst c vs s k = do
_ -> badConst
Grab -> doGrab Grab'
Harvest -> doGrab Harvest'
Ignite -> case vs of
[VDir d] -> do
Combustion.igniteCommand c d
return $ Out VUnit s k
_ -> badConst
Swap -> case vs of
[VText name] -> do
loc <- use robotLocation
@ -2280,19 +2169,6 @@ execConst c vs s k = do
where
remTime = r ^. recipeTime
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
ensureEquipped :: HasRobotStepState sig m => Text -> m Entity
ensureEquipped itemName = do
inst <- use equippedDevices
@ -2547,7 +2423,7 @@ execConst c vs s k = do
return other
holdsOrFail :: (Has (Throw Exn) sig m) => Bool -> [Text] -> m ()
holdsOrFail a ts = a `holdsOr` cmdExn c ts
holdsOrFail = holdsOrFail' c
holdsOrFailWithAchievement :: (Has (Throw Exn) sig m) => Bool -> [Text] -> Maybe GameplayAchievement -> m ()
holdsOrFailWithAchievement a ts mAch = case mAch of
@ -2555,7 +2431,7 @@ execConst c vs s k = do
Just ach -> a `holdsOr` cmdExnWithAchievement c ts ach
isJustOrFail :: (Has (Throw Exn) sig m) => Maybe a -> [Text] -> m a
isJustOrFail a ts = a `isJustOr` cmdExn c ts
isJustOrFail = isJustOrFail' c
returnEvalCmp = case vs of
[v1, v2] -> (\b -> Out (VBool b) s k) <$> evalCmp c v1 v2
@ -2657,11 +2533,6 @@ purgeFarAwayWatches = do
-- Some utility functions
------------------------------------------------------------
-- | 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
-- | Requires that the target location is within one cell.
-- Requirement is waived if the bot is privileged.
isNearbyOrExempt :: Bool -> Cosmic Location -> Cosmic Location -> Bool

View File

@ -0,0 +1,227 @@
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Some entities are "combustible". A command, `ignite`, will
-- initiate combustion on such an entity.
-- Furthermore, combustion can spread to (4-)adjacent entities, depending
-- on the 'ignition' property of that entity.
--
-- Short-lived robots are used to illustrate the combusting entity as
-- well as to initiate the delayed combustion of its neighbors.
module Swarm.Game.Step.Combustion where
import Control.Applicative (Applicative (..))
import Control.Carrier.State.Lazy
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_, void, when)
import Data.Text qualified as T
import Linear (zero)
import Swarm.Game.CESK (emptyStore, initMachine)
import Swarm.Game.Display
import Swarm.Game.Entity hiding (empty, lookup, singleton, union)
import Swarm.Game.Entity qualified as E
import Swarm.Game.Location
import Swarm.Game.Robot
import Swarm.Game.State
import Swarm.Game.Step.Util
import Swarm.Game.Universe
import Swarm.Language.Context (empty)
import Swarm.Language.Pipeline (ProcessedTerm)
import Swarm.Language.Pipeline.QQ (tmQ)
import Swarm.Language.Syntax
import Swarm.Language.Text.Markdown qualified as Markdown
import Swarm.Util hiding (both)
import System.Clock (TimeSpec)
import Prelude hiding (Applicative (..), lookup)
igniteCommand :: (HasRobotStepState sig m, Has (Lift IO) sig m) => Const -> Direction -> m ()
igniteCommand c d = do
(loc, me) <- lookInDirection d
-- Ensure there is an entity here.
e <-
me `isJustOrFail` ["There is nothing here to", verb <> "."]
-- Ensure it can be ignited.
(e `hasProperty` Combustible)
`holdsOrFail` ["The", e ^. entityName, "here can't be", verbed <> "."]
-- Remove the entity from the world.
updateEntityAt loc (const Nothing)
flagRedraw
-- Start burning process
let selfCombustibility = (e ^. entityCombustion) ? defaultCombustibility
createdAt <- getNow
combustionDurationRand <- addCombustionBot e selfCombustibility createdAt loc
let neighborLocs = map (offsetBy loc . flip applyTurn north . DRelative . DPlanar) listEnums
forM_ neighborLocs $ igniteNeighbor createdAt combustionDurationRand
where
verb = "ignite"
verbed = "ignited"
holdsOrFail = holdsOrFail' c
isJustOrFail = isJustOrFail' c
-- | Construct a "combustion robot" from entity and position
-- and add it to the world.
-- It has low priority and will be covered
-- by placed entities.
-- The "combustion bot" represents the burning of a single
-- entity; propagating the fire to neighbors is handled upstream,
-- within the `ignite` command.
addCombustionBot ::
Has (State GameState) sig m =>
Entity ->
Combustibility ->
TimeSpec ->
Cosmic Location ->
m Integer
addCombustionBot inputEntity combustibility ts loc = do
botInventory <- case maybeCombustionProduct of
Nothing -> return []
Just n -> do
maybeE <- uses entityMap (lookupEntityName n)
return $ maybe [] (pure . (1,)) maybeE
combustionDurationRand <- uniform durationRange
let combustionProg = combustionProgram combustionDurationRand combustibility
void $
addTRobot $
mkRobot
()
Nothing
"fire"
(Markdown.fromText $ T.unwords ["A burning", (inputEntity ^. entityName) <> "."])
(Just loc)
zero
( defaultEntityDisplay '*'
& displayAttr .~ AWorld "fire"
& displayPriority .~ 0
)
(initMachine combustionProg empty emptyStore)
[]
botInventory
True
False
ts
return combustionDurationRand
where
Combustibility _ durationRange maybeCombustionProduct = combustibility
-- Triggers the ignition of the entity underfoot with some delay.
ignitionProgram :: Integer -> ProcessedTerm
ignitionProgram waitTime =
[tmQ|
wait $int:waitTime;
try {
ignite down;
noop;
} {};
selfdestruct
|]
-- | A system program for a "combustion robot", to burn an entity
-- after it is ignited.
--
-- For efficiency, we determine a priori (i.e. the instant
-- the combustion robot is spawned) whether any neighbors will eventually
-- be burned, based on probabilities.
--
-- Note that it is possible that new neighbors may be introduced while
-- combustion is in progress. Although it may be more realistic to subject
-- these to possible combustion as well, we do not bother.
--
-- Though if we did actually want to do that, some options are:
--
-- 1. Create sub-partitions (of say, 10-tick duration) of the combustion duration
-- to re-evaluate opportunities to light adjacent entities on fire.
-- 2. Use the `watch` command to observe for changes to adjacent entities.
-- Note that if we "wake" from our `wait` due to the `watch` being triggered,
-- we would need to maintain bookkeeping of how much time is left.
-- 3. Spawn more robots whose sole purpose is to observe for changes to neighbor
-- cells. This would avoid polluting the logic of the currently burning cell
-- with logic to manage probabilities of combustion propagation.
combustionProgram :: Integer -> Combustibility -> ProcessedTerm
combustionProgram combustionDuration (Combustibility _ _ maybeCombustionProduct) =
[tmQ|
wait $int:combustionDuration;
if ($int:invQuantity > 0) {
try {
place $str:combustionProduct;
} {};
} {};
selfdestruct
|]
where
(invQuantity, combustionProduct) = case maybeCombustionProduct of
Nothing -> (0, "")
Just p -> (1, p)
-- | We treat the 'ignition' field in the 'Combustion' record
-- as a /rate/ in a Poisson distribution.
-- Ignition of neighbors depends on that particular neighbor entity's
-- combustion /rate/, but also on the duration
-- that the current entity will burn.
igniteNeighbor ::
Has (State GameState) sig m =>
TimeSpec ->
Integer ->
Cosmic Location ->
m ()
igniteNeighbor creationTime sourceDuration loc = do
maybeEnt <- entityAt loc
forM_ maybeEnt igniteEntity
where
igniteEntity e =
when (e `hasProperty` Combustible) $ do
threshold <- uniform (0, 1)
when (probabilityOfIgnition >= threshold) $ do
ignitionDelayRand <- uniform (0, 1)
let ignitionDelay =
floor
. min (fromIntegral sourceDuration)
. negate
$ log ignitionDelayRand / rate
addIgnitionBot ignitionDelay e creationTime loc
where
neighborCombustibility = (e ^. entityCombustion) ? defaultCombustibility
rate = E.ignition neighborCombustibility
probabilityOfIgnition = 1 - exp (negate $ rate * fromIntegral sourceDuration)
-- | Construct an invisible "ignition robot" and add it to the world.
-- Its sole purpose is to delay the `ignite` command for a neighbor
-- that has been a priori determined that it shall be ignited.
addIgnitionBot ::
Has (State GameState) sig m =>
Integer ->
Entity ->
TimeSpec ->
Cosmic Location ->
m ()
addIgnitionBot ignitionDelay inputEntity ts loc =
void $
addTRobot $
mkRobot
()
Nothing
"firestarter"
(Markdown.fromText $ T.unwords ["Delayed ignition of", (inputEntity ^. entityName) <> "."])
(Just loc)
zero
( defaultEntityDisplay '*'
& invisible .~ True
)
(initMachine (ignitionProgram ignitionDelay) empty emptyStore)
[]
[]
True
False
ts

181
src/Swarm/Game/Step/Util.hs Normal file
View File

@ -0,0 +1,181 @@
{-# 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, join, when)
import Data.Array (bounds, (!))
import Data.IntMap qualified as IM
import Data.List (find)
import Data.Map qualified as M
import Data.Maybe (fromMaybe)
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.Robot
import Swarm.Game.State
import Swarm.Game.Universe
import Swarm.Game.World qualified as W
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
didChange <-
fmap (fromMaybe False) $
zoomWorld subworldName $
W.updateM @Int (W.locToCoords loc) upd
when didChange $
wakeWatchingRobots cLoc
-- | 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
-- * Exceptions
-- | 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
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
getNow :: Has (Lift IO) sig m => m TimeSpec
getNow = sendIO $ System.Clock.getTime System.Clock.Monotonic
------------------------------------------------------------
-- Some utility functions
------------------------------------------------------------
-- | Set a flag telling the UI that the world needs to be redrawn.
flagRedraw :: (Has (State GameState) sig m) => m ()
flagRedraw = needsRedraw .= True
-- | 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 multiWorld
forM (M.lookup swName mw) $ \w -> do
let (w', a) = run (runState w n)
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))
-- | 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
adjs <- use @GameState adjList
names <- use @GameState nameList
i <- uniform (bounds adjs)
j <- uniform (bounds names)
return $ T.concat [adjs ! i, "_", names ! j]

View File

@ -52,6 +52,8 @@ data Capability
CGrab
| -- | Execute the 'Harvest' command
CHarvest
| -- | Execute the 'Ignite' command
CIgnite
| -- | Execute the 'Place' command
CPlace
| -- | Execute the 'Give' command
@ -215,6 +217,7 @@ constCaps = \case
Turn -> Just CTurn
Grab -> Just CGrab
Harvest -> Just CHarvest
Ignite -> Just CIgnite
Place -> Just CPlace
Give -> Just CGive
Equip -> Just CEquip

View File

@ -162,6 +162,8 @@ data Const
Grab
| -- | Harvest an item from the current location.
Harvest
| -- | Ignite a combustible item
Ignite
| -- | Try to place an item at the current location.
Place
| -- | Give an item to another robot at the current location.
@ -539,6 +541,10 @@ constInfo c = case c of
[ "Leaves behind a growing seed if the harvested item is growable."
, "Otherwise it works exactly like `grab`."
]
Ignite ->
command 1 short . doc "Ignite a combustible item in the specified direction." $
[ "Combustion persists for a random duration and may spread."
]
Place ->
command 1 short . doc "Place an item at the current location." $
["The current location has to be empty for this to work."]

View File

@ -725,6 +725,7 @@ inferConst c = case c of
Turn -> [tyQ| dir -> cmd unit |]
Grab -> [tyQ| cmd text |]
Harvest -> [tyQ| cmd text |]
Ignite -> [tyQ| dir -> cmd unit |]
Place -> [tyQ| text -> cmd unit |]
Give -> [tyQ| actor -> text -> cmd unit |]
Equip -> [tyQ| text -> cmd unit |]

View File

@ -39,7 +39,7 @@ toFacade = \case
Facade f -> f
Ref e -> mkFacade e
getEntityName :: EntityFacade -> EntityName
getEntityName :: EntityFacade -> E.EntityName
getEntityName (EntityFacade name _) = name
data MapEditingBounds = MapEditingBounds

View File

@ -19,7 +19,7 @@ import Data.Set qualified as Set
import Data.Text qualified as T
import Data.Tuple (swap)
import Swarm.Game.Display (Display, defaultChar)
import Swarm.Game.Entity (entitiesByName)
import Swarm.Game.Entity (EntityName, entitiesByName)
import Swarm.Game.Location
import Swarm.Game.Scenario
import Swarm.Game.Scenario.Topography.Area (AreaDimensions (..), getAreaDimensions)

View File

@ -1115,6 +1115,7 @@ displayProperties :: [EntityProperty] -> Widget Name
displayProperties = displayList . mapMaybe showProperty
where
showProperty Growable = Just "growing"
showProperty Combustible = Just "combustible"
showProperty Infinite = Just "infinite"
showProperty Liquid = Just "liquid"
showProperty Unwalkable = Just "blocking"

View File

@ -130,6 +130,8 @@ library
Swarm.Game.ScenarioInfo
Swarm.Game.State
Swarm.Game.Step
Swarm.Game.Step.Combustion
Swarm.Game.Step.Util
Swarm.Game.Terrain
Swarm.Game.Value
Swarm.Game.World

View File

@ -290,6 +290,7 @@ testScenarioSolutions rs ui =
, testSolution Default "Testing/144-subworlds/basic-subworld"
, testSolution Default "Testing/144-subworlds/subworld-mapped-robots"
, testSolution Default "Testing/144-subworlds/subworld-located-robots"
, testSolution Default "Testing/1355-combustion"
, testSolution Default "Testing/1379-single-world-portal-reorientation"
, testSolution Default "Testing/1399-backup-command"
, testGroup