HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Haskell Text-Adventure Game

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
textgameadventurehaskell

Problem

I just started making a text adventure game in Haskell. Because this is the largest project I have done in Haskell, I wanted to ask about it here before I got too far on it. I am still pretty new to Haskell, so I would really appreciate any advice at all on how this could be improved.

To compile the game, I just used ghc Main.

Here is my code:

Main.hs

import System.IO (hFlush, stdout)
import Control.Monad (unless, when, guard)
import Control.Applicative ((), ())
import Data.Maybe (fromMaybe, isJust, fromJust)
import Control.Monad.State
import Data.Char (isSpace)

import Room (Room)
import qualified Room as Room
import Game (Game, GameState)
import qualified Game as Game
import Direction

import Item (Item)
import qualified Item as Item

type GameResponse = IO (String, Game)

trim :: String -> String
trim = foldr pickChars []
  where
    pickChars ' ' (' ':xs) = ' ':xs
    pickChars c1 (x:xs)  = c1:x:xs
    pickChars c1 [] = [c1]

travel :: Direction -> GameState
travel d = state $ \g -> fromMaybe ("You can't go that way.", g)
  (flip runState g . Game.enterRoom 
    Room.roomInDirection d (Game.currentRoom g))

main = do
  (msg, _)  GameResponse
play x = do
  (msg, gameData) > putStrLn ""
  putStr "> "
  hFlush stdout
  response  getLine
  if response `elem` ["q", "quit"]
    then return ("Adiós!", gameData)
    else play . return $ runState (exec response) gameData

exec :: String -> GameState
exec "look" = state $ \g -> (Room.nameWithDescription $ Game.currentRoom g, g)
exec s
  | isJust direction = travel (fromJust direction)
  | take 4 s == "take" || take 3 s == "get" = Game.takeItem s
  | s `elem` ["i", "inv", "inventory"] = Game.displayInv
  | s `elem` [" ", ""] = return ""
  | otherwise = return "What?"
  where
    direction = directionFromString s


Direction.hs

```
module Direction
( Direction(..)
, directionFromString
) where

import Data.Char (toLower)

data Direction = North | NorthEast | East | SouthEast
| South | SouthWest

Solution

The last two lines of pickChars collapse to pickChars c xs = c:xs. trim = unwords . words may be to your liking.

StateT can handle play's state-passery, but you'll have to redefine GameState as Monad m => StateT Game m String to allow play's IO actions.

main = putStrLn =>= play) Game.gameData

play :: StateT Game IO String
play msg = do
  unless (null msg) $ putStrLn msg >> putStrLn ""
  putStr "> "
  hFlush stdout
  response  getLine
  if response `elem` ["q", "quit"]
    then return "Adiós!"
    else exec response >>= play


exec "look" = gets $ Room.nameWithDescription . Game.currentRoom

| Just direction runState everywhere misses the point.

lens can help with nested data structures. An example:

data Room = Room
  { _name :: String
  , _description :: String
  , _directions :: Map Direction String
  , _visited :: Bool
  , _items :: [Item]
  } _deriving (Show, Eq)

makeFields ''Room

find :: String -> Lens' Game Room
find n = roomMap . singular (ix n)

enterRoom :: String -> GameState
enterRoom n = do
  r <- use $ find n
  v <- find n . Room.visited <<.= True
  currentRoom .= r
  return $ r ^. if v then Room.name else Room.nameWithDescription


Note that currentRoom is the version of r that does not have its visited set to True yet. This could have been averted if currentRoom merely contained the room's name.

The recommendation to use abstractions theoretically also goes for travel, but what's needed here isn't available in the common libraries. Using my package prototype pointed-alternative and missing StateT combinator getsT:

travel :: Direction -> GameState
travel d = ascertain "You can't go that way."
  $ Game.enterRoom
  =<< getsT (Room.roomInDirection d . Game.currentRoom)

Code Snippets

main = putStrLn =<< evalStateT (Game.initGame >>= play) Game.gameData

play :: StateT Game IO String
play msg = do
  unless (null msg) $ putStrLn msg >> putStrLn ""
  putStr "> "
  hFlush stdout
  response <- trim <$> getLine
  if response `elem` ["q", "quit"]
    then return "Adiós!"
    else exec response >>= play
data Room = Room
  { _name :: String
  , _description :: String
  , _directions :: Map Direction String
  , _visited :: Bool
  , _items :: [Item]
  } _deriving (Show, Eq)

makeFields ''Room

find :: String -> Lens' Game Room
find n = roomMap . singular (ix n)

enterRoom :: String -> GameState
enterRoom n = do
  r <- use $ find n
  v <- find n . Room.visited <<.= True
  currentRoom .= r
  return $ r ^. if v then Room.name else Room.nameWithDescription
travel :: Direction -> GameState
travel d = ascertain "You can't go that way."
  $ Game.enterRoom
  =<< getsT (Room.roomInDirection d . Game.currentRoom)

Context

StackExchange Code Review Q#159069, answer score: 4

Revisions (0)

No revisions yet.