Hunt the Wumpus

GolfScript, 163

:n;:`"You shot the wumpus.
""The wumpus ate you.
""The pit swallowed you.
"{19:|rand}2*0|{[:,~,4%"ftvh"=.,+,@-]{20%}%}:^{;.^.+.3$?>"You feel a breeze.
"1$6"You smell a wumpus.
"4$8{$?-1>*p}2*'"#{'|):|';`head -1`}"'++~{3%}/={=3$=|{"Your shot missed.
"p@^3rand=@@}if}{=@;}if.[|4$6$]?.)!}do])=

The score is obtained by taking the byte count (290), adding the number of strings used for interaction with the user (6) and subtracting the combined length of those strings (133). The linefeeds are part of the strings and contribute to the byte count.

Milestones

  1. Ported professorfish's answer from Bash to GolfScript. Score: 269

  2. Acted on Peter Taylor's suggestions in the comments. Score: 250

  3. Peter Taylor refactored my entire code and helped me to compress the lookup table. Score: 202

  4. Replaced the lookup table of adjacent rooms with a mathematical approach. Score: 182

  5. Refactored input, output and the function supporting the mathematical approach. Score: 163

A big “Thank you!” goes to Peter Taylor for all his help.

How it works

The 20 rooms are represented as the vertexes of a dodecahedron, which have been assigned numbers from 0 to 19 in the following fashion:

Dodecahedral graph

To find the rooms which are adjacent to room N and order them in clockwise fashion, we have to consider four cases:

  • If N ≡ 0 mod 4 (blue vertexes), the adjacent room are 19 - N, N + 2 mod 20 and N - 2 mod 20.

  • If N ≡ 1 mod 4 (green vertexes), the adjacent room are 19 - N, N - 4 mod 20 and N + 4 mod 20.

  • If N ≡ 2 mod 4 (yellow vertexes), the adjacent room are 19 - N, N - 2 mod 20 and N + 2 mod 20.

  • If N ≡ 3 mod 4 (red vertexes), the adjacent room are, 19 - N, N + 4 mod 20 and N - 4 mod 20.

# The function “p” is implemented as “{`print n print}”. By storing an empty string in 
# “n” and nullifying “`”, “p” becomes an alias for “print”.

