left4deadrl-0.0.1: src/Left4DeadRL.hs
{-# LANGUAGE NoImplicitPrelude #-}
-- A Left4Dead Roguelike
--
-- Andrew Pennebaker
module Main where
import qualified System.Random as Random
import qualified System.Random.Shuffle as Shuffle
import Prelude
import Data.Maybe (fromJust)
import Control.Monad (when, replicateM)
import Data.List (sortBy)
import HsCharm
type Pos = (Int, Int)
data Game = Game {
dungeon :: [[Cell]],
messages :: [String],
rogueLoc :: Pos,
safehouseExitLoc :: Pos,
safehouseEntranceLoc :: Pos
}
sqrtInt :: Int -> Int
sqrtInt = floor . (sqrt :: Double -> Double) . fromIntegral
-- Assumes each row is the same length
width :: Game -> Int
width = length . (!! 0) . dungeon
height :: Game -> Int
height = length . dungeon
data Cell = Cell {
tile :: Tile,
occupant :: Maybe Monster
}
data Tile = Tile {
tileName :: String,
-- ASCII symbol
tileSymbol :: String,
-- From 0.0 (easily traversed) to 1.0 (impassible)
terrain :: Float
}
data Monster = Monster {
monsterName :: String,
-- ASCII symbol
monsterSymbol :: String,
-- Attack points
ap :: Int,
-- Health points
hp :: Int,
-- How far the monster can see
horizon :: Int
}
instance Show Tile where
show = tileSymbol
instance Show Monster where
show = monsterSymbol
instance Show Cell where
show c = case occupant c of
Nothing -> (show . tile) c
Just m -> show m
messageSpace :: Int
messageSpace = 3
voicemail :: Game -> [String]
voicemail = take (messageSpace - 1) . messages
space :: Tile
space = Tile {
tileName = "floor",
tileSymbol = " ",
terrain = 0.0
}
emptyCell :: Cell
emptyCell = Cell {
tile = space,
occupant = Nothing
}
emptySpace :: Cell
emptySpace = emptyCell {
tile = space
}
wall :: Tile
wall = Tile {
tileName = "wall",
tileSymbol = "#",
terrain = 1.0
}
emptyWall :: Cell
emptyWall = emptyCell {
tile = wall
}
safehouseExit :: Tile
safehouseExit = Tile {
tileName = "safehouse exit",
tileSymbol = "]",
terrain = 0.0
}
emptySafehouseExit :: Cell
emptySafehouseExit = emptyCell {
tile = safehouseExit
}
safehouseEntrance :: Tile
safehouseEntrance = Tile {
tileName = "safehouse entrance",
tileSymbol = "[",
terrain = 0.0
}
emptySafehouseEntrance :: Cell
emptySafehouseEntrance = emptyCell {
tile = safehouseEntrance
}
defaultRogue :: Monster
defaultRogue = Monster {
monsterName = "rogue",
monsterSymbol = "@",
ap = 1,
hp = 10,
horizon = 10
}
zombie :: Monster
zombie = Monster {
monsterName = "zombie",
monsterSymbol = "z",
ap = 1,
hp = 1,
horizon = 5
}
withinBounds :: Game -> Pos -> Bool
withinBounds g (x, y) = (x >= 0) && (x <= width g - 1) && (y >= 0) && (y <= height g - 1)
-- Assumes x and y are within bounds.
getCell :: Game -> Pos -> Cell
getCell g (x, y) = (dungeon g !! y) !! x
-- Assumes x and y are within bounds.
putCell :: Game -> Pos -> Cell -> Game
putCell g (x, y) c = g { dungeon = rowsBefore ++ [curRow'] ++ rowsAfter }
where
rows = dungeon g
(rowsBefore, curRow:rowsAfter) = splitAt y rows
(colsBefore, _:colsAfter) = splitAt x curRow
curRow' = colsBefore ++ [c] ++ colsAfter
moveOccupant :: Game -> Pos -> Pos -> Game
moveOccupant g a b = g''
where
aCell = getCell g a
bCell = getCell g b
bCell' = bCell { occupant = occupant aCell }
aCell' = aCell { occupant = Nothing }
g' = putCell g b bCell'
g'' = putCell g' a aCell'
move :: Game -> Key -> IO Game
move g k = do
let (x, y) = rogueLoc g
let (x', y') = case k of
KeyUp -> (x, y - 1)
KeyDown -> (x, y + 1)
KeyRight -> (x + 1, y)
KeyLeft -> (x - 1, y)
_ -> (x, y)
if withinBounds g (x', y')
then do
let b = getCell g (x', y')
case occupant b of
-- Monster in the way
Just _ -> return $ strike g (x, y) (x', y')
-- Nothing in the way
_ -> if passable g (x', y')
then do
-- Move rogue from cell a to cell b.
let g' = moveOccupant g (x, y) (x', y')
return $ g' { rogueLoc = (x', y') }
else do
let t = tile b
return $ g { messages = ("There is a " ++ tileName t ++ " in the way."):voicemail g }
else
return $ g { messages = "Edge of the world.":voicemail g }
blotMessages :: Game -> [String] -> IO ()
blotMessages g ms = blotMessages' ms (height g + messageSpace - 1)
where
blotMessages' :: [String] -> Int -> IO ()
blotMessages' [] _ = return ()
blotMessages' (m:ms') row = do
moveCursor 0 row
hCenterString m
blotMessages' ms' (row - 1)
blotCell :: Game -> Pos -> IO ()
blotCell g (x, y) = do
let c = getCell g (x, y)
moveCursor x y
(blotString . show) c
blotDungeon :: Game -> IO ()
blotDungeon g = do
let rLoc = rogueLoc g
let r = fromJust $ occupant $ getCell g rLoc
let fog = [
(x, y) |
x <- [0 .. width g - 1],
y <- [0 .. height g - 1],
dist rLoc (x, y) < horizon r || safehouseEntranceLoc g == (x, y)
]
mapM_ (blotCell g) fog
return ()
win :: Game -> Bool
win g = safehouseEntranceLoc g == rogueLoc g
lose :: Game -> Bool
lose g = case (occupant . getCell g . rogueLoc) g of
Just r -> ((0 ==) . hp) r
_ -> True
blotRecap :: Game -> String -> IO ()
blotRecap g s = do
clearScreen
vCenterString $ s ++ " Play again (y/n)?"
k <- getKey
case k of
KeyY -> restart
KeyN -> return ()
KeyQ -> return ()
KeyEscape -> return ()
_ -> blotRecap g s
strike :: Game -> Pos -> Pos -> Game
strike g a b = g'
where
aCell = getCell g a
bCell = getCell g b
m1 = occupant aCell
m2 = occupant bCell
g' = case (m1, m2) of
(Just m1', Just m2') -> if (hp m2'' <= 0) && (monsterName m2'' /= "rogue")
then
-- Delete monster.
putCell (g { messages = ("Killed a " ++ monsterName m2' ++ "."):voicemail g }) b (bCell { occupant = Nothing })
else
-- Reinstert monster.
placeMonster (g { messages = ("Hit a " ++ monsterName m2'' ++ "."):voicemail g }) b m2''
where
m2'' = m2' { hp = hp m2' - ap m1' }
-- One of the monsters is no longer there.
_ -> g
passable :: Game -> Pos -> Bool
passable g p = not (occupied c) && (0.0 == terrain (tile c))
where
c = getCell g p
dist :: Pos -> Pos -> Int
dist (x1, y1) (x2, y2) = sqrtInt $ dx * dx + dy * dy
where
dx = x1 - x2
dy = y1 - y2
-- Ignore corner adjacencies
neighbors :: Game -> Pos -> [Pos]
neighbors g (x, y) = filter (withinBounds g) [
(x + 1, y),
(x - 1, y),
(x, y + 1),
(x, y - 1)
]
-- For testing purposes
greedyPath :: Game -> Pos -> Pos -> Maybe [Pos]
greedyPath g s e
| 1 == dist s e = Just [s, e]
| otherwise = Just [s, b', e]
where
b' = case sortBy (\a b -> compare (dist a e) (dist b e)) $ filter (passable g) $ neighbors g s of
[] -> e
(x:_) -> x
-- Let each monster respond.
respond :: Game -> [Pos] -> IO Game
respond g [] = return g
respond g (a:as) = do
let r = rogueLoc g
case occupant $ getCell g a of
Just m -> if dist a r == 1 -- Monster is next to rogue.
then do
let g' = (strike g a r) { messages = ("You were struck by a " ++ monsterName m ++ "."):voicemail g }
respond g' as
else do
stdGen <- Random.getStdGen
let creep = head $ Shuffle.shuffle' zombieCreep (length zombieCreep) stdGen
if creep
then case greedyPath g a r of
Just (_:p:_) -> do
-- Move monster one step along path.
let g' = moveOccupant g a p
respond g' as
-- No path. Monster will sit.
_ -> respond (g { messages = ("A " ++ monsterName m ++ " sat down."):voicemail g }) as
else respond g as
-- Monster is no longer there.
_ -> respond g as
occupied :: Cell -> Bool
occupied c = case occupant c of
Just _ -> True
_ -> False
-- The location of every monster that is not a rogue
monsters :: Game -> [Pos]
monsters g = [
(x, y)
|
x <- [0 .. width g - 1],
y <- [0 .. height g - 1],
occupied $ getCell g (x, y),
(x, y) /= rogueLoc g
]
loop :: Game -> IO ()
loop g = case (win g, lose g) of
(True, _) -> blotRecap g "You made it!"
(_, True) -> blotRecap g "You were overwhelmed."
_ -> do
clearScreen
blotDungeon g
blotMessages g (reverse $ messages g)
k <- getKey
when (k `notElem` [KeyEscape, KeyQ])
(do
g' <- if k `elem` [KeyUp, KeyDown, KeyRight, KeyLeft]
then move g k
else return g
g'' <- respond g' (monsters g')
loop g'')
generateRow :: Int -> IO [Cell]
generateRow w = replicate w . head . Shuffle.shuffle' (emptyWall : replicate 10 emptySpace) 11 <$> Random.getStdGen
generateSafehouseRow :: Int -> IO [Cell]
generateSafehouseRow w = do
cells <- replicate (w - 2) . head . Shuffle.shuffle' (emptyWall:replicate 10 emptySpace) 11 <$> Random.getStdGen
return $ emptySafehouseEntrance:(cells ++ [emptySafehouseExit])
generateDungeon :: Int -> Int -> IO [[Cell]]
generateDungeon w h = do
as <- replicateM ((h - 1) `div` 2) (generateRow w)
b <- generateSafehouseRow w
cs <- replicateM ((h - 1) `div` 2) (generateRow w)
return $ as ++ (b:cs)
commonZombies :: Game -> Int
commonZombies g = sqrtInt $ width g * height g
zombieCreep :: [Bool]
zombieCreep = [True, False, False, False]
placeMonster :: Game -> Pos -> Monster -> Game
placeMonster g (x, y) r = putCell g (x, y) c'
where
c = getCell g (x, y)
c' = c { occupant = Just r }
placeMonsters :: Game -> [Monster] -> IO Game
placeMonsters g [] = return g
placeMonsters g (m:ms) = do
x <- Random.getStdRandom $ Random.randomR (0, width g - 1)
y <- Random.getStdRandom $ Random.randomR (0, height g - 1)
-- If cell is occupied or impassible, reroll.
if not (passable g (x, y))
then placeMonsters g (m:ms)
else do
let g' = placeMonster g (x, y) m
placeMonsters g' ms
newGame :: IO Game
newGame = do
w <- getWidth
h <- getHeight
-- Reserve space for messages
let (w', h') = (w, h - messageSpace)
let exitLoc = (w' - 1, h' `div` 2)
let rLoc = exitLoc
let entranceLoc = (0, h' `div` 2)
d <- generateDungeon w' h'
let g = Game {
dungeon = d,
messages = ["Get to the safehouse on the other side."],
rogueLoc = rLoc,
safehouseExitLoc = exitLoc,
safehouseEntranceLoc = entranceLoc
}
let g' = placeMonster g rLoc defaultRogue
placeMonsters g' (replicate (commonZombies g) zombie)
restart :: IO ()
restart = newGame >>= loop
main :: IO ()
main = do
startCharm
restart
endCharm