Seekers of Wump

Okay yay Wumpus Wumpus Wumpus! We love the Wumpus! Everyone for Wumpus! :woman_cartwheeling: :tada:

Since it seems like several people here are Wumpus fans, here we can discuss our Wumpus thoughts. I feel like Hunt the Wumpus is a special game that deserves its own particular kind of attention…in my case I think it’s by far the earliest game historically that I’ve ever gotten “really into,” where for a period of time I was totally wrapped up in it and it was hard for me to tear myself away and do anything else. Also, I think it’s really neat, as the conversation we were having in the No Context Computing thread shows, how much each person comes to their own distinctive way of relating to Wumpus. No two people seem to get quite the same things out of it, which I think is remarkable for such a spare game (and maybe even in some ways comes from its spareness). This thread might become an interesting way to observe that in practice.

Anyway, I ended up writing so much that I went over the character limit for one post. :upside_down_face: Since it’s in several sections, I guess what I’ll do is make each section its own post. Hopefully that’s the right thing to do.

Starting with the next post, I’ll just talk freely about the game and not worry about what I might be revealing. Even though it is kind of a simple, formal sort of game, there are certain things I think you have to figure out specially the first time you play that it is possible for someone else to spoil for you. So, if you’ve never played Wumpus and you’re curious, I encourage you to play it at least a bit first before reading on. There are many different versions and variants out there and probably any of them would do in their own ways. Gregory Coresun, the original author of Wumpus, said, "For myself, the soul of the game is in the idea and fun of it rather than the program or the computer which hosts it." @Gate88 made a nifty online graphical version as I recently learned. The version I originally played is the one my Linux distro provides, and you may be able to take the same approach if you’re in that sort of environment. EDIT: Lily tried out this DOS version on Archive and says it works well, if you want to play the original or at least something very close.

14 Likes

When I first played Wumpus, about a year-and-a-half ago, the version I ended up playing was not the very original one that Coresun wrote in HP BASIC in the early '70s, but rather the early '90s C version from the bsdgames package. I didn’t realize these different versions existed at the time—I had heard about Wumpus from Lily, searched my Linux distro’s packages to see if it was there, saw it was (under the package name wumpus :stuck_out_tongue: ), and installed and began playing it basically right then, without really looking into it in much detail so as to leave myself unspoiled. As it happens, the bsdgames version is actually a bit different from the original in various ways which turned out to be significant for the experience I had.

One of the ways in which they vary is that the original tells you immediately in the instructions that the cave is a dodecahedron:

  THE WUMPUS LIVES IN A CAVE OF 20 ROOMS. EACH ROOM
HAS 3 TUNNELS LEADING TO OTHER ROOMS. (LOOK AT A
DODECAHEDRON TO SEE HOW THIS WORKS-IF YOU DON'T KNOW
WHAT A DODECAHEDRON IS, ASK SOMEONE)

The bsdgames version leaves the cave layout more ambiguous:

The Wumpus typically lives in a cave of twenty rooms, with each room having
three tunnels connecting it to other rooms in the cavern. Caves may vary,
however, depending on options specified when starting the game.

What are these options, you might wonder…? At the outset of play, you may:

  • change the total number of rooms, to between 10–150,
  • change the number of connections-per-room, with a minimum of 2,
  • change the number of arrows you start with,
  • change how many rooms have bats,
  • change how many rooms have pits, and
  • enable a hard mode, which:
    • increases the number of bats and pits a random amount each, between 1 and half the number of rooms plus 1, and
    • starts the player at least three rooms away from the Wumpus (i.e. not “nearby” to it in the game’s terminology), as long as there’s less than 40% as many connections-per-room as there is total rooms (by default this condition would apply under the traditional cave layout, because the ratio is then 15%—3 connections-per-room to 20 rooms).

As you might imagine, one of the things the bsdgames version does to support these features is to generate a new cave each time at the outset of play, with a degree of randomness involved. The original doesn’t provide any of these options, leaving it free to hardcode the cave layout—it’s the same every time in that version.

Since I was a brand new player, I didn’t change any of these options and just started the game right away, being greeted with the following text:

You're in a cave with 20 rooms and 3 tunnels leading from each room.
There are 3 bats and 3 pits scattered throughout the cave, and your
quiver holds 5 custom super anti-evil Wumpus arrows.  Good luck.

I immediately started trying to sketch out the cave layout, figuring that I could determine an optimal Wumpus-hunting strategy once I understood the logic behind it. After a bit I did start to creep up on the idea that the cave was dodecahedral…

                                                         -------------
                                                         |     NN    |
                                                         | p/b/w     |
                                                         | nr: p,b,w |
                                                         -------------
                                                        /             \
                                           -------------               -------------
                                           |      9    |               |     NN    |
                                           | p/b/w     |               | p/b/w     |
                                           | nr: p,b,w |               | nr: p,b,w |
                                           -------------               -------------
                                          /             \                           \
                             -------------               -------------               -------------
                             |     14    |               |     3     |               |     NN    |
                             | p/b/w     |               | p/b/w     |               | p/b/w     |
                             | nr: p,b,w |               | nr: p,b,w |               | nr: p,b,w |
                             -------------               -------------               -------------
                            /             \             ?             \             /             \
               -------------               -------------               -------------               -------------
               |      2    |               |      1    |               |     NN    |               |     NN    |
               | p/b/w     |               | p/b/w     |               | p/b/w     |               | p/b/w     |
               | nr: p,b,w |               | nr: p,b,w |               | nr: p,b,w |               | nr: p,b,w |
               -------------               -------------               -------------               -------------
              /             \                                         /                           /             \
 -------------               -------------               -------------               -------------               -------------
 |     11    |               |     18    |               |     NN    |               |     NN    |               |     NN    |
 | p/b/w     |               | p         |               | p/b/w     |               | p/b/w     |               | p/b/w     |
 | nr: p,b,w |               | nr: p,b,w |               | nr: p,b,w |               | nr: p,b,w |               | nr: p,b,w |
 -------------               -------------               -------------               -------------               -------------
                            /             \             ?             \             /             \
               -------------               -------------               -------------               -------------
               |      7    |               |     12    |               |     NN    |               |     NN    |
               | p/b/w     |               | p/b/w     |               | p/b/w     |               | p/b/w     |
               | nr: p,b,w |               | nr: p,b,w |               | nr: p,b,w |               | nr: p,b,w |
               -------------               -------------               -------------               -------------

 ▲ ▲ ▲ ▲ ▲
▲▼▲▼▲▼▲▼▲▼
▼ ▼ ▼ ▼ ▼

…or at least sort of dodecahedral…

   /
◄▶◄
 ◄▶
◅▶◄ ◅
 ◄▶
◄▶◄ ◄
 ◄▶◄
/▶◄ ◄
                          7ᵥ
                         /
    1?B???|       4B!!!!|
          \      /
           BnB---|
                 \
                  KnW---|
                 /       \
           8nP---|        I?W???|
          /      \
    9?P???|       6-----|        D?W???|
                 /       \      /
           7nWB--|        HnW---|
          /      \              \
        4B!^      A?WB??|        5?W???|

Although much of the cave did seem to fit the structure of a dodecahedron, I kept finding odd rooms in the cave that seemed to clearly subvert this premise. In particular, sometimes there would be a room with a one-way connection, rather subtly…like, the game might say, “You are in room 3,” and, “There are tunnels to rooms 2, 13, and 18,” but then if you took the path to room 2, it would say, “There are tunnels to rooms 5, 8, and 11,” with no path back to room 3. Once I noticed this I found it shocking and mystifying. Less totally bizarre but still rather unsettling, some of the connections seemed to go between rooms far across the cave, as if the connection was a secret passage or magic portal or something, and occasionally a room would have one of its paths looping back to itself so that it only had 2 actual paths away. Since the game appeared to generate a new cave each playthrough, with a different set of connections between rooms and a randomized starting location for the player, I at first despaired a bit at figuring it out, since the game seemed to be too dangerous for covering the whole cave in one go and the geometry seemed ultimately non-Euclidean and rather fiendish (around this time in my notes there’s the passage rather impractical to map out the whole dungeon...?).

