Haskell roguelike - Actors
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.
- ActorClass is the type of actor. This allows us to treat different actors the same way based on a class
- Aid is a unique identifier per actor
- Actor the main actor type
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
- The change here is getting noEmptyMapWithActors and then getting visibleEntities from that (see the patch below).
- The actors’ entities, including the player’s actors’ entity, are added to (a copy) of the world map
- Notice that the player replace the entity at that position, this will be addressed in later chapters
Player:
Snake:
Bug:
Door - open:
Door - closed:
Chapters
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)
+