packages feed

hscharm-0.0.1: src/RL.hs

-- Roguelike
module Main where

import qualified System.Random as Random
import qualified System.Random.Shuffle as Shuffle

import HsCharm

import Control.Monad (when, replicateM)
import Data.List (find, delete, intercalate)

data Game = Game {
  width :: Int,
  height :: Int,
  messages :: [String],
  level :: [[Monster]],
  rogue :: Monster,
  monsters :: [Monster]
  }

defaultGame :: Game
defaultGame = Game {
  width = 80,
  height = 24 - messageSpace,
  messages = [],
  level = replicate (24 - messageSpace) (replicate 80 defaultFloor),
  rogue = defaultRogue,
  monsters = []
  }

voicemail :: Game -> [String]
voicemail = take 2 . messages

data Monster = Monster {
  symbol :: String,
  loc :: (Int, Int),
  impassible :: Bool,
  hp :: Int
  } deriving (Eq)

instance Show Monster where
  show = symbol

defaultFloor :: Monster
defaultFloor = Monster {
  symbol = " ",
  loc = (0, 0),
  impassible = False,
  hp = 0
  }

defaultWall :: Monster
defaultWall = Monster {
  symbol = "#",
  loc = (0, 0),
  impassible = True,
  hp = 0
  }

defaultRogue :: Monster
defaultRogue = Monster {
  symbol = "@",
  loc = (0, 0),
  impassible = True,
  hp = 10
  }

defaultZombie :: Monster
defaultZombie = Monster {
  symbol = "z",
  loc = (0, 0),
  impassible = True,
  hp = 1
  }

cellAt :: Game -> (Int, Int) -> Monster
cellAt g (x, y) = (level g !! y) !! x

thingAt :: Game -> (Int, Int) -> Monster
thingAt g p = case find ((p ==) . loc) (monsters g) of
  Just m -> m
  _ -> cellAt g p

attack :: Game -> Monster -> IO Game
attack g m = case symbol m of
  "#" -> return g { messages = "Immobile wall.":voicemail g }
  "z" -> do
    let m' = m { hp = hp m - 1 }
    let ms = delete m (monsters g)
    return g {
      monsters = if hp m' == 0 then ms else m':ms,
      messages = "You hit a zombie.":voicemail g
      }
  _ -> return g

move :: Game -> Key -> IO Game
move g KeyUp
  | y == 0 = return $ g { messages = "Edge of the world.":voicemail g }
  | impassible c = attack g c
  | otherwise = return $ g {
    rogue = r { loc = (x, y - 1) },
    messages = "You moved up!":voicemail g
    }
  where
    r = rogue g
    (x, y) = loc r
    c = thingAt g (x, y - 1)

move g KeyDown
  | y == height g - 1 = return $ g { messages = "Edge of the world.":voicemail g }
  | impassible c = attack g c
  | otherwise = return $ g {
    rogue = r { loc = (x, y + 1) },
    messages = "You moved down!":voicemail g
    }
  where
    r = rogue g
    (x, y) = loc r
    c = thingAt g (x, y + 1)

move g KeyRight
  | x == width g - 1 = return $ g { messages = "Edge of the world.":voicemail g }
  | impassible c = attack g c
  | otherwise = return $ g {
    rogue = r { loc = (x + 1, y) },
    messages = "You moved right!":voicemail g
    }
  where
    r = rogue g
    (x, y) = loc r
    c = thingAt g (x + 1, y)

move g KeyLeft
  | x == 0 = return $ g { messages = "Edge of the world.":voicemail g }
  | impassible c = attack g c
  | otherwise = return $ g {
    rogue = r { loc = (x - 1, y) },
    messages = "You moved left!":voicemail g
    }
  where
    r = rogue g
    (x, y) = loc r
    c = thingAt g (x - 1, y)

move g _ = return g

messageSpace :: Int
messageSpace = 3

blotMessages :: [String] -> Int -> IO ()
blotMessages [] _ = return ()
blotMessages (m:ms) row = do
  moveCursor 0 row
  hCenterString m
  blotMessages ms (row - 1)

blotLevel :: [[Monster]] -> IO ()
blotLevel lev = do
  moveCursor 0 0
  (blotString . intercalate "\n" . map (intercalate "" . map show)) lev

blotMonster :: Monster -> IO ()
blotMonster m = do
  let (x, y) = loc m
  moveCursor x y
  blotString $ show m

loop :: Game -> IO ()
loop g = do
  blotLevel $ level g

  mapM_ blotMonster (monsters g)

  blotMonster $ rogue g

  -- Clear messages
  blotMessages (replicate 3 $ intercalate "" $ replicate (width g) " ") (height g + messageSpace - 1)

  blotMessages (reverse $ messages g) (height g + messageSpace - 1)

  k <- getKey

  when (k `notElem` [KeyEscape, KeyQ])
    (do
        g' <- if k `elem` [KeyUp, KeyDown, KeyRight, KeyLeft]
              then move g k
              else return g
        loop g')

generateRow :: Int -> IO [Monster]
generateRow w = do
  stdGen <- Random.getStdGen
  let monster = head $ Shuffle.shuffle' [defaultFloor, defaultFloor, defaultWall] 3 stdGen
  return $ replicate w monster

generateLevel :: Int -> Int -> IO [[Monster]]
generateLevel w h = replicateM h (generateRow w)

zombies :: Game -> Int
zombies g = width g `div` 10

generateMonsters :: Game -> [Monster] -> IO Game
generateMonsters g [] = return g
generateMonsters g (m:ms) = do
  let r = (loc . rogue) g

  x <- Random.getStdRandom $ Random.randomR (0, width g - 1)
  y <- Random.getStdRandom $ Random.randomR (0, height g - 1)

  if r == (x, y)
    then generateMonsters g (m:ms)
    else do
      let c = cellAt g (x, y)

      case symbol c of
        " " -> do
          let placedMonsters = monsters g
          let locs = map loc placedMonsters

          if (x, y) `elem` locs
            then generateMonsters g (m:ms)
            else do
              let m' = m { loc = (x, y) }
              let g' = g { monsters = m':placedMonsters }
              generateMonsters g' ms
        _ -> generateMonsters g (m:ms)

main :: IO ()
main = do
  startCharm

  w <- getWidth
  h <- getHeight

  -- Reserve space for messages
  let h' = h - messageSpace

  locX <- Random.getStdRandom $ Random.randomR (0, w - 1)
  locY <- Random.getStdRandom $ Random.randomR (0, h' - 1)

  lev <- generateLevel w h'
  let g = defaultGame {
        width = w,
        height = h',
        level = lev,
        rogue = defaultRogue {
          loc = (locX, locY)
          }
        }

  g' <- generateMonsters g (replicate (zombies g) defaultZombie)

  loop g'

  endCharm