However, I gradually came to understand the game better…

wumpus warnings come from two rooms away...?

bats can drop you into hazardous rooms

pit warnings can refer to more than one pit (probably also true of bats)

if you start in a room with a pit, the game won't tell you so

maybe near pits/bats is 1 away and near wumpus is 1–2 away?

game does involve a certain amount of luck

7ish of the rooms are unsafe to enter

…and my sharpened skills allow me to stay alive longer, leading to even more tortuous and confused sketches of the cave:

        ?
1   K   B
 \ /   / \
  7   2   F
   \ / \ / \
    I   5---6
    |
    J
   / \
  8   G




 |-----------------------|
 |             D----6    |
 |            / \   |\   |
 ||------4   K |-3  5-F  |
 ||      |   | | v / /  /
 ||      E   7 | 2--B  /
 ||     / \ / + /     /
 -+----A   1 / 8   G-/
  |    |   | |  \ / \
  |    9   | |   J   4
  |        | |   |   |
  |        | |---I   H
  |        |      \ / \
  |        |-------C   |
  |--------------------|



           18
          /
         /    /-\
        19---16  4
            /     \
            10   17
           / \   /
         14   -9-
          |    |
          1   20    6
           \ /   \ /
            7    13
             \     \
              8-19  3
             /   \ /
            2     18



         ------18----\
        /       |     \
       /   ----12      \
      /   /      \      \
     / |-+---4---17     |
     | | |  /    |      |
     19+-+16-|   |      3->2---|
      || |   -10 |      | /\   |
      ||-+--\ / \|      | | \  |
      |  1--14   9     |  |  \ |
      \   \      |     | 11-?| |
       \   7----20     / /   | |
        \  |      \   / 15\  | |
         --8       -13  /  \ | |
           |         | /    -5 |
           |         6-------/ |
           |-------------------|

-17-09-10-16-04-
-02-11-15-06-05-

At this point I decided that ASCII wasn’t the ideal format to visually express such twisty things, and decided to try describing one of them in DOT language so that I could have Graphviz draw it, thinking that might give me new insights of some kind:

Once I had this image in hand I got the feeling that it would be helpful to try to arrange the nodes into a straight line as much as possible, to make it easier to see cycles and larger-scale structure in it:

Here I began to actually understand…sort of. :stuck_out_tongue:

maybe the extra path at 3 (3->2) was generated because of the
pits at 20 and 6—although of course the direct paths to 20 and 6
are at 13, but that would otherwise make 13 a dead end (the area
it and 20 "block" is accessible via 1->7 however)

speaking in general i do suspect that the game makes some
adjustments to its dodecahedral structure based on the positions
of hazards—at the very least, the current iteration appears to
have some attributes that disturb this structure:
    * the triangular cycle between 06-15-05
    * the apparent fact that (nearby) 11 has only two edges
    * the apparent fact that 3 has 4 edges but one is one-way,
      and as a complement 2 has three explicit edges but an extra
      "implicit" edge since 3 connects to it but not the other
      way around
    * the fact that you would be able to complete a hamiltonian
      circuit except for the oddness around 15/11/5

At this point, I suspected that the game created a dodecahedral map, then placed pits+bats+Wumpus, and then changed the map connections in places where the hazard placements implied some sort of design outcomes the developers wanted to rule out. I think that’s a reasonable hypothesis, but it’s not actually how the algorithm works—it sets up the whole cave layout first, then places hazards completely at random. (This can indeed lead to arguably-regrettable design outcomes, like the player not being able to reach certain parts of the cave sometimes, but I guess the developers didn’t care. :stuck_out_tongue: ) However, I think the most important part for the sake of skillful play, I had grasped the character of the cave’s shape correctly: with 20 rooms and 3 connections-per-room it comes out at least 2/3 like a dodecahedron, with a path connecting all the rooms in a big loop (a Hamiltonian cycle like a dodecahedron has), and 1/3 basically random—you can imagine something like a broken 3D model of a dodecahedron with some of the faces pointing the wrong way and clipping through each other and things. :stuck_out_tongue:

That understanding, combined with all the mechanical things I’d figured out along the way, was enough to make me feel like I was starting to play the game at a level I was happy with. To answer one last mechanical question I had, I conducted a series of expriments to determine how far away you could theoretically hit the Wumpus with an arrow (5 rooms away—another thing the original Wumpus tells you that the bsdgames version leaves in the dark). Once I had figured that out, I decided that I was pleased with my investigations, and that I wouldn’t mind reading this lovely article by Coresun, which I had come across after Lily first mentioned the game to me but had been holding off on to avoid spoilers. Then I realized that I couldn’t have been playing quite the same game he had written, and started wandering through the source code of both versions.

9 Likes

:wrench: :coffee: :sunrise_over_mountains: 𝓓𝓮𝓼𝓲𝓰𝓷𝓮𝓻’𝓼 𝓓𝓲𝓽𝓬𝓱 :sunrise_over_mountains: :teapot: :gear:

You may join me in the Ditch at sunset with this nice pot of tea and symbolic gears and wrenches if you’d like to consider some of the game design insights all this might evince.

This game really stands out to me for being under 1000 lines of code, including the static data like the text strings, and yet compelling enough to keep me obsessively occupied for hours. I think, in the broadest strokes, it achieves this in a rather similar manner to Wizardry: it has this kind of twin track of figuring out a rather devilish maze while contending with a set of “in menu” mechanics, but in a way where the maze and the menu mechanics interact and dovetail with one another, and you have to use your understanding of both together in concert to really play well. However, Wizardry achieves the maze-mapping side of this largely through clever by-hand map design, whereas the version of Wumpus I played gets it through clever procedural generation.

One thing I think is interesting is that the original Wumpus might well not have had this kind of effect on me to the same extent. You would still be left with the “menu mechanics,” which I found very entertaining to investigate in their own right, but the map is basically revealed to you from the outset in that game, so there’s nothing for you to really figure out. I think that version isn’t hurt at all by having an auto-mapper; you basically just want to keep track of where the pits and bats are and such which is all pretty rote. Judging by his article, I think Coresun was just so excited about having a dodecahedral maze over a grid-based one that he just wanted to make it an explicit outwards-facing part of the game and didn’t really dwell that much on the intrigue of reasoning out the cave layout.

That said, I think even in the version I played, if it was still just a dodecahedron it wouldn’t have taken me nearly as much time and effort to figure it out. The way it looks sort of like a dodecahedron but not quite leads to kind of a funny experience for the player: at first you’re like “Hmmm, seems kinda polygonal” and then you sort of overemphasize that perspective, and then when you realize it’s not quite right you’re really shocked—or at least that was what I experienced. Evaluating how much it resembled a dodecahedron or not and if there was any greater logic behind that was a huge part of the gameplay for me.

Of course, the way it works is actually kind of simple—it just connects all the rooms in a big loop and then makes semi-random connections between them until they each have 3 outgoing. The combination of order and randomness there is very compelling: if it was just a big loop, or just random connections, you could figure out either one quickly, but it takes a lot more effort to identify a subtle combination of both. Furthermore, the way the random-connections-part of the map generation works means that many of the random connections are one-way, but a few of them end up being two-way. This gives three “layers” of connection types, which occur at different rates:

  1. Hamiltonian cycle two-way connections. This is the bulk of them, 2/3rds, with default settings.
  2. Randomly-chosen one-way connections. This is the bulk of the remaining 1/3, generally somewhere between 100% and 75% of it.
  3. Randomy-chosen two-way connections. This is the smaller part of the remaining 1/3, somewhere between 0 and 25% on average.

