💾 Archived View for thrig.me › blog › 2023 › 12 › 21 › boxed-in.gmi captured on 2023-12-28 at 15:28:59. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

Boxed In

rogue - it's got rooms

    --------+--
    |.........|
    +.........|
    -----+-----

So how does one go about drawing such a room on a computer? The above is from rogue, a game that for better or worse spawned the roguelike genre. One way simply bangs out the walls and whatnot.

    void draw_room(struct room *rp) {
        int j, k;
        move(rp->r_pos.y, rp->r_pos.x + 1);
        vert(rp->r_max.y - 2); /* Draw left side */
        move(rp->r_pos.y + rp->r_max.y - 1, rp->r_pos.x);
        horiz(rp->r_max.x); /* Draw bottom */
        move(rp->r_pos.y, rp->r_pos.x);
        horiz(rp->r_max.x);    /* Draw top */
        vert(rp->r_max.y - 2); /* Draw right side */
        /* Put the floor down */
        for (j = 1; j < rp->r_max.y - 1; j++) {
            move(rp->r_pos.y + j, rp->r_pos.x + 1);
            for (k = 1; k < rp->r_max.x - 1; k++)
                addch(FLOOR);
        }
        /* Put the gold there */
        if (rp->r_goldval) mvaddch(rp->r_gold.y, rp->r_gold.x, GOLD);
    }

The gold gets placed as that is part of the room struct. The doors "+" are not placed by this code. Subsequent roguelikes usually change the walls to be "#" instead of "-" and "|". Simple, but there are various problems. For one, the shape is restricted by the code; different shapes will require new "draw_*" functions. The gold probably should be handled elsewhere; I would probably do it with the item generation.

Prefab

One solution is to have a stack of prefabricated rooms to sometimes replace the regular boring rectangles with. Difficulties here include how to wire up passages to the prefab, as one might imagine a triangular prefab where a passage can only be added to a particular side, or something would need to "drill into" the prefab space to link up with the inside of the room.

       ---
      -|.|-
     -|...|-
    -|.....|-
    |.......|
    ---------

Pretty ugly? This is probably among other reasons why folks switched to using "#" for the walls.

       ###
      ##.##
     ##...##
    ##.....##
    #.......#
    #########

One solution to the doors problem would be to specify where a door can go, or more likely to indicate where a door potentially can go.

       #+#
      ##.##
     ##...##
    ##.....##
    +.......+
    ####+####

With this prefab one or more of the "+" would be replaced with an actual door that maybe hooks up to a passage; the remainder would need to be overwritten with "#" wall symbols. A result would look something like the following with some in-progress passages extending from two randomly chosen doors.

       ###
      ##.##
     ##...##
    ##.....########
    #.......+.........
    ####+##########
 ........#
    ######

At some point one might think to support even more complicated things, and to have sort of a mini-language to allow for even more customization. The prefabs could also be rotated for variety. Lua scripting is common here.

Turtle graphics

A different approach is to draw rooms using various instructions. In this case there may not be a "draw_room" function but rather a machine that acts on instructions. An agent (thought of as a turtle, traditionally) would have a position, orientation, and perhaps other state associated. The instructions would include directives such as "move forward", "turn right", and so forth.

Grammar A

    F - move turtle forward
    L - turn left
    R - turn right
    d - start drawing ("pencil" down)
    u - stop drawing ("pencil" up)
    . - switch to drawing floor
    - - switch to drawing a horizontal wall
    | - switch to drawing a vertical wall

With this probably too simple grammar and some graph paper one can draw a room from the instructions:

    -dFFFFFFFFFFFR|FR.FFFFFFFFF|FLFL.FFFFFFFFF|FR-FRFFFFFFFFFF

Details such as not running off the edge of the graph paper are left as an exercise for the reader, or how to solve the "ensure that only horizontal walls are drawn when moving horizontally" problem, which the above does not specify: the turtle is assumed to be facing in a horizontal direction at the start. "#" draws itself the same regardless of the turtle's orientation, and maybe room decoration could be handled by some other bit of code.

Various improvements might be suggested; the runs of "FFFFFFFFFFF" do not scale and are easy for humans to get wrong. One might instead read a small integer and use that as a repeat count for the subsequent command, so "FFFFFFFFFF" would be replaced with "10F". Commands without a numeric prefix would assume a repeat count of one. Also helpful would be the ability to save the state of the agent and then restore to that point. "[" and "]" are fairly traditional for this task, where "[" saves the current state (position, orientation, character to draw, etc) and "]" restores to the previous saved state.

