Pipes in Haskell
Table of Contents
Github | http://github.com/ambuc/pipes |
---|
Introduction⌗
This project remakes the game Pipe Mania in Haskell using Brick, a declarative terminal user interface library and box-drawing characters, which are a form of semigraphics for drawing lines and rectangles. Pipe Mania comes in many forms, such as an updated Metro design, a vintage Windows 95 theme, and online flash game. Each variant is slightly different, so I didn’t really copy any one game in particular.
You can browse the source at github.com/ambuc/pipes, but this post will be a short walkthrough of the most interesting parts of its development.
Graphics⌗
Pipes runs in the terminal, consumes keyboard events, and redraws a screen full of Unicode characters every frame. Box Drawing characters are used to render empty/full, connected/isolated pipes at all angles and thicknesses. These characters have been around since v1.0.0 (October 1991) and are commonly used for drawing terminal interfaces such as tmux
.
How do we consume a tile and decide which character to draw? In our case, we have more data than which legs are absent or present – we want to have control over each of the four limbs, and we want to dictate whether that limb is absent, present, or present and bold.
Braille⌗
In Unicode, Braille patterns are encoded in a sensible way. There are typically six dots, but there can be as many as eight. Those dots are indexed like so:
Indexing Base 16
======== =========
(1) (4) ( 1) ( 8)
(2) (5) ( 2) (10)
(3) (6) ( 4) (20)
(7) (8) (40) (80)
So, for example, the Unicode position of the character ⠓ (which has dots 1, 2, and 5 raised) can be computed by taking the sum of the hexidecimal powers of two which correspond with the desired inputs, plus some offset, like so:
$$[1,2,5] \rightarrow \Sigma [1_{16},2_{16},10_{16}] \rightarrow 13_{16} \rightarrow (2800_{16} + 13_{16}) \rightarrow \text{U+2813} \rightarrow \boxed{⠓}$$
This is only possible because the Unicode characters are ordered lexicographically:
Code Point Character Dots Binary
========== ========= ===== ========
U+2801 ⠁ 1 00000001
U+2802 ⠂ 2 00000010
U+2803 ⠃ 1,2 00000011
U+2804 ⠄ 3 00000100
Box-Drawing Characters⌗
This is not the case for box-drawing characters, which are grouped primarily by shape, secondarily by rotation, and tertiarily by thickness.
0 1 2 3 4 5 6 7 8 9 A B C D E F
U+250x ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
U+251x ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
U+252x ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
U+253x ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
U+254x ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
Given some input data like {n: bold, e: absent, s: bold, w: regular}
, what is our preferred schema? My answer was this:
N 012012012012012012012012012012012012012012012012012012012012012012012012012012012
E 0--1--2--0--1--2--0--1--2--0--1--2--0--1--2--0--1--2--0--1--2--0--1--2--0--1--2--
S 0--------1--------2--------0--------1--------2--------0--------1--------2--------
W 0--------------------------1--------------------------2--------------------------
╵╹╶└┖╺┕┗╷│╿┌├┞┍┝┡╻╽┃┎┟┠┏┢┣╴┘┚─┴┸╼┶┺┐┤┦┬┼╀┮┾╄┒┧┨┰╁╂┲╆╊╸┙┛╾┵┹━┷┻┑┥┩┭┽╃┯┿╇┓┪┫┱╅╉┳╈╋
So that we can convert our desired weights into an enum on $[0,1,2]$ and index into some hard-coded schema
string, like so:
0 1 2
n: bold ==> . . X ==> 2 * 3^0
e: absent ==> X . . ==> 0 * 3^1
s: bold ==> . . X ==> 2 * 3^2
w: regular ==> . X . ==> + 1 * 3^3
----------
47 -> schema[47] -> ┨
So that’s how Pipes renders tiles of various orientations/thicknesses/shapes.
Animation⌗
One of the goals of writing this game was that it would not only respond to cursor movement and rotation (the way all Pipes Mania-style games do) but that the pipes which were connected to the drain, i.e. had water in them, would pulse gently.
This animation relies on tweening, which uses heterogeneous characters like ╼
and ╾
in-between homogeneous characters like ─
and ━
to give the appearance of finer-resolution motion. Here is an example:
Horizontal Vertical
Locomotion Locomotion
========== =================
0 ─── 0 ½ 1 ½ 2 ½ 3 ½ 4
1 ╾── │ ╿ ┃ ╽ │ │ │ │ │
2 ━── │ │ │ ╿ ┃ ╽ │ │ │
3 ╼╾─ │ │ │ │ │ ╿ ┃ ╽ │
4 ─━─
5 ─╼╾
When we explore the connected graph of pipes, we mark each limb as being an inlet or an outlet and then generate the appropriate tile given both the distance from the tap and the current time. This lets us stagger the animation across the tile so that the water appears to flow in from the limb nearer to the tap and flow out from the others.
Conclusion⌗
Brick is the perfect framework for a project like this. If you’re interested in playing with the code (or even just playing the game), you can get started by installing Stack, the Haskell Tool Stack and running:
$ git@github.com:ambuc/pipes.git
$ cd pipes
$ stack build
$ stack exec pipes-exe