# Haskell roguelike - Field of view

Posted on April 2, 2018

# Field of view

In most roguelike games you need to calculate the actor’s fields of view, i.e. how far they can see from where they currently are and is anything blocking their line of site in any direction.

Below is what the fov logic added in this chapter will look like.

You should be able to see that

• The player has a fov distance of three tiles in all directions.
• Some entities are transparent e.g. an open door.
• Some entities are not transparent and block the fov, e.g. a wall
• Each time the player moves the fov is updated and redrawn

## Implementing fov

There are numerous options for fov algorithms. The Field of Vision has a nice set of options if you are looking for alternatives.

I decided to use Bresenham’s line algorithm. It is fast and simple enough to see how it works. The algorithm calculates how to draw a strait line on a “pixelated” surface. In a roguelike the tiles count as pixels.

To calculate an actor’s fov the process is roughly as follows

1. Calculate the bounding box. I.e. the maximum possible distance the actor could see if nothing blocked the way. (In the image below this is the red square)
2. Use Bresenham’s line algorithm to trace a line to each tile in the bounding square
3. Stop tracing each line when it gets to the bounding block or if the current tile is not transparent.
4. The set of all visited points is then where the actor can see

## Bresenham’s line algorithm

I got the original haskell code from wiki.haskell and made a few minor changes. This will give us an list of (x, y) for each line we trace to the bounding tiles.

###### 10_fov/src/GameEngine.hs (554 to 580)
``````-- | Bresenham's algorithm
bline :: (Int, Int) -> (Int, Int) -> [(Int, Int)]
bline pa@(xa, ya) pb@(xb, yb) =
let r = map maySwitch . Lst.unfoldr go \$ (x1, y1, 0) in

case r of
(p:_) | p == pa -> r
_ -> Lst.reverse r

where
steep = abs (yb - ya) > abs (xb - xa)
maySwitch = if steep then (\(x,y) -> (y,x)) else identity
((x1, y1), (x2, y2)) = case Lst.sort [maySwitch pa, maySwitch pb] of
[a, b] -> (a, b)
_ -> ((0, 0), (0, 0)) -- This case is never matched, but fixes partial match warning
deltax = x2 - x1
deltay = abs (y2 - y1)
ystep = if y1 < y2 then 1 else -1
go (xTemp, yTemp, err)
| xTemp > x2 = Nothing
| otherwise = Just ((xTemp, yTemp), (xTemp + 1, newY, newError))
where
tempError = err + deltay
(newY, newError) = if (2 * tempError) >= deltax
then (yTemp + ystep, tempError - deltax)
else (yTemp, tempError)``````

## Bounding box

###### 10_fov/src/GameEngine.hs (542 to 549)
``````-- | Get the bounds for a fov distance
boundingPoints :: Int -> WorldPos -> [WorldPos]
boundingPoints distance (WorldPos (atx, aty)) =
Lst.nub \$
[WorldPos (atx - distance + d, aty - distance) | d <- [0..distance * 2]] <>
[WorldPos (atx - distance, aty - distance + d) | d <- [0..distance * 2]] <>
[WorldPos (atx - distance + d, aty + distance) | d <- [0..distance * 2]] <>
[WorldPos (atx + distance, aty - distance + d) | d <- [0..distance * 2]]``````

The code above is calculating each point in the bounding box. Lets look at how it works

• `(atx, aty)` is the actors current position
• Each of the four list comprehensions draws one side of the square
• `[0..distance * 2]`. The `* 2` is because the fov is `distance` tiles to both sides of the actor. E.g for a distance of 3, this generates `[0,1,2,3,4,5,6]`
• At each side you start at a the far end of the fov and move towards the other end.

The code does this for all four sides, combines the results and removes any duplicates.

E.g. For a starting position of `(100, 10)`

`[(atx - distance + d, aty - distance) | d <- [0..distance * 2]]`

Generates

`[(97,7),(98,7),(99,7),(100,7),(101,7),(102,7),(103,7)]`

I.e. it is drawing the bounding left line from top to bottom.

## Fov

As mentioned above once we have the bounding box, we then need to trace all lines from the actor to each point on the bounding box.

###### 10_fov/src/GameEngine.hs (524 to 537)
``````-- | Calculate the field of view from a position
calcFov :: Int -> (WorldPos -> Bool) -> WorldPos -> [(WorldPos, [WorldPos])]
calcFov fovDistance isEntityTransparent fromPos'@(WorldPos fromPos) =
let boundries = getBoundries fromPos' in
go <\$> boundries

