|
1 | 1 | {-# LANGUAGE AllowAmbiguousTypes #-} |
2 | 2 | {-# LANGUAGE DerivingStrategies #-} |
3 | | -{-# LANGUAGE GeneralizedNewtypeDeriving #-} |
4 | 3 | {-# LANGUAGE TypeFamilies #-} |
5 | 4 |
|
6 | 5 | -- | |
|
16 | 15 | -- indexed by 32-bit signed integers, so they correspond to a |
17 | 16 | -- \( 2^{32} \times 2^{32} \) torus). |
18 | 17 | module Swarm.Game.World ( |
19 | | - -- * Worlds |
| 18 | + -- * World function |
20 | 19 | WorldFun (..), |
21 | 20 | runWF, |
22 | 21 | worldFunFromArray, |
| 22 | + |
| 23 | + -- * Worlds |
23 | 24 | World, |
24 | | - MultiWorld, |
| 25 | + newWorld, |
25 | 26 |
|
26 | 27 | -- ** Tile management |
27 | 28 | loadCell, |
28 | 29 | loadRegion, |
29 | 30 |
|
30 | | - -- ** World functions |
31 | | - newWorld, |
32 | | - lookupCosmicTerrain, |
33 | | - lookupTerrain, |
34 | | - lookupCosmicEntity, |
35 | | - lookupEntity, |
36 | | - update, |
37 | | - |
38 | | - -- ** Monadic variants |
| 31 | + -- ** Monadic functions |
39 | 32 | lookupTerrainM, |
40 | 33 | lookupEntityM, |
41 | 34 | lookupContentM, |
42 | 35 | updateM, |
| 36 | + -- TODO: #2555 |
| 37 | + -- loadCellM |
| 38 | + -- loadRegionM, |
43 | 39 |
|
44 | | - -- ** Runtime updates |
| 40 | + -- * Multi-Worlds |
| 41 | + MultiWorld, |
| 42 | + lookupCosmicTerrain, |
| 43 | + lookupCosmicEntity, |
| 44 | + |
| 45 | + -- * Runtime updates |
45 | 46 | WorldUpdate (..), |
46 | 47 |
|
47 | | - -- * Re-export |
| 48 | + -- * Re-Exports |
48 | 49 | Seed, |
| 50 | + module Coords, |
49 | 51 | ) where |
50 | 52 |
|
51 | | -import Control.Algebra (Has) |
52 | | -import Control.Arrow ((&&&)) |
53 | | -import Control.Effect.State (State, get, modify, state) |
54 | | -import Control.Lens |
55 | | -import Data.Array qualified as A |
56 | | -import Data.Array.IArray |
57 | | -import Data.Array.Unboxed qualified as U |
58 | | -import Data.Bifunctor (second) |
59 | | -import Data.Bits |
60 | | -import Data.Foldable (foldl') |
61 | | -import Data.Int (Int32) |
62 | | -import Data.IntMap qualified as IM |
63 | | -import Data.Map (Map) |
64 | | -import Data.Map.Strict qualified as M |
65 | | -import Data.Maybe (fromMaybe) |
66 | | -import Data.Semigroup (Last (..)) |
67 | | -import Data.Yaml (FromJSON, ToJSON) |
68 | | -import GHC.Generics (Generic) |
69 | | -import Swarm.Game.Entity (Entity, entityHash) |
70 | | -import Swarm.Game.Location |
71 | | -import Swarm.Game.Scenario.Topography.Modify |
72 | | -import Swarm.Game.Terrain (TerrainMap, TerrainType (BlankT), terrainByIndex, terrainName) |
73 | | -import Swarm.Game.Universe |
74 | | -import Swarm.Game.World.Coords |
| 53 | +import Swarm.Game.World.Coords as Coords |
75 | 54 | import Swarm.Game.World.DSL.Gen (Seed) |
76 | | -import Swarm.Util ((?)) |
77 | | -import Swarm.Util.Erasable |
78 | | -import Prelude hiding (Foldable (..), lookup) |
79 | | - |
80 | | ------------------------------------------------------------- |
81 | | --- World function |
82 | | ------------------------------------------------------------- |
83 | | - |
84 | | --- | A @WorldFun t e@ represents a 2D world with terrain of type @t@ |
85 | | --- (exactly one per cell) and entities of type @e@ (at most one per |
86 | | --- cell). |
87 | | -newtype WorldFun t e = WF {getWF :: Coords -> (t, Erasable (Last e))} |
88 | | - deriving stock (Functor) |
89 | | - deriving newtype (Semigroup, Monoid) |
90 | | - |
91 | | -runWF :: WorldFun t e -> Coords -> (t, Maybe e) |
92 | | -runWF wf = second (erasableToMaybe . fmap getLast) . getWF wf |
93 | | - |
94 | | -instance Bifunctor WorldFun where |
95 | | - bimap g h (WF z) = WF (bimap g (fmap (fmap h)) . z) |
96 | | - |
97 | | --- | Create a world function from a finite array of specified cells. |
98 | | -worldFunFromArray :: Monoid t => Array (Int32, Int32) (t, Erasable e) -> WorldFun t e |
99 | | -worldFunFromArray arr = WF $ \(Coords (r, c)) -> |
100 | | - if inRange bnds (r, c) |
101 | | - then second (fmap Last) (arr ! (r, c)) |
102 | | - else mempty |
103 | | - where |
104 | | - bnds = bounds arr |
105 | | - |
106 | | ------------------------------------------------------------- |
107 | | --- Tiles and coordinates |
108 | | ------------------------------------------------------------- |
109 | | - |
110 | | --- | The number of bits we need in each coordinate to represent all |
111 | | --- the locations in a tile. In other words, each tile has a size of |
112 | | --- @2^tileBits x 2^tileBits@. |
113 | | --- |
114 | | --- Currently, 'tileBits' is set to 6, giving us 64x64 tiles, with |
115 | | --- 4096 cells in each tile. That seems intuitively like a good size, |
116 | | --- but I don't have a good sense for the tradeoffs here, and I don't |
117 | | --- know how much the choice of tile size matters. |
118 | | -tileBits :: Int |
119 | | -tileBits = 6 |
120 | | - |
121 | | --- | The number consisting of 'tileBits' many 1 bits. We can use this |
122 | | --- to mask out the tile offset of a coordinate. |
123 | | -tileMask :: Int32 |
124 | | -tileMask = (1 `shiftL` tileBits) - 1 |
125 | | - |
126 | | --- | If we think of the world as a grid of /tiles/, we can assign each |
127 | | --- tile some coordinates in the same way we would if each tile was a |
128 | | --- single cell. These are the tile coordinates. |
129 | | -newtype TileCoords = TileCoords {unTileCoords :: Coords} |
130 | | - deriving (Eq, Ord, Show, Ix, Generic) |
131 | | - |
132 | | -instance Rewrapped TileCoords t |
133 | | -instance Wrapped TileCoords |
134 | | - |
135 | | --- | Convert from a cell's coordinates to the coordinates of its tile, |
136 | | --- simply by shifting out 'tileBits' many bits. |
137 | | -tileCoords :: Coords -> TileCoords |
138 | | -tileCoords = TileCoords . over (_Wrapped . both) (`shiftR` tileBits) |
139 | | - |
140 | | --- | Find the coordinates of the upper-left corner of a tile. |
141 | | -tileOrigin :: TileCoords -> Coords |
142 | | -tileOrigin = over (_Wrapped . both) (`shiftL` tileBits) . unTileCoords |
143 | | - |
144 | | --- | A 'TileOffset' represents an offset from the upper-left corner of |
145 | | --- some tile to a cell in its interior. |
146 | | -newtype TileOffset = TileOffset Coords |
147 | | - deriving (Eq, Ord, Show, Ix, Generic) |
148 | | - |
149 | | --- | The offsets of the upper-left and lower-right corners of a tile: |
150 | | --- (0,0) to ('tileMask', 'tileMask'). |
151 | | -tileBounds :: (TileOffset, TileOffset) |
152 | | -tileBounds = (TileOffset (Coords (0, 0)), TileOffset (Coords (tileMask, tileMask))) |
153 | | - |
154 | | --- | Compute the offset of a given coordinate within its tile. |
155 | | -tileOffset :: Coords -> TileOffset |
156 | | -tileOffset = TileOffset . over (_Wrapped . both) (.&. tileMask) |
157 | | - |
158 | | --- | Add a tile offset to the coordinates of the tile's upper left |
159 | | --- corner. NOTE that for efficiency, this function only works when |
160 | | --- the first argument is in fact the coordinates of a tile's |
161 | | --- upper-left corner (/i.e./ it is an output of 'tileOrigin'). In |
162 | | --- that case the coordinates will end with all 0 bits, and we can |
163 | | --- add the tile offset just by doing a coordinatewise 'xor'. |
164 | | -plusOffset :: Coords -> TileOffset -> Coords |
165 | | -plusOffset (Coords (x1, y1)) (TileOffset (Coords (x2, y2))) = Coords (x1 `xor` x2, y1 `xor` y2) |
166 | | - |
167 | | -instance Rewrapped TileOffset t |
168 | | -instance Wrapped TileOffset |
169 | | - |
170 | | --- | A terrain tile is an unboxed array of terrain values. |
171 | | -type TerrainTile t = U.UArray TileOffset t |
172 | | - |
173 | | --- | An entity tile is an array of possible entity values. Note it |
174 | | --- cannot be an unboxed array since entities are complex records |
175 | | --- which have to be boxed. |
176 | | -type EntityTile e = A.Array TileOffset (Maybe e) |
177 | | - |
178 | | -type MultiWorld t e = Map SubworldName (World t e) |
179 | | - |
180 | | --- | A 'World' consists of a 'WorldFun' that specifies the initial |
181 | | --- world, a cache of loaded square tiles to make lookups faster, and |
182 | | --- a map storing locations whose entities have changed from their |
183 | | --- initial values. |
184 | | --- |
185 | | --- Right now the 'World' simply holds on to all the tiles it has |
186 | | --- ever loaded. Ideally it would use some kind of LRU caching |
187 | | --- scheme to keep memory usage bounded, but it would be a bit |
188 | | --- tricky, and in any case it's probably not going to matter much |
189 | | --- for a while. Once tile loads can trigger robots to spawn, it |
190 | | --- would also make for some difficult decisions in terms of how to |
191 | | --- handle respawning. |
192 | | -data World t e = World |
193 | | - { _worldFun :: WorldFun t e |
194 | | - , _tileCache :: M.Map TileCoords (TerrainTile t, EntityTile e) |
195 | | - , _changed :: M.Map Coords (Maybe e) |
196 | | - } |
197 | | - |
198 | | --- | Create a new 'World' from a 'WorldFun'. |
199 | | -newWorld :: WorldFun t e -> World t e |
200 | | -newWorld f = World f M.empty M.empty |
201 | | - |
202 | | -lookupCosmicTerrain :: |
203 | | - TerrainMap -> |
204 | | - Cosmic Coords -> |
205 | | - MultiWorld Int e -> |
206 | | - TerrainType |
207 | | -lookupCosmicTerrain tm (Cosmic subworldName i) multiWorld = |
208 | | - fromMaybe BlankT $ do |
209 | | - x <- M.lookup subworldName multiWorld |
210 | | - y <- (`IM.lookup` terrainByIndex tm) . lookupTerrain i $ x |
211 | | - return $ terrainName y |
212 | | - |
213 | | --- | Look up the terrain value at certain coordinates: try looking it |
214 | | --- up in the tile cache first, and fall back to running the 'WorldFun' |
215 | | --- otherwise. |
216 | | --- |
217 | | --- This function does /not/ ensure that the tile containing the |
218 | | --- given coordinates is loaded. For that, see 'lookupTerrainM'. |
219 | | -lookupTerrain :: (IArray U.UArray t) => Coords -> World t e -> t |
220 | | -lookupTerrain i (World f t _) = |
221 | | - ((U.! tileOffset i) . fst <$> M.lookup (tileCoords i) t) |
222 | | - ? fst (runWF f i) |
223 | | - |
224 | | --- | A stateful variant of 'lookupTerrain', which first loads the tile |
225 | | --- containing the given coordinates if it is not already loaded, |
226 | | --- then looks up the terrain value. |
227 | | -lookupTerrainM :: |
228 | | - forall t e sig m. |
229 | | - (Has (State (World t e)) sig m, IArray U.UArray t) => |
230 | | - Coords -> |
231 | | - m t |
232 | | -lookupTerrainM c = do |
233 | | - modify @(World t e) $ loadCell c |
234 | | - lookupTerrain c <$> get @(World t e) |
235 | | - |
236 | | -lookupContentM :: |
237 | | - forall t e sig m. |
238 | | - (Has (State (World t e)) sig m, IArray U.UArray t) => |
239 | | - Coords -> |
240 | | - m (t, Maybe e) |
241 | | -lookupContentM c = do |
242 | | - modify @(World t e) $ loadCell c |
243 | | - w <- get @(World t e) |
244 | | - return (lookupTerrain c w, lookupEntity c w) |
245 | | - |
246 | | -lookupCosmicEntity :: Cosmic Coords -> MultiWorld t e -> Maybe e |
247 | | -lookupCosmicEntity (Cosmic subworldName i) multiWorld = |
248 | | - lookupEntity i =<< M.lookup subworldName multiWorld |
249 | | - |
250 | | --- | Look up the entity at certain coordinates: first, see if it is in |
251 | | --- the map of locations with changed entities; then try looking it |
252 | | --- up in the tile cache first; and finally fall back to running the |
253 | | --- 'WorldFun'. |
254 | | --- |
255 | | --- This function does /not/ ensure that the tile containing the |
256 | | --- given coordinates is loaded. For that, see 'lookupEntityM'. |
257 | | -lookupEntity :: Coords -> World t e -> Maybe e |
258 | | -lookupEntity i (World f t m) = |
259 | | - M.lookup i m |
260 | | - ? ((A.! tileOffset i) . snd <$> M.lookup (tileCoords i) t) |
261 | | - ? snd (runWF f i) |
262 | | - |
263 | | --- | A stateful variant of 'lookupEntity', which first loads the tile |
264 | | --- containing the given coordinates if it is not already loaded, |
265 | | --- then looks up the terrain value. |
266 | | -lookupEntityM :: |
267 | | - forall t e sig m. |
268 | | - (Has (State (World t e)) sig m, IArray U.UArray t) => |
269 | | - Coords -> |
270 | | - m (Maybe e) |
271 | | -lookupEntityM c = do |
272 | | - modify @(World t e) $ loadCell c |
273 | | - lookupEntity c <$> get @(World t e) |
274 | | - |
275 | | --- | Update the entity (or absence thereof) at a certain location, |
276 | | --- returning an updated 'World' and a Boolean indicating whether |
277 | | --- the update changed the entity here. |
278 | | --- See also 'updateM'. |
279 | | -update :: |
280 | | - Coords -> |
281 | | - (Maybe Entity -> Maybe Entity) -> |
282 | | - World t Entity -> |
283 | | - (World t Entity, CellUpdate Entity) |
284 | | -update i g w@(World f t m) = |
285 | | - (wNew, classifyModification (view entityHash) entityBefore entityAfter) |
286 | | - where |
287 | | - wNew = World f t $ M.insert i entityAfter m |
288 | | - entityBefore = lookupEntity i w |
289 | | - entityAfter = g entityBefore |
290 | | - |
291 | | --- | A stateful variant of 'update', which also ensures the tile |
292 | | --- containing the given coordinates is loaded. |
293 | | -updateM :: |
294 | | - forall t sig m. |
295 | | - (Has (State (World t Entity)) sig m, IArray U.UArray t) => |
296 | | - Coords -> |
297 | | - (Maybe Entity -> Maybe Entity) -> |
298 | | - m (CellUpdate Entity) |
299 | | -updateM c g = do |
300 | | - state @(World t Entity) $ update c g . loadCell c |
301 | | - |
302 | | --- | Load the tile containing a specific cell. |
303 | | -loadCell :: (IArray U.UArray t) => Coords -> World t e -> World t e |
304 | | -loadCell c = loadRegion (c, c) |
305 | | - |
306 | | --- | Load all the tiles which overlap the given rectangular region |
307 | | --- (specified as an upper-left and lower-right corner, inclusive). |
308 | | -loadRegion :: |
309 | | - forall t e. |
310 | | - (IArray U.UArray t) => |
311 | | - (Coords, Coords) -> |
312 | | - World t e -> |
313 | | - World t e |
314 | | -loadRegion reg (World f t m) = World f t' m |
315 | | - where |
316 | | - tiles = range (over both tileCoords reg) |
317 | | - t' = foldl' (\hm (i, tile) -> maybeInsert i tile hm) t (map (id &&& loadTile) tiles) |
318 | | - |
319 | | - maybeInsert k v tm |
320 | | - | k `M.member` tm = tm |
321 | | - | otherwise = M.insert k v tm |
322 | | - |
323 | | - loadTile :: TileCoords -> (TerrainTile t, EntityTile e) |
324 | | - loadTile tc = (listArray tileBounds terrain, listArray tileBounds entities) |
325 | | - where |
326 | | - tileCorner = tileOrigin tc |
327 | | - (terrain, entities) = unzip $ map (runWF f . plusOffset tileCorner) (range tileBounds) |
328 | | - |
329 | | ---------------------------------------------------------------------- |
330 | | --- Runtime world update |
331 | | ---------------------------------------------------------------------- |
332 | | - |
333 | | --- | Enumeration of world updates. This type is used for changes by |
334 | | --- /e.g./ the @drill@ command which must be carried out at a later |
335 | | --- tick. Using a first-order representation (as opposed to /e.g./ |
336 | | --- just a @World -> World@ function) allows us to serialize and |
337 | | --- inspect the updates. |
338 | | -data WorldUpdate e = ReplaceEntity |
339 | | - { updatedLoc :: Cosmic Location |
340 | | - , originalEntity :: e |
341 | | - , newEntity :: Maybe e |
342 | | - } |
343 | | - deriving (Eq, Ord, Show, Generic, FromJSON, ToJSON) |
| 55 | +import Swarm.Game.World.Function |
| 56 | +import Swarm.Game.World.Multi |
| 57 | +import Swarm.Game.World.Pure |
| 58 | +import Swarm.Game.World.Stateful |
| 59 | +import Swarm.Game.World.Update |
0 commit comments