Combat + GUI

This Week's Music

Reddit

Last week was supposed to include the "intro to monsters" stuff, but I skipped on that. So we'll set an intro to monsters, have the monsters be able to fight back, and maybe throw some GUI around as well.

This is also the point where we have to start picking more things for ourselves. Game design is somewhat an open question, though we've got a few lights in the dark to guide us. If we were making an imperative game we could go read a book like Game Programming Patterns. Since we're doing this in a more functional style we have to strike out on our own most of the time.

Monsters

First we want a way to represent monsters. We'll start with a few easy types, and we'll say that the player is also a monster so that we can make combat run easy regardless of who attacks who.

module Creature

data Species = Global    -- ^ Has long range sight
             | Player    -- ^ the player's species
             | Rustacian -- ^ So fast they can attack twice 10% of turns
             | Undefined -- ^ Hurts to touch him.
             deriving (Eq, Read, Show)

data Creature = Creature {
    species :: Species,
    curHP :: Int,
    maxHP :: Int,
    creatLocation :: (Int,Int)
    } deriving (Eq, Ord, Read, Show)

Then we add a field with the Dungeon type to store creatures.

data Dungeon = Dungeon {
    dungeonWidth :: Int,
    dungeonHeight :: Int,
    dungeonTiles :: Vector Terrain,
    dungeonCreatures :: [Creature]
    } deriving (Read, Show)

We'll also need to have the dungeon generators provide monsters. For now they'll provide an empty list.

Now we just need to update Main.hs to use this new stuff. The highlight of which is this

bumpPlayer :: Direction -> GameState -> GameState
bumpPlayer dir game = let
    theDungeon = dungeon game
    theCreatures = dungeonCreatures theDungeon
    oldPlayer = head theCreatures
    (px,py) = creatLocation oldPlayer
    targetx = px + case dir of
        East -> 1
        West -> -1
        _ -> 0
    targety = py + case dir of
        North -> 1
        South -> -1
        _ -> 0
    in case getTerrainAt (targetx,targety) (dungeon game) of
        Wall -> game
        _ -> let
            newPlayer = oldPlayer { creatLocation = (targetx,targety) }
            newCreatures = newPlayer : (drop 1 theCreatures)
            newDungeon = theDungeon { dungeonCreatures = newCreatures }
            in game {
                dungeon = newDungeon,
                visible = (fov (\p -> getTerrainAt p (dungeon game) == Wall) 100 (targetx,targety)) }

Welp... that's... not looking good. We're unpacking all the way down by hand, then updating just one tiny little spot, and packing it all back up by hand. Does it work? Yes, it works. commit

But is it good code? Does it help us walk the Golden Path? No.

We could try to clean it up a little bit by making the shifting into its own little function:

shiftDir :: Direction -> (Int, Int) -> (Int, Int)
shiftDir dir (x,y) = case dir of
    North -> (x,y+1)
    South -> (x,y-1)
    East -> (x+1,y)
    West -> (x-1,y)

But that doesn't address the heart of the problem: nested data structures are not pleasant in Haskell without some assistance. In the past we've had assistance like map and fold and mapM_. This time we're going for a new kind of assistance that's not just one function, but a whole family of capability called lenses.

Lenses: ASCII Vomit Magic

Lenses are a subject that is simple at the surface but that goes quite deep. You can look on youtube, either for an hour long talk about the theory behind them, or shorter videos about just how to use them. You can find blog posts about it too if that's your speed.

The simple version is that a lens encapsulates the concept of the path from the outside of a structure to a data somewhere within the structure. Except that since the data we get to is often itself another sub-structure, we can use another lens to go deeper from there. Down until we get where we want. This lets us have getters and setters, but we can also do some weird things that traditional getters and setters won't do, like traversals and zooms. Check the blog post or watch the demos in the video if you want to see that sort of stuff right away, because for now we'll be doing the simpler stuff.

How do we unlock these powers? Well a lens is actually just a weird sort of function, not some crazy framework you have to buy in to. However, the lens library does provide a little magic to get us setup quickly. We'll be using TemplateHaskell again, and we'll let some templates from the lens library build our lens functions for us. By calling a template thingy called makeLenses we can have the compiler auto-generate the lenses for a record. By convention, it makes a lens for each field starting with an underscore, and the name of the lense is the field's name without the underscore. Let's start with Creature.hs, since it's smallest right now.

