Hunt the Wumpus

Loading

Hunt the Wumpus is a text-based adventure game developed by Gregory Yob in 1973. The original game was written BASIC was a big hit in the late 70’s. This version is a Python reinterpretation of the classic Basic Game. In this game, the player navigates through a series of connected caves arranged in the shape of a dodecahedron as they hunt the dreaded Wumpus.

Unlike the classic version, this game uses dictionaries, lists, and recursive programming to simulate the same feel as the original. I wanted to avoid using object-oriented programming for this version and stay with multiple functions. Based on Gregory Yob’s version, the original version had several gosubs in the code. To mimic that, I used functions and recursive calls.

The turn-based game continues until you kill the Wumpus, get killed yourself, or quit. The game is a classic, and I remember when it came out to the gaming community in the seventies, and it’s still fun to play even today.

Wumpus

I feel a draft....
You are in room: 9
Tunnels lead to rooms:  8 , 18 , 10
You have 5 arrows

Shoot, Move, Help, or Quit (S-M-H-Q): m
Where To: 10
You are in room: 10
Tunnels lead to rooms:  9 , 2 , 11
You have 5 arrows

Shoot, Move, Help, or Quit (S-M-H-Q): m
Where To: 2
Bats nearby....
You are in room: 2
Tunnels lead to rooms:  1 , 10 , 3
You have 5 arrows

Shoot, Move, Help, or Quit (S-M-H-Q): m
Where To: 1
ZAP -- Super Bat Snatch! Elsewhereville for you!!
You are in room: 1
Tunnels lead to rooms:  5 , 8 , 2
You have 5 arrows

Shoot, Move, Help, or Quit (S-M-H-Q): q
Good Bye.


In this article, I will break down the different functions to describe their intended usage and how they fit into the game. Click on the GitHub logo on the right to see the full python source code.

Search Function

The first function is the search function. I created this game version with lists that contain the adjacent rooms, bat and pit locations. To have simple method of returning if an opponent or hazard is located nearby or in the same room, this function would return true or false after a search is completed.

def search(my_list, location):
    # my basic list search. This function is used for all Boolean searches
    # regarding lists. We input a single integer, and if that integer is in
    # the list, return True, otherwise False.
    for i in range(len(my_list)):
        if my_list[i] == location:
            return True
    return False

The Shooting Function

This is one of the complex functions to write for this game. A simple arrow shooting from  your room to another connected room would be simple to code and execute. The original game had the fantastic crooked arrow. This arrow would allow the user to shoot around corners and travel up to five rooms away. What made this function difficult was mapping out the number of rooms and the intended rooms that the user wanted the arrow to travel. Once these are inputted, take that route through the dodecahedron and determine if the Wumpus is within the arrow’s flight path.

How I did this was to create a list to hold the rooms. I prepopulated the array with your location and added the user’s rooms to that list. In the end, I would end up with a list containing two to six rooms. Then that list is run through a while loop verifying that space is connected in the game board. If so, it then is checked to see if the Wumpus is in that room. This action is cycled through all of the inputted rooms until the last is finally checked. If the selected room is not attached to the room, it is deemed just to hit the wall.

