Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

42 Astoundingly Useful Scripts and Automations for the Macintosh

Work faster and more reliably. Add actions to the services menu and the menu bar, create drag-and-drop apps to make your Macintosh play music, roll dice, and talk. Create ASCII art from photos. There’s a script for that in 42 Astounding Scripts for the Macintosh.

Hunt the Wumpus: Conditional code and include files in SuperBASIC

Jerry Stratton, August 12, 2020

I smell a Wumpus

Uh oh… never bump a wumpus.

I never played Hunt the Wumpus back in the day. I bought my computer in the summer of 1980, and by then the game had become legendary both in its fame and in its obscurity. I never saw a copy of 101 BASIC Computer Games nor did I have access to the college computer networks that were its lair. So when I saw a version of it in the Best of Creative Computing Volume 1, I decided to type it in and see how it works.

It’s clearly designed for what would have been considered a wide output format a few years later. The longest string that gets printed is 56 characters long, “BOTTOMLESS PITS - TWO ROOMS HAVE BOTTOMLESS PITS IN THEM”, from the instructions.

That string, surprisingly, would have fit just fine on my first computer, a TRS-80 Model 1. The Model 1 had 64-characters per line. But most home computers used an unmodified television for output. That many characters per line would have been unreadable on those computers, so they had widths usually ranging from 32 to 401 characters per line.

Coming about two years before the first mass market home computer, this program was probably meant for university computers, and may well have used a teletype or printer for its output. Such output devices usually had 80 characters per line, but common printers did exist with 64 characters per line, and of course room was needed for margins. If you assume ten character margins, that leaves 60 characters for output, which comes very close to the 56 character maximum.

Also interesting: the listing was meant for much younger eyes than mine. The 227 lines of code are all on one page. You can see it on page 250 of the book at the Internet Archive.

Like most programs of its era, following the logic is like following a strand of spaghetti, although it’s not nearly the mass of text that programs like it would become when memory became limited.

So of course, after verifying that it worked in PC-BASIC, I decided to convert it to SuperBASIC and create a version that I could play on the Color Computer (Zip file, 12.0 KB).

The purpose of the game is to hunt the wumpus before it hunts you. You have very high-tech arrows that you can fire through multiple rooms, allowing you to shoot the wumpus from more than one room away. This is occasionally useful because if you shoot and miss, the wumpus might be disturbed and move into an adjacent room. If the wumpus moves into the room you’re in, it will eat you.

You also want to avoid pits—you die if you fall in them—and avoid the giant bats that will lift you and move you into another room. That other room might be one that contains a pit or the wumpus, and it also might be one that leads to a pit and/or the wumpus. Neither is a good situation, although the latter will at least give you a bit greater than a one-in-three chance of survival.