{-# LANGUAGE Trustworthy, TemplateHaskell #-}

module Creature where

-- lens
import Control.Lens

data Species = Global    -- ^ Has long range sight
             | Player    -- ^ the player's species
             | Rustacian -- ^ So fast they can attack twice 10% of turns
             | Undefined -- ^ Hurts to touch him.
             deriving (Eq, Ord, Read, Show)

data Creature = Creature {
    _cSpecies :: Species,
    _cHP :: Int,
    _cMaxHP :: Int,
    _cLocation :: (Int,Int)
    } deriving (Eq, Ord, Read, Show)

makeLenses ''Creature

So what does this let us do? We will need a value to operate on.

RL-tut> let bill = Creature Player 12 12 (5,5)
RL-tut> bill
Creature {_cSpecies = Player, _cHP = 12, _cMaxHP = 12, _cLocation = (5,5)}

we can "get" a field and it kinda reads "forward" like in C/Python/Java/etc

RL-tut> bill ^. cSpecies
Player

We can "set" a field, and get the new version back

RL-tut> bill & cHP .~ 5
Creature {_cSpecies = Player, _cHP = 5, _cMaxHP = 12, _cLocation = (5,5)}

The type of (.~) is kinda gnarly at first.

RL-tut> :t (.~)
(.~) :: ASetter s t a b -> b -> s -> t

Let's give it a setter and a value to set.

RL-tut> :t cHP .~ 5
cHP .~ 5 :: Creature -> Creature

The (&) is a flipped ($)

RL-tut> :t (&)
(&) :: a -> (a -> b) -> b

So now it's kinda obvious why we get a Creature back when we do all that

RL-tut> :t bill & cHP .~ 5
bill & cHP .~ 5 :: Creature

as promised, we can chain our lensing by composing a lens from our module with the _1 lens, which targets the first field of a tuple. We compose lenses with the normal function composition operator: (.)

RL-tut> bill & cLocation . _1 .~ 10
Creature {_cSpecies = Player, _cHP = 12, _cMaxHP = 12, _cLocation = (10,5)}

We can also "modify", which produces a new value based on the old value instead of overwriting the old value without looking at it. To modify we use (%~)

RL-tut> bill & cLocation . _2 %~ (*4)
Creature {_cSpecies = Player, _cHP = 12, _cMaxHP = 12, _cLocation = (5,20)}

And now you know why this section is called "ASCII Vomit Magic". Note that even the set and modify operations never affected the bill value, they gave back new Creature values, the same as x + 4 makes a new value without overwriting the old. All of the immutable rules from before are still in effect, we just have a new way to express ourselves.

Let's move on to Dungeon.hs and give it the same lens update.

-- | A single floor of the complete dungeon complex.
data Dungeon = Dungeon {
    _dWidth :: Int,
    _dHeight :: Int,
    _dTiles :: Vector Terrain,
    _dCreatures :: [Creature] -- ^ by convention, the player must always be first in the list.
    } deriving (Read, Show)

makeLenses ''Dungeon

Other than the fields being new names, not much else has to change here. We're not really doing much nesting or updating in the Dungeon module yet, so using the normal fields is still fine. One catch: since Dungeon limits what it exports, we have to remember to add all the lenses to the export list.

Time to move on to Main.hs. Since we're giving the GameState a fresh coat of paint we'll name better field names and put some haddock comments on them.

-- | Holds the entire state of the program in a single big blob.
data GameState = GameState {
    _generator :: PCGen, -- ^ The RNG for the game
    _world :: Dungeon, -- ^ Just one Dungeon for now, but will be more later
    _playerVision :: Set (Int,Int) -- ^ Cached because it changes relatively rarely compared to refreshes
    } deriving (Read, Show)

makeLenses ''GameState

mkPlayer :: (Int,Int) -> Creature
mkPlayer loc = Creature Player 12 12 loc

Now let's try mkGameState. This time we'll add the player into the creatures list of the dungeon using a lens based modification instead of a field based update.

-- | Constructs a GameState with the player at 5,5
mkGameState :: PCGen -> Int -> Int -> GameState
mkGameState gen xMax yMax = let
    loc = (5,5)
    (d,g) = rogueDungeon xMax yMax gen
    d' = d & dCreatures %~ (mkPlayer loc :)
    in GameState g d' $ fov (\p -> getTerrainAt p d' == Wall) 100 loc