def shooting_time(home, Wumpus, arrows, wumpus_dead):
    wumpus_dead = False
    you_died = False
    arrow_path = []
    flight_path = []
    x = 0
    # get the number of rooms that the user wants to have the arrow fly through
    number_rooms = int(builtins.input("Number of rooms(1 - 5) [0 - not shooting]: "))

    # get the rooms to the maximum of the number of rooms that the user
    # wants the arrow to fly through
    if number_rooms in range(0, 6):
        my_arrows = arrows - 1
        x = 0
        arrow_path.insert(0, home)
        while x != number_rooms:
            x += 1
            arrow_path.insert(x, int(builtins.input("Room #%d " % x)))

        # part two - the flight of the arrow.
        # now to figure out the flight path..
        # we will start with the home position.
        print("twaaaang!")
        if my_arrows < 1:
            print("You have used you last arrow.")

        # we want to start with our location. So we are going to
        # do a quick insert into the flight_path list to do a search
        # to see if the first room that the user selects is a valid one.

        x = -1
        # with the first room selected, is this room a valid room
        # for the flight path? If so, we will continue cycling through
        # the path.
        while x < number_rooms + 1:
            x += 1
            if x < number_rooms + 1:
                if x > 0:
                    print("The arrow is entering room # %d " % arrow_path[x])
                # This is where construct our list array that contains the adjacent rooms.
                flight_path.clear()
                flight_path.insert(0, game_board[arrow_path[x]]["room1"])
                flight_path.insert(1, game_board[arrow_path[x]]["room2"])
                flight_path.insert(2, game_board[arrow_path[x]]["room3"])

                # now, if the while loop is at the last room, this will check that room by itself.
                # I found that without it, there was a constant indexing issue.
                if x == number_rooms:

                    # did we hit the Wumpus?
                    if wumpus == arrow_path[x]:
                        print("You hear a howl in the distance.\nYou killed the Wumpus.")
                        return my_arrows, True, False
                    if home == arrow_path[x]:
                        # why did you shoot yourself?
                        print("Smack! Right in the backside.\nYou shot yourself!\nYou died.")
                        return my_arrows, False, True
                    else:
                        # nope, we just hit the wall
                        print("Smack right into the wall.")
                        return my_arrows, False, False
                else:
                    # now , we check the flight of the arrow to see if it hit the Wumpus or just a wall.
                    if not search(flight_path, arrow_path[x + 1]):
                        print("Smack right into the wall.")
                        return my_arrows, False, False
                    else:
                        if wumpus == arrow_path[x]:
                            print("You hear a howl in the distance.\nYou killed the Wumpus.")
                            return my_arrows, True, False

    elif number_rooms != 0:
        shooting_time(home, Wumpus, arrows, wumpus_dead)

    return my_arrows, wumpus_dead, you_died

The Bats

Where are the bats? The game has two rooms full of bats, and where they are. The bat’s location is stored in a list called bats. Since this is the first of the hazards that their locations are determined, they will be placed anywhere within the board.

def set_bats(bats):
    # we have two bats, so let's place them in the game.
    bat_one = random.randint(1, 20)
    bat_two = random.randint(1, 20)

    # Both bats should be in their own rooms. Let's make sure of that.
    if bat_one != bat_two:
        bats.clear()
        bats.insert(0, bat_one)
        bats.insert(1, bat_two)
    else:
        set_bats(bats)
    return bats

The Pits

Pits are the second of the hazards to be placed on the board. Since they are second, when they are placed, they are checked against the location of the bats. Bats and pits can’t be in the same room. If by chance, the pit is placed where a bat is located, the function is called again to put the pit in another location. This recursive call is done until all pits are not where the bats are already calling home.

def set_pits(pits, bats):
    # why do we have pits in the cave? I don't know, but its a hazard.
    # For the game, we have only two.
    pit_one = random.randint(1, 20)
    pit_two = random.randint(1, 20)

    # first check, the bats can't be where the pits are. That would be
    # too easy.
    if search(bats, pit_one) or search(bats, pit_two):
        set_pits(pits, bats)

    # also, the pits can't be in the same room.
    elif pit_one != pit_two:
        pits.clear()
        pits.insert(0, pit_one)
        pits.insert(1, pit_two)
    else:
        set_pits(pits, bats)
    return pits

The Wumpus Function

Now is the time to set the Wumpus up in its new home. To determine the location of the bats, pits, and even you are fed into this function. The Wumpus can’t be in the same room as the bats. It seems that they don’t get along. But, the Wumpus doesn’t mind the pits, so that check is ignored. But, the Wumpus is not allowed to be put in the same room as you. Initially, your location is set to zero, so the Wumpus can be anywhere in the other eighteen rooms. The check for your site comes in when the Wumpus moves from its room to another. Then the review is to ensure that the creature isn’t dumped on top of you.