:n;:`

# Push the messages corresponding to the three possible outcomes of the game.

"You shot the wumpus.\n""The wumpus ate you.\n""The pit swallowed you.\n"

# Place the wumpus and the pit in randomly selected rooms different from room 19; place 
# the player in room 19, with his back to room 0.

{19:|rand}2*0|

# Function “^” takes a single number as its argument and returns an array of all the
# adjacent rooms to the room that number corresponds to.

{

  [

    :,~       # Store the room number in “,” and negate it ( ~N ≡ 19 - N mod 20 )

    ,4%       # Push the room number modulus 4.

    "ftvh"=   # If it is equal to 0|1|2|3, push 102|116|118|104 ≡ 2|-4|-2|4 mod 20.

    .,+,@-    # Determine the room number plus and minus the integer from above.

  ]{20%}%     # Take all three room numbers modulus 20.

 }:^

{             # STACK: Strings Pit Wumpus Previous Current Function|Index

  ;           # STACK: Strings Pit Wumpus Previous Current

  # Find the adjacent rooms to the current room, duplicate them and remove the rooms 
  # before the first occurrence of the previous room. Since the rooms are ordered in
  # clockwise fashion, the array of adjacent rooms will begin with the rooms 
  # corresponding to the following directions: “Back Left Right”

  .^.+.3$?>   # STACK: Strings Pit Wumpus Previous Current Adjacent

  # Push two more messages and their respective triggers.

  "You feel a breeze.\n"1$6"You smell a wumpus.\n"4$8

  # STACK: ... Pit Wumpus Previous Current Adjacent String Adjacent 6 String Adjacent 8

  # Do the following twice: Duplicate the nth stack element and check if it's present in 
  # the array of adjacent rooms. If so, print the string below it.

  {$?-1>*p}2*

  # Read one line (direction, action, LF) from STDIN. The counter “|” is needed so the 
  # result won't get cached.

  '"#{'|):|';`head -1`}"'++~

  {3%}/       # Replace 1|2|3|4|5|LF with their character codes modulus 3 (1|2|0|1|2|1).

  ={          # If the player shoots an arrow:

    =3$=      # Determine the specified room and check if it corresponds to the wumpus.

      |       # If it does, push and invalid room number ( | > 19 ).

      # If it does not, say so and move the wumpus to a randomly selected adjacent room.

      {"Your shot missed."p@^3rand=@@}

    if

  }{           # If the player moves:

    =@;        # Place him into the selected room.

  }if

  # STACK: Pit Wumpus Previous Current Invalid?

  # Determine if the player's current room number is either invalid, the wumpus's room
  # number or the pit's room number (first match).

  .[|4$6$]?

  # If there is no match, the index is -1 and incrementing and negating it yields “true”.

  # STACK: Strings Pit Wumpus Precious Current Invalid? Index Boolean

# Repeat loop is the boolean is falsy. If repeated, the first instruction of the loop 
# will pop the index.

}do      

# Consolidate the entire stack into an array. And pop its last element: the index.
# Replace the array with the element corresponding to that index.

])=

# GolfScript will execute “print n print”.

REV0 C++ (Visual Studio on Windows) 405

#include"stdafx.h"
#include<stdlib.h>
#include<time.h>
int main(){srand(time(NULL));char i,h=rand()%19,w=rand()%19,p=19,d=0,q,e,m[]="e@LwQMQOSOLT";while(p-h&&p-w){for(i=3;i--;){q=(p+m[p%4*3+i])%20;if(q==w)puts("you smell a wumpus");if(q==h)puts("you feel a breeze");}scanf_s("%d",&i);e=(d+i/10)*m[p%4]%3;q=(p+m[p%4*3+e])%20;if(i%5){if(q==w){puts("YOU KILLED THE WUMPUS!");h=p;}else{puts("arrow missed");w=(w+m[w%4*3+rand()%3])%20;}}else{p=q;d=e;if(p==h)puts("YOU FELL IN A HOLE!");}if(p==w)puts("THE WUMPUS GOT YOU!");}}

Below is a playthrough, demonstrating that (provided you don't start right next to a hazard) with correct play you can always win. The player feels a breeze, turns back and does a complete counterclockwise loop. As it takes him exactly 5 moves to feel a breeze again, he knows the hole to his right, and gets as far away as possible. Similarly, when he smells the wumpus, not knowing whether it is right or left, he turns back and does a clockwise loop. It takes him 5 moves to smell the wumpus again, so he knows it is to the left and shoots with certainty.

If he had looped the other way he would have found the wumpus sooner and known it was in the same direction he was turning.

enter image description here

REV1 C (GCC on Cygwin), 431-35% bonus = 280.15

#define u(t,s,c) if(t){puts(s);c;}
i,d,e,a,b;main(){srand(time(0));char q,p=19,h=rand()%p,w=rand()%p,*m="e@LwQMQOSOLT-\\/\n \v ";  
while(p-h&&p-w){
  for(i=3;i--;){q=(p+m[p%4*3+i])%20;u(q==w,"you smell a wumpus",a|=2<<p)u(q==h,"you feel a breeze",b|=1<<p)}
  for(i=20;i--;)printf("%c%c",i==p?m[d+12]:48+(a>>i&2)+(b>>i&1),m[i%4+15]);
  scanf("%d",&i);e=(d+i/10)*m[p%4]%3;q=(p+m[p%4*3+e])%20;
  if(i%5){u(q-w,"arrow missed",w=(w+m[w%4*3+rand()%3])%20;a=0)else u(1,"YOU KILLED THE WUMPUS!",h=p)}
  else{p=q;d=e;u(p==h,"YOU FELL IN A HOLE!",)}
  u(p==w,"THE WUMPUS GOT YOU!",)}}

Newlines added for clarity. The changes from Rev 0 are as follows:

A big thankyou to @Dennis for recommending GCC compiler on Cygwin Linux emulator for Windows. This compiler does not require the includes in the rev 0 program, and it allows default int type for variables and main. This is a life-changing golfing tip!

Additionally running in Linux means that \f does cause the cursor to move down without doing a carriage return (unlike in Windows where it just produces a printable symbol.) This has allowed considerable shortening of the printf statement that prints the board

Several additional tips from Dennis in the comments, and one of my own: change of condition when checking if arrow hit the wumpus: if(q==w)>if(q-w) (..else.. is reversed)

Addition of graphic display showing the information the player knows about where a wumpus is smelt / a breeze is felt to claim the 35% bonus. (I deleted the old debug version of this which showed the exact position of the wumpus and hole. It can be seen in the edit history.)

REV2 C (GCC on Cygwin), 389-35% bonus = 252.85

#define Q(N) (N+"QTLOQMQOSOLT"[N%4*3+e])%20
#define P printf(
i,d,e,a,b;main(){int p=19,q=srand(&p),h=rand()%p,w=rand()%p;
while(p-h&&p-w){
  for(e=3;e--;){q=Q(p);q-w||P"You smell a wumpus\n",a|=2<<p);q-h||P"You feel a breeze\n",b|=1<<p);}
  for(i=20;i--;)P"%c%c",i-p?48+(a>>i&2)+(b>>i&1):"-\\/"[d],"\n \v "[i%4]);
  scanf("%d",&i);e=(d+i/9)*"edde"[p%4]%3;q=Q(p);
  if(i%5){e=rand()%3;w=q-w?P"Your arrow didn't hit anything\n",a=0)&Q(w):(p=20);}
  else p=q,d=e;
}
P p-20?p-w?"YOU FELL IN A HOLE!\n":"THE WUMPUS GOT YOU!\n":"YOU KILLED THE WUMPUS!\n");}

Thanks again to Dennis for refactoring my code:

Char constant m[] replaced with literals (I didn't know you could index a literal.)

Seeding of random numbers with stack variable (system dependent, some systems randomise memory allocation as a security measure.)

Macro with puts replaced with a macro with printf and additional code which must be executed when the message displayed placed inside printf arguments (advantage taken of the face that printf does not print the last few arguments if there aren't enough format specfiers in the format string.) if replaced by ||

Calculation of new position of player/wumpus placed inside new macro.

Win/lose messages placed outside while loop. if replaced by conditional operator.

Use of conditional operator in line for shooting arrow. If the player misses, this requires both printing a message and adjusting wumpus position. Dennis offered a couple of ways of combining printf and the calculation of wumpus position into a single expression, but I have gone with one of my own. printf returns the number of characters printed, which for Your arrow didn't hit anything\n is 31 (11111 binary.) So, 31&Q(w)==Q(w).

My other contribution to this edit has been elimination of some unnecessary brackets.

Output

Here the player has already found where the Wumpus is, but chooses to do a thorough explore to find out exactly where the pit is, too. Unlike my old debug version which showed where the wumpus and pit were throughout the game, this shows only the rooms where the player has visited and felt a breeze (1) smelt the wumpus (2) or both (3). (If the player shoots an arrow and misses, the variable a containing the wumpus position info is reset.)

enter image description here

ICOSAHEDRON REPRESENTATION

Note: this section is based on rev 1

My star feature! There is no graph in my code. To explain how it works, see the world map below. Any point on the icosahedron can be represented by a latitude 0-3 and a longitude 0-4 (or a single number, long*4+lat.) The longitude line marked on the map passes only through those faces with longitude zero, and the latitude line passes through the centre of the faces with latitude zero.

The player can be oriented on 3 possible axes, represented by the symbols as follows: north-south- northeast-southwest\ northwest-southeast /. In any given room he has exactly one exit on each of these axes available to him. In the display shown the player makes a complete clockwise loop. It is generally easy to identify from the player marking where he came from, and therefore where he is allowed to go to.

The one case that is a little difficult for the uninitiated eye is the fourth one. When you see a slant in one of these polar rows, the player has come from the polar cell nearest the outside end of the slant and is facing generally towards the equator. Thus the player is facing southeast and his options are: 15(SOUTH, the cell to the right) 25(northEAST, the cell above) or 35(northWEST, the cell below.)

So, basically I map the icosahedron to a 5x4 grid, with cells numbered 19 to 0 in the order they are printed. The move is made by adding or subtracting from the current position, depending on the player's latitude and direction, per the table below.

If the player goes off the bottom (west) of the board, he comes back on the top (east) side and vice versa, so his position is taken modulo 20. Generally the moves are coded into m[] by adding ascii 80 (P) to the raw value giving the characters shown below, but principle any multiple of 20 can be added without affecting the operation.

Table of addition values for moves

Direction Symbol Latitude 0  1  2  3     Latitude 0 1 2 3

0, N-S      -             1 -1  1 -1              Q O Q O  
1, NE-SW    \            -4  1 -1  4              L Q O T
2, NW-SE    /             4 -3  3 -4              T M S L

The player's input (divided by 10 to remove the second digit) is added to his current direction and taken modulo 3 to get his new direction. This works fine in the majority of cases. However there is an issue when he is in a polar room and moves toward the pole. It will be clear when folding the map below that if he leaves the room facing "northeast" he will enter the new square facing "southeast" so a correction must be made. This is done in the line e=(d+i/10)*m[p%4]%3; by the multiplication by m[p%4]. The first four values of m[] are selected such that, in addition to their function above, they also have the characteristic m[1]%3==m[2]%3==1 and m[0]%3==m[3]%3==2. This leaves the direction alone for the equatorial rooms and applies the necessary correction for polar rooms.

The logical time to do the correction would be after the move. However to save characters it is done before the move. Therefore certain values in m[] must be transposed. So the last 2 characters are LT instead of TL per the table above for example.

enter image description here

UNGOLFED CODE

this is rev 1 code, which is less obfuscated than rev 2.

This will run on GCC / Linux. I have included in the comments the extra code needed to make it run on Visual studio / Windows. It's a big difference!

//Runs on gcc/linux. For visual studio / windows, change printf(...) 
//to printf(" %c%c%c",9*(i%4==1),i==p?m[d+12]:48+(a>>i&2)+(b>>i&1),10*!(i%2)) and uncomment the following lines
//#include"stdafx.h"
//#include<stdlib.h>
//#include<time.h>
//#pragma warning(once:996;once:430) //allow the use of scanf instead of scanf_s, allow default type=int. 
//Though rather than using the pragma, it is shorter to follow compiler recommendation and use scanf_s and int.

#define u(t,s,c) if(t){puts(s);c;}  //if(test){puts(string);additional code;}

i,     //player input, loop counter
d,e,   //current and proposed direction
a,b;   //bit flags for where wumpus smelt / breeze felt
 
main(){
    srand(time(0));
    char q,p=19,h=rand()%p,w=rand()%p,  //Initialise player, hole and wumpus. q stores proposed player position.
    *m="e@LwQMQOSOLT-\\/\n \f ";        //Chars 0-11: movetable. Chars 12-14:symbol for player. Chars 15-18: graphics format.   

    while(p-h&&p-w){

        // Print warnings
        for(i=3;i--;){q=(p+m[p%4*3+i])%20;u(q==w,"you smell a wumpus",a|=2<<p)u(q==h,"you feel a breeze",b|=1<<p)}
            
        // graphic display 
        for(i=20;i--;)printf("%c%c",i==p?m[d+12]:48+(a>>i&2)+(b>>i&1),m[i%4+15]);
        
        // Get player input and work out direction and room 
        scanf("%d",&i);
        e=(d+i/10)*m[p%4]%3;
        q=(p+m[p%4*3+e])%20;

        // i%5 is false if player inputs 5 (move player) otherwise true (shoot arrow) 
        if(i%5)
        {u(q-w,"arrow missed",w=(w+m[w%4*3+rand()%3])%20;a=0)else u(1,"YOU KILLED THE WUMPUS!",h=p)}
        else{p=q;d=e;u(p==h,"YOU FELL IN A HOLE!",)}
        u(p==w,"THE WUMPUS GOT YOU!",)
    }

}

ISSUES AND CURIOISITIES

I have taken advantage of the point mentioned by @professorfish, if the wumpus and pit start in random places, there is no need for the player to start in a random place. The player always starts in room 19 facing north.

I understand that as the wumpus is "unaffected by the pit" the wumpus can start in, or enter the room where the pit is. In general this simplifies things except for one point. I have no specific variable to indicate the game is over; it is over when the player coincides with the wumpus or pit. So when the player wins I display the winning message but move the pit to the player to kludge out of the loop! I can't put the player in the pit as the wumpus might be there and I would get a message about the wumpus that I don't want.

The rev0program worked perfectly in visual studio, but the IDE said "stack corrupted around variable i" on exit. This is because scanf is trying to put an int into a char. Dennis reported incorrect behaviour on his Linux machine because of this. Anyway it is fixed by use of the correct type in rev 1.

The line for displaying the board in rev 0 is clumsy and appears slightly different on other platforms. In printf(" %c%c%c") the middle %c is the printable character displayed. The last %c is either ASCII 0 or ASCII 10 (\n, newline with carriage return in Windows.) There appears to be no character in Windows that works in the console, that will go down a line without giving a carriage return. If there was I wouldn't need the first c% (ASCII 0, or ASCII 9 tab before latitude 1 character. Tabs are notoriously undefined in their behaviour.) The leading space improves formatting (puts latitude 3 & 2 characters nearer latitude 1 character.) Rev 1 has a revison of this line which uses a \f formfeed character and therefore needs no format character at the start of the printf. This makes it shorter, but the \f does not work in Windows.


GolfScript, 269 characters

{puts}:|;20,{;9{rand}:r~}$3<(:>"B:%d`w85>2n+Fup`y/>@D-=J7ldnx/W5XsLAb8~"{32-}%"`\24"{base}/3/{[.[~@].[~@]]}%:A=3r=0=:F;~:W;:P;{>A={0=F=}?:^P&!!{"You feel a breeze"|}*^W&!!{"You smell a wumpus"|}*'"#{'9.?r';STDIN.gets()}"'++~);(3%^=\4`={W={"Your arrow hit the wumpus"|0}{"Your arrow didn't hit anything"|W A=0=3r=:W>=.!\{"The wumpus catches you"|}*}if}{>:F;:>W=.!\{"You ran into the wumpus"|}*>P=.!\{"You fell into the pit"|}*&}if}do