[toggle code]

  • # Hunt the Wumpus
  • # for the CoCo
  • # From Gregory Yob
  • # Creative Computing Best 1
  • # Jerry Stratton
  • # astoundingscripts.com
  • #path of arrow
  • dim %arrowLegs%(5)
  • #caves
  • dim %locations%(20,3)
  • dim %hazards%(6), %savedHazards%(6)
  • cls
  • wrap-center Welcome to
  • wrap-center \^LHunt\^Q the \^LWumpus\^Q
  • #IFDEF speech
    • #INCLUDE speech.bas
    • %speech$ = "Welcome to Hunt the Wumpus"
    • gosub speakSpeech
  • #ENDIFDEF
  • print
  • print "Instructions? (Y/N)";
  • gosub getYesNo
  • if %answer$="y" then gosub showInstructions
  • #set up cave
  • for %cave%=1 to 20
    • for %node%=1 to 3
      • read %locations%(%cave%, %node%)
    • next %node%
  • next %cave%
  • #dodecahedron vertices
  • data
  • 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
  • enddata
  • gosub newSetup
  • cls
  • wrap-center \^LHunt\^Q the \^LWumpus\^Q
  • # game loop
  • loop
    • %arrowCount% = 5
    • %playerLocation% = %hazards%(1)
    • %finished% = 0
    • loop
      • gosub displayLocation
      • gosub playerAction
      • on %action% gosub shoot, movePlayer
    • endloop unless (%finished% = 0)
    • if (%finished% = 1) then
      • %speech$ = "Hee hee hee..."
      • gosub gloatAndPrint
      • %speech$ = "The Wumpus'll getcha next time!!"
      • gosub gloatAndPrint
    • else
      • %speech$ = "Ha ha ha - you lose!"
      • gosub gloatAndPrint
    • endif
    • print "Same setup?";
    • gosub getYesNo
    • if (%answer$ = "y") then
      • gosub restoreSetup
    • else
      • gosub newSetup
    • endif
  • endloop
  • sub restoreSetup
    • for %hazard% = 1 to 6
      • %hazards%(%hazard%) = %savedHazards%(%hazard%)
    • next %hazard%
  • endsub
  • # locate hazards
  • # 1-player
  • # 2-wumpus
  • # 3,4-pits
  • # 5,6-bats
  • sub newSetup
    • for %hazard% = 1 to 6
      • loop
        • %hazardLocation% = rnd(20)
        • %previousHazard% = %hazard%-1
        • loop while (%previousHazard% > 0)
          • break if (%hazards%(%previousHazard%) = %hazardLocation%)
          • %previousHazard%--
        • endloop
      • endloop unless (%previousHazard% > 0)
      • %hazards%(%hazard%) = %hazardLocation%
      • %savedHazards%(%hazard%) = %hazards%(%hazard%)
    • next %hazard%
  • endsub
  • sub displayLocation
    • #clear out near hazards
    • for %nearHazard% = 1 to 3
      • %nearHazards%(%nearHazard%) = 0
    • next %nearHazard%
    • #collect hazards
    • for %hazard% = 2 to 6
      • for %node% = 1 to 3
        • if (%locations%(%playerLocation%, %node%) = %hazards%(%hazard%)) then
          • %nearHazards%((%hazard%+1)/2) = 1
        • endif
      • next %node%
    • next %hazard%
    • #hazard warnings
    • for %nearHazard% = 1 to 3
      • if (%nearHazards%(%nearHazard%) = 1) then
        • switch
          • case (%nearHazard% = 1)
            • %speech$ = "I smell a \^LWumpus\^Q!"
            • gosub gloatAndPrint
          • case (%nearHazard% = 2)
            • %speech$ = "I feel a draft."
            • gosub gloatAndPrint
          • case (%nearHazard% = 3)
            • %speech$ = "I hear bats!"
            • gosub gloatAndPrint
        • endswitch
      • endif
    • next %nearHazard%
    • print "You are in room ";%playerLocation%
    • print "Tunnels lead to ";
    • for %node% = 1 to 3
      • print %locations%(%playerLocation%, %node%);
    • next %node%
    • print
  • endsub
  • #option is returned in %action%
  • sub playerAction
    • print "\^LShoot or \^LMove?";
    • loop
      • a$=inkey$
    • endloop unless (a$<>"s" and a$<>"m")
    • print
    • if (a$="s") then
      • %action% = 1
    • else
      • %action% = 2
    • endif
  • endsub
  • sub shoot
    • loop
      • input "Number of rooms (1-5)";%legCount%
    • endloop unless (%legCount%<1 or %legCount%>5)
    • for %leg% = 1 to %legCount%
      • loop
        • %prompt$ = "Room #" + str$(%leg%)
        • gosub getNewRoom
        • %arrowLegs%(%leg%) = %newRoom%
        • break if (%leg% <=2)
        • break if (%newRoom% <> %arrowLegs%(%leg%-2))
        • wrap Arrows aren't that crooked. Try another room.
      • endloop
    • next %leg%
    • #shoot arrow
    • %arrowLocation% = %playerLocation%
    • for %leg% = 1 to %legCount%
      • %newLocation% = 0
      • for %node% = 1 to 3
        • if (%locations%(%arrowLocation%, %node%) = %arrowLegs%(%leg%)) then
          • %newLocation% = %arrowLegs%(%leg%)
        • endif
      • next %node%
      • if (%newLocation% = 0) then
        • #no tunnel for arrow
        • %speech$ = "You hear a ping..."
        • gosub gloatAndPrint
        • %arrowLocation% = %locations%(%arrowLocation%,rnd(3))
      • else
        • %arrowLocation% = %newLocation%
      • endif
      • switch
        • case (%arrowLocation% = %playerLocation%)
          • %speech$ = "Ouch! Arrow got you!"
          • gosub gloatAndPrint
          • %finished% = -1
          • return
        • case (%arrowLocation% = %hazards%(2))
          • %speech$ = "Aha! You got the Wumpus!"
          • gosub gloatAndPrint
          • %finished% = 1
          • return
      • endswitch
    • next %leg%
    • print "Missed"
    • gosub moveWumpus
    • if %finished% < 0 then return
    • #reduce arrow count
    • %arrowCount%--
    • if (%arrowCount% < 1) then
      • %finished% = -1
      • wrap You are out of arrows. You are defenseless in a dangerous environment.
      • pause .5
      • wrap-center You are dead.
    • endif
  • endsub
  • sub movePlayer
    • loop
      • %prompt$ = "Where to"
      • gosub getNewRoom
      • #legal move?
      • for %node% = 1 to 3
        • break if (%locations%(%playerLocation%, %node%) = %newRoom%)
      • next %node%
      • print "Not possible..."
    • endloop
    • %playerLocation% = %newRoom%
    • gosub roomResults
  • endsub
  • sub roomResults
    • switch
      • case (%playerLocation% = %hazards%(2))
        • %speech$ = "... oops! Bumped a wumpus!"
        • gosub gloatAndPrint
        • gosub moveWumpus
        • if %finished% < 0 then return
      • case (%playerLocation% = %hazards%(3) or %playerLocation% = %hazards%(4))
        • %speech$ = "Yyyiiiieeee . . . fell in pit!"
        • gosub gloatAndPrint
        • %finished% = -1
        • return
      • case (%playerLocation% = %hazards%(5) or %playerLocation% = %hazards%(6))
        • %speech$ = "Zap--super bat snatch!"
        • gosub gloatAndPrint
        • %speech$ = "Elsewhereville for you!"
        • gosub gloatAndPrint
        • %playerLocation% = rnd(20)
        • gosub roomResults
    • endswitch
  • endsub
  • sub moveWumpus
    • %wumpusMove% = rnd(4)
    • if (%wumpusMove% < 4) then
      • %hazards%(2) = %locations%(%hazards%(2), %wumpusMove%)
    • endif
    • if (%hazards%(2) = %playerLocation%) then
      • %speech$ = "Tsk tsk tsk-\^LWumpus\^Q got you!"
      • gosub gloatAndPrint
      • %finished% = -1
    • endif
  • endsub
  • sub getYesNo
    • loop
      • %answer$=inkey$
    • endloop unless (%answer$<>"y" and %answer$<>"n")
    • print
  • endsub
  • sub getNewRoom
    • loop
      • print %prompt$;
      • input %newRoom%
    • endloop unless (%newRoom% < 1 or %newRoom% > 20)
  • endsub
  • sub showInstructions
    • cls
    • wrap-center \^LHunt\^Q the \^LWumpus\^Q
    • print
    • wrap 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 a D&D player.)
    • pause
    • cls
    • wrap Two rooms have bottomless pits in them. If you go there, you fall into the pit & lose!
    • print
    • wrap Two other rooms have super bats. If you go there, a bat grabs you and takes you to some other room at random, which might be troublesome.
    • pause
    • cls
    • wrap The wumpus is not bothered by the hazards (he has sucker feet and is too big for a bat to lift). Usually he is asleep. Two things wake him up: your entering his room or your shooting an arrow. If the wumpus wakes, he might move one room or he might stay still. After that, if he is where you are, he eats you.
    • pause
    • cls
    • wrap Each turn you may move or shoot a crooked arrow. You have 5 arrows. You lose when you run out. Aim by telling the computer the rooms you want to shoot the arrow into. If the arrow can't go that way it moves at random to a next room.
    • pause
    • cls
    • wrap If the arrow hits the wumpus, you win.
    • print
    • wrap If the arrow hits you, you lose.
    • wrap When you are one room away from something dangerous, you will smell the wumpus, hear bats, or feel a draft from the pit.
    • pause
  • endsub
  • sub gloatAndPrint
    • print %speech$
    • #IFDEF speech
      • gosub speakSpeech
    • #ENDIFDEF
  • endsub