where
getBoundries (WorldPos (x, y)) = boundingPoints fovDistance (WorldPos (x, y))

go toPos'@(WorldPos toPos) =
let line = WorldPos <\$> bline fromPos toPos in
let isTransparentOrStart p = p == fromPos' || isEntityTransparent p in
let (m, r) = Lst.span isTransparentOrStart line in
(toPos', m <> Lst.take 1 r)``````

calcFov does exactly that.

1. Gets the bounding box
2. For each point on the boundary, draw a line from the actor to the point.
3. Keep every point in the line that is transparent, or is the starting point. Stop as soon as its not

In the go function that is called for each line, Data.List.span is used to find the visible part of the line.

``````-- https://hackage.haskell.org/package/base/docs/Data-List.html#v:span
-- span, applied to a predicate p and a list xs, returns a tuple where first
--  element is longest prefix (possibly empty) of xs of elements that satisfy p
--  and second element is the remainder of the list
--
-- span p xs is equivalent to (takeWhile p xs, dropWhile p xs)
--
span :: (a -> Bool) -> [a] -> ([a], [a])``````

This code

``````let (m, r) = Lst.span isTransparentOrStart line in
(toPos', m <> Lst.take 1 r)``````

First gets a tuple of `(transparent_points, not_transparent_points)`. It then takes one from the `not_transparent_points` and adds it to the result. This is done as the actor can see the tile that blocks the light, but not past it.

For now isTransparent says everything apart from walls are transparent. Later on we’ll add closed doors etc.

###### 10_fov/src/GameEngine.hs (585 to 589)
``````isTransparent :: Map WorldPos Entity -> WorldPos -> Bool
isTransparent wmap pos =
case Map.lookup pos wmap of
Nothing -> True
Just e -> (e ^. enType) /= E.Wall``````

flatFov is a helper function to get a unique list of coordinates in the field of view from the list of lines.

###### 10_fov/src/GameEngine.hs (613 to 615)
``````flatFov :: Maybe [(WorldPos, [WorldPos])] -> [WorldPos]
flatFov Nothing = []
flatFov (Just fov) = Lst.nub . Lst.concat \$ snd <\$> fov``````

## Actor changes

Actors gets two new properties

###### 10_fov/src/GameCore.hs (32 to 33)
``````  , _acFov :: !(Maybe [(WorldPos, [WorldPos])])
, _acFovDistance :: !Int``````
1. acFov is the current field of view
2. acFovDistance is how far each actor can see. This means that each actor can have a different fov distance. In this example the player can see for three tiles, and enemies can only see for two tiles.

# Updating the fov

Each actor’s fov must be kept up to date. To keep things simple I’m updating the fov when an actor is added and each time an actor moves.

###### 10_fov/src/GameEngine.hs (435 to 437)
``````updateActorFov :: World -> Actor -> Actor
updateActorFov w a =
a & acFov .~ Just (calcFov (a ^. acFovDistance) (isTransparent \$ w ^. wdMap) (a ^. acWorldPos))``````

At the end of bootWorld we call updateActorFov for all actors.

###### 10_fov/src/GameEngine.hs (81 to 98)
``````bootWorld :: Host.Connection -> (Int, Int) -> Text -> Rnd.StdGen -> World
bootWorld conn screenSize mapData std =
let
config = mkConfig
bug = mkEnemyActor "bug1" E.Bug (6, -2)
snake = mkEnemyActor "snake1" E.Snake (8, -4)

w1 = World { _wdPlayer = mkPlayer
, _wdConfig = config
, _wdActors = Map.fromList [ (bug ^. acId, bug)
, (snake ^. acId, snake)
]
}
in

-- Calculate the actors fov
updateAllActors w1 updateActorFov``````
###### 10_fov/src/GameEngine.hs (450 to 455)
``````-- | Update all actors, including the player's actor
updateAllActors :: World -> (World -> Actor -> Actor) -> World
updateAllActors w fn =
let w2 = w & (wdPlayer . plActor) %~ fn w in
let w3 = w2 & wdActors %~ fmap (fn w2) in
w3``````

## Moving actors

tryMoveActor is also update to call updateActorFov

###### 10_fov/src/GameEngine.hs (422 to 431)
``````  if canMove
then
let
movedActor = actor & acWorldPos .~ tryWorldTo'
w2 = updatePlayerViewport \$ updateActor world movedActor
pa = w2 ^. wdPlayer ^. plActor
in
Just \$ updateActor w2 (updateActorFov w2 pa)
else
Nothing``````

# Drawing darkness

## Darkness tile

We need a tile to use to draw the “darkness”. This is a tile that is 100% black.

###### 10_fov/src/Entities.hs (26 to 26)
``  , (E.Dark      , (43, 11), Nothing)``

## Darkness overlay

When the tiles are drawn we now need to only show tiles that the player can see, i.e. tiles in their fov. We could use another layer for this, but its pointless to draw a layer and then immediately hide the tiles. Instead an overlay is used to replace any tiles that are not visible.

###### 10_fov/src/GameEngine.hs (594 to 608)
``````darknessFovOverlay :: Player -> Actor -> Map PlayerPos Tile
darknessFovOverlay player actor =
let
(screenWidth, screenHeight) = player ^. plScreenSize

-- Create a full grid of darkness
blackBg = Map.fromList [ (PlayerPos (x, y), E.getTile E.Dark)
| x <- [0..(screenWidth - 1)]
, y <- [0..(screenHeight - 1)]
]

lightAt = worldCoordToPlayer (player ^. plWorldTopLeft) <\$> flatFov (actor ^. acFov)
in
-- Remove the darkness overlay at any position that is to be lit
foldr Map.delete blackBg lightAt``````
1. Create a map *blackBg that is 100% full of darkness tiles.
2. Create a list of positions that are visible in lightAt
3. Delete all positions in the blackBg map that are in the lightAt list

The foldr line could have been written more explicitly like this

``  foldr (\lit acc -> Map.delete lit acc) blackBg lightAt``

But foldr provides the arguments in the correct order for Map.delete so there is no need for that.

## Drawing

And finally drawTilesForPlayer can be changed to draw the darkness overlay. Take a look at the diff below, it should help put these changes in context. I’ll highlight the main changes below.

###### 10_fov/src/GameEngine.hs (278 to 333)
``````drawTilesForPlayer :: World -> Map WorldPos Entity -> [Map PlayerPos Tile]
drawTilesForPlayer world entityMap =
let
-- Entity base layer
entities = mkLayer entityMap
-- Darkness
darknessOverlay = darknessFovOverlay (world ^. wdPlayer) (world ^. wdPlayer ^. plActor)
-- Darkness hides entity
baseLayer = Map.union darknessOverlay entities

-- Actor layer on top
actorMap = Map.fromList \$ (\a -> (a ^. acWorldPos, a ^. acEntity)) <\$> getAllActors world
inViewActors = Map.filterWithKey inView actorMap
actorLayer = mkLayer inViewActors
visibleActorLayer = Map.filterWithKey (\wp _ -> isNotDarkness wp baseLayer) actorLayer
in
-- Layers
-- 0: Entities (with darkness overlay)
-- 1: Actors
[baseLayer, visibleActorLayer]

where
player = world ^. wdPlayer

-- Top left of player's grid
(WorldPos (topX, topY)) = player ^. plWorldTopLeft

-- Players screen/grid dimensions
(screenX, screenY) = player ^. plScreenSize

-- Bottom right corner
(bottomX, bottomY) = (topX + screenX, topY - screenY)

isNotDarkness :: PlayerPos -> Map PlayerPos Tile -> Bool
isNotDarkness wp ts =
case Map.lookup wp ts of
Nothing -> True
Just t -> t ^. tlId /= E.getTile E.Dark ^. tlId

inView (WorldPos (x, y)) _ =
x >= topX && x < bottomX && y > bottomY && y <= topY

mkLayer :: Map WorldPos Entity -> Map PlayerPos Tile
mkLayer entities =
let
-- Filter out blank
noEmptyMap = Map.filter (\e -> e ^. enTile ^. tlName /= "blank") entities

-- Only get the entitys that are at positions on the player's screen
visibleEntitys = Map.filterWithKey inView noEmptyMap

-- Get the tile for each entity
tileMap = (^. enTile) <\$> visibleEntitys
in
-- Get it with player positions
Map.mapKeys (worldCoordToPlayer \$ player ^. plWorldTopLeft) tileMap``````

The entities base layer contains the entities and the darkness overlay. Map.union is used to combine the two map. Since Map.union prefers the first map over the second, this means that entities are only selected if the tiles is not dark.

###### 10_fov/src/GameEngine.hs (282 to 287)
``````  -- Entity base layer
entities = mkLayer entityMap
-- Darkness
darknessOverlay = darknessFovOverlay (world ^. wdPlayer) (world ^. wdPlayer ^. plActor)
-- Darkness hides entity
baseLayer = Map.union darknessOverlay entities``````

The actor layer is already filtered to only select actors in the view port. Now we need to also only select actors that are not hidden by the darkness layer.

###### 10_fov/src/GameEngine.hs (291 to 295)
``````  -- Actor layer on top
actorMap = Map.fromList \$ (\a -> (a ^. acWorldPos, a ^. acEntity)) <\$> getAllActors world
inViewActors = Map.filterWithKey inView actorMap
actorLayer = mkLayer inViewActors
visibleActorLayer = Map.filterWithKey (\wp _ -> isNotDarkness wp baseLayer) actorLayer``````
###### 10_fov/src/GameEngine.hs (318 to 322)
``````  isNotDarkness :: PlayerPos -> Map PlayerPos Tile -> Bool
isNotDarkness wp ts =
case Map.lookup wp ts of
Nothing -> True
Just t -> t ^. tlId /= E.getTile E.Dark ^. tlId``````

And the layers are then returned as they were before, except of course that now the base layer also has the darkness tiles.

###### 10_fov/src/GameEngine.hs (298 to 302)
``````  in
-- Layers
-- 0: Entities (with darkness overlay)
-- 1: Actors
[baseLayer, visibleActorLayer]``````

# Changes

## src/Entities.hs

`diff -w -B -a -d -u -b --new-file 09_viewport_scroll/src/Entities.hs 10_fov/src/Entities.hs`
`+++ 10_fov/src/Entities.hs`
`@@ -22,6 +22,9 @@`
`            , (E.Player    , ( 8,  3), Nothing)`
`            , (E.Bug       , (25,  3), Nothing)`
`            , (E.Snake     , (38,  4), Nothing)`
`+`
`+           , (E.Dark      , (43, 11), Nothing)`
`+`
`            ]`
`   in`
`   let mkData (typ, pos@(x, y), l) (tiles', entities', loads') =`

## src/EntityType.hs

`diff -w -B -a -d -u -b --new-file 09_viewport_scroll/src/EntityType.hs 10_fov/src/EntityType.hs`
`+++ 10_fov/src/EntityType.hs`
`@@ -11,5 +11,6 @@`
`                 | Player`
`                 | Bug`
`                 | Snake`
`+                | Dark`
`                 | Unknown`
`                 deriving (Show, Eq, Ord)`

## src/GameCore.hs

`diff -w -B -a -d -u -b --new-file 09_viewport_scroll/src/GameCore.hs 10_fov/src/GameCore.hs`
`+++ 10_fov/src/GameCore.hs`
`@@ -28,8 +28,11 @@`
`                    , _acEntity :: !Entity`
`                    , _acWorldPos :: !WorldPos`
`                    , _acStdGen :: !Rnd.StdGen`
` `
`+                   , _acFov :: !(Maybe [(WorldPos, [WorldPos])])`
`+                   , _acFovDistance :: !Int`
`+`
`+                   }`
` `
` data Player = Player { _plConn :: !Host.Connection`
`                      , _plActor :: !Actor`

## src/GameEngine.hs

`diff -w -B -a -d -u -b --new-file 09_viewport_scroll/src/GameEngine.hs 10_fov/src/GameEngine.hs`
`+++ 10_fov/src/GameEngine.hs`
`@@ -7,6 +7,7 @@`
` import Protolude hiding (Map)`
` import           Data.Map.Strict (Map)`
` import qualified Data.Map.Strict as Map`
`+import qualified Data.List as Lst`
` import qualified Data.List.Index as Lst`
` import qualified Data.Text as Txt`
` import qualified Data.Text.IO as Txt`
`@@ -76,21 +77,27 @@`
`       Right \$ bootWorld conn (width, height) mapData std`
` `
` `
`+`
` bootWorld :: Host.Connection -> (Int, Int) -> Text -> Rnd.StdGen -> World`
` bootWorld conn screenSize mapData std = `
`   let`
`     config = mkConfig`
`     bug = mkEnemyActor "bug1" E.Bug (6, -2)`
`     snake = mkEnemyActor "snake1" E.Snake (8, -4)`
`    `
`+    w1 = World { _wdPlayer = mkPlayer`
`         , _wdConfig = config`
`         , _wdMap = loadWorld E.loadTexts mapData`
`         , _wdActors = Map.fromList [ (bug ^. acId, bug)`
`                                    , (snake ^. acId, snake)`
`                                    ]`
`         }`
`+  in`
`+`
`+  -- Calculate the actors fov`
`+  updateAllActors w1 updateActorFov`
`+`
`+`
`   where`
`     mkConfig =`
`       Config { _cfgKeys = Map.fromList [ ("up"      , "Move:up")`
`@@ -130,13 +134,14 @@`
`              , _plViewPortStyle = ViewPortBorder 2`
`              }`
` `
`     mkPlayersActor =`
`       Actor { _acId = Aid "player"`
`             , _acClass = ClassPlayer`
`             , _acEntity = E.getEntity E.Player`
`             , _acWorldPos = WorldPos (1, -1)`
`             , _acStdGen = std`
`+            , _acFovDistance = 3`
`+            , _acFov = Nothing`
`             }`
` `
`     mkEnemyActor aid e (x, y) =`
`@@ -145,6 +150,8 @@`
`             , _acEntity = E.getEntity e`
`             , _acWorldPos = WorldPos (x, y)`
`             , _acStdGen = std`
`+            , _acFovDistance = 2`
`+            , _acFov = Nothing`
`             }`
`     `
` `
`@@ -270,15 +278,29 @@`
` drawTilesForPlayer :: World -> Map WorldPos Entity -> [Map PlayerPos Tile]`
` drawTilesForPlayer world entityMap =`
`   let`
`+`
`     -- Entity base layer`
`+    entities = mkLayer entityMap`
`+    -- Darkness`
`+    darknessOverlay = darknessFovOverlay (world ^. wdPlayer) (world ^. wdPlayer ^. plActor)`
`+    -- Darkness hides entity`
`+    baseLayer = Map.union darknessOverlay entities`
`+`
`+`
` `
`     -- Actor layer on top`
`     actorMap = Map.fromList \$ (\a -> (a ^. acWorldPos, a ^. acEntity)) <\$> getAllActors world`
`+    inViewActors = Map.filterWithKey inView actorMap`
`+    actorLayer = mkLayer inViewActors`
`+    visibleActorLayer = Map.filterWithKey (\wp _ -> isNotDarkness wp baseLayer) actorLayer`
`+`
`+`
`   in`
`+    -- Layers`
`+    -- 0: Entities (with darkness overlay)`
`+    -- 1: Actors`
`+    [baseLayer, visibleActorLayer]`
`+`
` `
`   where`
`     player = world ^. wdPlayer`
`@@ -292,6 +314,14 @@`
`     -- Bottom right corner`
`     (bottomX, bottomY) = (topX + screenX, topY - screenY) `
` `
`+`
`+    isNotDarkness :: PlayerPos -> Map PlayerPos Tile -> Bool`
`+    isNotDarkness wp ts =`
`+      case Map.lookup wp ts of`
`+        Nothing -> True`
`+        Just t -> t ^. tlId /= E.getTile E.Dark ^. tlId`
`+`
`+  `
`     inView (WorldPos (x, y)) _ =`
`       x >= topX && x < bottomX && y > bottomY && y <= topY`
` `
`@@ -391,14 +418,26 @@`
`                   ([], Nothing) -> True`
`                   _ -> False`
`       in`
`+`
`       if canMove`
`       then`
`+        let`
`+          movedActor = actor & acWorldPos .~ tryWorldTo'`
`+          w2 = updatePlayerViewport \$ updateActor world movedActor`
`+          pa = w2 ^. wdPlayer ^. plActor`
`+        in`
`+          Just \$ updateActor w2 (updateActorFov w2 pa)`
`       else`
`         Nothing`
` `
` `
`+`
`+updateActorFov :: World -> Actor -> Actor`
`+updateActorFov w a =`
`+  a & acFov .~ Just (calcFov (a ^. acFovDistance) (isTransparent \$ w ^. wdMap) (a ^. acWorldPos))`
`+`
`+`
`+`
` -- | Update either the player's actor, or one of the world actors`
` updateActor :: World -> Actor -> World`
` updateActor w actor =`
`@@ -408,6 +447,15 @@`
` `
` `
` `
`+-- | Update all actors, including the player's actor`
`+updateAllActors :: World -> (World -> Actor -> Actor) -> World`
`+updateAllActors w fn =`
`+  let w2 = w & (wdPlayer . plActor) %~ fn w in`
`+  let w3 = w2 & wdActors %~ fmap (fn w2) in`
`+  w3`
`+`
`+`
`+`
` -- | Update the player's view port`
` updatePlayerViewport :: World -> World`
` updatePlayerViewport w =`
`@@ -482,3 +520,97 @@`
`       (x, y)`
` `
` `
`+`
`+-- | Calculate the field of view from a position`
`+calcFov :: Int -> (WorldPos -> Bool) -> WorldPos -> [(WorldPos, [WorldPos])]`
`+calcFov fovDistance isEntityTransparent fromPos'@(WorldPos fromPos) =`
`+  let boundries = getBoundries fromPos' in`
`+  go <\$> boundries`
`+`
`+  where`
`+    getBoundries (WorldPos (x, y)) = boundingPoints fovDistance (WorldPos (x, y))`
`+`
`+    go toPos'@(WorldPos toPos) =`
`+      let line = WorldPos <\$> bline fromPos toPos in`
`+      let isTransparentOrStart p = p == fromPos' || isEntityTransparent p in`
`+      let (m, r) = Lst.span isTransparentOrStart line in`
`+      (toPos', m <> Lst.take 1 r)`
`+`
`+`
`+`
`+`
`+-- | Get the bounds for a fov distance`
`+boundingPoints :: Int -> WorldPos -> [WorldPos]`
`+boundingPoints distance (WorldPos (atx, aty)) =`
`+  Lst.nub \$`
`+    [WorldPos (atx - distance + d, aty - distance) | d <- [0..distance * 2]] <>`
`+    [WorldPos (atx - distance, aty - distance + d) | d <- [0..distance * 2]] <>`
`+    [WorldPos (atx - distance + d, aty + distance) | d <- [0..distance * 2]] <>`
`+    [WorldPos (atx + distance, aty - distance + d) | d <- [0..distance * 2]]`
`+`
`+              `
`+`
`+`
`+-- | Bresenham's algorithm`
`+-- | https://wiki.haskell.org/Bresenham%27s_line_drawing_algorithm`
`+bline :: (Int, Int) -> (Int, Int) -> [(Int, Int)]`
`+bline pa@(xa, ya) pb@(xb, yb) =`
`+  let r = map maySwitch . Lst.unfoldr go \$ (x1, y1, 0) in`
`+`
`+  case r of`
`+    (p:_) | p == pa -> r`
`+    _ -> Lst.reverse r`
`+`
`+  where`
`+    steep = abs (yb - ya) > abs (xb - xa)`
`+    maySwitch = if steep then (\(x,y) -> (y,x)) else identity`
`+    ((x1, y1), (x2, y2)) = case Lst.sort [maySwitch pa, maySwitch pb] of`
`+                             [a, b] -> (a, b)`
`+                             _ -> ((0, 0), (0, 0)) -- This case is never matched, but fixes partial match warning`
`+    deltax = x2 - x1`
`+    deltay = abs (y2 - y1)`
`+    ystep = if y1 < y2 then 1 else -1`
`+    go (xTemp, yTemp, err)`
`+        | xTemp > x2 = Nothing`
`+        | otherwise = Just ((xTemp, yTemp), (xTemp + 1, newY, newError))`
`+        where`
`+          tempError = err + deltay`
`+          (newY, newError) = if (2 * tempError) >= deltax`
`+                            then (yTemp + ystep, tempError - deltax)`
`+                            else (yTemp, tempError)`
`+`
`+`
`+  `
`+`
`+isTransparent :: Map WorldPos Entity -> WorldPos -> Bool`
`+isTransparent wmap pos =`
`+  case Map.lookup pos wmap of`
`+    Nothing -> True`
`+    Just e -> (e ^. enType) /= E.Wall`
`+`
`+`
`+  `
`+`
`+darknessFovOverlay :: Player -> Actor -> Map PlayerPos Tile`
`+darknessFovOverlay player actor =`
`+  let`
`+    (screenWidth, screenHeight) = player ^. plScreenSize`
`+`
`+    -- Create a full grid of darkness`
`+    blackBg = Map.fromList [ (PlayerPos (x, y), E.getTile E.Dark)`
`+                           | x <- [0..(screenWidth - 1)]`
`+                           , y <- [0..(screenHeight - 1)]`
`+                           ] `
`+`
`+    lightAt = worldCoordToPlayer (player ^. plWorldTopLeft) <\$> flatFov (actor ^. acFov)`
`+  in`
`+  -- Remove the darkness overlay at any position that is to be lit`
`+  foldr Map.delete blackBg lightAt`
`+`
`+`
`+  `
`+`
`+flatFov :: Maybe [(WorldPos, [WorldPos])] -> [WorldPos]`
`+flatFov Nothing = []`
`+flatFov (Just fov) = Lst.nub . Lst.concat \$ snd <\$> fov`
`+`