Skip to content

Commit a9b7009

Browse files
committed
Add subworlds
1 parent efb70df commit a9b7009

26 files changed

+584
-251
lines changed

data/scenarios/Testing/00-ORDER.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@
3838
1295-density-command.yaml
3939
1138-structures
4040
1356-portals
41+
144-subworlds

src/Swarm/Doc/Gen.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import Data.Containers.ListUtils (nubOrd)
3232
import Data.Either.Extra (eitherToMaybe)
3333
import Data.Foldable (find, toList)
3434
import Data.List (transpose)
35+
import Data.List.NonEmpty qualified as NE
3536
import Data.Map.Lazy (Map)
3637
import Data.Map.Lazy qualified as Map
3738
import Data.Maybe (fromMaybe, isJust)
@@ -52,7 +53,7 @@ import Swarm.Game.Failure.Render qualified as F
5253
import Swarm.Game.Recipe (Recipe, loadRecipes, recipeInputs, recipeOutputs, recipeRequirements, recipeTime, recipeWeight)
5354
import Swarm.Game.ResourceLoading (getDataFileNameSafe)
5455
import Swarm.Game.Robot (equippedDevices, instantiateRobot, robotInventory)
55-
import Swarm.Game.Scenario (Scenario, loadScenario, scenarioRobots)
56+
import Swarm.Game.Scenario (Scenario, loadScenario, scenarioRobots, scenarioWorlds, worldName)
5657
import Swarm.Game.WorldGen (testWorld2Entites)
5758
import Swarm.Language.Capability (Capability)
5859
import Swarm.Language.Capability qualified as Capability
@@ -551,10 +552,10 @@ classicScenario = do
551552
fst <$> loadScenario "data/scenarios/classic.yaml" entities
552553

553554
startingDevices :: Scenario -> Set Entity
554-
startingDevices = Set.fromList . map snd . E.elems . view equippedDevices . instantiateRobot 0 . head . view scenarioRobots
555+
startingDevices s = Set.fromList . map snd . E.elems . view equippedDevices . instantiateRobot (worldName $ NE.head $ view scenarioWorlds s) 0 . head . view scenarioRobots $ s
555556

556557
startingInventory :: Scenario -> Map Entity Int
557-
startingInventory = Map.fromList . map swap . E.elems . view robotInventory . instantiateRobot 0 . head . view scenarioRobots
558+
startingInventory s = Map.fromList . map swap . E.elems . view robotInventory . instantiateRobot (worldName $ NE.head $ view scenarioWorlds s) 0 . head . view scenarioRobots $ s
558559

559560
-- | Ignore utility entities that are just used for tutorials and challenges.
560561
ignoredEntities :: Set Text

src/Swarm/Game/Log.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import Data.Text (Text)
3434
import GHC.Generics (Generic)
3535
import Swarm.Game.CESK (TickNumber)
3636
import Swarm.Game.Location (Location)
37+
import Swarm.Game.Universe (Cosmo)
3738

3839
-- | Severity of the error - critical errors are bugs
3940
-- and should be reported as Issues.
@@ -61,8 +62,9 @@ data LogEntry = LogEntry
6162
-- ^ The name of the robot that generated the entry.
6263
, _leRobotID :: Int
6364
-- ^ The ID of the robot that generated the entry.
64-
, _leLocation :: Location
65+
, _leLocation :: Maybe (Cosmo Location)
6566
-- ^ Location of the robot at log entry creation.
67+
-- "Nothing" represents omnipresence for the purpose of proximity.
6668
, _leText :: Text
6769
-- ^ The text of the log entry.
6870
}

