A lot of learning projects involve writing games: people have
written clones of Tetris, Asteroids, Space Invaders, and even first
person shooters (Frag) in Haskell. As I'm far less clever than these
people, I thought I'd start with something a bit simpler: Dope Wars.
Dope Wars is basically a trading game. In 30 turns, you move from
one location to another, buying and selling, er, drugs on the streets
of New York. It's a fairly simple concept, but one which includes
elements like:
- Input and Output
- Game state
- Random numbers
all of which seem like a good way to learn Monads and the other
building blocks that you need to actually do anything useful in a
functional programming language like Haskell.
So I had the idea, wrote up a couple of datatypes and some
functions, and then forgot about it. Then, when Greg McCarroll
mentioned that he'd accept talks about other languages for the London Perl Workshop on December 1st, I thought it would be a great opportunity to push myself to do it by proposing a lightning talk.
Only problem: I now have to actually write the game in order to talk
about it (*). So… here goes. In this post, I'm going to show a first
draft for the game prompt.
State
OK, people often find it most convenient to do State using “Monadsâ€.
I think I'm going to leave that for now, and just thread state
explicitly. Mainly because I haven't yet got around to learning how to
use the State Monad. Hopefully this will eventually become annoying
enough that it will give me impetus to learn the monadic version.
Anyway, the idea here is that we'd have some function playTurn that will look a bit like this:
> playTurn :: GameState -> IO ()
> playTurn gs = let gs' = doSomething gs
> in playTurn gs'
(There is an interesting post on haskell-cafe about a more sophisticated monadic representation of the prompt).
So we just need to work out how to represent this GameState object. To start off with, we'll want to store information like
- Which turn it is
- What our score is (how much money we have)
- Where we are on the game map
We could create a normal tuple:
> data GameState = GameState Integer Integer Location
> -- turn score location
and then pattern match on this, but it's going to get horrible if we
add any fields later on! In Perl I'd just use a hash, but remember that
Haskell Data.Map objects map from one type to another, and we might
well have values of various types.
When I asked on #haskell, dons and firefly told me about the record syntax:
> data GameState = GameState {
> turn :: Integer,
> score :: Integer,
> location :: Location
> } deriving Show
>
> -- for now:
> type Location = Integer
Defining the original state is easy:
> startState = GameState {
> turn = 1,
> score = 0,
> location = 0
> }
And to “modify†it (or rather to clone it, overriding certain
fields) there is a convenient syntax that just lets us declare those
fields which have changed:
> movedToBronx = gs { location = bronx }
Setting a field to a value is easy, but we might want to define some mutators to change the field relative to its current value:
> nextTurn :: GameState -> GameState
> nextTurn gs = gs { turn = succ $ turn gs }
>
> modScore :: Integer -> GameState -> GameState
> modScore d gs = gs { score = score gs + d }
The record syntax will work even when we inevitably add new fields later. Yay!
Prompt
Now, the game cycle is a bit more complicated than the version I
suggested above, as it will allow IO actions in it. Something perhaps
like this:
> playTurn gs = do showStatus gs
> putStr prompt
> s <- getLine
> let f = parseLine s
> let gs' = f gs
> if isEnd gs'
> then endGame gs'
> else playTurn gs'
For now we'll just stub some of the declarations we need. showStatus can just show the GameState record (which is why we derived the Show class).
> showStatus gs = putStrLn $ show gs
We may as well set the prompt to the dollar sign, appropriately:
> prompt = "$ "
Though we'll need to parse the line read from standard input to one
of the commands like “Buy 4 X†or “Goto location 3“, right now, we'll
just stub in a function that increments the score and the turn counter:
> parseLine s = nextTurn
> . modScore 10
We need to know if we're at the end of the game, and take action appropriately.
> isEnd gs = turn gs > maxTurns
>
> maxTurns = 3
>
> endGame gs = do putStrLn "Game over!"
> putStrLn $ "Your score was " ++ (show $ score gs)
> return ()
And here's a transcript
*Main> playTurn startState
GameState {turn = 1, score = 10, location = 0}
> buy 2 foo
GameState {turn = 2, score = 20, location = 0}
> sell 4 bar
GameState {turn = 3, score = 30, location = 0}
> goto quuxtown
Game over!
Your score was 40
(*) to be fair, it's only a 5 minute “Lightning Talkâ€, so I could
probably get away without even writing the whole game, but I'll feel
better if I actually know what I'm talking about…