I think there’s a kind of deep mechanical principle you can extract from that…when you’re designing any kind of system or space you’re planning for the player to analyze and make their own conclusions about, they’ll tend to notice any simple large-scale or omnipresent patterns in it quickly, which can be fun for the player as a way to get in at first. That’s like the Hamiltonian cycle connections here. However, once they’ve got those down, you can keep them intrigued by also incorporating more subtle complicating or wildly different patterns that you interleave with the simple large-scale or omnipresent ones; they’ll tend to notice those a bit later, maybe in proportion to how heavily they’ve been added in, and you can keep them going for a while that way. Here, we have the secondarily prominent one-way connection pattern, and then even a tertiary two-way connection pattern along with that. In a way, this is kind of similar to something like top-down visual composition, where you set up the large-scale structure of the scene first, then introduce its sub-parts, then add details, etc.

I at least have a hunch that this idea can be fruitfully applied not only to something like procedural vertices-and-edges map generation, but probably to many different mechanical systems and structures where you want to provide a sense of intriguing depth. Now that I’m writing this, I’ll keep thinking about it and try experimenting and see if I can hone in on these ideas in more detail. I would be interested to hear from anyone else who has ideas about this line of thinking.

7 Likes

:file_cabinet: :open_file_folder: :white_small_square: :white_medium_small_square: 🅲:o2:🅳🅴🆁’🆂・🅲🆄:parking::b::o2::a:🆁🅳 :white_medium_square: :zero: :one: :eight_pointed_black_star:

For those who like to sit down and chat with their computers, let’s crack open the Cupboard and peek inside and see how both the original Wumpus and the bsdgames version articulate their cave layouts in their code. That way you can go and make your own maze game! Or all sorts of other things, honestly.

The Original: Hardcoded

We can do the original Wumpus first because it’s simpler—as aforementioned the cave layout is hardcoded there. Here it is, in HP BASIC:

0068  REM- SET UP CAVE (DODECAHEDRAL NODE LIST)
0070  DIM S(20,3)
0080   FOR J=1 TO 20
0090    FOR K=1 TO 3
0100    READ S(J,K)
0110    NEXT K
0120   NEXT J
0130  DATA 2,5,8,1,3,10,2,4,12,3,5,14,1,4,6
0140  DATA 5,7,15,6,8,17,1,7,9,8,10,18,2,9,11
0150  DATA 10,12,19,3,11,13,12,14,20,4,13,15,6,14,16
0160  DATA 15,17,20,7,16,18,9,17,19,11,18,20,13,16,19

I used this book to make sense of this, since naturally I don’t know HP BASIC offhand, it not being 1973. :stuck_out_tongue:

REM is for comments (unsurprisingly). Apparently it stands for “REMark.” Everything after the REM is ignored.

DIM creates a multidimensional array. In this case, we have essentially a 20-element list-of-lists with each sublist having 3 elements, named S. You can also think of this as a 20x3 matrix. Access is 1-indexed, as you can see. The FOR/NEXT statements are probably self-explanatory, but the READ/DATA parts are a little more subtle for a modern reader.

Basically (ha ha), any DATA statements in the program are all read and concatenated together into a single “stream” in the order they occur by the implementation before the program runs. Then during execution, every time a READ statement runs, the implementation retrieves one or more items from the “data stream” and advances an internal offset into the “stream” accordingly. So, even though the DATA statements here come after the READ, their effect—which is to put a long stream of numbers into the program’s data—is actually applied beforehand. :cyclone:_:cyclone: These are the first READ and DATA statements the program contains, so the ultimate effect is that the program sets up a multidimensional array of integers like so, expressed in a more modern kind of array syntax:

S = [
  [ 2,  5,  8],
  [ 1,  3, 10],
  [ 2,  4, 12],
  [ 3,  5, 14],
  [ 1,  4,  6],
  [ 5,  7, 15],
  [ 6,  8, 17],
  [ 1,  7,  9],
  [ 8, 10, 18],
  [ 2,  9, 11],
  [10, 12, 19],
  [ 3, 11, 13],
  [12, 14, 20],
  [ 4, 13, 15],
  [ 6, 14, 16],
  [15, 17, 20],
  [ 7, 16, 18],
  [ 9, 17, 19],
  [11, 18, 20],
  [13, 16, 19],
]

You can use this diagram to help make sense of this data structure. Basically, S[1] would yield [2,5,8], meaning that room 1 has connections to rooms 2, 5, and 8. So, the index of the subarray in S is used to represent the number of the room that the subarray itself stores the connections of.

This is an adjacency list representation of an undirected graph, so named because the inner lists describe which vertices a given vertex is adjacent to. It’s a convenient and compact way to represent a graph. (You can also represent a graph as an adjacency matrix, where the node indices number both the rows and columns, and the cells of the matrix are 1-bit flags indicating whether the row vertex has an edge with the column vertex or vice-versa. This is rather space-inefficient unless the number of edges is close to the square of the number of vertices, so the adjacency list representation is more common, but the adjacency matrix can come in handy when there are tons of edges or when the performance of a given algorithm is dominated by how fast you can check if one vertex is adjacent to another.)

The advantage of just hardcoding the cave layout like this is that it can be any shape you please, without you having to go to the trouble to design an algorithm that can generate it. The coding is as easy as can be, since you’re just describing structured data. On the other hand, though, you have to figure out how to number the vertices and record all their connections yourself. This isn’t necessarily very hard—like, if you want to base your graph on a polygon, you can just look at a model of one, assign numbers to each of its corners, and figure out the connections from there—but recording all the data could be rather tedious, especially if you want a large or complex graph. If you like the by-hand approach but you’re getting sick of typing in numbers, you might consider creating some sort of level editor, or repurposing an existing program as a level editor.

bsdgames: procedural generation

The bsdgames implementation gives the player a set of parameters to describe the cave they want and then generates the cave accordingly at the outset of play. Here’s the function that generates the cave, which is written in ANSI C:

static void
cave_init(void)
{
	int i, j, k, lnk;
	int delta;

	/*
	 * This does most of the interesting work in this program actually!
	 * In this routine we'll initialize the Wumpus cave to have all rooms
	 * linking to all others by stepping through our data structure once,
	 * recording all forward links and backwards links too.  The parallel
	 * "linkcount" data structure ensures that no room ends up with more
	 * than three links, regardless of the quality of the random number
	 * generator that we're using.
	 */
	srandom((int)time((time_t *)0));

	/* initialize the cave first off. */
	for (i = 1; i <= room_num; ++i)
		for (j = 0; j < link_num ; ++j)
			cave[i].tunnel[j] = -1;

	/*
	 * Choose a random 'hop' delta for our guaranteed link.
	 * To keep the cave connected, we need the greatest common divisor
	 * of (delta + 1) and room_num to be 1.
	 */
	do {
		delta = (random() % (room_num - 1)) + 1;
	} while (gcd(room_num, delta + 1) != 1);

	for (i = 1; i <= room_num; ++i) {
		lnk = ((i + delta) % room_num) + 1;	/* connection */
		cave[i].tunnel[0] = lnk;		/* forw link */
		cave[lnk].tunnel[1] = i;		/* back link */
	}
	/* now fill in the rest of the cave with random connections */
	for (i = 1; i <= room_num; i++)
		for (j = 2; j < link_num ; j++) {
			if (cave[i].tunnel[j] != -1)
				continue;
try_again:		lnk = (random() % room_num) + 1;
			/* skip duplicates */
			for (k = 0; k < j; k++)
				if (cave[i].tunnel[k] == lnk)
					goto try_again;
			cave[i].tunnel[j] = lnk;
			if (random() % 2 == 1)
				continue;
			for (k = 0; k < link_num; ++k) {
				/* if duplicate, skip it */
				if (cave[lnk].tunnel[k] == i)
					k = link_num;

				/* if open link, use it, force exit */
				if (cave[lnk].tunnel[k] == -1) {
					cave[lnk].tunnel[k] = i;
					k = link_num;
				}
			}
		}
	/*
	 * now that we're done, sort the tunnels in each of the rooms to
	 * make it easier on the intrepid adventurer.
	 */
	for (i = 1; i <= room_num; ++i)
		qsort(cave[i].tunnel, link_num,
		    sizeof(cave[i].tunnel[0]), int_compare);

#ifdef DEBUG
	if (debug)
		for (i = 1; i <= room_num; ++i) {
			(void)printf("<room %d  has tunnels to ", i);
			for (j = 0; j < link_num; ++j)
				(void)printf("%d ", cave[i].tunnel[j]);
			(void)printf(">\n");
		}
#endif
}