Bats, Drafts, and Wumpus

Bats are probably safer than drafts. Probably.

This uses a new feature from the latest version of superBASIC: the #INCLUDE directive. The speech cartridge subroutines are in a separate file, because they’re useful in more than one program. I store the file, “speech.bas” in .basic off of my home directory:

[toggle code]

  • //Speech/Sound Cartridge subroutines
  • //Jerry Stratton, astoundingscripts.com
  • #expects text in %speech$
  • sub speakSpeech
    • for i=1 to len(%speech$)
      • gosub waitForVoice
      • poke %soundPort%, asc(mid$(%speech$,i,1))
    • next i
    • gosub waitForVoice
    • #carriage return
    • poke %soundPort%, 13
    • pause .3
  • endsub
  • sub prepareSpeech
    • %soundInput% = &HFF00
    • %soundPort% = &HFF7E
    • %soundReset% = &HFF7D
    • #reset cart
    • poke %soundReset%, 1
    • poke %soundReset%, 0
    • //save values to disable cartridge
    • //to run a different cartridge on the MPI
    • %soundInput1% = peek(%soundInput%+1)
    • %soundInput3% = peek(%soundInput%+3)
    • %soundInput35% = peek(%soundInput%+35)
    • #enable sound multiplexer
    • poke %soundInput%+1,52
    • poke %soundInput%+3,63
    • poke %soundInput%+35,60
  • endsub
  • sub disableSpeech
    • #disable sound multiplexer
    • poke %soundInput%+1, %soundInput1%
    • poke %soundInput%+3, %soundInput3%
    • poke %soundInput%+35, %soundInput35%
  • endsub
  • sub waitForVoice
    • loop
      • %voiceReady% = peek(%soundPort%) and 128
    • endloop unless (%voiceReady% = 0)
  • endsub
  • #prepare voice
  • gosub prepareSpeech

