Scroll is a roguelike, with a twist, which I won't reveal until I've
finished building it. I'll just say: A playable roguelike pun, set in a
filesystem near you.
I'm creating Scroll as part of the
7DRL Challange.
If all goes well, I'll have a usable roguelike game finished in 7 days.
This is my first time developing a roguelike, and my first time writing a
game in Haskell, and my first time writing a game to a time limit. Wow!
First, some groundwork. I'm writing Scroll in Haskell, so let's get the
core data types and monads and IO squared away. Then I can spend days 2-7
writing entirely pure functional code, in the Haskell happy place.
To represent the current level, I'm using a Vector of Vectors of Chars.
Actually, MVectors, which can be mutated safely by pure code running inside
the ST monad, so it's fast and easy to read or write any particular
location on the level.
-- Writes a Char to a position in the world.
writeWorld :: Pos -> Char -> M ()
writeWorld (x, y) c = modWorld $ \yv -> do
xv <- V.read yv y
V.write xv x c
showPlayer :: M ()
showPlayer = writeWorld (5,8) '@'
(I wish these Vectors had their size as part of their types. There are
vector libraries on hackage that do, but not the standard vector library,
which has mutable vectors. As it is, if I try to access outside the bounds
of the world, it'll crash at runtime.)
Since the game will need some other state, I'm using the state monad. The
overall monad stack is
type M = StateT S (ST RealWorld). (It could be
forall s. StateT S (ST s), but I had some trouble getting that to type
check, so I fixed
s to
RealWorld, which is ok since it'll be run using
stToIO.
Next, a concept of time, and the main event loop. I decided to use a
continutation passing style, so the main loop takes
the current continuation, and runs it to get a snapshot of the state to
display, and a new continutation. The advantage of using continuations
this way is that all the game logic can be handled in the pure code.
I should probably be using the Cont monad in my monad stack, but
I've not learned it and lack time. For now I'm handling the
continuations by hand, which seems ok.
updateWorld :: Step
updateWorld (Just 'Q') = do
addMessage "Are you sure you want to quit? [yn]"
next $ \i -> case i of
Just 'y' -> quit
_ -> continue
updateWorld input = do
addMessage ("pressed " ++ show input)
continue
Finally, I wrote some ncurses display code, which is
almost working.
Start time: After midnight last night. Will end by midnight next Friday.
Lines of code written today: 368
Craziest type signature today:
writeS :: forall a. ((Vec2 a -> ST RealWorld ()) -> M ()) -> Pos -> a -> M ()
By the way, there's a whole
LambdaHack library for
Haskell, targeted at just this kind of roguelike construction.
It looks excellent. I'm not using it for two reasons:
- Scroll is going to be unusual in a lot of ways, and LambdaHack probably
makes some assumptions that don't fit.
mainSer :: (MonadAtomic m, MonadServerReadRequest m) => [String] -> COps -> (m () -> IO ()) -> (COps -> DebugModeCli -> ((FactionId -> ChanServer ResponseUI RequestUI -> IO ()) -> (FactionId -> ChanServer ResponseAI RequestAI -> IO ()) -> IO ()) -> IO ()) -> IO ()
That's a lot of stuff to figure out! I only have a week, so it's
probably easier to build my own framework, and this gives me an
opportunity to learn more generally useful stuff, like how to use
mutable Vectors.
There was also a small syntax bugfix, but functionally nothing has changed because bash was doing the right thing even with that small error.
If you have started using Occasional Rsnapshot, you definitely want now Occasional Rsnapshot v1.3.1. If you haven't started and you don't have backups, please start doing backups, and while you're at it, you might want to try Occasional Rsnapshot (with Rsnapshot).