We can take it step-by-step. First we declare our variables at the top of the function, since this is old-school C:

	int i, j, k, lnk;
	int delta;

Then we seed the random number generator with the current number of seconds since the Epoch:

	srandom((int)time((time_t *)0));

Then we initialize a data structure called cave. This is defined and declared globally near the top of the program:

/* simple cave data structure; +1 so we can index from '1' not '0' */
static struct room_record {
	int tunnel[MAX_LINKS_IN_ROOM];
	int has_a_pit, has_a_bat;
} cave[MAX_ROOMS_IN_CAVE+1];

You may note that cave is basically an adjacency-list representation of a graph like S in the original Wumpus, although it also carries some flags with each vertex indicating whether it has a pit or bat. cave[1] returns the information for room 1, and cave[1].tunnel gives room 1’s adjacency list. cave[1].tunnel[0] would be the index of the first room in room 1’s adjacency list. (They leave cave[0] empty so the rooms can be 1-indexed; I would probably take care of that using an access function so as not to waste space, but you can do what you like since the wasted space is of essentially no consequence here anyway. In a language where you can redefine the array-subscripting operator for a type, like C++ or Ruby, you can base the subscripting on 1 without the user of the type needing to employ any unusual syntax).

Anyway, here’s the initialization of cave, to return to cave_init():

	/* initialize the cave first off. */
	for (i = 1; i <= room_num; ++i)
		for (j = 0; j < link_num ; ++j)
			cave[i].tunnel[j] = -1;

This puts -1 for every entry in every room’s adjacency list. -1 is used here to represent “no connection.” (You might consider assigning a value like this -1 to a named constant like no_cnctn or something instead of just writing -1 in a place like this, both for the sake of semantic clarity and in case you later decide you want to have -1 in an adjacency list mean something else).

At this point, the cave layout looks like this:

 1:  -1,  -1,  -1
 2:  -1,  -1,  -1
 3:  -1,  -1,  -1
 4:  -1,  -1,  -1
 5:  -1,  -1,  -1
 6:  -1,  -1,  -1
 7:  -1,  -1,  -1
 8:  -1,  -1,  -1
 9:  -1,  -1,  -1
10:  -1,  -1,  -1
11:  -1,  -1,  -1
12:  -1,  -1,  -1
13:  -1,  -1,  -1
14:  -1,  -1,  -1
15:  -1,  -1,  -1
16:  -1,  -1,  -1
17:  -1,  -1,  -1
18:  -1,  -1,  -1
19:  -1,  -1,  -1
20:  -1,  -1,  -1

Next we ensure that every room can be reached from every other room—this is where the big loop, i.e. the Hamiltonian cycle, comes from.

	/*
	 * Choose a random 'hop' delta for our guaranteed link.
	 * To keep the cave connected, we need the greatest common divisor
	 * of (delta + 1) and room_num to be 1.
	 */
	do {
		delta = (random() % (room_num - 1)) + 1;
	} while (gcd(room_num, delta + 1) != 1);

	for (i = 1; i <= room_num; ++i) {
		lnk = ((i + delta) % room_num) + 1;	/* connection */
		cave[i].tunnel[0] = lnk;		/* forw link */
		cave[lnk].tunnel[1] = i;		/* back link */
	}

The way this works conceptually is that you pick a distance between vertices—what they call delta here—and then step through the vertices with two indices, one starting at 1 and one starting at 1 + (delta + 1), then move to 2 and 2 + (delta + 1), etc., looping an index back around to 1 whenever it goes past the index of the last vertex, and making connections going both ways between each pair of vertices referenced this way. As they note in the comments, (delta + 1) and room_num (the highest room number, 20 by default) must be coprime, or some of the index pairs will lead to redundant or self-referencing connections and you won’t end up with a Hamiltonian cycle. (To gain some intuition about why this is, I recommend thinking about polyrhythms, and which combinations of meters layered polyrhythmically produce overlapping beats vs. not; it’s the same principle from a different angle.)

The cave layout at this point will vary depending on the delta; here’s what it would look like with a delta of 2:

 1:  4, 18,  -1
 2:  5, 19,  -1
 3:  6, 20,  -1
 4:  7,  1,  -1
 5:  8,  2,  -1
 6:  9,  3,  -1
 7: 10,  4,  -1
 8: 11,  5,  -1
 9: 12,  6,  -1
10: 13,  7,  -1
11: 14,  8,  -1
12: 15,  9,  -1
13: 16, 10,  -1
14: 17, 11,  -1
15: 18, 12,  -1
16: 19, 13,  -1
17: 20, 14,  -1
18:  1, 15,  -1
19:  2, 16,  -1
20:  3, 17,  -1

You can see for example that 1 connects to 4 (i.e. 1 + (2 + 1)) and vice-versa.

This takes care of the first two entries in every room’s adjacency list. For the third entries, or the rest of the entries if the player has set an alternate link_num value in the options, the program connects rooms at random, more or less:

	/* now fill in the rest of the cave with random connections */
	for (i = 1; i <= room_num; i++)
		for (j = 2; j < link_num ; j++) {
			if (cave[i].tunnel[j] != -1)
				continue;
try_again:		lnk = (random() % room_num) + 1;
			/* skip duplicates */
			for (k = 0; k < j; k++)
				if (cave[i].tunnel[k] == lnk)
					goto try_again;
			cave[i].tunnel[j] = lnk;
			if (random() % 2 == 1)
				continue;
			for (k = 0; k < link_num; ++k) {
				/* if duplicate, skip it */
				if (cave[lnk].tunnel[k] == i)
					k = link_num;

				/* if open link, use it, force exit */
				if (cave[lnk].tunnel[k] == -1) {
					cave[lnk].tunnel[k] = i;
					k = link_num;
				}
			}
		}

For each room, we iterate through its adjacency list from the third entry on. If the current entry is not -1, we skip to the next entry and don’t do anything, because that means it’s already a legit connection (hopefully :stuck_out_tongue: ). Then we pick a random room. We iterate through the adjacency list entries before the current one and see if that room is already listed. If not, we add it to the adjacency list at the current location; if so, we pick another random room and try again until it works. Then we flip a coin. If heads, we go to the next adjacency list entry and repeat. Otherwise, we iterate through all the entries in the adjacency list of the destination room. If we find the number of the current source room there, we immediately stop, go to the next adjacency list entry, and start the whole loop again. Otherwise, if we find a -1 entry in the adjacency list of the destination room, we set it to the current room number, and then also go back to the top of the loop and start again.