def set_wumpus(home, pits, bats):
    # set a random location for the Wumpus.
    wumpus = random.randint(1, 20)

    # let's not set the Wumpus in the same room as the bats.
    if search(bats, wumpus):
        set_wumpus(home, pits, bats)

    # now to check to see that the new location for the Wumpus
    # is not on top of you.
    if wumpus != home:
        return wumpus
    else:
        set_wumpus(home, pits, bats)

The Home Function

Now, for you. Your location can be anywhere with no current hazards, and you can start in any of the sixteen open rooms. This function determines where and verifies that there are no hazards in the room that is selected for you.

def set_home(Wumpus, bats, pits, home):
    # where do we start? Let's see
    my_start = random.randint(1, 20)

    # first check, we don't want to be set right on top of bats or pits.
    if search(bats, my_start) or search(pits, my_start):
        set_home(Wumpus, bats, pits, home)

    # sometimes I have found that due to the recursive feature, my_start will
    # be set to None. We don't want that.
    if my_start is not None:
        return my_start
    else:
        set_home(Wumpus, bats, pits, home)

    # lastly, we don't want to be just set down upon the Wumpus.
    # let's give the user a chance.
    if my_start == wumpus:
        set_home(Wumpus, bats, pits, home)

    # also, the new room can't be the same as the previous.
    if my_start == home:
        set_home(Wumpus, bats, pits, home)

    return my_start

The Action Function


Now for some action. This function takes care of the action phase of the game. Since this game is designed as a turn-based game, this function is where the user selects the command of their choice. The options are Shoot, Move, Help or Quit. I added Help and Quit since the original game didn’t have these options.

When the user selects Move, the command is processed in this function. There are checks to see if the set room is connected to your current space, and if not, a simple error is returned.

Several variables are passed through this function, such as if you died or the Wumpus is dead. Their actions are handled in other functions, and since this function calls them, they are passed to the game loop in the primary function.

def action(home, room1, room2, room3, arrows, wumpus):
    wumpus_dead = False
    you_died = False

    # what do you want to do? Let's find out.
    my_action = builtins.input("Shoot, Move, Help, or Quit (S-M-H-Q): ")
    my_action = my_action.strip()  # get rid of any blanks
    my_action = my_action.lower()  # make everything lowercase. It's just easier to work with.
    if my_action == 'm':  # move
        new_location = int(input("Where To: "))
        if new_location == home:  # why do you want to move to the same room as you are already?
            print("Okay?")
        elif new_location == room1 or new_location == room2 or new_location == room3:
            home = new_location
        else:  # The user wants to move to a room that isn't connected or even exists.
            print("Where?")
            action(home, room1, room2, room3, arrows, wumpus)
    elif my_action == 'h':
        # get help. The instructions are in the other file in the instructions function.
        board.instructions()
    elif my_action == 's':
        # shooting time, but first do we have any arrows?
        if arrows < 1:
            print("You have already used your last arrow.")
            # I like recursive programming. If things fall out, just call the function again.
            action(home, room1, room2, room3, arrows, wumpus)
        else:
            # yes, we have arrows, and are ready to use them.
            arrows, wumpus_dead, you_died = shooting_time(home, wumpus, arrows, wumpus_dead)

    elif my_action == 'q':
        # Let's quit.
        print("Good Bye.")
        exit()
    else:
        action(home, room1, room2, room3, arrows, wumpus)

    return home, arrows, wumpus_dead, you_died

Checking for Baddies

Where are the baddies? Every cycle of the game, this function is called. Here is where we will process all of the hazards and where you are in the game. If any of the conditions are met, their actions will be processed. I placed the Wumpus first on the checklist since if, the Wumpus catches you, the game is over. Just like the original game, if you are in the same room as the Wumpus, there is a 25% chance that you scare the beast, and it flees to another room. Otherwise, it will eat you.