Well, using (:) with a slice looks kinda... weird at first. As with the other operator slices we've been using like (+2) and (*4), When you write (OP val) it's the same as (\x -> x OP val), and similarly (val OP) is the same as (\x -> val OP x). Really though we can say that our code just wants to be happy. Let's update the game with lenses too.

-- | Converts a key press into the appropriate game state update.
gameUpdate :: Key -> GameState -> GameState
gameUpdate key = case key of
    Key'Up -> bumpPlayer North
    Key'Down -> bumpPlayer South
    Key'Left -> bumpPlayer West
    Key'Right -> bumpPlayer East
    Key'R -> \gameState -> let
        gen = _generator gameState
        (xMax,yMax) = both view (world.dWidth, world.dHeight) gameState
        in mkGameState gen xMax yMax
    _ -> id

both is a lens modifier that lets you do a thing with a tuple of lenses and get a tuple of results back. view just gets the value at the target point. Now for the trouble that prompted this whole lens deal:

bumpPlayer :: Direction -> GameState -> GameState
bumpPlayer dir game = let
    pLocPath = world . dCreatures . _head . cLocation
    plLoc = game ^?! pLocPath
    targLoc = shiftDir dir plLoc
    in case getTerrainAt targLoc (_world game) of
        Wall -> game
        _ -> let
            newGame = set pLocPath targLoc game
            in newGame { _playerVision =
                fov (\p -> getTerrainAt p (_world game) == Wall) 100 targLoc }

Well that is much nicer. There's a catch. When pLocPath is bound but no accepting an argument, GHC tries to pick a specific typeclass instance to use with it. To keep it general without having to repeat ourselves, we can make pLocPath take an argument, or we can give it the general form type signature, or we can turn on a GHC flag called NoMonomorphismRestriction to stop GHC from picking the instance early.

And now our program compiles and does almost exactly what it did before. Just more concisely. Maybe in a more obscure way as well. For example, we're now using (^?!) instead of (^.), but why? Well, because the _head part of the lens path can fail, so we need to use (^?) instead of (^.), except that we know it won't ever fail because there's always at least 1 creature in the dungeon (the player), so we can (unsafely) force the access with the (^?!) variant instead.

Is all this new lens stuff worth it? It's a lot to learn, huge piles if you really get deep into it. I was chatting on the IRC channel as I worked on this, and edwardk had this to add:

The main thing the lens stuff gives you is a more consistent format for writing code that works with maps and records and json bibs and bobs, etc. For any one use-case you may not get much mileage out of it --Edward K, the guy who gave that hour long talk I linked

In other words, it's just another new world of code reuse for us to explore. Gotta Catch 'em All.

commit

Adding Some Monsters

Now that the player is a Creature within the Dungeon, we can add some more Creature values too. Since we already have the concept of rooms during dungeon generation, let's put one creature in each room. First we want a way to make a random monster at all. Since dungeon generation happens in a MonadRandom, we'll keep using MonadRandom. I guess this goes in the Creature module. But... it doesn't know how to pick an available location during the dungeon process. Well, we'll just have to pass that in.

-- | Randomly makes an enemy value at the location specified.
randEnemy :: MonadRandom m => (Int,Int) -> m Creature
randEnemy loc = do
    s <- uniform [Global,Rustacian,Undefined]
    pure $ Creature s 10 10 loc

For now, all monsters have 10hp I guess. Game balance isn't quite the point at the moment. Now we add a way to pick a random location within a room.

-- | Given a room, selects a random point within it.
randWithinRoom :: (MonadRandom m) => Room -> m (Int,Int)
randWithinRoom (Room xlow ylow xhigh yhigh) = do
    x <- getRandomR (xlow,xhigh)
    y <- getRandomR (ylow,yhigh)
    pure (x,y)

Now, of course, we use mapM to do our heavy lifting within the rogue dungeon generation.

        -- draw the rooms
        mapM_ (drawRoom width vec) rooms
        -- place monsters
        creatureList <- mapM randCreatureInRoom rooms

--

randCreatureInRoom :: MonadRandom m => Room -> m Creature
randCreatureInRoom room = do
    loc <- randWithinRoom room
    randEnemy loc

And a way to draw all these fancy new creatures should be put in its own little part of the Main module.

toTileID :: Creature -> Word8
toTileID c = case c ^. cSpecies of
    Global -> ord 'g'
    Player -> ord '@'
    Rustacian -> ord 'r'
    Undefined -> ord 'u'

