making maps with just ascii
something i thought too much about maybe
what's a map?
In creating the explorer format, I wanted to have easy support for doing 2D, Super Metroid-style maps. In essence this is a grid, which each block on the grid showing a specific style of square or rectangle, correlating with the area the player is in.
Ignoring the text labels (and elevator shafts), essentially this map shows two things:
- Whether a grid on the map is part of the map (pink) or not (black).
- How each grid block connects (or doesn't connect) to adjacent blocks, visualized by "walls."
use in twine
Twine (or Twee, in this case), is basically a fancy way of reading a text file (much like how this page is a fancy way of reading a Markdown file). It would be great to have the map data in that text file as well, and in a fairly easy to read and write format.
That last requirement basically rules out XML (cause it isn't 2009 anymore) and JSON (I love you JSON, but visualizing a map from JSON is a nightmare).
A simple map could be done using just ASCII characters, especially if you author it in a monospace font. Here's a simple 10x10 map:
xxxxxxxxxx xxxx00xxxx xxxx00xxxx xxx0000xxx xxx000000x xx00000xxx xx00xxxxxx xx000xxxxx xx00000xxx xxxxxxxxxx
x means "The player can't go here" and
0 means "the player can be here"
And here's how it would appear:
Easy! Time to wipe our hands and call it a day, except...
So we've achieved goal #1, show where the player can be. But what about goal #2, show the connections between rooms?
Surely we'll need more than
0 to denote whether a block has walls. But how? We could use a separate piece of data to say something like "coordinate (1,0) has walls on the top and right, but not bottom and left," but that starts getting real clunky again. Is there a way we can encode that information into a single character?
So, if we look at a single block, we can have a wall on top, right, bottom, and left, and any combination of those, which is 16 combinations:
- no walls
- top only
- left only
- bottom only
- right only
- top and left
- top and bottom
- top and right
- left and bottom
- left and right
- bottom and right
- top, left, and bottom
- top, left, and right
- left, bottom, and right
- bottom, right, top
- top, left, bottom, right (all of the walls!)
Now, this would work with any system, but 16 is a number system we see a lot, especially in web development: hexadecimal! We can attribute a character 0-9 and a-f to represent each of the 16 states above
sorta random choice
We can pick an arbitrary mapping of hex char to wall configuration, and that'll work fine as long as it's clear in our code (i.e. a
const somewhere). But we can do one better (or worse, depending on how much you understand of binary).
Since we have 4 walls per block, and the max number of bits a hex char would need is 4, we can do a kinda sorta bitmask to map a hex character to a wall state.
First, we do have to (arbitrarily) assign a bit to a wall. I chose to go from most significant bit to least, following the same order that things like
border follow in CSS: top, right, bottom, left:
|top and bottom||1||0||1||0||1010||10||a|
So now we can use a single character to determine how many walls need to be on each part of the map. Using this we can create single rooms, hallways, and corners fairly easily!
So now we can take our map from earlier, and turn each grid into a hex char that represents the walls around it:
xxxxxxxxxx xxxx9cxxxx xxxx14xxxx xxx922cxxx xxx1882aex xx9022exxx xx34xxxxxx xx92cxxxxx xx3a2aexxx xxxxxxxxxx
(note: I did set up some of the interior walls to "double up" and show on both adjacent blocks in order to be more prominent, but how that is displayed can be up to you!)
(double note: looking back, using
x to denote a non-space is still a little rough to read here. However, since the only valid values for a space is
[0-9a-f], you can use whatever other character to denote a non-space, like
_ or even
And that comes out looking something like this:
We now have our small use case fulfilling the above criteria:
- We know where the player can and can't be
- We know how each block connects to the others (in this case, this is one large room with some walls the player has to go around)
This was my first time working directly with bit maps and bit masking. You can see the code that taskes the ascii char and converts it to drawing a border in the explorer format source. Could I have made that code nicer? Probably. There's maybe some bitshifting that can happen to make this a bit smoother, but that's for another time, I guess!