That’s all a bit of a mouthful :flushed: so just to summarize, the basic effect for a default cave with 3 connections-per-room is:

  1. For a given room, if it doesn’t already have a third connection, make the third connection to a room that the given room isn’t already connected to.
  2. Flip a coin. If heads, go to step 4.
  3. If the destination room doesn’t already have a third connection, make it link back to the given room.
  4. Pick a new given room and repeat, until every room has 3 unique connections.

You can see how this would tend to produce more one-way connections than two-way, because there’s always a 50% chance to force a one-way connection, but then on top of that, sometimes the destination room already has a third connection to somewhere else even if the coin flip doesn’t force a one-way path. In practice, I’ve tested and you often end up with around 75–100% one-way connections for the random ones using this algorithm with the default cave settings.

Here’s what the cave might look like at this point:

 1:  4, 18,  3
 2:  5, 19, 14
 3:  6, 20,  1
 4:  7,  1, 17
 5:  8,  2, 11
 6:  9,  3, 16
 7: 10,  4,  3
 8: 11,  5,  8
 9: 12,  6,  2
10: 13,  7, 10
11: 14,  8,  2
12: 15,  9, 17
13: 16, 10,  6
14: 17, 11,  2
15: 18, 12,  9
16: 19, 13,  6
17: 20, 14,  4
18:  1, 15,  2
19:  2, 16, 13
20:  3, 17, 12

After all that complexity, the finale is simple:

/*
	 * now that we're done, sort the tunnels in each of the rooms to
	 * make it easier on the intrepid adventurer.
	 */
	for (i = 1; i <= room_num; ++i)
		qsort(cave[i].tunnel, link_num,
		    sizeof(cave[i].tunnel[0]), int_compare);

This just sorts the adjacency lists in ascending order—disguising the varying origins of the connections. :stuck_out_tongue: That’s how the game displays them to the player.

I think I can kind of guess at the thought process that led to this algorithm. If you look at this diagram of a Hamiltonian cycle through a regular dodecahedron from Wikimedia Commons

470px-Hamiltonian_path_bg

…you can see how it’s hard to approach the non-Hamiltonian-cycle edges (the dotted-line edges here) in a systematic way algorithmically. There are other Hamiltonian cycles you can find in a dodecahedron, but you always end up with this kind of sundry collection of other edges to complete the dodecahedron with afterwards. If you want to have other numbers of vertices, which imply other shapes, you can still find ways to complete a Hamiltonian cycle with them, but you’re still left with a sundry collection of edges that doesn’t necessarily even follow whatever patterns you might have been able to find in the dodecahedron’s sundry edges. I can only imagine that the programmers threw up their hands at this and said “Who cares if it generates a regular polyhedron or not, that’s kind of boring anyway!” and decided to just pick the sundry edges at random, seeing as how they already feel sort of random. As I discussed above, I feel like that ended up producing an excellent game design, but I can imagine that the programmers weren’t necessarily seeing it from that angle—they may have just wanted to add some customization options as a way to add features onto the original Wumpus, and just did this pragmatically as a relatively easy way to support those options.

Anyway, I think this is a pretty cool algorithm! It can generate a wide variety of cave layouts, it makes for fascinating gameplay, and it’s not even very complicated as these things go. It might have looked kind of thorny depending on your level of comfort with C, but in a more expressive language it wouldn’t take much code at all to implement—and you’ve already got, like, half of a game going at the point that you have an algorithm like this. What’s more, if you want to generate a map or some other kind of graph structure for your game, you might be able to find ways to tweak or extend this algorithm and get yet new cool graphs that aren’t even possible within this version of Wumpus. One of the fun things about procedural generation, I feel like, is that once you have a small piece of code that gives varied results, another small piece of code added on can give exponentially more varied results. :flushed:

11 Likes

Okay, there’s my huuuuuuuuge Wumpus schpiel!! :crazy_face: Isn’t it amazing that a little computer game from the early '70s—augmented a bit in the early '90s in my case—could lead to so much? I really think it’s incredible. What a game. What a Wumpus.

I want to just take a moment here to note that without the wonders of free software I would not have been able to say nearly as much about these two games. Because the source code for both of them is freely available and redistributable, we can understand them so much more deeply than we would be able to otherwise. Without the source code, even if we were able to totally reverse-engineer the games from machine code which is often impractical with larger software, we would not have the social communication from the programmer(s) that the source represents, which allows us to understand their thinking in a way that’s irreversibly wiped out by the process of compilation. Proprietary software is thereby bad for the gaming world because it makes it needlessly difficult for game designers, developers, and theorists to learn things from games that interest them, things they could only learn by reading the source. It holds us back as a culture! In the time when Wumpus was first written, it was the norm to share your source code—and that environment brought us Wumpus!

Anyway, I’m very curious to hear from other people about their own Wumpus thoughts and experiences. @a_new_duck, I found this page where someone describes making a nonviolent (marshmallow-based :smile:) Wumpus mod as a child. I feel like I read someone in a '70s computer magazine or something also making reference to not liking the “hunting” theme of the game on the same account (if I can find it I’ll share it in this thread). I felt a bit sad about it myself when I started playing—it seemed rather cruel to me to just storm into the cave and start attacking the Wumpus who was just there minding their own business and not necessarily eating anyone. It’s kind of interesting to me how much that sentiment seems to turn up among Wumpus players, considering that so many computer games are focused entirely on attacking random creatures in the game environment without prompting much hesitation from the players—although, I suppose it is commonly the conceit that you are attacked first. In Wizardry terms, it might be considered “Evil” to attack the Wumpus. :stuck_out_tongue:

10 Likes

Now this is the kind of thread Select Button is made for

11 Likes

Awww thanks ^^

1 Like

I have nothing significant to add to this thread aside from appreciation for all this deep wumpus exploration

but i want to say the TI box art for hunt the wumpus is excellent

9 Likes

Yeah, I love how it’s like, sort of pure wacky visual art in a sense that feels rather to me like it would make for a fun cloth napkin design, and yet it does give you a visceral feeling of what you’re in store for too. As a side note, I think it’s fun that the TI 99/4A carts were apparently all called “Command Modules,” like you might load them onto the little computer on the moon lander.

2 Likes

Back when a_new_duck was hosting the wumpus jam on itch, I tried my hand at making a simple port of Hunt the Wumpus to the Channel F, mostly as a coding exercise — nothing elaborate or even remotely ambitious — just translating the original BASIC code line by line in a workmanlike fashion.

It was going pretty well, and I might have ended up finishing it, but I ran into a bit of a snag on some line or another where some line of code required me to do some form of indirection or pointer arithmetic that I couldn’t for the life of me figure out how to do with the F8’s instruction set, which led to me getting frustrated and giving up (I ended up finding the answer a few months later, and it was as simple as I thought it had to be).

I had thought of eventually making a version more idiomatic to the console after finishing my straight port, but I was stuck in a creative rut so I couldn’t think of any way of differentiating it from the TI-99 version (but now that I say that I have a couple ideas…). Also, I had my own wedding to plan, so between that and the jam ending and my then-still-unresolved programming issue, I had lost interest in the project entirely by the time I had regained my creative energies.

Unfortunately, I lost the source code when my SSD died a couple years back, so all I have left is this test image of my string printing function:

image

(scintillating, i know)

Reaching the point where I left off wouldn’t be too difficult — the difficulty would just be me finding the time to do it, between all the other nonsense I want to do.

8 Likes

I adore the design of the wumpus on this cover art, it manages to be so memorable and original, perfectly addressing the question of ‘what is the wumpus’ with a creature that only raises more questions

7 Likes

Wow rock on, that’s extremely cool :smile_cat: I wouldn’t knock yourself at all for doing a straight port in that context—it’s not exactly a trivial undertaking, and I’m sure the Channel F community would have appreciated it even in the late '70s!

