Introduction

Motivation

Welcome to the prologue (or zeroth part) of what I hope will be a complete series on how to make a roguelike game in Haskell. The original roguelike tutorial has been endlessly used (I remember going through it all with Python in…god knows when. Probably early 2010) and loved and adapted to many other languages and frameworks - for instance Rust with rltk, JavasSript with rot.js, C# with RogueSharp; for Godot, for Unity, and a ton more I’m missing. “r/roguelikedev does the roguelike tutorial” has become a yearly event. There’s a great roguelike fan Discord server with an active dev chat. Making roguelikes is great fun, and it’s easier than people think.

There’s a kind of mental jump going between “I can solve all these problems in a given language” and “I can write an application (possibly a game)”. Maybe the reason these tutorial series are so popular, despite being very clear that nothing they are introducing to the reader is new to them, is because they provide a welcoming helping hand. They take you from being able to write a bunch of classes or functions to an actual game with a randomly generated dungeon with monsters and combat and items and all that cool stuff.

This is a bit of a challenge to bring to Haskell. There’s a reputation about how purity and functional programming isn’t suited to gamedev. This is completely mostly false, of course - it’s far more doable than you think! A lot of the flak Haskell gets is that it’s complicated (not unless you make it so), you need to know all about monads and algebraic effects and functional lenses (you don’t), or that it’s not suited to most sorts of application (again, not true). I want to dispel these fictions.