If you venture into a pit room, the game is over again. But, if the bats catch you, you are randomly placed in another room. There is a chance that you could be placed in a room with the Wumpus or even Pits, and you can never tell.

Another feature of this function is to identify if any of the adjacent rooms have a hazard. If it is present, several warnings are printed to help you out. The game does try to help you to win.

def check_for_baddies(wumpus, bats, pits, room1, room2, room3, home):
    dead = False  # are we dead yet?

    # wumpus
    # first check, is the Wumpus nearby?
    if wumpus == room1 or wumpus == room2 or wumpus == room3:
        print("I smell a Wumpus!")
    # is the Wumpus in the same room?
    elif wumpus == home:
        wake_wumpus = random.randint(1, 100)
        if wake_wumpus > 25:  # too bad, the odds were not in your favor.
            print("TSK TSK TSK - Wumpus Got You.\nGame Over.")
            return True, Wumpus
        else:
            # you scared the Wumpus. How is that possible.
            print("You bump an something and feel it run away from you.")
            wumpus = set_wumpus(home, pits, bats)

    # bats
    # Check for bats nearby...
    if search(bats, room1) or search(bats, room2) or search(bats, room3):
        print("Bats nearby....")
    # if true, then you are in the same room as bats.
    elif search(bats, home):
        print("ZAP -- Super Bat Snatch! Elsewhereville for you!!")
        home = set_home(wumpus, bats, pits, home)

    # pits
    # are there pits nearby? if so, where is the draft?
    if search(pits, room1) or search(pits, room2) or search(pits, room3):
        print("I feel a draft....")
    elif search(pits, home):
        # you walked in a pit. Too bad. Game over.
        print("YYYIIIEEEE  FELL IN A PIT!!\nGame Over.")
        dead = True

    return dead, Wumpus

Main Loop

Here we find the main loop of the game. When the game is started this function is called and the game board is set up and all of the pieces are put in play. Also within this function is the main game loop. It will continue to loop until you win, die, or quit the game.

def start_game(my_board):
    bats = []
    pits = []

    arrows = 5
    wumpus_dead = False
    bats = set_bats(bats)
    pits = set_pits(pits, bats)
    wumpus = set_wumpus(0, pits, bats)
    home = set_home(wumpus, bats, pits, 0)
    dead = False

    while not dead:
        if wumpus_dead:
            print("Hee Hee - The Wumpus will getcha next time!!")
            break

        dead, wumpus = check_for_baddies(wumpus, bats, pits, game_board[home]["room1"], game_board[home]["room2"],
                                         game_board[home]["room3"], home)
        if not dead:
            # print("bats: ", bats, "pits: ", pits, "home: ", home, "wumpus: ", wumpus)
            print("You are in room: %d" % home)
            print("Tunnels lead to rooms: ", game_board[home]["room1"], ",", game_board[home]["room2"], ",",
                  game_board[home]["room3"])
            print("You have %d arrows" % arrows)
            print("")
            home, arrows, wumpus_dead, dead = action(home, game_board[home]["room1"], game_board[home]["room2"],
                                                     game_board[home]["room3"], arrows, wumpus)


if __name__ == '__main__':
    print("Wumpus\n")
    game_board = board.dodecahedron
    start_game(game_board)


The program is separated into two parts. The first part that we already went through is the game, and the other is the game board. Here I create the dodecahedron and set up all the rooms from a preconfigured dictionary. The dictionary called dodecahedron contains all twenty rooms and their adjacent rooms. The only function within the board.py is the instructions of the game.

I could have placed all of this within the same python file but decided during writing that breaking the game board from the game was a decent choice. The following is the board.py contents.