-- | Selects the color for the creature.
toForeground :: Creature -> V4 Float
toForeground c = case c ^. cSpecies of
    Global -> V4 0 0.75 0 1 -- green
    Player -> V4 1 1 1 1 -- white
    Rustacian -> V4 1 0 0 1 -- red
    Undefined -> V4 1 1 1 .5 -- ĉu griza?

-- | At the moment a creature always has a black backround, this is used for
-- uniformity with the tileID and foreground picking. In the future there might
-- be something fancier that picks a background color (like based on damage
-- taken or something).
toBackround :: Creature -> V3 Float
toBackround c = V3 0 0 0

And we update our draw code to use this

        -- draw to the screen
        gameState <- liftIO $ readIORef gameRef
        (rows,cols) <- getRowColCount
        let openID = fromIntegral $ ord '.'
            openBG = V3 0 0 0
            openFG = V4 1 1 1 1
            wallID = 5
            wallBG = V3 0.39 0.15 0.04
            wallFG = V4 0.4 0.4 0.4 1
            hiddenWallID = fromIntegral $ ord '*'
            hiddenOpenID = fromIntegral $ ord ' '
            d = _world gameState
            cellCount = rows*cols
            updateList = map (\cellIndex -> let
                (r,c) = cellIndex `divMod` cols
                x = c
                y = rows - (r+1)
                visibleTiles = _playerVision gameState
                in if S.member (x,y) visibleTiles
                    then case getCreatureAt (x,y) d of
                        Nothing -> case getTerrainAt (x,y) d of
                            Open -> (openID, openBG, openFG)
                            Wall -> (wallID, wallBG, wallFG)
                        Just c -> (toTileID c, toBackround c, toForeground c)
                    else case getTerrainAt (x,y) d of
                        Open -> (hiddenWallID, openBG, openFG)
                        Wall -> (hiddenOpenID, openBG, openFG)) [0 .. cellCount -1]

Now we have creatures. commit

Creatures Screenshot

Fighting

"Murder Simulator Tycoon"

Right now the player can just step over and through monsters. That's no good, so let's prevent that.

bumpPlayer :: Direction -> GameState -> GameState
bumpPlayer dir game = let
    pLocPath = world . dCreatures . _head . cLocation
    plLoc = game ^?! pLocPath
    targLoc = shiftDir dir plLoc
    in case getCreatureAt targLoc (_world game) of
        Just c -> game -- TODO: Player attacking code here
        Nothing -> case getTerrainAt targLoc (_world game) of
            Wall -> game
            _ -> let
                newGame = set pLocPath targLoc game
                in newGame { _playerVision =
                    fov (\p -> getTerrainAt p (_world game) == Wall) 100 targLoc }

And now we're to the point where we have to think about what the combat mechanics for our game are. Well, since things will start taking damage, let's adjust the drawing to show that. An easy way to let the player know that they're having an effect is to have the creature's background transition from black to red as the creature takes damage. We'll also change the Rustacian to be orange, since Ferris the Crab is an orangeish color anyway, and that way their letter won't blend into their background so much if it's a strong red.

toForeground :: Creature -> V4 Float
toForeground c = case c ^. cSpecies of
    Global -> V4 0 0.75 0 1 -- green
    Player -> V4 1 1 1 1 -- white
    Rustacian -> V4 1 0.5 0 1 -- orange
    Undefined -> V4 1 1 1 0.5 -- ĉu griza?

toBackround :: Creature -> V3 Float
toBackround c = let
    damagePercent = 1 - (fromIntegral $ _cHP c) / (fromIntegral $ _cMaxHP c)
    in V3 damagePercent 0 0

For fighting, we'll say that enemies have 6hp, and the player does 1d6 damage to them on a hit. The player hits with a 5+ on a 1d20, and others hit with a 10+.

doMelee :: MonadRandom m => Creature -> Creature -> m (Creature, Creature)
doMelee attacker defender = do
    let damageRange = case _cSpecies attacker of
            Player -> (1,6)
            _ -> (1,1)
        targetNumber = case _cSpecies attacker of
            Player -> 5
            _ -> 10::Int
    attackRoll <- getRandomR (1,20)
    if attackRoll >= targetNumber
        then do
            damageRoll <- getRandomR damageRange
            let blowback = if _cSpecies defender == Undefined then 1 else 0
            pure (
                attacker & cHP %~ (subtract blowback),
                defender & cHP %~ (subtract damageRoll))
        else pure (attacker, defender)