This series is not intended for complete beginners in Haskell - unfortunate, but it would simply be too much - but the intended level is at the advanced beginner. Perhaps having taken most of a functional programming course, or having read Learn You A Haskell or Real World Haskell. If you are coming from another functional language (F#, Scala, OCaml) then most of the concepts should be familiar to you and it is mostly syntax that isn’t.

If the following list doesn’t make you run away screaming, then fingers are crossed that this should be understandable:

  • The Haskell language
    • Datatypes, records, algebraic data types (ADTs), polymorphism, typeclasses, constraints, and functions
    • Polymorphism through type parameters and polymorphic functions
    • Data structures - lists/vectors/arrays, maps, sets;
    • map/fmap/mapM, filter, fold;
  • Monads
    • do-notation;
    • Basic monads - at the very least State and IO,
    • Simple monad stacks, in whatever form (StateT s IO a or MonadState s m => m a or State s :> es => Eff es a) - or at least the concept of putting two monads together,
  • modules,
  • cabal - adding build-depends, modules, building and running projects.

It’d be very useful to know about, but these will be covered in a way that doesn’t require prior knowledge:

  • Language extensions (e.g. OverloadedLabels, OverloadedStrings, NoImplicitPrelude, TypeApplications, etc)
  • mtl
  • Basic roguelike concepts like generating tile-based dungeons and an obsession of rendering things in ASCII characters, but that’s why you’re here - right?

Planned tutorial structure

Some style notes

My primary objective for this tutorial is to write easily understandable Haskell, not necessarily the most performant or the way I would do it personally.

Experiment

I’ll use Experiment bubbles to briefly describe possible alternative ways to implement something in a more complex way. These are completely optional! They’ll often have a convenient link to an expanded discussion at the bottom of the page, like this: Other useful tools, libraries, and techniques for Haskell gamedev.

Project structure setup

In the following steps, we will set up an empty cabal project, install the relevant libraries, and verify that indeed the hell of library paths has been solved.

This is assuming you have a working Haskell setup. For painless installation, ghcup is recommended. I’ve tested the code with GHC 9.8.2 and with 9.12.2, so it should be fine with 9.10 and probably 9.4/9.6.

Installing bearlibterminal

First, we need to download bearlibterminal, which is the graphics library that roguefunctor is built on. Check for prebuilt binaries at the bottom of the README on GitHub. If you are on a platform that doesn’t have a prebuilt binary (for example, an M1 Macbook with Apple Silicon) then you can build it yourself:


git clone https://github.com/cfyzium/bearlibterminal.git
mkdir bearlibterminal/Build
cd bearlibterminal/Build
cmake ../
make all

With the library file (.so,.dll, .dylib) you have two options:

  • you can either install this yourself into /usr/lib or equivalent.
  • you can copy the library file into some directory of your project and pass the library option to cabal with --extra-lib-dirs=/path/to/the/library. The downside of this method is that you do also need to set LD_LIBRARY_PATH(Linux) or DYLD_LIBRARY_PATH (Mac) or something else (Windows) to actually do cabal run, because it doesn’t copy the library into the cabal build directory. If you have a better way to manage this setup, please share your insights! Pull requests are welcome.
Experiment

There is a WIP branch of bearlibterminal-hs that tries to avoid this by having Cabal build the C library. It’s available in my repository on a branch here: https://github.com/ppkfs/bearlibterminal-hs.git.

Making a new cabal project

Let’s begin by making a new project directory and initialising a blank cabal project. We’ll go for the unexciting name hs-rogue.

mkdir hs-rogue
cd hs-rogue
cabal init

This will start the interactive wizard for setting up a new cabal project.

Warning

If you don’t get an interactive command line prompt, you’re probably using an old version of cabal. It’s very useful to have at least version 3.4 for GHC2021. If not, there will be a full cabal file available at the end of the post.

The options of importance:

  • Choose “Just an executable project” since we’re focusing on a standalone app.
  • Ensure we use Cabal version 3.4 minimum for efficiency and to leverage default-language: GHC2021.
  • Use GHC2021 as it enables useful language extensions.

The reason I suggest GHC2021 is because of the following language extensions - there’s probably more that are enabled and used, but these are the major ones:

  • TupleSections - A quick timesaver - ( , a) rather than \x -> (x, a).
  • DisambiguateRecordFields - Avoids needing to prefix record fields, which simplifies updates.
  • DeriveFunctor - Automatically derive functor instances for data types wrapping an a with additional data.
Experiment

DeriveGeneric is also beneficial for deriving LabelOptic instances via optics (which will gently be introduced in part 3) and simplifying JSON serialization with FromJSON/ToJSON.

This gives us hs-rogue.cabal pre-populated. Currently (as of June 2025), one of the libraries we need - roguefunctor - is not on Hackage, so cabal cannot automatically download it. We need a cabal.project file that specifies where the repositories for this library can be found as well as the packages in our project. We only have one package, but otherwise cabal run will complain that the project file lists no packages to run. Create cabal.project in the root directory of the project and add the following:

source-repository-package
  type: git
  location: https://github.com/PPKFS/roguefunctor.git
  tag: main
packages:
  hs-rogue.cabal

We specify that for the roguefunctor library, it’s available as a git repo at the above URL and the version of the repository we want is the main branch.

Whilst we’re at it, it’s safe to assume we would like haskell-language-server to work with this project. Whilst the language server shouldn’t have a problem working without it, it can sometimes be a bit…temperamental. To ensure smooth operation of haskell-language-server, add a hie.yaml configuration file for explicit path guidance to our executable component.

cradle:
  cabal:
    - path: "hs-rogue/app"
      component: "executable:hs-rogue"

So now we will have tooltips and code actions and hints. Nice.

Initial dependencies

We’ve got one final setup step, and that’s adding some dependencies and extensions to the cabal file. These are the ones we’ll need for the first few parts of the tutorial. I’ll make sure in future parts to put all the modifications to the .cabal file at the start of the post. Under build-depends, add:

  build-depends:
    , base
    , bearlibterminal
    , containers
    , mtl
    , random
    , roguefunctor
    , text

From the top:

  • base - I’d suggest this tutorial might be a bit hard to follow if you’re unsure why we need this. :)

  • bearlibterminal and roguefunctor are the two libraries we are using for this tutorial. You could very easily do this tutorial without roguefunctor and just use bearlibterminal. However, it introduces some useful abstractions (like viewports, event handling, colours, field of view algorithm[s], and so on). We can spend more time making a game and less time writing engine infrastructure.

  • containers - our underlying architecture for storing game objects will be a Map of IDs to various things. Map and Set are also essential data structures more generally.

  • mtl - Originally this series was going to avoid bringing in mtl and optics but I quickly discovered it was quite awful to avoid them. For all we need of it (constraints rather than writing our own typeclasses), it’s very simple. optics won’t come in until the second half of part 3, which I plan to include a decent enough explanation of the idea of lenses so fear not!

  • text - String is horribly slow and memory hungry. The libraries are based around using Text and we will use Text as we should.

Running cabal build should compile the dependencies successfully; any issues might indicate problems with your C library installation.

Warning

If you get an error at this stage about cabal being unable to resolve conflicts, it could be one of two things: 1) you’ve not run cabal update, implied by messages like “could not find package ‘random’” or 2) your currently installed GHC version is too low. Whilst I am writing this using 9.8, it should work on 9.4 upwards.

Warning

If you cannot build the project because of missing libraries, make sure you are supplying --extra-lib-dirs with wherever your bearlibterminal library is!

Default extensions

