1
1
{-# LANGUAGE OverloadedStrings #-}
2
+ {-# LANGUAGE UndecidableInstances #-}
2
3
3
4
-- |
4
5
-- SPDX-License-Identifier: BSD-3-Clause
5
6
module Swarm.Game.Scenario.Topography.Navigation.Portal where
6
7
7
8
import Control.Monad (forM , forM_ , unless )
8
9
import Data.Aeson (FromJSON )
10
+ import Data.Bifunctor (first )
11
+ import Data.Functor.Identity
9
12
import Data.Int (Int32 )
10
13
import Data.List (intercalate )
11
14
import Data.List.NonEmpty (NonEmpty ((:|) ))
12
15
import Data.List.NonEmpty qualified as NE
13
16
import Data.Map qualified as M
14
- import Data.Maybe (listToMaybe )
15
- import Data.Text (Text )
17
+ import Data.Maybe (fromMaybe , listToMaybe )
16
18
import Data.Text qualified as T
17
19
import GHC.Generics (Generic )
18
20
import Linear (V2 )
19
21
import Swarm.Game.Location
20
22
import Swarm.Game.Scenario.Topography.Navigation.Waypoint
23
+ import Swarm.Game.Universe
21
24
import Swarm.Util (binTuples , quote )
22
25
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 )
27
27
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
30
35
-- ^ Note that waypoints defined at the "root" level are still relative to
31
36
-- the top-left corner of the map rectangle; they are not in absolute world
32
37
-- coordinates (as with applying the "ul" offset).
33
- , portals :: M. Map Location Location
38
+ , portals :: M. Map ( Cosmo Location ) ( Cosmo b )
34
39
}
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 )
36
43
37
44
data PortalExit = PortalExit
38
45
{ exit :: WaypointName
@@ -64,31 +71,59 @@ failUponDuplication message binnedMap =
64
71
where
65
72
duplicated = M. filter ((> 1 ) . NE. length ) binnedMap
66
73
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
69
88
-- * 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.
71
107
validateNavigation ::
72
108
(MonadFail m , Traversable t ) =>
109
+ SubworldName ->
73
110
V2 Int32 ->
74
111
[Originated Waypoint ] ->
75
112
t Portal ->
76
- m Navigation
77
- validateNavigation upperLeft unmergedWaypoints portalDefs = do
113
+ m ( Navigation Identity WaypointName )
114
+ validateNavigation currentSubworldName upperLeft unmergedWaypoints portalDefs = do
78
115
failUponDuplication " is required to be unique, but is duplicated in:" waypointsWithUniqueFlag
79
116
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
82
118
-- Portals can have multiple entrances but only a single exit.
83
119
-- 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.
85
122
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
92
127
93
128
let reconciledPortalPairs = concat nestedPortalPairs
94
129
@@ -97,17 +132,10 @@ validateNavigation upperLeft unmergedWaypoints portalDefs = do
97
132
failUponDuplication " has overlapping portal entrances exiting to" $
98
133
binTuples reconciledPortalPairs
99
134
100
- return $ Navigation bareWaypoints $ M. fromList reconciledPortalPairs
135
+ return . Navigation (pure bareWaypoints) . M. fromList $
136
+ map (first $ Cosmo currentSubworldName) reconciledPortalPairs
101
137
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
111
139
112
140
extractLoc (Originated _ (Waypoint _ loc)) = loc
113
141
correctedWaypoints =
@@ -116,5 +144,40 @@ validateNavigation upperLeft unmergedWaypoints portalDefs = do
116
144
(\ x -> (wpName $ wpConfig $ value x, fmap (offsetWaypoint upperLeft) x))
117
145
unmergedWaypoints
118
146
bareWaypoints = M. map (NE. map extractLoc) correctedWaypoints
119
-
120
147
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