Aww that’s too bad, although I’m glad you found the answer in the end!

Wow, does the BASIC really carry over that easily? The original Wumpus listing does read in a kind of “asm-like” way…with its IF/ELSE/FOR/GOTO/RETURN for control flow and blocks of static data interleaved with code and so on…but I imagine that how easy it would ultimately be would depend a lot on the programming environment. Naturally the BASIC elides the details of memory management—in some ways I feel like it has the atmosphere of a high-level interpreted language too. (Very odd in a way by modern standards. :stuck_out_tongue: )

What are your ideas? Is it okay if I ask? :sweat_smile: I’m really curious to know what other people speculate about or whatnot when they think about this game.

Sure, that’s pretty understandable. :woozy_face: Those are pretty strong counter-motives.

Sure it is!! :cowboy_hat_face: What was your print routine like? Can I ask? :stuck_out_tongue: I know you don’t have the source anymore but do you remember it in broad strokes at least? How did you encode the text? With only 4K available (is that right?) I imagine that would be a serious consideration! It must be interesting to develop for a console like that.

My main day-to-day interaction with assembly is x86-64 on my home computer :stuck_out_tongue: usually in the context of investigating compiler behavior which I guess is kind of boring by comparison…although I’ve done some programming in it by hand just for fun and education and that sort of thing. (Sometimes I fantasize about making 4K intros for Linux and like to kind of speculate about fun ways to do it, but I have yet to actually try my hand.) Other than that I’ve experimented with Sega Saturn romhacking some, and have fond memories of reading manuals for the SH-2 and the Saturn’s video and audio chips from the time I was really going at that…leafing through them now and seeing their diagrams and things I get warm sweet feelings which is making me giggle at myself a bit. :stuck_out_tongue: So I guess that is to say, I think low-level programming is really interesting and fun, but I have only a sparse idea of what it’s like to program on '70s hardware from that perspective—especially in an environment as constrained as it sounds like the Channel F is. From reading Donald Knuth I feel like I have a vague sense of what a '60s mainframe might be like to work with, but that honestly seems like kind of a luxurious context compared to the Channel F.

Yes truly, for instance I wonder what it smells like :flushed:

4 Likes

BASIC is a relatively straightforward imperative language, so adapting the control flow is relatively simple. Constant data arrays can just be baked into the ROM with db statements. GOTO is just an unconditional jump. GOSUB/RETURN are just normal instructions for almost any processor.* And IF/ELSE/FOR are just different flavors of a conditional branch. Reorganizing the code is largely unnecessary, except for legibility.

Of course, how many assembly instructions correlate to one line of BASIC varies wildly. Something like 690 o = 1 is still a one-liner in F8 assembly, while something like 610 on j-1 goto 615,625,625,635,635 requires baking a jump table into the rom, multiple instructions to load the address from it, and then doing the jump, which is probably 10 lines of assembly, at least. (I can’t remember what line tripped me up, but it might (maybe) have been that one, actually.)

* On the F8, they are actually much less normal than you’d want, but for simple programs it’s not too much of an issue.

The Channel F has a rudimentary, low-resolution/low-bitdepth framebuffer (write-only). One interesting thing about it is that one of the offscreen columns is used to provide row attributes: you can either have a row be in color (with a choice of three different backdrop colors), or have a row be in black and white.

My main thought with this is that you could force the rows the player is not on to be monochrome, creating the impression of darkness. Perhaps some trickery could be done with the loss of color information as well — maybe the map could come with some hazards pre-marked (e.g. pits), but also have some red herrings (e.g. harmless boulders). The player could place markers on the map at suspicious places. Each of the obstacles would look identical without color, so part of the cognitive load would be keeping track of what is what (and perhaps also dealing with the bat switching things on harder difficulties).

Anyhow, thinking about this made me feel like whipping up a simple mockup:

image

With the system using a frame-buffer, it would also be quite trivial to have the maze be revealed in a fog-of-war style like an RTS, rather than having the whole maze revealed from the start, like this perhaps:

image

(Another completely different idea would be to have it be largely faithful to the original game, but presented as a rudimentary first-person dungeon crawler, but that seems like an idea that’s probably been done before already.)

You could simply declare ASCII text strings in the assembly like dc "TEXT", though one caveat is that the assembler did not automatically null-terminate the strings, so if you wanted to encode your strings that way you’d have to type it like dc "TEXT", 0. (I guess you could also manually encode your strings like Pascal strings (length then text, with no terminator), but that would use up an extra register for counting.)

The string function wasn’t too weird — load a letter, bail if we ran into a \0, adjust the ASCII code to match the internal table (subtract by some constant), call the blit function, increment the x position by 6, and then repeat. IIRC, the blit function was interesting because it modified the x and y positions while drawing, so it had to reset those back to their original position at the end of each call. Oh, and since the blit function I used was 1 bit-per-pixel, it took an argument to specify the FG/BG colors, so by extension the print string function took the same arguments as well.

The graphics themselves were 1 bit-per-pixel bitmaps, with the rows bitpacked together. A weird optimization I had considered was to have the letters be 5x5 pixels (25 bits), but pack them into 3 bytes (instead of 4) by having the bottom-right pixel of each character take from the top-left pixel of the next. I’m pretty sure I labbed it out as being possible at one point, but it looks like I was using a 5x6 pixel font, so I don’t think I was that desperate for space.

Most commercial Channel F games were only 2K, though back in the day that was fairly common. Most early Atari 2600 games were also 2K, with only later releases upgrading to 4K and beyond. There are a handful of Channel F games that are 3K or 4K or even 6K, but ultimately the relative lack of larger titles of more expert craftsmanship was due to the system’s failure in the marketplace (I rambled a bit about this point here).

(Speaking of the 2600, it’s funny that the 2600 can only have 4 KB carts before needing to resort to bankswitching, while the Channel F has a full 64 KB address space that it could work with, in theory.)

The real memory limitation with the Channel F is actually the RAM — it has none. Instead, it has a scratchpad of 64 registers (1 byte each) inside the processor itself. A register known as the ISAR (indirect scratchpad access register) allows indirection to be applied to the scratchpad (though it has some odd quirks). Some carts have a healthy amount of built-in RAM, though the instructions and semantics for reading from the scratchpad and actual memory are entirely different things.

Slightly musty and sweaty, with a strong undercurrent of pleasant wet earth, and an inexplicable hint of citrus.

Random thought: I think it’s fairly likely that the Grue in Zork and the bat in Atari’s Adventure both stem from this game.

10 Likes

did anyone make a ‘save the wumpus’ game during the wumpus game jam

7 Likes

I keep coming back to this part and thinking about it more. One thing that happened as a result was that yesterday I realized that this algorithm is, from a certain perspective, unnecessarily complicated: you don’t actually need to do the tricky delta/gcd() stuff to ensure that all the rooms connect in a loop and yet occur with some variety. As always in programming, you can just introduce a bit of indirection instead: say that every room connects to the next-highest-index room modulo the total number of rooms, but also give each room an arbitrary “display number” that you show to the player, letting their actual indices in the adjacency lists be an implementation detail. Then the numbers can be totally random, spaced in accordance with a delta like that specified above, plainly sequential, or anything else. In fact, they don’t even need to be numbers: they’re totally aesthetic from the player’s perspective, aside from how they differentiate the rooms from one another. Once I realized this, I wrote the according cave-generation code, as well as a Wumpus-style text-based navigation system and the ability to render a graph of a generated cave with Graphviz. It’s similar to the bsdgames algorithm but more flexible, and it’s been given me intriguing and still rather inchoate sensations. I’ll say more about it in another post down the road as I figure out what I think about it more…stay tuned. :wink:

Right now, I actually want to focus on something else: even if the delta/gcd() stuff is arguably unnecessary in this case, the principle behind what makes it work in this context is both beautiful and widely useful, I think. I have the urge to go over it in more detail since I rather brushed over it before, so I thought I might as well in case anyone else is intrigued by it. To me it’s a lovely intersection of art and mathematics, and one with broad applicability (i.e. to both music and game design). I’ll start with the musical angle, because it’s more tactile, and work back up to game design. Don’t worry if you’re not familiar with any music theory—I’ll do my best not to assume anything. (I don’t mind questions at all either, if anyone has any.)

Perhaps the most basic concept in rhythm is that of pulse:

Although this pattern by itself contains little information in rhythmic terms, it does have one obvious property: the pulse comes at a certain rate, in this case 120 “beats-per-minute” or BPM. In the European classical tradition, music that feels around as fast as this is often in the “allegro moderato” i.e. “moderately quick and lively” vein…little elves skipping merrily in the languid light of late summer…

…quiet, burning anguish and fury, alone at the far reaches of the night, widening in scope…

…thousands of wild geese attacking…

Maybe I’m getting a little sidetracked. :stuck_out_tongue: I suppose at the very least, one thing you can say from this is that even something as nonspecific as a rough musical speed can suggest a fair bit to a composer. But, if you want to take the suggestion, you naturally have to go beyond the pure pulse to get musical effects like these.

One of the most basic things you can do to go further is play some of the pulses louder than others. Especially basic is to do so in a simple repeating pattern, at a gamboling 100 BPM in this case.

Probably you can nod your head or tap your foot in time with that and have a sense of where it “loops.” One of the beats is the loudest one, and you probably will have a natural tendency to hear that as the “start of the loop.” After that there are three other beats, for a total of four per “loop.” You can see this in its spectrogram as well:

In English-language musical jargon, what I’m calling a “loop” here is more often called a “measure,” or “bar.” You can say that the rhythmic pattern above has “four beats per measure.” This is commonly expressed with the notation “4/4,” a type of notation called a “time signature,” which you may well already know of. The top number is what’s important for the sake of our discussion here—that “4” gives the number of beats. (The bottom “4” means that a beat has “quarter-note length,” which I think will take us too far off track to discuss in more detail here, but I can go into it on its in an addendum if anyone is curious.)

Here’s 6/8, another common time signature, at a brisk 160 BPM:


In this case, we have 6 beats per measure. We can get an interesting effect if we switch back-and-forth between 6/8 and 4/4:


You can see that, although the measures stay the same length, the number of beats changes, which causes the beats to fall at different places in the measure. What if we play these two patterns on top of each other?

Let’s take this a little slower so we can hear what’s happening more easily.


You can hopefully both hear and see that beat 4 of the 6/8 and beat 3 of the 4/4 overlap. This is because beat 4 of the 6/8 occurs in the middle of the 6/8, and beat 3 of the 4/4 occurs in the middle of the 4/4; since the measure is the same length either way, they overlap at the halfway point. Another way of putting this is to say that 6 and 4 are both divisible by 2: if you divide something into 6 parts, and you also divide it into 4 parts, you’ll end up dividing it in half both times (3 + 3 vs. 2 + 2).

In this case, having the 4/4 and 6/8 overlap halfway across kind of makes the measure sound half as long. If we wanted to preserve the feel of the original measure length, one thing we could do is switch out the 6/8 for 3/4:


You can hear and see that now the two patterns only overlap on the first beat of the measure (the “downbeat” as it’s called). This is because the only factor 3 and 4 share is 1; no other positive integer divides them both. In number theory, 3 and 4 are said to be “coprime” for this reason.

When you layer rhythmic patterns in two different meters on top of one another like this, it’s called a “polyrhythm.” Because of the measure-shortening effect caused by using meters with non-coprime numbers of beats per measure, which tends to imply a different pattern than written—for instance the 6/8 vs. 4/4 example sounds rather like 3/4 vs. 2/4 instead—people tend to talk about polyrhythms where all the beats-per-measure of the meters used are coprime.

The Chopi culture of Mozambique is known worldwide for a distinctive traditional music style which makes intensive use of polyrhythm.

To abruptly travel from Mozambique to the Wumpuszőne, consider if we have a cave with only four rooms.

┌─┐ ┌─┐
│0│ │1│
└─┘ └─┘

┌─┐ ┌─┐
│2│ │3│
└─┘ └─┘

(I’m numbering them starting from 0, instead of from 1 like Wumpus does, because it makes the math a bit nicer.)

We want to connect them up so that every room is reachable from every other. In particular, we want to do this in a way where we start with the first room, 0, increment the room number by some amount modulo the total number of rooms (4 in this case), make a two-way connection between room 0 and the room with the next number, and then start with the next number and repeat, until we’ve covered all 4 rooms. We’ll call the amount we increment the room number by “Δ” (“delta”). The simplest Δ we could use is 1: that gives 0-1 (0 and 0+1 % 4), 1-2 (1 and 1+1 % 4), 2-3 (2 and 2+1 % 4), and 3-0 (3 and 3+1 % 4).

 Δ = 1
┌─┐ ┌─┐
│0│─│1│
└─┘ └─┘
   ╳
┌─┐ ┌─┐
│2│─│3│
└─┘ └─┘

1 and any other positive integer are always coprime, because their greatest common divisor can only be 1. As you can see, this works: all the rooms are reachable from one another. What if we use a Δ of 2?

 Δ = 2
┌─┐ ┌─┐
│0│ │1│
└─┘ └─┘
 │   │
┌─┐ ┌─┐
│2│ │3│
└─┘ └─┘

We get “overlapping beats!” Since we have 0-2, 1-3, 2-0, and 3-1, and the connections are two-way, 0-2 and 2-0 become one connection, and 1-3 and 3-1 do as well. The trouble is that the Δ (2) and the total number of rooms (4) are not coprime, because 2 and 4 share a factor of 2. The musical analog is to layer 4/4 and 2/4:


You can hear how the 2/4 vanishes into the 4/4; its beats overlap the 4/4 on 1 and 3. Just as with the prior cave-connecting example, we get two points of overlap. With the cave connections, any overlaps will mean we end up with less total connections, so that we run short of the number we need to connect up all the rooms. That’s why the Δ and the room count have to be coprime if we want each room to be reachable everywhere using this approach.

Here’s Δ = 3:

 Δ = 3
┌─┐ ┌─┐
│0│─│1│
└─┘ └─┘
   ╳
┌─┐ ┌─┐
│2│─│3│
└─┘ └─┘

It works out the same as Δ = 1, with all the rooms reachable from one another. We heard 4/4 and 3/4 earlier, of course, so we’ve encountered this already.

There is a general aesthetic principle we can extract from this discussion: whenever you’re layering discretely periodic things together in some sense, you can make the outcome “maximally busy” if you make all the periods mutually coprime. Conversely, if you want some points of overlap, you can hone in on exactly how many you’ll get by considering the common factors of the periods.

Another application I can think of to game design is in the context of a game like Harvest Moon: you can give the player more variety in their work day-to-day by having different types of farm tasks recur at mutually coprime numbers of days with one another. Likewise, in the case of turn-based RPG combat, if you have effects that take place over multiple turns, having the number of turns each effect lasts for be coprime with the others will give a lot of varied situations in combat from turn-to-turn. Perhaps you can think of yet more applications.

Also, as a side note, although this whole conversation has been about integers, there are ways of extending these ideas to the real and complex numbers. That’s its own topic, but if you’d like something to stimulate your imagination along those lines, here’s Steve Reich’s Piano Phase and some pictures of interference between spherical waves:

9 Likes

Thanks so much for the detailed response!

Now I’ll have to look into F8 programming, I’m curious what this is like in practice :stuck_out_tongue:

Very stylish :smiley:

I’m kind of intrigued by your approach there setting it up like Rogue or something since of course Wumpus is famously non-grid-based—were you thinking of adapting it to a Rogue-style context or is there something else going on there I’m not realizing? Could really change the game if so I think.

Perhaps not in game form directly, although there is of course

Ah, so you just used ASCII—I was kind of thinking maybe the very limited memory or other hardware constraints might push you in another direction in some sense. I see that the F8 is 8-bit though so I suppose ASCII would be fairly natural. Of course, since you don’t necessarily need the whole ASCII character set, you might be able to pack multiple chacters into a single byte and save space that way…It sounds like you probably weren’t feeling the pressure to use tricks like that yet at the point in development you were at though in any case.

As far as I know that kind of situation was commonplace on cart-based game systems through the '80s, owing to the speed of cart memory access! Makes the system cheaper. :stuck_out_tongue:

Looking at the TI 99 cover, my first thought was “darkroom chemicals” :upside_down_face: but after some thought I realized the blocky-black-lines-with-yellow-background look was probably subliminally stirring up my childhood memories of Kodak film canisters.

4 Likes

The VES wiki is where I got my start learning myself. You might also enjoy the disassembly I made of Dodge It, which I feel is emblematic of the middle period of the system’s lifespan (the readme also contains a brief explainer about calling conventions).

My main thought is that the act of “consolizing” a game like this is a matter of eschewing textual I/O in favor graphical output and controller input (like Atari’s Adventure, or I guess the TI-99 port of this (even though that’s a PC >_>)). A grid just happened to be the first and most intially-workable-seeming idea I had when I slapped the mock-up together : P

That said, yeah, switching to a fundamentally flatter geometry does change the game quite a bit, hence my ideas to add some additional complications to maintain interest (even if of a different flavor). Still, there are definitely ways of creating mazes with more interesting, non-Euclidean geometries while sticking to a grid — perhaps differently colored walls could act as different types of teleporters, for instance.

oooh that looks great. i especially like the crooked arrow there.

The most common optimization I’ve seen Channel F games use is limiting the alphabet to the letters in their corpus. (If I somehow used less than 16 unique characters in my text, I would probably pack two characters per byte.) You’re right though that these kinds of optimizations are more of a thing for later on in the dev cycle.

1 Like

Thanks, those are great links!! This illustration from the front of the F8 programmer’s guide is really dripping with atmosphere:

Immediately makes me think:

Your well-commented disassembly of Dodge It looks like a fantastic resource! I’m always curious how old games like this work so it’s lovely that you did that—a real public service! :smile_cat:

Going between that manual and your calling convention writeup I’m getting an impression of the oddness of calling and returning from functions on the F8 (so thank you, I’m glad to satisfy my curiousity on that point a bit :smile_cat: ). I guess like, we’re kind of spoiled by modern computers with megabytes of stack space per process :sweat_smile: Like, in a way, I suppose even modern CPUs work kind of similarly in a way—the F8’s PC0 seems analagous to the instruction pointer register on x86, with PC1, K, and to some extent the rest of the scratchpad serving as an extremely tiny “stack” to store the return addresses on, or something like that. :stuck_out_tongue: But when that’s all you have, I guess the idea of “blowing the stack” takes on a whole new level of severity :sweat_smile:

In retrospect I feel like it was a bit glib of me to say this…in my defense I was very sleepy and maybe shouldn’t have been trying to write a response. :sweat_smile: I’ve had this impression that most cart-based systems expected RAM to be present on the cart, and that the cart manufacturer would just pay for as much as they needed or whatever. But, although, it seems like that was the situation for some carts on some systems, it’s not so much the rule, I’m now gathering—as a notable counterexample, apparently the Famicom has 2K of general-purpose RAM onboard and shipped with no support for accessing RAM on the cart, which seems like a very different sort of environment (although apparently it got peripherals and things later that aimed to mitigate this, or something like that). Going backwards in history, the SG-1000 apparently had 1K of RAM and could access RAM on-cart, but I’m getting the impression that few of its games took advantage of this. The Atari 2600 sounds kind of similar to the Channel F, with 128 bytes of RAM/“scratch space” and limited support for on-cart RAM, which it sounds like a fair number of games did shell out for despite the limitations. Since the Channel F has half the “scratch space” of the Atari, I guess it would make sense if the situation there was similar, although I’m having a hard time figuring out how many of its games actually did include extra RAM at a glance. Anyway, in truth, the situation overall seems hard to generalize about…like everything always ends up being when you actually go and look. :stuck_out_tongue:

Sure, I guess that makes sense—although, I guess like, in a way, Wumpus gives you a small enough palette of actions that its text-and-parser-ness seems kind of coincidental to me in a sense, at least as far as the original game goes. I think there’s an argument to be made that making it more graphical and using game-controller-type input is kind of just a technological upgrade of it in a certain direction, console or no. That may have been the thought of the Ti-99 developer(s); looking a bit at that version it seems like a very nice “glitzed up” take on the original to me.

That makes sense :smiley_cat: Especially since there’s so few pixels.

Very true! I thought about that a lot when I first read Coresun’s article about the game. I understood in a way his “sick of grids” outlook given the situation he described :stuck_out_tongue: but of course there’s nothing stopping you from making an arbitrary graph of rooms in a grid format—if you add doors and teleporters (in a formal sense, you can skin them however you like oc) you can do everything Wumpus does and beyond. I feel a lot of resonance between the bsdgames Wumpus and Wizardry, as I guess I’ve continuously alluded to :stuck_out_tongue: , and of course Wizardry has a grid-based map (a genuinely mind-bending one at times :smile_cat: ).

Have you played Nethack? If not that might give you some interesting ideas. :smiley_cat: I like your concept of “different types of teleporters”—I bet that could make for stimulating map interaction. If you managed to pull something like that off on the Channel F I bet you would have one of the deepest games in its library at the very least. :smile_cat:

Also, on the note of your “fog of war” idea, Brogue has a neat idiom where if you don’t have a line of sight to an area of the map, it shows you what you last saw there (i.e. what you “remember” :stuck_out_tongue: ) but with the caveat that the area itself may have changed since you last saw it. That would probably be impractical without at least some extra memory on the Channel F, but I’ve always thought it was graceful. On a similar note, Dragon Quest gets at the mapping challenge of Wizardry, despite being top-down, by only showing a varying-number of squares around your character when you’re in a dungeon depending on how much light you can produce, which ends up very close to Wizardry in practice. The first-person perspective of Wizardry offers more opportunities for chicanery with teleporters (since you “see through the portal”) and makes you risk a combat encounter just to turn and look behind you and things, but overall I’d say Dragon Quest does a great job of replicating its mapping challenges in its own bite-size way.

I really wonder who drew it—it might have been Coresun himself, but it’s similar in style to other PCC publications I’ve seen, so maybe it was their “in-house artist” or whomever might pass for that in such a casual environment. :stuck_out_tongue: If you like that art style I have to recommend What to Do After You Hit Return, which might be my favorite book about games if I had to pick one. At some point I mean to write an article on it or something like that…it’s a treasure to me.

That makes sense! It’s kind of reassuring to me to know that ASCII was being used in a context like this back then—I knew it was an old standard and quite space-efficient as things go, but I was also thinking about how a typical paragraph of English text takes like 512 bytes or something in ASCII nevertheless, so I wondered if maybe people used something else even more compact. It sounds like ultimately there’s probably just not that much text in a typical Channel F game though. :stuck_out_tongue: I suppose they can always put it in the manual.

3 Likes