module Main where
import qualified Graphics.Vty as V
import Graphics.Vty.CrossPlatform (mkVty)
import Data.Array
import Control.Monad
import Control.Monad.RWS
import System.Random
data Player = Player
{ playerCoord :: Coord
} deriving (Show,Eq)
data World = World
{ player :: Player
, level :: Level
}
deriving (Show,Eq)
data Level = Level
{ levelStart :: Coord
, levelEnd :: Coord
, levelGeo :: Geo
-- building the geo image is expensive. Cache it. Though VTY should go
-- through greater lengths to avoid the need to cache images.
, levelGeoImage :: V.Image
}
deriving (Show,Eq)
data LevelPiece
= EmptySpace
| Rock
deriving (Show, Eq)
type Game = RWST V.Vty () World IO
type Geo = Array Coord LevelPiece
type Coord = (Int, Int)
main :: IO ()
main = do
vty <- mkVty V.defaultConfig
level0 <- mkLevel 1
let world0 = World (Player (levelStart level0)) level0
(_finalWorld, ()) <- execRWST play vty world0
V.shutdown vty
-- |Generate a level randomly using the specified difficulty. Higher
-- difficulty means the level will have more rooms and cover a larger area.
mkLevel :: Int -> IO Level
mkLevel difficulty = do
let size = 80 * difficulty
[levelWidth, levelHeight] <- replicateM 2 $ randomRIO (size,size)
let randomP = (,) <$> randomRIO (2, levelWidth-3) <*> randomRIO (2, levelHeight-3)
start <- randomP
end <- randomP
-- first the base geography: all rocks
let baseGeo = array ((0,0), (levelWidth-1, levelHeight-1))
[((x,y),Rock) | x <- [0..levelWidth-1], y <- [0..levelHeight-1]]
-- next the empty spaces that make the rooms
-- for this we generate a number of center points
centers <- replicateM (2 ^ difficulty + difficulty) randomP
-- generate rooms for all those points, plus the start and end
geo <- foldM (addRoom levelWidth levelHeight) baseGeo (start : end : centers)
return $ Level start end geo (buildGeoImage geo)
-- |Add a room to a geography and return a new geography. Adds a
-- randomly-sized room centered at the specified coordinates.
addRoom :: Int
-> Int
-- ^The width and height of the geographical area
-> Geo
-- ^The geographical area to which a new room should be added
-> Coord
-- ^The desired center of the new room.
-> IO Geo
addRoom levelWidth levelHeight geo (centerX, centerY) = do
size <- randomRIO (5,15)
let xMin = max 1 (centerX - size)
xMax = min (levelWidth - 1) (centerX + size)
yMin = max 1 (centerY - size)
yMax = min (levelHeight - 1) (centerY + size)
let room = [((x,y), EmptySpace) | x <- [xMin..xMax - 1], y <- [yMin..yMax - 1]]
return (geo // room)
pieceA, dumpA :: V.Attr
pieceA = V.defAttr `V.withForeColor` V.blue `V.withBackColor` V.green
dumpA = V.defAttr `V.withStyle` V.reverseVideo
play :: Game ()
play = do
updateDisplay
done <- processEvent
unless done play
processEvent :: Game Bool
processEvent = do
k <- ask >>= liftIO . V.nextEvent
if k == V.EvKey V.KEsc []
then return True
else do
case k of
V.EvKey (V.KChar 'r') [V.MCtrl] -> ask >>= liftIO . V.refresh
V.EvKey V.KLeft [] -> movePlayer (-1) 0
V.EvKey V.KRight [] -> movePlayer 1 0
V.EvKey V.KUp [] -> movePlayer 0 (-1)
V.EvKey V.KDown [] -> movePlayer 0 1
_ -> return ()
return False
movePlayer :: Int -> Int -> Game ()
movePlayer dx dy = do
world <- get
let Player (x, y) = player world
let x' = x + dx
y' = y + dy
-- this is only valid because the level generation assures the border is
-- always Rock
case levelGeo (level world) ! (x',y') of
EmptySpace -> put $ world { player = Player (x',y') }
_ -> return ()
updateDisplay :: Game ()
updateDisplay = do
let info = V.string V.defAttr "Move with the arrows keys. Press ESC to exit."
-- determine offsets to place the player in the center of the level.
(w,h) <- asks V.outputIface >>= liftIO . V.displayBounds
thePlayer <- gets player
let ox = (w `div` 2) - playerX thePlayer
oy = (h `div` 2) - playerY thePlayer
-- translate the world images to place the player in the center of the
-- level.
world' <- map (V.translate ox oy) <$> worldImages
let pic = V.picForLayers $ info : world'
vty <- ask
liftIO $ V.update vty pic
--
-- Image-generation functions
--
worldImages :: Game [V.Image]
worldImages = do
thePlayer <- gets player
theLevel <- gets level
let playerImage = V.translate (playerX thePlayer) (playerY thePlayer) (V.char pieceA '@')
return [playerImage, levelGeoImage theLevel]
imageForGeo :: LevelPiece -> V.Image
imageForGeo EmptySpace = V.char (V.defAttr `V.withBackColor` V.green) ' '
imageForGeo Rock = V.char V.defAttr 'X'
buildGeoImage :: Geo -> V.Image
buildGeoImage geo =
let (geoWidth, geoHeight) = snd $ bounds geo
-- seems like a the repeated index operation should be removable. This is
-- not performing random access but (presumably) access in order of index.
in V.vertCat [ geoRow
| y <- [0..geoHeight-1]
, let geoRow = V.horizCat [ i
| x <- [0..geoWidth-1]
, let i = imageForGeo (geo ! (x,y))
]
]
--
-- Miscellaneous
--
playerX :: Player -> Int
playerX = fst . playerCoord
playerY :: Player -> Int
playerY = snd . playerCoord