Note that 163 was subtracted from the character count for the hard-coded strings. If you want debug output indicating the room numbers add the following line right after the first occurence of ^:

'  YOU 'F'->'>+++puts'  DIRECTIONS [BRL] '^`+puts'  PIT 'P+puts'  WUMPUS 'W+puts 

An example session (with additional debug output):

  YOU 6->11
  DIRECTIONS [BRL] [6 7 16]
  PIT 7
  WUMPUS 5
You feel a breeze
25
  YOU 11->16
  DIRECTIONS [BRL] [11 17 15]
  PIT 7
  WUMPUS 5
35
  YOU 16->11
  DIRECTIONS [BRL] [16 6 7]
  PIT 7
  WUMPUS 5
You feel a breeze
15
  YOU 11->6
  DIRECTIONS [BRL] [11 10 1]
  PIT 7
  WUMPUS 5
15
  YOU 6->10
  DIRECTIONS [BRL] [6 15 5]
  PIT 7
  WUMPUS 5
You smell a wumpus
14
Your arrow didn't hit anything
  YOU 6->10
  DIRECTIONS [BRL] [6 15 5]
  PIT 7
  WUMPUS 0
25
  YOU 10->5
  DIRECTIONS [BRL] [10 14 0]
  PIT 7
  WUMPUS 0
You smell a wumpus
24
Your arrow hit the wumpus