src/Swarm/Game/Robot.hs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import Swarm.Game.Display (Display, curOrientation, defaultRobotDisplay, invisib
9494
import Swarm.Game.Entity hiding (empty)
9595
import Swarm.Game.Location (Heading, Location, toDirection)
9696
import Swarm.Game.Log
97+
import Swarm.Game.Universe
9798
import Swarm.Language.Capability (Capability)
9899
import Swarm.Language.Context qualified as Ctx
99100
import Swarm.Language.Requirement (ReqCtx)
@@ -167,7 +168,7 @@ data RobotPhase
167168
-- concrete robot we must have a location.
168169
type family RobotLocation (phase :: RobotPhase) :: * where
169170
RobotLocation 'TemplateRobot = Maybe Location
170-
RobotLocation 'ConcreteRobot = Location
171+
RobotLocation 'ConcreteRobot = Cosmo Location
171172

172173
-- | Robot templates have no ID; concrete robots definitely do.
173174
type family RobotID (phase :: RobotPhase) :: * where
@@ -269,13 +270,13 @@ robotDisplay = lens getDisplay setDisplay
269270
-- a getter, since when changing a robot's location we must remember
270271
-- to update the 'robotsByLocation' map as well. You can use the
271272
-- 'updateRobotLocation' function for this purpose.
272-
robotLocation :: Getter Robot Location
273+
robotLocation :: Getter Robot (Cosmo Location)
273274

274275
-- | Set a robot's location. This is unsafe and should never be
275276
-- called directly except by the 'updateRobotLocation' function.
276277
-- The reason is that we need to make sure the 'robotsByLocation'
277278
-- map stays in sync.
278-
unsafeSetRobotLocation :: Location -> Robot -> Robot
279+
unsafeSetRobotLocation :: Cosmo Location -> Robot -> Robot
279280
unsafeSetRobotLocation loc r = r {_robotLocation = loc}
280281

281282
-- | A template robot's location. Unlike 'robotLocation', this is a
@@ -308,11 +309,11 @@ robotID :: Getter Robot RID
308309
-- if the robot template didn't have a location already, just set
309310
-- the location to (0,0) by default. If you want a different location,
310311
-- set it via 'trobotLocation' before calling 'instantiateRobot'.
311-
instantiateRobot :: RID -> TRobot -> Robot
312-
instantiateRobot i r =
312+
instantiateRobot :: SubworldName -> RID -> TRobot -> Robot
313+
instantiateRobot swName i r =
313314
r
314315
{ _robotID = i
315-
, _robotLocation = fromMaybe zero (_robotLocation r)
316+
, _robotLocation = Cosmo swName $ fromMaybe zero $ _robotLocation r
316317
}
317318

318319
-- | The ID number of the robot's parent, that is, the robot that

src/Swarm/Game/Scenario.hs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ module Swarm.Game.Scenario (
3333
scenarioEntities,
3434
scenarioRecipes,
3535
scenarioKnown,
36-
scenarioWorld,
36+
scenarioWorlds,
37+
scenarioNavigation,
3738
scenarioRobots,
3839
scenarioObjectives,
3940
scenarioSolution,
@@ -45,18 +46,23 @@ module Swarm.Game.Scenario (
4546
getScenarioPath,
4647
) where
4748

49+
import Control.Arrow ((&&&))
4850
import Control.Lens hiding (from, (.=), (<.>))
4951
import Control.Monad (filterM)
5052
import Control.Monad.Except (ExceptT (..), MonadIO, liftIO, runExceptT, withExceptT)
5153
import Control.Monad.Trans.Except (except)
5254
import Data.Aeson
5355
import Data.Either.Extra (eitherToMaybe, maybeToEither)
56+
import Data.List.NonEmpty (NonEmpty ((:|)))
57+
import Data.List.NonEmpty qualified as NE
58+
import Data.Map qualified as M
5459
import Data.Maybe (catMaybes, isNothing, listToMaybe)
5560
import Data.Text (Text)
5661
import Data.Text qualified as T
5762
import Swarm.Game.Entity
5863
import Swarm.Game.Failure
5964
import Swarm.Game.Failure.Render
65+
import Swarm.Game.Location
6066
import Swarm.Game.Recipe
6167
import Swarm.Game.ResourceLoading (getDataFileNameSafe)
6268
import Swarm.Game.Robot (TRobot)
@@ -65,7 +71,9 @@ import Swarm.Game.Scenario.Objective.Validation
6571
import Swarm.Game.Scenario.RobotLookup
6672
import Swarm.Game.Scenario.Style
6773
import Swarm.Game.Scenario.Topography.Cell
74+
import Swarm.Game.Scenario.Topography.Navigation.Portal
6875
import Swarm.Game.Scenario.Topography.WorldDescription
76+
import Swarm.Game.Universe
6977
import Swarm.Language.Pipeline (ProcessedTerm)
7078
import Swarm.Util (failT)
7179
import Swarm.Util.Lens (makeLensesNoSigs)
@@ -91,7 +99,8 @@ data Scenario = Scenario
9199
, _scenarioEntities :: EntityMap
92100
, _scenarioRecipes :: [Recipe Entity]
93101
, _scenarioKnown :: [Text]
94-
, _scenarioWorld :: WorldDescription
102+
, _scenarioWorlds :: NonEmpty WorldDescription
103+
, _scenarioNavigation :: Navigation (M.Map SubworldName) Location
95104
, _scenarioRobots :: [TRobot]
96105
, _scenarioObjectives :: [Objective]
97106
, _scenarioSolution :: Maybe ProcessedTerm
@@ -122,6 +131,24 @@ instance FromJSONE EntityMap Scenario where
122131
rs <- v ..: "robots"
123132
let rsMap = buildRobotMap rs
124133

134+
rootWorld <- localE (,rsMap) (v ..: "world")
135+
subworlds <- localE (,rsMap) (v ..:? "subworlds" ..!= [])
136+
137+
let allWorlds = rootWorld :| subworlds
138+
let mergedWaypoints =
139+
M.fromList $
140+
map (worldName &&& runIdentity . waypoints . navigation) $
141+
NE.toList allWorlds
142+
143+
mergedPortals <-
144+
validatePortals
145+
. Navigation mergedWaypoints
146+
. M.unions
147+
$ map (portals . navigation)
148+
$ NE.toList allWorlds
149+
150+
let mergedNavigation = Navigation mergedWaypoints mergedPortals
151+
125152
Scenario
126153
<$> liftE (v .: "version")
127154
<*> liftE (v .: "name")
@@ -133,7 +160,8 @@ instance FromJSONE EntityMap Scenario where
133160
<*> pure em
134161
<*> v ..:? "recipes" ..!= []
135162
<*> pure known
136-
<*> localE (,rsMap) (v ..: "world")
163+
<*> pure allWorlds
164+
<*> pure mergedNavigation
137165
<*> pure rs
138166
<*> (liftE (v .:? "objectives" .!= []) >>= validateObjectives)
139167
<*> liftE (v .:? "solution")
@@ -178,8 +206,11 @@ scenarioRecipes :: Lens' Scenario [Recipe Entity]
178206
-- not have to scan them.
179207
scenarioKnown :: Lens' Scenario [Text]
180208

181-
-- | The starting world for the scenario.
182-
scenarioWorld :: Lens' Scenario WorldDescription
209+
-- | The subworlds of the scenario.
210+
scenarioWorlds :: Lens' Scenario (NonEmpty WorldDescription)
211+
212+
-- | Waypoints and inter-world portals
213+
scenarioNavigation :: Lens' Scenario (Navigation (M.Map SubworldName) Location)
183214

184215
-- | The starting robots for the scenario. Note this should
185216
-- include the base.
Lines changed: 98 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,45 @@
11
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE UndecidableInstances #-}
23

34
-- |
45
-- SPDX-License-Identifier: BSD-3-Clause
56
module Swarm.Game.Scenario.Topography.Navigation.Portal where
67

78
import Control.Monad (forM, forM_, unless)
89
import Data.Aeson (FromJSON)
10+
import Data.Bifunctor (first)
11+
import Data.Functor.Identity
912
import Data.Int (Int32)
1013
import Data.List (intercalate)
1114
import Data.List.NonEmpty (NonEmpty ((:|)))
1215
import Data.List.NonEmpty qualified as NE
1316
import Data.Map qualified as M
14-
import Data.Maybe (listToMaybe)
15-
import Data.Text (Text)
17+
import Data.Maybe (fromMaybe, listToMaybe)
1618
import Data.Text qualified as T
1719
import GHC.Generics (Generic)
1820
import Linear (V2)
1921
import Swarm.Game.Location
2022
import Swarm.Game.Scenario.Topography.Navigation.Waypoint
23+
import Swarm.Game.Universe
2124
import Swarm.Util (binTuples, quote)
2225

23-
-- | Note: The primary overworld shall use
24-
-- the reserved name \"root\".
25-
newtype SubworldName = SubworldName Text
26-
deriving (Show, Eq, Ord, Generic, FromJSON)
26+
type WaypointMap = M.Map WaypointName (NonEmpty Location)
2727

28-
data Navigation = Navigation
29-
{ waypoints :: M.Map WaypointName (NonEmpty Location)
28+
-- | Parameterized on the portal specification method.
29+
-- At the subworld parsing level, we only can obtain the planar location
30+
-- for portal /entrances/. At the Scenario-parsing level, we finally have
31+
-- access to the waypoints across all subworlds, and can therefore translate
32+
-- the portal exits to concrete planar locations.
33+
data Navigation a b = Navigation
34+
{ waypoints :: a WaypointMap
3035
-- ^ Note that waypoints defined at the "root" level are still relative to
3136
-- the top-left corner of the map rectangle; they are not in absolute world
3237
-- coordinates (as with applying the "ul" offset).
33-
, portals :: M.Map Location Location
38+
, portals :: M.Map (Cosmo Location) (Cosmo b)
3439
}
35-
deriving (Eq, Show)
40+
41+
deriving instance (Eq (a WaypointMap), Eq b) => Eq (Navigation a b)
42+
deriving instance (Show (a WaypointMap), Show b) => Show (Navigation a b)
3643

3744
data PortalExit = PortalExit
3845
{ exit :: WaypointName
@@ -64,31 +71,59 @@ failUponDuplication message binnedMap =
6471
where
6572
duplicated = M.filter ((> 1) . NE.length) binnedMap
6673

67-
-- | Enforces the following constraints:
68-
-- * portals can have multiple entrances but only a single exit
74+
failWaypointLookup :: MonadFail m => WaypointName -> Maybe a -> m a
75+
failWaypointLookup (WaypointName rawName) lookupResult = case lookupResult of
76+
Nothing ->
77+
fail $
78+
T.unpack $
79+
T.unwords
80+
[ "No waypoint named"
81+
, quote rawName
82+
]
83+
Just xs -> return xs
84+
85+
-- |
86+
-- The following constraints must be enforced:
87+
-- * portals based on plural waypoint multiplicity can have multiple entrances but only a single exit
6988
-- * no two portals share the same entrance location
70-
-- * global waypoint uniqueness when the "unique" flag is specified
89+
-- * waypoint uniqueness within a subworld when the "unique" flag is specified
90+
--
91+
-- == Data flow:
92+
--
93+
-- Waypoints are defined within a subworld and are namespaced by it.
94+
-- Optional intra-subworld uniqueness of Waypoints is enforced at WorldDescription
95+
-- parse time.
96+
-- Portals are declared within a subworld. The portal entrance must be a waypoint
97+
-- within this subworld.
98+
-- They can reference waypoints in other subworlds as exits, but these references
99+
-- are not validated until the Scenario parse level.
100+
--
101+
-- * Since portal /entrances/ are specified at the subworld level, validation that
102+
-- no entrances overlap can also be performed at that level.
103+
-- * However, enforcement of single-multiplicity on portal /exits/ must be performed
104+
-- at scenario-parse level, because for a portal exit that references a waypoint in
105+
-- another subworld, we can't know at the single-WorldDescription level whether
106+
-- that waypoint has plural multiplicity.
71107
validateNavigation ::
72108
(MonadFail m, Traversable t) =>
109+
SubworldName ->
73110
V2 Int32 ->
74111
[Originated Waypoint] ->
75112
t Portal ->
76-
m Navigation
77-
validateNavigation upperLeft unmergedWaypoints portalDefs = do
113+
m (Navigation Identity WaypointName)
114+
validateNavigation currentSubworldName upperLeft unmergedWaypoints portalDefs = do
78115
failUponDuplication "is required to be unique, but is duplicated in:" waypointsWithUniqueFlag
79116

80-
-- TODO(#144) Currently ignores subworld references
81-
nestedPortalPairs <- forM portalDefs $ \(Portal entranceName (PortalExit exitName@(WaypointName rawExitName) _)) -> do
117+
nestedPortalPairs <- forM portalDefs $ \(Portal entranceName (PortalExit exitName maybeExitSubworldName)) -> do
82118
-- Portals can have multiple entrances but only a single exit.
83119
-- That is, the pairings of entries to exits must form a proper mathematical "function".
84-
-- Multiple occurrences of entrance waypoints of a given name will replicate portal entrances.
120+
-- Multiple occurrences of entrance waypoints of a given name will result in
121+
-- multiple portal entrances.
85122
entranceLocs <- getLocs entranceName
86-
firstExitLoc :| otherExits <- getLocs exitName
87-
unless (null otherExits)
88-
. fail
89-
. T.unpack
90-
$ T.unwords ["Ambiguous exit waypoints named", quote rawExitName, "for portal"]
91-
return $ map ((,extractLoc firstExitLoc) . extractLoc) $ NE.toList entranceLocs
123+
124+
let sw = fromMaybe currentSubworldName maybeExitSubworldName
125+
f = (,Cosmo sw exitName) . extractLoc
126+
return $ map f $ NE.toList entranceLocs
92127

93128
let reconciledPortalPairs = concat nestedPortalPairs
94129

@@ -97,17 +132,10 @@ validateNavigation upperLeft unmergedWaypoints portalDefs = do
97132
failUponDuplication "has overlapping portal entrances exiting to" $
98133
binTuples reconciledPortalPairs
99134

100-
return $ Navigation bareWaypoints $ M.fromList reconciledPortalPairs
135+
return . Navigation (pure bareWaypoints) . M.fromList $
136+
map (first $ Cosmo currentSubworldName) reconciledPortalPairs
101137
where
102-
getLocs wpWrapper@(WaypointName rawName) = case M.lookup wpWrapper correctedWaypoints of
103-
Nothing ->
104-
fail $
105-
T.unpack $
106-
T.unwords
107-
[ "No waypoint named"
108-
, quote rawName
109-
]
110-
Just xs -> return xs
138+
getLocs wpWrapper = failWaypointLookup wpWrapper $ M.lookup wpWrapper correctedWaypoints
111139

112140
extractLoc (Originated _ (Waypoint _ loc)) = loc
113141
correctedWaypoints =
@@ -116,5 +144,40 @@ validateNavigation upperLeft unmergedWaypoints portalDefs = do
116144
(\x -> (wpName $ wpConfig $ value x, fmap (offsetWaypoint upperLeft) x))
117145
unmergedWaypoints
118146
bareWaypoints = M.map (NE.map extractLoc) correctedWaypoints
119-
120147
waypointsWithUniqueFlag = M.filter (any $ wpUnique . wpConfig . value) correctedWaypoints
148+
149+
validatePortals ::
150+
MonadFail m =>
151+
Navigation (M.Map SubworldName) WaypointName ->
152+
m (M.Map (Cosmo Location) (Cosmo Location))
153+
validatePortals (Navigation wpUniverse partialPortals) = do
154+
portalPairs <- forM (M.toList partialPortals) $ \(portalEntrance, portalExit@(Cosmo swName (WaypointName rawExitName))) -> do
155+
firstExitLoc :| otherExits <- getLocs portalExit
156+
unless (null otherExits)
157+
. fail
158+
. T.unpack
159+
$ T.unwords
160+
[ "Ambiguous exit waypoints named"
161+
, quote rawExitName
162+
, "for portal"
163+
]
164+
return (portalEntrance, Cosmo swName firstExitLoc)
165+
166+
return $ M.fromList portalPairs
167+
where
168+
getLocs (Cosmo swName@(SubworldName rawSwName) wpWrapper@(WaypointName exitName)) = do
169+
subworldWaypoints <- case M.lookup swName wpUniverse of
170+
Just x -> return x
171+
Nothing ->
172+
fail $
173+
T.unpack $
174+
T.unwords
175+
[ "Could not lookup waypoint"
176+
, quote exitName
177+
, "for portal exit because subworld"
178+
, quote rawSwName
179+
, "does not exist"
180+
]
181+
182+
failWaypointLookup wpWrapper $
183+
M.lookup wpWrapper subworldWaypoints

0 commit comments

Comments
 (0)