This post presents the techniques I used for the Haskell Tiny Game Jam. The goal was to implement a game that fits in 10 lines of 80 characters. I love this kind of challenge, and inspired by the possibilities, I submitted a few entries for each category.
Prelude
In this category, no imports are allowed, only the default Prelude from base is provided. As far as I could tell, only turn-based games are possible because:
- Line buffering can’t be disabled; the inputs are only available after pressing the enter key.
- Delay and background rendering are not an option.
For tiny-brot, I tried to implement a press-forward demo, similar to trackmania PF, where the render loop would be driven by the terminal baud rate. The user simply keeps pressing enter to render this animation:
Unfortunately this doesn’t look good in practice because the output is produced one character at a time, which causes the cursor to flicker.
For pure-doors I used interact
to implement this game engine:
-- engine1.hs
gameLoop :: String -> String
gameLoop (char:'\n':rest) = "Input is: " <> show char <> "!\n" <> gameLoop rest
gameLoop _ = "Done"
main = interact gameLoop
It is an interesting solution because it leverages Haskell laziness to evaluate a pure String -> String
function. As you can see, the gameLoop does not call getChar
, it simply pattern matches the input. This results in an interactive program:
$ runhaskell engine01.hs
w
Input is: 'w'!
q
Input is: 'q'!
Done
Note that there is nothing special with interact
, the game loop also runs with:
main = putStr . gameLoop =<< getContents
Laziness is such an interesting property, in retrospect, I should have called this game lazy-doors.
Base
In this category, all the base’s modules are available. In particular:
-
System.IO.hSetBuffering
enables reading the input one char at a time.
This is great to improve interactivity:
-- engine2.hs
import System.IO
eval :: Char -> IO ()
eval char = putStrLn ("Input is: " <> show char)
gameLoop :: IO ()
gameLoop = getChar >>= eval >> gameLoop
main = hSetBuffering stdin NoBuffering >> gameLoop
I used this setup for flower-seeds so that the user can simply press some keys to change the flower.
For lambda-ray, I used two other base modules to make a smooth animation:
-
Control.Concurrent.threadDelay
to pause between frame. -
System.Posix.Internals.puts
to print the frame in one syscall and avoid flickering.
-- engine3.hs
import Control.Concurrent
import System.Posix.Internals
render :: Int -> String
render t = "Frame: " <> show t
gameLoop :: Int -> IO ()
gameLoop t = puts (render t) >> threadDelay 100_000 >> gameLoop (t + 1)
main = gameLoop 0
I was happy to have found puts
, as it seems like the only
1
way in base to achieve a smooth animation.
Default
In this category, all the GHC’s default packages are available.
In particular, bytestring
can be used to process the user inputs without blocking the rendering loop:
-- engine4.hs
import Data.ByteString (ByteString, elemIndices, hGetNonBlocking)
import Control.Concurrent (threadDelay)
import System.IO (stdin, stdout, hSetBuffering, hSetEcho, BufferMode(NoBuffering))
eval :: ByteString -> String
eval input
| has 119 = "W pressed\n"
| has 115 = "S pressed\n"
| otherwise = ""
where
has = (/= []) . flip elemIndices input
gameLoop t = do
threadDelay 100_000
input <- hGetNonBlocking stdin 42
putStr (eval input)
gameLoop (t + 1)
main = hSetBuffering stdin NoBuffering >> hSetEcho stdin False >> gameLoop 0
I used this setup for tiny-space-program so that the simulation works as expected:
A similar implementation can be achieved for the base category using System.IO.hReady
2
. I used bytestring
because it fits the default category and it exhibits a nice behavior: when pressing a key, the terminal pauses before repeating the input. This results in the rocket plume flickering before going full-throttle which provides a little ignition animation for free :)
Terminal Escape Sequence
By printing a special sequence of bytes, we can control the cursor location, color, font styling, and other options on video text terminals. The bytes are usually produced using a library such as termcap or ncurses, but it is also possible to hard-code the common ANSI escape code. For example:
main = do
-- clear the screen
putStr "\^[c"
-- Move the cursor to row 3, column 5
putStr "\^[[3;5f"
-- Print in blue
putStr "\^[[34m"
putStr "❤ tiny game ❤"
putStrLn "\n\n"
Checkout the Unicode emoji list.
Hackage
In this category, any package available on Hackage can be used. The two most popular options are ansi-terminal-game and gloss. I used the former for lazy-march:
The package takes care of the frame rate as well as the terminal escape sequences.
Conclusion
I really enjoyed this Haskell Tiny Game Jam. At first it seemed like an impossible task, but after carefully reading the library’s documentation, I realized that a lot could be done within the 10x80 limit.
Implementing such tiny games took me on a fascinating journey, and it made me appreciate all the work done on Haskell even more. It’s really nice to see that it performs very well for such a niche use case.
I would like to thank the organizers, the participants and all the fine folks at #haskell-game.
hPutBuf
, but it uses foreign pointers, which is a bit overkill in this context.