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
RealWorld, which is ok since it'll be run using
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.