# This is where we build the game board. The board is in the shape of a dodecahedron
# with 20 rooms. Each room connects to three other numbered rooms. The dictionary here
# lays out the relationship of each room and the connected rooms.
dodecahedron = {
    1: {
        "room1": 5,
        "room2": 8,
        "room3": 2
    },
    2: {
        "room1": 1,
        "room2": 10,
        "room3": 3
    },
    3: {
        "room1": 2,
        "room2": 12,
        "room3": 4
    },
    4: {
        "room1": 3,
        "room2": 14,
        "room3": 5
    },
    5: {
        "room1": 4,
        "room2": 6,
        "room3": 1
    },
    6: {
        "room1": 15,
        "room2": 5,
        "room3": 7
    },
    7: {
        "room1": 6,
        "room2": 17,
        "room3": 8
    },
    8: {
        "room1": 7,
        "room2": 1,
        "room3": 9
    },
    9: {
        "room1": 8,
        "room2": 18,
        "room3": 10
    },
    10: {
        "room1": 9,
        "room2": 2,
        "room3": 11
    },
    11: {
        "room1": 10,
        "room2": 19,
        "room3": 12
    },
    12: {
        "room1": 11,
        "room2": 3,
        "room3": 13
    },
    13: {
        "room1": 12,
        "room2": 20,
        "room3": 14
    },
    14: {
        "room1": 13,
        "room2": 4,
        "room3": 15
    },
    15: {
        "room1": 14,
        "room2": 16,
        "room3": 6
    },
    16: {
        "room1": 17,
        "room2": 15,
        "room3": 20
    },
    17: {
        "room1": 16,
        "room2": 7,
        "room3": 18
    },
    18: {
        "room1": 17,
        "room2": 9,
        "room3": 19
    },
    19: {
        "room1": 18,
        "room2": 11,
        "room3": 20
    },
    20: {
        "room1": 19,
        "room2": 13,
        "room3": 16
    }
}


def instructions():
    print("Instructions")
    print("The dreaded Wumpus lives in a cave of 20 rooms. Each room has three tunnels leading to other rooms.")
    print("The shape is a classic dodecahedron. Like all unexplored caves, there are hazards.\n")
    print("Bottomless Pits - Two of the rooms have bottomless pits in them. If you go venture there, you will")
    print("fall into the pit and lose.\n")
    print("Superbats - Two rooms have super bats. If you venture into these rooms, they will grab you and take")
    print("you to some other room at random, and where they take you is unknown.\n")
    print("The Wumpus - The Wumpus is a smelly beast that likes to sleep. It is not bothered by the hazards,")
    print("for he has sucker feet and is too big for the bats. But the Wumpus doesn't like to be awakened.")
    print("If you enter his room, there is a slight chance that he will move to another room or eat you.")
    print("If he eats you, you lose.\n")
    print("Your Weapon - You have come here with your trusted bow and five arrows. Not just five arrows, five")
    print("crooked arrows. These special arrows can fly around corners up to five rooms away. To shoot them, you")
    print("have to indicate what rooms to pass through. If the rooms are interconnected, it will continue to the")
    print("next. If the rooms are not connected, they will just hit the wall.\n")
    print("But you are the brave adventurer. You have come to these caves to Hunt the Wumpus. Armed with your bow")
    print("and a somewhat good sense of direction, you start your hunt.\n")
    print("Remember, if you hit the Wumpus, you win. If you shoot yourself, you lose.\n")


def cave(room):
    print(dodecahedron[room].values)

The game is very straightforward. I tried to keep the game according to the spirit of the game that Gregory Yob developed many years ago. It was cutting edge in the early 70s and still an exciting game today.

When I was writing this version of the game, the final question I asked myself was, is there anything that I would change? There are two alterations that I would make to make the game a little more complicated.

The first is to remove the ability of the arrows to “kill” the Wumpus as the arrow passes through a room. Just the last room that the user selects in the arrow’s flight would possibly be able to strike the Wumpus. As the game exists, you can shoot through all the rooms without moving.

The second is to put a miss calculation in the arrow. Currently, if the arrow enters the room that contains the Wumpus, it’s a guaranteed hit.

What changes would you make?

Add Comment

Your email address will not be published. Required fields are marked *