Advgame-0.1: Advgame.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Main where
import Char (toUpper, toLower)
import Control.Monad (mapM_)
import Control.Monad.State hiding (forever)
import Control.Monad.Cont (runContT, callCC, ContT(..))
import Data.List (delete)
import Text.Printf (printf)
import qualified Control.Exception as E (catch, throw)
import System.IO (hFlush, stdout)
-- Objects in the game
data Object = Whiskeybottle | Bucket | Chain | Frog | Wizard | Well
deriving (Eq, Show, Read)
-- Directions you can walk
data Direction = West | East | Upstairs | Downstairs
deriving (Eq, Show, Read)
-- Rooms in the game
data Room = Garden | Attic | LivingRoom | InventoryRoom
deriving (Eq, Show, Read)
-- A Path from one place to another, and the entryway
data Path = Path {
dir :: Direction,
entryway :: String,
to :: Room
} deriving (Eq, Show)
-- a location: name, description, a list of paths to other Locations, and objects in that location
data Location = Location {
name :: Room,
desc :: String,
paths :: [Path],
objects :: [Object]
} deriving (Eq)
-- User commands
data Command = Walk Direction |
Pickup Object |
Splash Object Object |
Inventory |
Look |
Dunk Object Object |
Weld Object Object |
Quit |
Help
deriving (Eq, Show, Read)
-- default show instance for a Location is to show the description
instance Show Location where
show = show . desc
-- the objects that can be picked up
pickupable = [Whiskeybottle, Bucket, Chain, Frog]
-- locations
livingRoom = Location { name = LivingRoom,
desc = "You are in the living-room of a wizard's house. There is a wizard snoring loudly on the couch.",
paths = [Path West "door" Garden, Path Upstairs "stairway" Attic],
objects = [Whiskeybottle, Bucket, Wizard]}
garden = Location { name = Garden,
desc = "You are in a beautiful garden. There is a well in front of you.",
paths = [Path East "door" LivingRoom],
objects = [Chain, Frog, Well]}
attic = Location { name = Attic,
desc = "You are in the attic of the wizard's house. There is a giant welding torch in the corner.",
paths = [Path Downstairs "stairway" LivingRoom],
objects = []}
inventory = Location { name = InventoryRoom,
desc = "",
paths = [],
objects = []}
-- define a datatype to hold the game information
data GS = GS {dt :: [Location], curloc :: Location, welded :: Bool, bucketFull :: Bool} deriving (Show)
-- and a state transformer monad to automagically thread the state for us
newtype GameState a = GameState { runGameState :: StateT GS IO a }
deriving (Monad, MonadIO, MonadState GS)
-- the "world map"
worldMap :: GS
worldMap = GS { dt = [livingRoom, garden, attic, inventory], curloc = livingRoom, welded = False, bucketFull = False }
-- return a description of a path
describePath :: Path -> String
describePath p = "There is a " ++ (entryway p) ++ " going " ++ (show . dir $ p) ++ " from here."
-- print out a description of the location
describeLocation :: Location -> IO ()
describeLocation = putStrLn . desc
-- print out all the paths leading out of the current location
describePaths :: Location -> IO ()
describePaths = mapM_ (putStrLn . describePath) . paths
-- print out object descriptions at the current location
describeFloor :: Location -> IO ()
describeFloor = mapM_ putStrLn . map (printf "You see a %s on the floor." . show) . filter (flip elem pickupable) . objects
-- is an object at the specified location?
isAt :: Object -> Location -> Bool
isAt obj = elem obj . objects
-- print out location, path and object info
look :: Location -> IO ()
look loc = do
describeLocation loc
describePaths loc
describeFloor loc
-- is there a path leading *d* from *loc* ?
viableDir :: Direction -> Location -> Bool
viableDir d = or . map ((== d) . dir) . paths
-- given the name of a location, return the location object
getLoc :: Room -> [Location] -> Location
getLoc x = head . filter ((== x) . name)
-- capitalize a word
capitalize :: String -> String
capitalize word = (toUpper . head) word : (map toLower (tail word))
-- better version of read, returns Maybe instead of blowing up
maybeRead :: Read a => String -> Maybe a
maybeRead s = case reads s of
[(x,"")] -> Just x
_ -> Nothing
-- check if the command the user entered is valid
parseCommand :: String -> Maybe Command
parseCommand input =
case maybeRead (caps input) of
Just x -> return x
_ -> Nothing
where
caps = unwords . map capitalize . words
-- typing shortcut
io = liftIO
-- user is walking
walk :: Direction -> ContT () GameState ()
walk di = do
c <- gets curloc
t <- get
d <- gets dt
if viableDir di c
then do
io . putStrLn $ "Walking " ++ (show di)
put t{curloc = ((flip getLoc d). to . head $ filter ((== di) . dir) (paths c))}
else do
io . putStrLn $ "I can't walk that way"
-- user picking up an object
pickup :: Object -> ContT () GameState ()
pickup obj = do
c <- gets curloc
d <- gets dt
t <- get
if isAt obj c && obj `elem` pickupable
then do
let newl = c{objects = (delete obj (objects c))}
inv = getLoc InventoryRoom d
ninv = inv{objects = obj : (objects inv)}
put t{curloc = newl, dt = (newl : ninv : (delete inv (delete c d)))}
io . putStrLn $ "Picking up " ++ (show obj)
else do
io . putStrLn $ "I can't pick that up"
-- user splashing one object onto another
splash :: (Monad m) => (m () -> ContT () GameState ()) -> Object -> Object -> ContT () GameState ()
splash exit obj1 obj2 = do
d <- gets dt
w <- gets welded
b <- gets bucketFull
c <- gets curloc
let inv = getLoc InventoryRoom d
if and [name c == LivingRoom, w, b, obj1 == Bucket, obj2 == Wizard]
then do
io . putStrLn $ "Splashing the " ++ (show obj1) ++ " on " ++ (show obj2)
if not (isAt Frog inv)
then do io . putStrLn $ "the wizard awakens from his slumber and greets you warmly. he hands you the magic low-carb donut- you win! the end"
else do io . putStrLn $ "the wizard awakens and sees that you stole his frog. he is so upset he banishes you to the netherworlds- you lose! the end"
exit $ return ()
else do io . putStrLn $ "I can't splash like that"
-- user welding one onto another
weld :: Object -> Object -> ContT () GameState ()
weld obj1 obj2 = do
t <- get
d <- gets dt
c <- gets curloc
let inv = getLoc InventoryRoom d
if and [isAt Chain inv, isAt Bucket inv, name c == Attic, obj1 == Chain, obj2 == Bucket]
then do
io . putStrLn $ "Welding the " ++ (show obj1) ++ " to the " ++ (show obj2)
put t { welded = True }
else do io . putStrLn $ "I can't weld like that"
-- user dunking one object into another
dunk :: Object -> Object -> ContT () GameState ()
dunk obj1 obj2 = do
d <- gets dt
w <- gets welded
t <- get
c <- gets curloc
let inv = getLoc InventoryRoom d
if and [w, name c == Garden, obj1 == Bucket, obj2 == Well]
then do
io . putStrLn $ "Dunking the " ++ (show obj1) ++ " in the " ++ (show obj2)
put t { bucketFull = True }
else do io . putStrLn $ "I can't dunk like that"
help :: ContT () GameState ()
help = do
io . putStrLn $ "Available commands are:"
io . putStrLn $ "Walk [Direction]"
io . putStrLn $ "Pickup [Object]"
io . putStrLn $ "Splash [Object] [Object]"
io . putStrLn $ "Weld [Object] [Object]"
io . putStrLn $ "Dunk [Object] [Object]"
io . putStrLn $ "Inventory"
io . putStrLn $ "Look"
io . putStrLn $ "Quit"
io . putStrLn $ "Help\n"
-- run the "game"
run :: GameState ()
run = (`runContT` id) $ do
dummy <- callCC $ \exit -> forever $ do
t <- get -- the whooole thing
d <- gets dt -- world map
c <- gets curloc -- current location
-- read a command from the user
io . putStr $ "> "
io . hFlush $ stdout
line <- io getLine
case parseCommand line of
Nothing -> io . putStrLn $ "Invalid command!"
Just cmd -> do
case cmd of
Walk dir -> walk dir
Pickup o -> pickup o
Splash o1 o2 -> splash exit o1 o2
Weld o1 o2 -> weld o1 o2
Dunk o1 o2 -> dunk o1 o2
Inventory -> io . print . objects . getLoc InventoryRoom $ d
Look -> io . look $ c
Quit -> exit $ return ()
Help -> help
-- and shove the () back into the monad
return dummy
-- lather, rinse, repeat
where
forever a = a >> forever a -- forever and ever and ever and ever...
-- run the thing already!
main :: IO ()
main = do
look livingRoom
runStateT (runGameState run) worldMap
putStrLn "Goodbye!"