Well, I think I can certainly say this is out of beta now! This feels like a good time to publish the fleshed-out version of it seen in my various Dialog games this year. Note that this uses a few special dev features not available in the standard compiler yet! I’m hoping to get those into production soon, but for now, check out the ifcomp2025 branch to play with all the cool new toys.
In both The Wise-Woman’s Dog and Stage Fright, rooms have coordinates stored in a per-object variable as three-element lists: ($Room has coordinates [$X $Y $Z]). But the Z-coordinate isn’t used geographically—instead, it separates different map screens. So we start with some code to put a pretty headline above the map, showing which screen you’re currently on.
(name for region 0) \[ERROR\]
(name for region 1) Home
(name for region 2) Nahhanta Village
(name for region 3) Tarhuntassa City
(name for region 4) Nahhanta Fields
(name for region 5) Tarhuntassa Aqueducts
(name for region 6) Tarhuntassa Caves
(headline for region $N) %% Little flourish for the top of the map
+\~<(space 2)(name for region $N)(space 2)>\~+ %% Adds 10 chars to text width
(centered headline for region $N)
~(interpreter handles centering)
(current div width $FullWidth)
(headline width for region $N is $Used)
($FullWidth minus $Used into $Remaining)
($Remaining divided by 2 into $Margin)
(space $Margin) (headline for region $N)
(centered headline for region $N) %% If interpreter handles centering, or if current div width is not calculable, or if the headline is wider than the div (in which case the minus will fail), or anything else
(headline for region $N)
%% Dialog is really bad at measuring the width of text, so for now, we just make a lookup table for it
(headline width for region 0 is 17)
(headline width for region 1 is 14)
(headline width for region 2 is 26)
(headline width for region 3 is 26)
(headline width for region 4 is 25)
(headline width for region 5 is 31)
(headline width for region 6 is 27)
Now for the cool part: shortcuts! These are rooms that allow the player to move from one map to another. You never actually spend any time in them, but since they’re rooms like any other, they can appear on the automap and be hyperlinked and everything.
%% “Shortcuts” connect different parts of the automap, using a direction that doesn’t affect the coordinate system, but is still used in route-finding
#shortcut
(direction *)
(name *) moving to another map %% In case this ever gets printed, but it shouldn’t
(opposite of * is *)
(perform [go *]) %% Don’t do anything if GO TO generates this; it’s handled in (enter $)
%% Access predicate wrapping it
@($Room shortcuts to $Target)
(from $Room go #shortcut to $Target)
@(shortcut $Room)
(from $Room go #shortcut to $)
(narrate passing through $) %% Override this to describe the fast travel
(enter $Room) %% And this ensures we never spend any time in that room itself
($Room shortcuts to $Target)
(narrate passing through $Room)
(now) ($Room is visited)
(enter $Target)
(prevent entering (shortcut $Room)) %% Anything that blocks a room should also block shortcuts to that room
($Room shortcuts to $Target)
(prevent entering $Target)
Now, we need to assign these coordinates. We do this using a floodfill.
(automatically build the map)
(collect $Room)
*(room $Room)
($Room has coordinates $)
(into $Mapped)
(exhaust) (spread coordinates from $Mapped)
(startup) (automatically build the map)
(spread coordinates from $List)
(nonempty $List)
(collect $Next)
*($This is one of $List)
*(from $This go $Dir to $Way)
{
(room $Way)
($Next = $Way)
(or)
(door $Way)
(from $This through $Way to $Next)
}
~($Next has coordinates $)
($This has coordinates $ThisC)
($ThisC modified by $Dir becomes $NextC)
(now) ($Next has coordinates $NextC)
(into $NextGen)
(spread coordinates from $NextGen)
(spread coordinates from [])
Unfortunately, Dialog has no negative numbers, which means modifying the coordinates by direction is a bit complicated.
Note that The Wise-Woman’s Dog has no non-cardinal directions in it! Instead, “up” and “down” are drawn as northwest and southeast, respectively. This is why the Z-coordinate isn’t needed.
%% Of course, to do that, we need to know what effect each direction has on a set of coordinates
%% The convention we’re using here is that +X is east, +Y is south, +Z is a different map
%% For X and Y this is necessary to have the map oriented the conventional way, but for Z it’s arbitrary, and you can change it without breaking anything (e.g. a rogue-style dungeon might want 1 to be the surface and go down from there)
%% In this game, up and down are actually on the same XY plane, and Z is used to separate the map into components instead
([$X $Y $Z] modified by #north becomes [$X $Y1 $Z])
($Y minus 1 into $Y1)
([$X $Y $Z] modified by #south becomes [$X $Y1 $Z])
($Y plus 1 into $Y1)
([$X $Y $Z] modified by #west becomes [$X1 $Y $Z])
($X minus 1 into $X1)
([$X $Y $Z] modified by #east becomes [$X1 $Y $Z])
($X plus 1 into $X1)
%% For this game specifically, up is drawn as northwest and down as southeast
([$X $Y $Z] modified by #up becomes [$X1 $Y1 $Z])
($X minus 1 into $X1)
($Y minus 1 into $Y1)
([$X $Y $Z] modified by #down becomes [$X1 $Y1 $Z])
($X plus 1 into $X1)
($Y plus 1 into $Y1)
And a few little utilities:
(minimum of [$Head] is $Head)
(minimum of [$Head|$Tail] is $Min)
(minimum of $Tail is $Other)
(if) ($Head < $Other) (then)
($Min = $Head)
(else)
($Min = $Other)
(endif)
(maximum of [$Head] is $Head)
(maximum of [$Head|$Tail] is $Max)
(maximum of $Tail is $Other)
(if) ($Head > $Other) (then)
($Max = $Head)
(else)
($Max = $Other)
(endif)
And now it’s time to draw the map! Centering the map is easy on Å-machine (at least in the web interpreter), since we can just use text-align: center; on the status bar as a whole. On Z-machine, though, we have to get fancy.
@(draw map of layer $Z)
(draw map of layer $Z with centering [])
@(draw centered map of layer $Z)
(draw map of layer $Z with centering 1)
(global variable (map margin 0))
(draw automap margin)
(map margin $N)
($N > 0)
(space $N)
(draw automap margin) %% Fall back to nothing
(maximum delta-x supported $dX)
(current div width $Width)
%% Width: $Width (line)
($Width minus 1 into $Tmp) %% Since there’s an edge on both sides
%% Tmp: $Tmp (line)
($Tmp divided by 4 into $Rooms) %% Each room takes 3, each edge takes 1
%% Rooms: $Rooms (line)
($Rooms minus 1 into $dX) %% One room = zero dX
%% dX: $dX (line)
%% And the inverse operation
(delta-x $dX means $Margin margin)
(current div width $Full)
($dX plus 1 into $Rooms) %% xmin = xmax means 1 room
($Rooms times 4 into $Tmp) %% 3 for each room, 1 for each right edge
($Tmp plus 1 into $Used) %% Plus 1 for the left edge
($Full minus $Used into $Free)
($Free divided by 2 into $Margin)
(delta-x $ means 0 margin) %% Fall back to zero if the above fails for any reason
This (current div width $) is one of the fancy new toys in the upcoming version of Dialog. On any interpreter where this metric is useful, it reports the width of the current div in characters. On other interpreters (like the web terp or a screen reader), it fails.
But what if we need the opposite of a margin—that is, the map is too wide for the screen? Then we need to only draw a subset of the rooms!
%% Simplest case: no trimming needed
(trim range $LIn - $RIn to a maximum of $Delta around $ into $LIn - $RIn)
($RIn minus $LIn into $DeltaIn)
~($DeltaIn > $Delta)
%% More complicated case: trimming needed
(trim range $LIn - $RIn to a maximum of $Delta around $Center into $LOut - $ROut)
%% First, calculate a bunch of values we need
($Delta divided by 2 into $LeftHalf)
($Delta modulo 2 into $Tmp)
($LeftHalf plus $Tmp into $RightHalf)
%% Now, three cases to check
(if) ($Center plus $RightHalf into $Cmp) ($Cmp > $RIn) (then)
($RIn minus $Delta into $LOut)
($RIn = $ROut)
(elseif) ($LeftHalf plus $LIn into $Cmp) ($Center < $Cmp) (then) %% Equivalent to: Center - LeftHalf < LIn
($LOut = $LIn)
($ROut = $Delta)
(else)
($Center minus $LeftHalf into $LOut)
($Center plus $RightHalf into $ROut)
(endif)
Putting this all together:
(draw map of layer $Z with centering $C)
(collect $Room)
*(usable room $Room at [$ $ $Z])
(into $Rooms)
(collect $X)
*($Room is one of $Rooms)
($Room has coordinates [$X $ $])
(into $Xs)
(collect $Y)
*($Room is one of $Rooms)
($Room has coordinates [$ $Y $])
(into $Ys)
(maximum of $Xs is $XMax)
(minimum of $Xs is $XMin)
(maximum of $Ys is $YMax)
(minimum of $Ys is $YMin)
(if)
(maximum delta-x supported $dX)
%% Maximum dX: $dX (line)
(current room $Location)
($Location has coordinates [$X $ $])
%% Original: $XMin - $XMax (line)
(then)
(trim range $XMin - $XMax to a maximum of $dX around $X into $XMinNew - $XMaxNew)
%% Range: $XMinNew - $XMaxNew (line)
(else)
($XMinNew = $XMin)
($XMaxNew = $XMax)
(endif)
(if) ~($C = []) ~(interpreter handles centering) (then)
($XMaxNew minus $XMinNew into $dXNew)
(delta-x $dXNew means $Margin margin) %% Falls back to 0 if this can’t be calculated for any reason
(now) (map margin $Margin)
(else)
(now) (map margin 0)
(endif)
(draw map of layer $Z from $XMinNew to $XMaxNew and $YMin to $YMax 1)
So now we’ve figured out which block of rooms to draw. We just need to draw that block.
First, we figure out if we’ll need any extra lines:
(need exits line on $Z above $Y)
%% Are there any exits below row $Y of layer $Z?
%% Not considering X limits on this for simplicity
*(room $Room)
($Room has coordinates $Coords)
($Coords = [$ $Y $Z])
{ (exit #up from $Coords) (or) (exit #north from $Coords) }
(need exits line on $Z below $Y)
%% Same as above
*(room $Room)
($Room has coordinates $Coords)
($Coords = [$ $Y $Z])
{ (exit #down from $Coords) (or) (exit #south from $Coords) }
Then, we draw the entire map:
(draw map of layer $Z from $XMin to $XMax and $Y to $YMax $)
($Y > $YMax) %% Base case: no more recursion
(if) (need exits line on $Z below $YMax) (then)
(draw automap margin)
(draw exits above $Y of $Z from $XMin to $XMax) %% Just draw final row of exits
(line)
(endif)
(draw map of layer $Z from $XMin to $XMax and $Y to $YMax $First)
(if) ($First = 0) (or) (need exits line on $Z above $Y) (then)
(draw automap margin)
(draw exits above $Y of $Z from $XMin to $XMax)
(line)
(endif)
(draw automap margin)
(draw row $Y of layer $Z from $XMin to $XMax)
(line)
($Y plus 1 into $YNext)
(draw map of layer $Z from $XMin to $XMax and $YNext to $YMax 0)
You’ll notice Dialog really likes doing things recursively. We use tail recursion here to draw all the rows of the map one by one. And we’ll do the same to draw the columns within each row (specifically the rows of rooms, not the rows of exits between them):
(draw row $Y of layer $Z from $X to $XMax)
($X > $XMax) %% Base case
($X minus 1 into $XM1)
(if) (exit #east from [$XM1 $Y $Z]) (then)
-
(else)
(space 1)
(endif)
(draw row $Y of layer $Z from $X to $XMax)
($X plus 1 into $XP1)
($X minus 1 into $XM1)
%% West exit
(if) (exit #west from [$X $Y $Z]) (or) (exit #east from [$XM1 $Y $Z]) (then)
-
(else)
(space 1)
(endif)
%% Room
(if) (usable room $Room at [$X $Y $Z]) (then)
(if) ($Room shortcuts to $Target) (then)
%% We’ve already set $Target, we don’t need to do it again
(else)
($Room = $Target)
(endif)
(collect words)
(if) (current room $Target) (then)
look
(else)
go to (the linking $Target)
(endif)
(into $Command)
(if) (use hyperlinks in map) (then)
(link $Command) (full room glyph $Room)
(else)
(full room glyph $Room)
(endif)
(else)
(space 3)
(endif)
(draw row $Y of layer $Z from $XP1 to $XMax)
(use hyperlinks in map) %% The map gets unreadable in the debugger if links are included
~(in debugger)
This “room glyph” determines how the room should be displayed. In Wise-Woman’s Dog, a whole bunch of different special characters are used here. In Stage Fright, it’s given a red glow if there’s a solvable puzzle. Adjust it to suit your needs. I’m showing the Stage Fright code here, but the file attached actually contains both versions.
(full room glyph (shortcut $Room)) %% Shortcuts don’t use the surrounding parentheses; their (room glyph $) implementation is expected to print three characters instead of one (perhaps --> or <--, or v or ^ with a (space 1) on either side)
(room glyph $Room)
(full room glyph $Room) %% For regression testing we want different characters
($Room has puzzle $ usable)
(in debugger)
(span @puzzle) { \[ (no space) (room glyph $Room) (no space) \] (no space) }
(full room glyph $Room)
($Room has puzzle $ usable)
(span @puzzle) { \( (no space) (room glyph $Room) (no space) \) (no space) }
(full room glyph $Room)
\( (no space) (room glyph $Room) (no space) \) (no space)
($Room has puzzle $Obj usable)
*($Obj has ancestor $Room)
(puzzle $Obj)
~(solved $Obj)
(solvable $Obj)
Or, for Wise-Woman’s Dog:
(room glyph (current room $))
\@
(room glyph ($ has puzzle $Obj usable))
($Obj is worth $ shekels) %% Treasure
!
(room glyph ($ has puzzle $ usable))
(pointers enabled) %% Don’t show this if pointers are turned off
?
(room glyph (underwater $))
\~
(room glyph #stelaroom/#citycenter) %% We could look for a (stela $) object, but given the amulet, it’s easier just to list these two explicitly
\#
(room glyph $) %% Default: a single space
(space 1)
Now, that handles the rows of rooms. What about the rows of exits?
(final exits above $X $Y $Z) %% Cut-down version of the below for the very right edge
($Y minus 1 into $YM1)
($X minus 1 into $XM1)
(if) (exit #down from [$XM1 $YM1 $Z]) (then)
\\
(else)
(space 1)
(endif)
(draw exits above $Y of $Z from $X to $XMax)
($X > $XMax) %% Base case
(final exits above $X $Y $Z)
(draw exits above $Y of $Z from $X to $XMax)
($X plus 1 into $XP1)
($X minus 1 into $XM1)
($Y minus 1 into $YM1)
%% “Northwest” (i.e. up) exit
(if) (exit #up from [$X $Y $Z]) (or) (exit #down from [$XM1 $YM1 $Z]) (then)
\\
(else)
(space 1)
(endif)
(space 1)
%% North exit
(if) (exit #north from [$X $Y $Z]) (or) (exit #south from [$X $YM1 $Z]) (then)
\|
(else)
(space 1)
(endif)
(space 1)
(draw exits above $Y of $Z from $XP1 to $XMax)
In the above code, we’ve checked a lot if rooms and directions are “usable”. What does that actually mean?
%% These two predicates determine if rooms and exits should be drawn on the map
%% Currently, rooms are shown if they’re visited, and exits are shown if they’re unblocked
%% But by altering these, you could make all rooms be shown (visited or not), or make blocked exits show, and so on
%% In this case, doors are shown regardless of blocking
(usable room $Room at $Coords)
*(room $Room)
($Room has coordinates $Coords)
(usable $Room)
(usable ($Room is visited))
(exit $ from $Coords) %% For a bit of disorientation, add random exits
(player is disoriented)
(current room $Room)
($Room has coordinates $Coords)
(random from 1 to 2 into 1) %% Since there are only six mapped directions in this game, and one of them will always be genuine, a 50% chance gets us a good mess
(exit $Dir from $Coords)
(usable room $Room at $Coords)
(from $Room go $Dir to $Other)
{ (room $Other) (or) (door $Other) }
Notice that “player is disoriented” code? That makes exits appear and disappear randomly every time the map is drawn, when the player is unable to get their bearings in Wise-Woman’s Dog. I’ve included it here as an example of how to adjust the “which exits are usable” procedure.
And finally, we toss it in the status bar! Another of the cool toys in the dev build is (status bar $ with height $), which lets us alter the height of the status bar at runtime. So we want to know how tall our map should be:
(map of layer $Z needs $Lines lines)
(collect $Room)
*(usable room $Room at [$ $ $Z])
(into $Rooms)
(collect $Y)
*($Room is one of $Rooms)
($Room has coordinates [$ $Y $])
(into $Ys)
(maximum of $Ys is $YMax)
(minimum of $Ys is $YMin)
($YMax minus $YMin into $DY) %% How many lines of rooms do we have?
($DY plus 1 into $Tmp1) %% (Counting both endpoints)
($Tmp1 times 2 into $Tmp2) %% How many lines do we need for these rooms plus exits?
%% If we need a top exits line, add one
(if) (need exits line on $Z above $YMin) (then)
($Tmp2 plus 1 into $Tmp3)
(else)
($Tmp2 = $Tmp3)
(endif)
%% If we *don’t* need a *bottom* exits line, *remove* one (since we counted every row as one line of rooms and one line of exits)
(if) ~(need exits line on $Z below $YMax) (then)
($Tmp3 minus 1 into $Lines)
(else)
($Tmp3 = $Lines)
(endif)
And for an example of how to invoke this all:
(style class @status)
height: auto;
(style class @map)
text-align: center;
font-family: monospace;
margin-top: 0.33em;
margin-bottom: 0.33em;
(style class @headline)
font-family: Garamond, serif;
text-align: center;
font-weight: bold;
(draw automap $IncludeMeta)
(current room $Room)
($Room has coordinates [$ $ $Z])
(map of layer $Z needs $Height lines)
($Height plus 1 into $Hp1) %% Extra line for headline
(status bar @status with height $Hp1) {
(div @headline) (centered headline for region $Z)
(div @map) (draw centered map of layer $Z)
}
Or if you only want it inline, for a MAP command:
(draw inline automap)
(current room $Room)
($Room has coordinates [$ $ $Z])
(div @headline) (headline for region $Z)
(div @map) (draw map of layer $Z)
Voilà! The code is a bit messy, but it’s extensively commented, and hopefully this little guided tour helps too. The file I’m uploading here is an amalgam of Wise-Woman’s Dog and Stage Fright with the best features from both, but it’s fully expected that you’ll update it to support your own needs: for example, if you have diagonal exits, or you want up and down to change the Z-coordinate instead of being drawn diagonally.
automap.dg (13.5 KB)
Have fun!