Haskell roguelike - Actors

Posted on April 2, 2018

start prev next

Actors

The game is going to need some other characters to interact with. I’ve called these actors. Lets add two actors a bug and a snake.

We start by adding the entity types

05_actors/src/EntityType.hs (8 to 16)
data EntityType = Blank
                | Door
                | DoorClosed
                | Wall
                | Player
                | Bug
                | Snake
                | Unknown
                deriving (Show, Eq, Ord)

associate them with tiles

05_actors/src/Entities.hs (19 to 26)
  let is = [ (E.Blank     , (41, 13), Nothing)
           , (E.Door      , (26, 15), Just "+")
           , (E.DoorClosed, (21, 15), Just "-")
           , (E.Wall      , ( 9, 14), Just "w")
           , (E.Player    , ( 8,  3), Nothing)
           , (E.Bug       , (25,  3), Nothing)
           , (E.Snake     , (38,  4), Nothing)
           ]

and add the actor types.

The actor has a System.Random.StdGen property acStdGen, this will be used later when random behaviour is required.

05_actors/src/GameCore.hs (21 to 32)
data ActorClass = ClassPlayer
                | ClassEnemy
                deriving (Show, Eq)

newtype Aid = Aid Text deriving (Show, Eq, Ord)

data Actor = Actor { _acId :: !Aid
                   , _acClass :: !ActorClass
                   , _acEntity :: !Entity
                   , _acWorldPos :: !WorldPos
                   , _acStdGen :: !Rnd.StdGen
                   }

Accessing actors

You could make actors a special type of entity, in which case you would not need a separate actor store. The problem is that you often need to work only with actors and scanning the entity map each time is very inefficient. I’ve chosen to keep actors separate in wdActors. This means that a single position in the world could have both and entity and a tile, which makes sense e.g. an actor standing on a floor tile.

05_actors/src/GameCore.hs (44 to 48)
data World = World { _wdPlayer :: !Player
                   , _wdConfig :: !Config
                   , _wdMap :: !(Map WorldPos Entity)
                   , _wdActors :: !(Map Aid Actor)
                   }

Player and/or actor?

Another decision you need to make is if the player is simply another actor or something special. If the player is simply an actor then, as above, each time you need to work with the player you need to scan wdActors. If you have it separate then whenever you need to perform an operation on all actors then you need to work on wdActors and the player. Neither are ideal but I found that have the player have its own separate actor to be much simpler. A helper function that gets the player’s actor and the other actors, removes all the issues.

05_actors/src/GameCore.hs (36 to 40)
data Player = Player { _plConn :: !Host.Connection
                     , _plActor :: !Actor
                     , _plScreenSize :: !(Int, Int)
                     , _plWorldTopLeft :: !WorldPos
                     }
05_actors/src/GameEngine.hs (283 to 285)
getAllActors :: World -> [Actor]
getAllActors world =
  world ^. wdPlayer ^. plActor : Map.elems (world ^. wdActors)

Adding actors

You could add a load text and have the world CSV parse know how to create actors. However I’ve chosen to manually add actors in bootWorld, along with the player’s actor.

05_actors/src/GameEngine.hs (80 to 120)
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)
  in
   
  World { _wdPlayer = mkPlayer
        , _wdConfig = config
        , _wdMap = loadWorld E.loadTexts mapData
        , _wdActors = Map.fromList [ (bug ^. acId, bug)
                                   , (snake ^. acId, snake)
                                   ]
        }
  where
    mkConfig =
      Config { _cfgKeys = Map.fromList [("t", "test")] }

    mkPlayer =
      Player { _plConn = conn
             , _plScreenSize = screenSize
             , _plWorldTopLeft = WorldPos (0, 0)
             , _plActor = mkPlayersActor
             }

    mkPlayersActor =
      Actor { _acId = Aid "player"
            , _acClass = ClassPlayer
            , _acEntity = E.getEntity E.Player
            , _acWorldPos = WorldPos (1, -1)
            , _acStdGen = std
            }

    mkEnemyActor aid e (x, y) =
      Actor { _acId = Aid aid
            , _acClass = ClassEnemy
            , _acEntity = E.getEntity e
            , _acWorldPos = WorldPos (x, y)
            , _acStdGen = std
            }

Draw!

Now to draw the actors

