Venzone, an ASCII adventure/platformer written in Haskell


I am pleased to announce venzone, an ASCII adventure/platformer written in Haskell.


You can find binaries (linux/win/mac) on the game page or you can follow the instructions on hackage and compile it yourself.

I wrote venzone to field-test ansi-terminal-game, a terminal library which aims to be no-frills, practical and cross compatible as possible.

Many thanks to everyone who collaborated to this game via contributions or feedback.

Games are meant to be played; if you have enjoyed this, spread the word and help me reach a larger audience!


Tools/libraries I used

  • microlens: writing venzone was a way to gently force myself to learn (and memorise) lens syntax. I chose microlens and it was a perfect match. Compilation time not overly bloated, documentation clear and most importantly maintainers are very responsive. If I can, I will never use H98 accessors once again.

  • darcs: in a world where most people have migrated to git, I still am a fond user of darcs. Extremely nice UX, cherry picking, cosy and comfortable all around. I usually self-host my darcs repos (another advantage of darcs: no need for anything bar ftp access, works on the cheapest of hostings), but this time I tried darcshub: fine platform, easy to collaborate with other folks, simple interface, will move some more projects on it.

  • ghcid, specifically, ghcid with -Wall turned on: I usually put -Wall in ghc-options just before the release, but this time I started with it and it was an epiphany. So many variable-shadowing errors caughts via ghcid which would have otherwise been debugged through awkward traceShow sessions.

  • lentil: I designed it myself and I am still fond of it. Write some code, litter it with todos, come later and sort/slice/dice those TODOs when you are not in a scripting rush. Terminal friendly, unix-philosophy compliant.

Things I would have done differently

  • How to pack/ship things? My requirements are simple: “Hello $Person who just compiled my program, run command and you will create a zip (with the exe and data) ready for releasing. Hand it to me, please.”
    My poor attempt is broken in so many ways and I spent more time on it than I would like to admit.
    The idea looks simple (1. fetch the exe, 2. fetch the level files and data, 3. zip it), I encountered these problems:

    1. zip modules on Hackage are fussy on setting executable permissions (fair, since it is not specified in the standard – but still annoying since zip is the only ubiquitous format used natively in all the three major platforms);
    2. some very useful libraries (e.g. shelly) need a Unix compatibility layer to be installed on Windows;
    3. installing Cygwin on Wine is a nightmare (Wine itself being the child of an unholy marriage).

    I ended up writing a different batch script for Windows and getting frustrated meanwhile. For sure bigger projects cannot have a hand-maintained .sh for every platform, so I need to investigate how they manage the release part.

  • testing: QuickCheck and hspec were Godsent, but having one level to test all properties was dumb in retrospect. For once it is slow, and if you break one thing (during a refactor, etc.), all the (even unrelated) tests turn red immediately. Next time, one level-file per test: a bit more cumbersome at first but hopefully smoother later.

  • Typing: venzone was meant to be a 1-room test for ansi-terminal-game and at first, to keep things simple, I chose a sum-type approach for game entities.
    As encouraging feedback came, the project mushroomed and sum-types became more and more laborious to use/maintain: constant pattern matching, difficulty to work with typeclasses over the sum type, all in all not a good experience.
    Chattig in #haskell-game, EvanR suggested an alternative, more sensible approach, i.e. a single datatype containing a number of lists, each list devoted to a single NPC-type; each NPC tagged with a unique ID. Collisions and similar stuff are dealt by having a function that abstracts the relevant data (e.g. World -> [(ID, Coordinates)]) and another one that pushes the results back in ([(ID, Outcome)] -> World -> World).
    Another approach could be ECS, with two libraries on Hackage to choose from.