Grammar B

    integer - set the repeat count to this value
    [ - save state
    ] - restore state
    F - move turtle forward (honors repeat count)
    L - turn left (honors repeat count)
    R - turn right (honors repeat count)
    U - set draw nothing
    . - set draw floor
    - - set draw horizontal wall
    | - set draw vertical wall

With this grammar the room drawing code might change to the following (other variations could have the same result).

    -F[10F]R|F[L.9F|F]F[L.9F|F]-FL10F

The code should probably allow for whitespace and comments so that one can do breakdowns of the instructions.

    -F[10F]         draw the top of the room (with room width)
    R|              setup for drawing left edge of room
    F[L.9F|F]       another left edge, interior, and right edge
    F[L.9F|F]       ... as many times as the room has height
    -FL10F          draw the bottom of the room

This is shorter than the previous code, though compared to the "draw_room" function a critic may ask "how does one draw rooms of arbitrary dimensions using this grammar?" One room dimension is the number 10 (or 9 followed by a different character), and the other is "repeat this code fragment" repeated between some moves. This is pretty awkward to change. One could write code that generates suitable turtle code to generate a room of arbitrary dimensions, but now you have two problems. Another approach would be to make the grammar a bit more complicated.

Maybe the room could be drawn differently so there would not be "10" or "9" for the different parts of the room? And how to handle drawing the height of the room some number of times?

If one was in a hurry then "Q[4,0,10,4]" might be invented that would draw a quadrant starting at (4,0) with lengths (10,4) on the edges, but then you'd have the equivalent of "draw_room" in addition to the turtle graphics thing, and the complexity of both a "draw_room" and a turtle graphics system.

Sidequest: a working implementation on a computer

An actual code implementation will help at some point, as it may reveal problems or tradeoffs to consider, and being able to quickly visualize whether some bit of turtle code does what you think it helps one write more correct turtle code.

    SBCL> (TURTLE "-dFFFFFFFFFFFR|FR.FFFFFFFFF|FLFL.FFFFFFFFF|FR-FRFFFFFFFFFF")
    "
         -----------
         |.........|
         |.........|
         -----------
    "

Or you could con someone into trying out the rules on a piece of graph paper, if you have access to humans who want to do that. A second opinion is important to reveal flaws in what you (or they, or both) think a bit of turtle code should do, opinions on how to improve the grammar, etc.

Grammar C

The goal is to better support drawing rectangles of arbitrary sizes, and perhaps other things as well. A rectangle usually is defined by a starting coordinate and then either the length of the edges or the ending coordinate. This is two pairs of numbers, while the grammars above only support zero or one number at a time. Again, one could write code that generates suitable turtle code from those numbers, but that is a branch not taken here.

    integer     numeric prefix optionally used by next command
    !<nreg>     set <nreg> to the prefix
    @<nreg>     set prefix to value of <nreg> plus the prefix
    [           save state
    ]           restore state
    F           move turtle forward (prefix times, or once)
    B           move turtle backwards (prefix times, or once)
    L           turn left (prefix times, or once)
    R           turn right (prefix times, or once)
    A<sreg>     apply string register <reg> (prefix times, or once)
    P<char>     set draw character to <char>
    U           pen up
    D           pen down
    {<sreg>...} save turtle code ... to string register <sreg>

For the now obligatory room-drawing example one solution is

    11!a4!b{oBP-DF[-1@aF]}{iP|F[LP.-2@aFP|F]}UAoR-2@bAiUFLAo

which is barely shorter than the turtle code for grammar "A" but has the advantage that the room's width and height can be specified.

    11!a                set numeric "a" register to 11 (width)
    4!b                 set height
    {oBP-DF[-1@aF]}     "o" draws an outside edge of the room  ----
    {iP|F[LP.-2@aFP|F]} "i" draws the inside                   |--|
    UAo                 apply the top by calling "o"
    R-2@b               repeating "b" 4-2 times...
      Ai                  apply the inside by calling "i"
    UFL                 get turtle into position for
    Ao                  drawing the bottom of the room

This is easier if you designed the line noise (on the assumption that the rules make sense to you) and is probably at first gibberish to anyone else. In an actual game I would probably stick to writing regular code, to better preserve what remains of my sanity points.

Piled Higher and Deeper

One idea would be to set the angle of orientation instead of using compass directions (North, South, East, West). Probably this would map to integer vector values, as fractional values can be problematic on a grid. The LISP implementation uses one of four vectors for the direction of travel, so why not allow forward steps of (1,3) or whatever instead of only the four?

Some may notice vague similarities between Grammar C and roff. Another direction you could take it is towards FORTH (or PostScript) where you might support "4 8 10 20 room" to draw a room. But that branch was not taken here.

And we haven't even covered doors or passages, so we're still boxed in.