05_actors/src/GameEngine.hs (239 to 278)
drawTilesForPlayer :: World -> Map WorldPos Entity -> Map PlayerPos Tile
drawTilesForPlayer world entityMap =
  let
    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) 

    -- Filter out blank
    noEmptyMap = Map.filter (\e -> e ^. enTile ^. tlName /= "blank") entityMap 

    -- Add the actors to the map.
    -- Notice that this will replace whatever entity was there (for this draw)
    -- This fold works by
    --    - Starting with the map of entities that are not blank
    --    - Inserting each actor into the updated map (the accumulator)
    -- getAllActors is called to get the player's actor and all other actors
    noEmptyMapWithActors = foldr
                           (\actor accum -> Map.insert (actor ^. acWorldPos) (actor ^. acEntity) accum)
                           noEmptyMap
                           (getAllActors world)

    -- Only get the entitys that are at positions on the player's screen
    visibleEntitys = Map.filterWithKey (inView topX topY bottomX bottomY) noEmptyMapWithActors

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

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

Player:

Snake:

Bug:

Door - open:

Door - closed:

Chapters

start prev next

Changes

src/Entities.hs

diff -w -B -a -d -u -b --new-file 04_load_map/src/Entities.hs 05_actors/src/Entities.hs
--- 04_load_map/src/Entities.hs
+++ 05_actors/src/Entities.hs
@@ -16,11 +15,16 @@
 loadTexts :: Map Text Entity
 
 (tiles, entities, loadTexts) =
+
   let is = [ (E.Blank     , (41, 13), Nothing)
            , (E.Door      , (26, 15), Just "+")
            , (E.DoorClosed, (21, 15), Just "-")
            , (E.Wall      , ( 9, 14), Just "w")
+           , (E.Player    , ( 8,  3), Nothing)
+           , (E.Bug       , (25,  3), Nothing)
+           , (E.Snake     , (38,  4), Nothing)
            ]
