Venzone, an ASCII adventure/platformer written in Haskell

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

venzone-cast

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!

30 Likes

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.

9 Likes

This is great, thanks! I just gave it a try with ghc-9.6 and it worked out of the box :slight_smile:

It looks like you switched to git, would you mind telling us what motivated that change?

4 Likes

This looks incredible!

1 Like

An approach I think fits very well in these kind of scenarios is newtype Int + Pattern Synonyms + optional COMPLETE pragma + Synonym Bundle. For example

Module Entities where ( -- explicit export list is mandatory
   ... , 
   -- Bundle four Synonyms within one type, 
   -- so other module can do: import Entities (Entity (...))
   Entity (Player, Bird, Stone, NotPlayer)

)


newtype Entity = Entity Int deriving (Eq, Ord, Show)

pattern Player = Entity 0
pattern Bird = Entity 1
pattern Stone = Entity 2
pattern NotPlayer <- Entity ((> 0) -> True)

-- surely, not themost type safe thing but very practical.
{-# COMPLETE Player, Bird, Stone #-}
{-# COMPLETE Player, NotPlayer #-}

-- Compiler doesn't complain about incomplete patterns.
f :: Entity -> String
f Player = "That's the player!"
f NotPlayer = "That isn't the player :("

-- Compiler does! complain about incomplete patterns.
g :: Entity -> String
g Player = "That's the player!"
g Bird = "It is flying, must be a bird"
2 Likes

This has slipped through my mail client, thanks everyone for your supportive words!

It looks like you switched to git, would you mind telling us what motivated that change?

I love darcs and for single projects I find its UX sensible, non-intrusive, a pleasure to use during development. git has, alas, a larger userbase and can possibly pull more contributors .

When I found stagit (a simple, genius of a program that creates a static site from your git repository) I decded to switch to switch ansi-terminal-game to git. I received good feedback for the move and then I switched all of my repos.

An approach I think fits very well in these kind of scenarios is newtype Int + Pattern Synonyms + optional COMPLETE pragma + Synonym Bundle. For example

This is quite amazing and would have saved me some headaches in late development! And I agree with the comment, at a certain point I wanted more and more practicality, even at the expense of type-safety.


There is a game-development article that is making rounds lately, Leaving Rust gamedev after three years (shared on Haskell gamedev matrix by @romes).

As the title implies it is Rust, but speaks to the Haskell community too (you don’t even need to read it all, just scroll the section titles: “Making a fun & interesting games is about rapid prototyping and iteration, Rust’s values are everything but that”, “GUI situation in Rust is terrible”).

With venzone I wanted to finally make:

  1. a game that I would have liked to play myself;
  2. a game that, through limitations, could say something new (for some players, it did); and
  3. a game that could be useful in retrospect to evaluate the experience of game development in Haskell land.

I can say that I have learned some lessons, I hope to see more and more productions in Haskell in the future.

4 Likes