We have to put a type on the value we compare to attackRoll because otherwise things are a little too general and GHC has trouble figuring it out.

Now, updating a creature using lenses is kinda hard at the moment because we're using a junky list, which is good for filtering and mapping but not for in place updates. So we'll just write the function for it.

-- | Updates the creature at the location specified with the function specified.
updateCreatureAt :: (Int,Int) -> (Creature->Creature) -> Dungeon -> Dungeon
updateCreatureAt loc cf d = d {
    _dCreatures = map (\c -> if _cLocation c == loc then cf c else c) (_dCreatures d)}

Now we make that TODO into the combat code:

        Just c -> let
            attacker = game ^?! world . dCreatures . _head
            defender = c
            ((nAtk,nDef),nGen) = runRand (doMelee attacker defender) (_generator game)
            atkDungeon = updateCreatureAt plLoc (const nAtk) (_world game)
            defDungeon = updateCreatureAt targLoc (const nDef) atkDungeon
            liveDungeon = defDungeon & dCreatures %~ (filter isAlive)
            in GameState nGen liveDungeon (_playerVision game)

We run the combat between the attacker and defender, then we update the dungeon to use the new creatures. The attacker might get modified too, so we have to do both. I made sure to include the idea that the attacker might be adjusted during the melee process just to make sure that we didn't get a design where that'd be hard to add later on accident. After that's done we filter away any dead creatures so that they won't show up any more. Then the turn is done and we return.

Great! And now we can attack monsters and see them taking damage. commit

Except that if the player does die, the game keeps going with the player controlling the next creature in the list. Whoops. We don't really have a start or end screen for things like that. Maybe later.

Creatures That Fight Back

Right now only the player takes turns. Obviously the monsters should do things too.

All we have to do is take the code that lets the player move and adjust it around a little so that the monsters take turns too. Ugh that's gonna take forever! Actually no, it took like 2 minutes.

-- | Attempts to bump the player one step in the direction given. This doesn't
-- do combat since we don't have other creatures, but this should do combat code
-- in the future if there's a creature in the way.
bumpPlayer :: Direction -> GameState -> GameState
bumpPlayer dir game = let
    pLocPath = world . dCreatures . _head . cLocation
    plLoc = game ^?! pLocPath
    targLoc = shiftDir dir plLoc
    in case getCreatureAt targLoc (_world game) of
        Just c -> processAttack plLoc targLoc game
        Nothing -> case getTerrainAt targLoc (_world game) of
            Wall -> game
            _ -> let
                newGame = set pLocPath targLoc game
                in newGame { _playerVision =
                    fov (\p -> getTerrainAt p (_world game) == Wall) 100 targLoc }

-- | Processes an attack from the attacker position to the defender position,
-- given the gamestate. If either position lacks a creature then no change is
-- made to the game state.
processAttack :: (Int,Int) -> (Int,Int) -> GameState -> GameState
processAttack atkLoc defLoc game = let
    attackerMay = getCreatureAt atkLoc (_world game)
    defenderMay = getCreatureAt defLoc (_world game)
    in case (attackerMay,defenderMay) of
        (Just attacker, Just defender) -> let
            ((nAtk,nDef),nGen) = runRand (doMelee attacker defender) (_generator game)
            atkDungeon = updateCreatureAt atkLoc (const nAtk) (_world game)
            defDungeon = updateCreatureAt defLoc (const nDef) atkDungeon
            liveDungeon = defDungeon & dCreatures %~ (filter isAlive)
            in GameState nGen liveDungeon (_playerVision game)
        _ -> game

It's probably not the most efficient possible code that could be written, but I don't think too many melee attacks will be happening per turn on average, so it's probably okay.

But wait, now we need to have the monsters do something. We can't always use the result of bumpPlayer once it returns, because sometimes the player didn't actually take a turn. So we'll have to resolve that within the function.

bumpPlayer :: Direction -> GameState -> GameState
bumpPlayer dir game = let
    pLocPath = world . dCreatures . _head . cLocation
    plLoc = game ^?! pLocPath
    targLoc = shiftDir dir plLoc
    maybeTurnTakenGame = case getCreatureAt targLoc (_world game) of
        Just c -> Just $ processAttack plLoc targLoc game
        Nothing -> case getTerrainAt targLoc (_world game) of
            Wall -> Nothing
            _ -> let
                newGame = set pLocPath targLoc game
                in Just $ newGame { _playerVision =
                    fov (\p -> getTerrainAt p (_world game) == Wall) 100 targLoc }
    in maybe game runEnemies maybeTurnTakenGame