+
   in
   let mkData (typ, pos@(x, y), l) (tiles', entities', loads') =
         let (entity, tile) = mkEntityAndTile (x * 100 + y) typ pos in

src/EntityType.hs

diff -w -B -a -d -u -b --new-file 04_load_map/src/EntityType.hs 05_actors/src/EntityType.hs
--- 04_load_map/src/EntityType.hs
+++ 05_actors/src/EntityType.hs
@@ -4,9 +4,14 @@
 
 import Protolude
 
+
 data EntityType = Blank
                 | Door
                 | DoorClosed
                 | Wall
+                | Player
+                | Bug
+                | Snake
                 | Unknown
                 deriving (Show, Eq, Ord)
+

src/GameCore.hs

diff -w -B -a -d -u -b --new-file 04_load_map/src/GameCore.hs 05_actors/src/GameCore.hs
--- 04_load_map/src/GameCore.hs
+++ 05_actors/src/GameCore.hs
@@ -11,13 +11,30 @@
 import qualified Data.Text as Txt
 import           Data.Map.Strict (Map)
 import qualified Data.Aeson as Ae
+import qualified System.Random as Rnd
 import           Control.Lens.TH (makeLenses)
 
 import qualified GameHost as Host
 import qualified EntityType as E
 
 
+data ActorClass = ClassPlayer
+                | ClassEnemy
+                deriving (Show, Eq)
+
+newtype Aid = Aid Text deriving (Show, Eq, Ord)
+
+data Actor = Actor { _acId :: !Aid
+                   , _acClass :: !ActorClass
+                   , _acEntity :: !Entity
+                   , _acWorldPos :: !WorldPos
+                   , _acStdGen :: !Rnd.StdGen
+                   }
+
+
+
 data Player = Player { _plConn :: !Host.Connection
+                     , _plActor :: !Actor
                      , _plScreenSize :: !(Int, Int)
                      , _plWorldTopLeft :: !WorldPos
                      }
@@ -24,10 +41,10 @@
 
 
 
-
 data World = World { _wdPlayer :: !Player
                    , _wdConfig :: !Config
                    , _wdMap :: !(Map WorldPos Entity)
+                   , _wdActors :: !(Map Aid Actor)
                    }
 
 
@@ -111,3 +124,4 @@
 makeLenses ''Player
 makeLenses ''Entity
 makeLenses ''Tile
+makeLenses ''Actor

src/GameEngine.hs

diff -w -B -a -d -u -b --new-file 04_load_map/src/GameEngine.hs 05_actors/src/GameEngine.hs
--- 04_load_map/src/GameEngine.hs
+++ 05_actors/src/GameEngine.hs
@@ -14,6 +14,7 @@
 import qualified Data.Aeson.Text.Extended as Ae
 import qualified Data.ByteString.Lazy as BSL
 import qualified Codec.Compression.BZip as Bz
+import qualified System.Random as Rnd
 import           Control.Lens (_1, (^.), (.~), (%~))
 import qualified Control.Arrow as Ar
 import           Control.Concurrent.STM (atomically, readTVar, newTVar, modifyTVar', TVar)
@@ -37,9 +37,9 @@
   case parseCommand initCmd of
     Just ("init", cmdData) -> do
       mapData <- Txt.readFile "worlds/simple.csv"
+      std <- Rnd.getStdGen
       
-      case initialiseConnection conn cmdData mapData of
-
+      case initialiseConnection conn cmdData mapData std of
         Right world -> do
           worldV <- atomically $ newTVar world
           sendConfig conn $ world ^. wdConfig
@@ -66,24 +66,31 @@
         _ -> Nothing
       
 
-
-initialiseConnection :: Host.Connection -> [Text] -> Text -> Either Text World
-initialiseConnection conn cmdData mapData = 
+initialiseConnection :: Host.Connection -> [Text] -> Text -> Rnd.StdGen -> Either Text World
+initialiseConnection conn cmdData mapData std = 
   case parseScreenSize cmdData of
     Nothing ->
       Left "missing / invalid screen size"
 
     Just (width, height) ->
-      Right $ bootWorld conn (width, height) mapData
+      Right $ bootWorld conn (width, height) mapData std
 
 
-bootWorld :: Host.Connection -> (Int, Int) -> Text -> World
-bootWorld conn screenSize mapData = 
-  let config = mkConfig in
+
+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)
+  in
    
   World { _wdPlayer = mkPlayer
         , _wdConfig = config
         , _wdMap = loadWorld E.loadTexts mapData
+        , _wdActors = Map.fromList [ (bug ^. acId, bug)
+                                   , (snake ^. acId, snake)
+                                   ]
         }
   where
     mkConfig =
@@ -93,6 +100,23 @@
       Player { _plConn = conn
              , _plScreenSize = screenSize
              , _plWorldTopLeft = WorldPos (0, 0)
+             , _plActor = mkPlayersActor
+             }
+
+    mkPlayersActor =
+      Actor { _acId = Aid "player"
+            , _acClass = ClassPlayer
+            , _acEntity = E.getEntity E.Player
+            , _acWorldPos = WorldPos (1, -1)
+            , _acStdGen = std
+            }
+
+    mkEnemyActor aid e (x, y) =
+      Actor { _acId = Aid aid
+            , _acClass = ClassEnemy
+            , _acEntity = E.getEntity e
+            , _acWorldPos = WorldPos (x, y)
+            , _acStdGen = std
              }
 
     
@@ -235,8 +253,19 @@
       -- Filter out blank
     noEmptyMap = Map.filter (\e -> e ^. enTile ^. tlName /= "blank") entityMap
 
+    -- Add the actors to the map.
+    -- Notice that this will replace whatever entity was there (for this draw)
+    -- This fold works by
+    --    - Starting with the map of entities that are not blank
+    --    - Inserting each actor into the updated map (the accumulator)
+    -- getAllActors is called to get the player's actor and all other actors
+    noEmptyMapWithActors = foldr
+                           (\actor accum -> Map.insert (actor ^. acWorldPos) (actor ^. acEntity) accum)
+                           noEmptyMap
+                           (getAllActors world)
+
     -- Only get the entitys that are at positions on the player's screen
-    visibleEntitys = Map.filterWithKey (inView topX topY bottomX bottomY) noEmptyMap
+    visibleEntitys = Map.filterWithKey (inView topX topY bottomX bottomY) noEmptyMapWithActors
 
     -- Get the tile for each entity
     tileMap = (^. enTile) <$> visibleEntitys
@@ -248,3 +277,10 @@
     inView topX topY bottomX bottomY (WorldPos (x, y)) _ =
       x >= topX && x < bottomX && y > bottomY && y <= topY
 
+
+
+
+getAllActors :: World -> [Actor]
+getAllActors world =
+  world ^. wdPlayer ^. plActor : Map.elems (world ^. wdActors)
+

Chapters

start prev next