Convert this to Extended Color BASIC, without speech capability, using:

  • superbasic cwumpus.txt > XWUMPUS.BAS

The archive (Zip file, 12.0 KB) comes with both the superBASIC code and the resulting CoCo Extended Color BASIC code, so you don’t have to have superBASIC to get it working. You will of course have to have either a CoCo or an emulator such as XRoar.

The archive, in fact, comes with three versions of WUMPUS. Besides the original WUMPUS.BAS from Creative Computing, it also includes CWUMPUS.BAS. I use XWUMPUS.BAS in the XRoar emulator. I use CWUMPUS.BAS on the Color Computer. The CWUMPUS version uses the speech synthesizer to gloat at you. Create this version using the “speech” switch:

  • superbasic cwumpus.txt --switch speech > CWUMPUS.BAS

This version of Hunt the Wumpus uses superBASIC’s #IFDEF directive to include or exclude speech synthesizer code from the final program. Without that switch, the BASIC code for the gloatAndPrint routine is:

  • 1500 REM GLOAT AND PRINT
  • 1510 PRINT SP$
  • 1520 RETURN

With that switch, the code is:

  • 1500 REM GLOAT AND PRINT
  • 1510 PRINT SP$
  • 1520 GOSUB 3300
  • 1530 RETURN

Where line 3300 begins the speakSpeech subroutine (in the #INCLUDE file which itself is inside an #IFDEF) to say whatever is in SP$:

  • 3300 REM EXPECTS TEXT IN SP$
  • 3310 FOR I=1 TO LEN(SP$)
  • 3320 GOSUB 3400
  • 3330 POKE SP, ASC(MID$(SP$,I,1))
  • 3340 NEXT I
  • 3350 GOSUB 3400
  • 3360 REM CARRIAGE RETURN
  • 3370 POKE SP, 13
  • 3380 FOR I=1 TO 138:NEXT I
  • 3390 RETURN

The 3400 routine that is GOSUBbed a couple of times is the waitForSpeech subroutine. It waits for the speech synthesizer to be ready for another letter:

  • 3400 REM WAIT FOR VOICE
  • 3410 VR = PEEK(SP) AND 128:IF VR = 0 THEN 3410
  • 3420 RETURN

Compare those routines, and the ease and readability of calling them, to their corresponding superBASIC lines at the bottom of my version of Hunt the Wumpus.

The #IFDEF capability is especially useful for making different versions of a program for different cartridges or other add-ons. As you can see in the code, the program uses a lot of superBASIC’s other functionality as well:

  • I use wrap, especially in the instructions, to automatically wrap the printed text for the Color Computer screen. The Color Computer is one of those early computers that has 32 characters per line, so it needs careful wrapping.
  • The main game loop becomes a lot more obvious, as do the subroutines. In the book, Gregory Yob recommends modifying the game in various ways; with the subroutines broken out that becomes a lot easier, should you take him up on the challenge.
  • It uses \L and \Q to highlight some of the text. Lowercase on the Color Computer is normally displayed in reverse video, and this was commonly used to highlight letters and words.

Good luck hunting the wumpus!

In response to TRS-80 Color Computer Programming Tools: The TRS-80 Color Computer was a fascinating implementation of the 6809 computer chip, and was, with from the Color Computer 1 through 3, possibly the longest-running of the old-school personal computers.

  1. The VIC-20 had a 22-character-wide screen!

  1. <- Reversi