runEnemies :: GameState -> GameState
runEnemies game = game

Sweet, how do we run the enemies? Well, right now we'll just have them be pretty basic. If a Rustacian or Global sees the player, they'll move and/or attack. An Undefined will move in a random direction each turn, which might bump the player, but probably not most of the time.

So first we need the list of things that will get a chance to take their turn after the player does. The player is always the first creature, so we'll grab all the creatures after the player in the list.

runEnemies :: GameState -> GameState
runEnemies game = let
    enemies = drop 1 (game ^. world . dCreatures)
    in game

Once again we're taking a list of inputs plus a starting value and then using the inputs one at a time to update our starting value over and over. This sure sounds like a fold situation to me. Like with the SightLine stuff, remember? But since our creature updates will want to be MonadRandom so that we can have them pick random directions and stuff, we'll use foldM from Control.Monad this time.

foldM :: (Monad m, Foldable t) => (b -> a -> m b) -> b -> t a -> m b

and we use it like this:

runEnemies :: GameState -> GameState
runEnemies game = let
    enemies = drop 1 (game ^. world . dCreatures) -- all NPCs
    dungeon = _world game
    generator = _generator game
    (nDungeon,nGen) = runRand (foldM runEnemy dungeon enemies) generator
    in GameState nGen nDungeon (_playerVision game)

runEnemy :: MonadRandom m => Dungeon -> Creature -> m Dungeon
runEnemy game actor = pure game

Now we check what species we have

runEnemy :: MonadRandom m => Dungeon -> Creature -> m Dungeon
runEnemy game actor = case _cSpecies actor of
    Player -> pure game -- this should never happen, we hope.
    Global -> pure game
    Rustacian -> pure game
    Undefined -> pure game

Seems like the Undefined is easiest to do first.

    Undefined -> do
        moveCheck <- random
        when moveCheck $ do
            dir <- random
            bumpMonster (_cLocation actor) dir dungeon

Now we need to have bumpMonster, but we'll get to that once we have the rules for the other monsters in place. Next is the Rustacian.

    Rustacian -> do
        let mySight = fov (\p -> getTerrainAt p dungeon == Wall) 3 (_cLocation actor)
            plLoc = dungeon ^?! dCreatures . _head . cLocation
        if (S.member plLoc mySight)
            then do
                let myLoc = (_cLocation actor)
                bumpMonster myLoc (directionFromTo myLoc plLoc) dungeon
            else do
                moveCheck <- random
                when moveCheck $ do
                    dir <- random
                    nextDungeon <- bumpMonster (_cLocation actor) dir dungeon
                    fast <- randomR (1,10)
                    if (fast == 10)
                        then runEnemy nextDungeon actor ---????

Do you see the problem? Our rules are that the creature should be able to go again 10% of the time, but we can't just make a recursive call because the creature we'd pass down to the recursion wouldn't have the same location as we just bumped to, and we'll use our location to check if we can see the player. Instead, we'll need to be able to identify creatures by something that won't change about them and that's unique to them. For now we'll just stick an identifier number on each one.

newtype CreatureID = CreatureID Int deriving Eq

...
    _cSpecies :: Species,
    _cID :: CreatureID,
    _cHP :: Int,
...

randEnemy :: MonadRandom m => (Int,Int) -> m Creature
randEnemy loc = do
    s <- uniform [Global,Rustacian,Undefined]
    -- Note: It's possible that two creatures will end up with the same
    -- identifier randomly, but gosh I don't even care about the odds of that in
    -- a small game like this because that's super unlikely.
    ident <- random
    pure $ Creature s (CreatureID ident) 10 10 loc

Okay so now we can go back and write the whole enemy code thing. When it's all said and done, it looks like this:

runEnemy :: MonadRandom m => Dungeon -> CreatureID -> m Dungeon
runEnemy dungeon myID = case getCreatureByID myID dungeon of
    (Just actor) | isAlive actor -> case _cSpecies actor of
        Player -> pure dungeon -- this should never happen, we hope.
        Global -> do
            let myLoc = (_cLocation actor)
                mySight = fov (\p -> getTerrainAt p dungeon == Wall) 300 myLoc
                plLoc = dungeon ^?! dCreatures . _head . cLocation
            if (S.member plLoc mySight)
                then bumpMonster myLoc (directionFromTo myLoc plLoc) dungeon
                else do
                    moveCheck <- getRandom
                    if moveCheck
                        then do
                            dir <- getRandom
                            bumpMonster (_cLocation actor) dir dungeon
                        else pure dungeon
        Rustacian -> do
            let myLoc = (_cLocation actor)
                mySight = fov (\p -> getTerrainAt p dungeon == Wall) 3 myLoc
                plLoc = dungeon ^?! dCreatures . _head . cLocation
            nextDungeon <- if (S.member plLoc mySight)
                then bumpMonster myLoc (directionFromTo myLoc plLoc) dungeon
                else do
                    moveCheck <- getRandom
                    if moveCheck
                        then do
                            dir <- getRandom
                            bumpMonster (_cLocation actor) dir dungeon
                        else pure dungeon
            fast <- getRandomR (1,10)
            if (fast == (10::Int))
                then runEnemy nextDungeon myID
                else pure nextDungeon
        Undefined -> do
            moveCheck <- getRandom
            if moveCheck
                then do
                    dir <- getRandom
                    bumpMonster (_cLocation actor) dir dungeon
                else pure dungeon
    _ -> pure dungeon

Lots of duplication, but it works. The funny thing with the | character in the case expression is a "guard". For that case to be selected it has to have a pattern match and then also pass the boolean test. That way if a monster dies because of another monster the dead one doesn't get a turn while they're dead. We could clean this code up more later on if we had to, but let's move on to how we'll bump the monsters around.

bumpMonster :: MonadRandom m => (Int,Int) -> Direction -> Dungeon -> m Dungeon
bumpMonster loc dir dun = let
    targLoc = (shiftDir dir loc)
    in case getCreatureAt targLoc dun of
        -- no one in the way, we just adjust the creature's location
        Nothing -> pure $ updateCreatureAt loc (\c -> c{_cLocation=targLoc}) dun
        -- someone is in our way!
        Just enemy -> case _cSpecies enemy of
            -- and it's the player!
            Player -> do
                pure dun -- OH CRAP WRONG TYPE
            _ -> pure dun

Do you see the problem? We want to call processAttack, but it's expecting an entire GameState. We've only got a dungeon. I think, in this case, processAttack is the offender. It should be using MonadRandom too, and having the caller deal with merging the dungeon and the generator back up. So let's redo that and finish off bumpMonster. While we're at it, we should probably make bumpMonster respect walls.

bumpMonster :: MonadRandom m => (Int,Int) -> Direction -> Dungeon -> m Dungeon
bumpMonster loc dir dun = let
    targLoc = (shiftDir dir loc)
    in case getTerrainAt targLoc dun of
        -- a wall is in the way :(
        Wall -> pure dun
        _ -> case getCreatureAt targLoc dun of
            -- no one and nothing in the way, we just adjust the creature's location
            Nothing -> pure $ updateCreatureAt loc (\c -> c{_cLocation=targLoc}) dun
            -- someone is in our way!
            Just enemy -> case _cSpecies enemy of
                -- and it's the player!
                Player -> processAttack loc targLoc dun
                _ -> pure dun

And now the monsters move and attack the player. commit

GUI

I'm not a user interface expert, but we should probably at least show the player how many hit points they have. We just have to adjust our draw code.

            player = d ^?! dCreatures . _head
            infoString = "HP: " ++ (show $ _cHP player) ++ "/ " ++ (show $ _cMaxHP player) ++ repeat ' '
            updateList = map (\cellIndex -> let
                (r,c) = cellIndex `divMod` cols
                x = c
                y = rows - (r+1)
                visibleTiles = _playerVision gameState
                in if y == 0
                    then ((fromIntegral.ord) (infoString !! x), openBG, openFG)
                    else if S.member (x,y) visibleTiles
                        then case getCreatureAt (x,y) d of
                            Nothing -> case getTerrainAt (x,y) d of
                                Open -> (openID, openBG, openFG)
                                Wall -> (wallID, wallBG, wallFG)
                            Just c -> (toTileID c, toBackround c, toForeground c)
                        else case getTerrainAt (x,y) d of
                            Open -> (hiddenWallID, openBG, openFG)
                            Wall -> (hiddenOpenID, openBG, openFG)) [0 .. cellCount -1]

Annnnd, bam. We're done. That's actually the whole section. final commit

results matching ""

    No results matching ""