Finally we can add a couple of default extensions. GHC2021 automatically contains most of the “key” extensions for writing comfy modern Haskell, but there’s at least three more I suggest:

  default-extensions:
    LambdaCase
    NoImplicitPrelude
    OverloadedStrings
    default-language: GHC2021 -- if it's not already added
  • LambdaCase saves an awful lot of time to just write \case rather than \x -> case x of.
  • NoImplicitPrelude removes the implicit import Prelude from every module, thus allowing you to use your own Prelude. This works better than cabal mixins, which aren’t great when it comes to integrating with HLS. In this tutorial, we’ll write our own HsRogue.Prelude - a very thin wrapper around Prelude but with some extra re-exports that are still quite strangely not exported by the regular one - Text, forM_, (&), void, and so on - as well as some imports we want to use everywhere in our project, like Rogue.Geometry.V2.
  • OverloadedStrings allows string literals to be interpreted as any instance of IsString rather than just as String. This is because we want to use Text over String for efficiency, as you should do in Haskell.
  • RecordWildCards will likely make an appearance on a per-module basis.
Experiment

In roguefunctor there is also a custom prelude - Rogue.Prelude - that is a thin wrapper around relude. Whilst I do like to use relude myself - Text over String by default, no partial functions, polymorphic show, and many other nice-to-haves - Rogue.Prelude is geared towards using effectful and optics rather than mtl.

Opening a window

To verify that everything has been set up correctly, you can open a window by copying this into Main.hs - a full explanation will be in Part 1, along with drawing some things to the screen. In short, we use withWindow to open a window (with some provided options) and run our main event loop. The main loop refreshes the window, blocks until some input event is received (either a window event or a keypress) and loops if this is anything other than a window close event or an Escape key press.

module Main where

import Prelude -- we'll make our own prelude in part 1

import BearLibTerminal ( terminalRefresh )
-- note that pattern synonyms - such as TkResized, TkClose, and TkEscape -
-- have a different import syntax.
import BearLibTerminal.Keycodes

import Control.Monad ( when )
import Control.Monad.IO.Class ( MonadIO(..) )

import Rogue.Config ( WindowOptions(..), defaultWindowOptions )
import Rogue.Events ( BlockingMode(..), handleEvents )
import Rogue.Geometry.V2 ( V2(..) )
import Rogue.Window ( withWindow )

screenSize :: V2
screenSize = V2 100 50

main :: IO ()
main =
  withWindow
  defaultWindowOptions { size = Just screenSize, title = Just "HsRogue - Part 0" }
  (return ()) -- no init logic
  (const $ liftIO runLoop)
  (return ()) -- no shutdown logic

runLoop :: IO ()
runLoop = do
  terminalRefresh
  -- event handling
  shouldContinue <- handleEvents Blocking $ \case
    TkClose -> return False
    TkEscape -> return False
    _ -> return True
  when (and shouldContinue) runLoop
Note

I’ll be mostly using explicit import lists for external libraries to make it clearer where everything is coming from. Whilst it is a good practice to have, it’s not something I normally do!

You can run your program with cabal run hs-rogue. If everything goes to plan, you should now have a black window open!

Warning

If you can build but not run the project because of missing libraries, make sure you’ve copied libbearterminal.so/dylib/dll to a location on your PATH or use export LD_LIBRARY_PATH/DYLIB_LIBRARY_PATH.

Maybe there will be more things, which can be added later.

Wrapping up

Well, that’s it for part 0. Not exactly much coding, but we can now hit the ground running for Part 1!

The code - well, a blank project structure - is available at the accompaying github repo here.

Hopefully this was understandable - any feedback is greatly appreciated. You can find me on the various functional programming/roguelike Discords as ppkfs and on Bluesky as @ppkfs.bsky.social.

Feedback is greatly appreciated! Feel free to reach out with any questions or suggestions.

Hope to see you for Part 1 soon!

Postscript

Full cabal file

cabal-version: 3.6
name:          hs-rogue-part0
version:       0.0.0.0
synopsis:      Roguelike tutorial in Haskell.
description:   Part 0 of the roguelike tutorial in Haskell.
homepage:
  https://github.com/PPKFS/roguelike-tutorial-parts/tree/part-0

bug-reports:   https://github.com/PPKFS/roguelike-tutorial-parts/issues
license:       MIT
author:        Avery
maintainer:    Avery <ppkfs@outlook.com>
copyright:     2024-2025 Avery
category:      Game
build-type:    Simple
tested-with:   GHC ==9.12.2 || ==9.8.2

executable hs-rogue
  main-is:            Main.hs
  default-extensions:
    LambdaCase
    NoImplicitPrelude
    OverloadedStrings

  build-depends:
    , base
    , bearlibterminal
    , containers
    , random
    , roguefunctor

  hs-source-dirs:     app
  default-language:   GHC2021
  ghc-options:        -Wall