Skip to content

Commit 5cc7b1d

Browse files
authored
Tear the World asunder (#2568)
* split World into smaller modules and re-export * also re-export `Coords` for convenience * split from #2555 * depends on #2567 The idea is to have Tile and Function modules separate from World, instead of having to scroll past them. Then World functions get split into a Pure interface and a monadic/stateful one that should match (some are missing) and be preferred as it will implicitly use cache (it is currently not very used). In the next PR, the monadic interface will be improved with metrics...
1 parent 861b6db commit 5cc7b1d

File tree

11 files changed

+471
-309
lines changed

11 files changed

+471
-309
lines changed

src/swarm-scenario/Swarm/Game/State/Landscape.hs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import Swarm.Game.State.Config
5252
import Swarm.Game.Terrain (TerrainType (..), terrainIndexByName)
5353
import Swarm.Game.Universe as U
5454
import Swarm.Game.World
55-
import Swarm.Game.World.Coords
5655
import Swarm.Game.World.DSL (runWorld)
5756
import Swarm.Util.Erasable
5857
import Swarm.Util.Lens (makeLensesNoSigs)
Lines changed: 22 additions & 306 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{-# LANGUAGE AllowAmbiguousTypes #-}
22
{-# LANGUAGE DerivingStrategies #-}
3-
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
43
{-# LANGUAGE TypeFamilies #-}
54

65
-- |
@@ -16,328 +15,45 @@
1615
-- indexed by 32-bit signed integers, so they correspond to a
1716
-- \( 2^{32} \times 2^{32} \) torus).
1817
module Swarm.Game.World (
19-
-- * Worlds
18+
-- * World function
2019
WorldFun (..),
2120
runWF,
2221
worldFunFromArray,
22+
23+
-- * Worlds
2324
World,
24-
MultiWorld,
25+
newWorld,
2526

2627
-- ** Tile management
2728
loadCell,
2829
loadRegion,
2930

30-
-- ** World functions
31-
newWorld,
32-
lookupCosmicTerrain,
33-
lookupTerrain,
34-
lookupCosmicEntity,
35-
lookupEntity,
36-
update,
37-
38-
-- ** Monadic variants
31+
-- ** Monadic functions
3932
lookupTerrainM,
4033
lookupEntityM,
4134
lookupContentM,
4235
updateM,
36+
-- TODO: #2555
37+
-- loadCellM
38+
-- loadRegionM,
4339

44-
-- ** Runtime updates
40+
-- * Multi-Worlds
41+
MultiWorld,
42+
lookupCosmicTerrain,
43+
lookupCosmicEntity,
44+
45+
-- * Runtime updates
4546
WorldUpdate (..),
4647

47-
-- * Re-export
48+
-- * Re-Exports
4849
Seed,
50+
module Coords,
4951
) where
5052

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
7554
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

Comments
 (0)