14
\$\begingroup\$

I've made an iteration of the text game Hunt The Wumpus. The specification came from the text Programming Principles and Practice Using C++ by Bjarne Stroustrup.

Implement a version of the game "Hunt the Wumpus". Hunt the Wumpus" (or just "Wump") is a simple (non-graphically) computer game originally invented by Gregory Yob. The basic premise is that a rather smelly monster lives in a dark cave consisting of connected rooms. Your job is to slay the wumpus using bow and arrow. In addition to the wumpus, the cave has two hazards: bottomless pits and giant bats. If you enter a room with a bat, the bat picks you up and drops you into another room. If you enter a room with a bottomless pit, its the end of the game for you. If you enter the room with the Wumpus he eats you. When you enter a room you will be told if a hazard is nearby:

"I smell the wumpus": It´s in an adjoining room. "I feel a breeze": One of the adjoining rooms is a bottomless pit. "I hear a bat": A giant bat is in an adjoining room.

For your convenience, rooms are numbered. Every room is connected by tunnels to three other rooms. When entering a room, you are told something like " You are in room 12; there are tunnels to rooms 1,13, and 4: move or shoot?" Possible answers are m13 ("Move to room 13") and s13-4-3 ("Shoot an arrow through rooms 13,4, and 3"). The range of an arrow is three rooms. At the start of the game, you have five arrows. The snag about shooting is that it wakes up the wumpus and he moves to a room adjoining the one he was in - that could be your room. Be sure to have a way to produce a debug output of the state of the cave.

New to using pointers and containers of pointers - would love feedback on my limited use of them to keep track of connections between rooms!

Looking to improve whatever I can - all feedback is welcome!

#include <iostream>
#include <vector>
#include <limits>
#include <sstream>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ GLOBAL VARIABLES ~ ~ ~ ~ ~ ~ ~ ~ ~ ~

constexpr int max_connections = 3;
constexpr int num_rooms = 16;
constexpr int num_bats = 2;

int num_arrows = 5;

// ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ROOM ~ ~ ~ ~ ~ ~ ~ ~ ~ ~

struct Room {
// holds an occupant. carries a vector of pointers ("connections") to 3 other rooms.
    Room(int rn)
        : rn{ rn }, occ{ EMPTY } { }

    enum Occupant { EMPTY, BAT, PIT, WUMPUS };

    bool needs_connections() { return (connections.size() < max_connections); }
    bool is_connected_to(int);
    void add_connection(Room* r) { connections.push_back(r); }
    bool is_empty() { return (occ == EMPTY); }
    void set_occupant(Occupant o) { occ = o; }
    Occupant display();

    int rn;
    Occupant occ;
    std::vector<Room*> connections;
};

bool Room::is_connected_to(int i)
// returns whether this room is connected to another one, specified by an int
{
    for (Room* connection : connections)
        if (connection->rn == i) return true;
    return false;
}

bool all_rooms_conneted(std::vector<Room> rooms)
// runs through a vector of rooms and returns whether they're all connected
{
    for (int i = 0; i < rooms.size(); ++i)
        if (rooms[i].connections.size() < 3) return false;
    return true;
}

Room::Occupant Room::display()
{
    std::cout << "You are in room " << rn << std::endl;

    // Current Occupants
    if (occ == BAT) {
        std::cout << "There's a bat in here!" << std::endl;
        return BAT;
    }
    else if (occ == PIT) {
        std::cout << "There's a pit in here!" << std::endl;
        return PIT;
    }
    else if (occ == WUMPUS) {
        std::cout << "There's a wumpus in here!" << std::endl;
        return WUMPUS;
    }
    else if (occ == EMPTY) {
        std::cout << "The room is empty." << std::endl;

        std::cout << "It is connected to rooms ";
        for (Room* connection : connections)
            std::cout << connection->rn << " ";
        std::cout << std::endl << std::endl;

        // Neighboring
        for (Room* r : connections)
        {
            if (r->occ == BAT)
                std::cout << "There's a bat nearby!" << std::endl;
            if (r->occ == PIT)
                std::cout << "There's a pit nearby!" << std::endl;
            if (r->occ == WUMPUS)
                std::cout << "There's a wumpus nearby!" << std::endl;
        }
        std::cout << std::endl;
        std::cout << "You have " << num_arrows << " arrows." << std::endl;
        std::cout << std::endl;

        return EMPTY;
    }

}

// ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ CAVE ~ ~ ~ ~ ~ ~ ~ ~ ~ ~

struct Cave {
// holds and connects a group of rooms
    Cave(int);
    void populate(Room::Occupant, int);
    int find_empty_room();
    int find_location_of(Room::Occupant);
    void move_the_wumpus();
    void display_debug();

    std::vector<Room> rooms;
};

void Cave::populate(Room::Occupant o, int population)
{
    for (int i = 0; i < population; ++i)
    {
        int room_num;
        while (true)
        {
            room_num = rand() % rooms.size();
            if (rooms[room_num].is_empty()) break;
        }
        rooms[room_num].set_occupant(o);
    }
}

Cave::Cave(int number_of_rooms)
// Constructor
{
    // Generate rooms
    for (int i = 0; i < number_of_rooms; ++i)
    {
        Room r(i);
        rooms.push_back(r);
    }

    // Connect rooms
    while (!all_rooms_conneted(rooms))
    {
        for (int this_room = 0; this_room < rooms.size(); ++this_room)  // cycle through all rooms
        {
            if (rooms[this_room].needs_connections())   // if this room still needs connections
            {
                int connection = 0;
                while (true)    // try until an appropriate room is found
                {
                    connection = rand() % number_of_rooms;

                    if (this_room == connection) continue;
                    if (rooms[connection].connections.size() == max_connections) continue;
                    if (rooms[this_room].is_connected_to(connection)) continue;

                    break;
                }

                rooms[this_room].add_connection(&rooms[connection]);
                rooms[connection].add_connection(&rooms[this_room]);
            }
        }
    }

    // Populate with entities
    populate(Room::Occupant::BAT, 2);
    populate(Room::Occupant::PIT, 2);
    populate(Room::Occupant::WUMPUS, 1);
}

int Cave::find_empty_room()
{
    int empty_room = 0;
    while (true)
    {
        empty_room = rand() % num_rooms;
        if (rooms[empty_room].occ == Room::Occupant::BAT
            || rooms[empty_room].occ == Room::Occupant::PIT
            || rooms[empty_room].occ == Room::Occupant::WUMPUS) continue;
        break;
    }
    return empty_room;
}

int Cave::find_location_of(Room::Occupant o)
{
    int result;
    for (int i = 0; i < rooms.size(); ++i)
        if (rooms[i].occ == o) return i;
    return -1;
}

void Cave::move_the_wumpus()
{
    // move the wumpus
    int wumpus_location = find_location_of(Room::Occupant::WUMPUS);
    int new_room = -1;
    while (true)
    {
        new_room = find_empty_room();
        if (new_room != wumpus_location) break;
    }

    rooms[wumpus_location].set_occupant(Room::Occupant::EMPTY);
    rooms[new_room].set_occupant(Room::Occupant::WUMPUS);
}

void Cave::display_debug()
{
    for (int i = 0; i < rooms.size(); ++i)
    {
        std::cout << "Room " << i << ": ";
        if (rooms[i].occ == Room::Occupant::WUMPUS) std::cout << "WUMPUS" << std::endl;
        else if (rooms[i].occ == Room::Occupant::BAT) std::cout << "BAT" << std::endl;
        else if (rooms[i].occ == Room::Occupant::PIT) std::cout << "PIT" << std::endl;
        else std::cout << std::endl;
    }
}


// ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ TURN ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ 

std::string make_lower(std::string str)
// return a lowercase version of a string
{
    std::string result;
    for (int i = 0; i < str.length(); ++i)
        result += tolower(str[i]);
    return result;
}

bool compare(std::string input, std::initializer_list<std::string> list)
// compares a string, made lowercase, to other strings. return true if it matches any of them.
{
    std::string str = make_lower(input);

    bool result = false;
    for (std::string s : list)
    {
        if (str == s) result = true;
    }
    return result;
}

void putback_string(std::istream& is, std::string s)
// puts a string back into an input stream
{
    for (char c : s)
        is.putback(c);
}

struct Turn
{
    Turn()  // Parse Player Input
    {
        std::string input;
        std::getline(std::cin, input);
        std::istringstream is{ input };

        std::string first_word;
        is >> first_word;

        if (compare(first_word, { "m", "move", "g", "go", "goto", "t", "travel" })) move = 'm';
        if (compare(first_word, { "f", "fire", "s", "shoot", "a", "arrow" })) move = 'f';
        if (compare(first_word, { "d", "debug" })) move = 'd';

        for (char c; is >> c;)
        {
            if (c == '-') continue;
            if (isdigit(c))
            {
                is.putback(c);
                int i;
                is >> i;
                targets.push_back(i);
            }
        }

        std::cout << std::endl;
    }

    char move = ' ';
    std::vector<int> targets;
};


// ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ GAME LOOP ~ ~ ~ ~ ~ ~ ~ ~ ~ ~



int main()
{
    srand(time(NULL));

    while (true)
    {
        Cave c(num_rooms);

        // pick appropriate starting room
        int current_room = c.find_empty_room();

        // game loop
        bool gameon = true;
        while (gameon)
        {
            // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ display ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
            Room::Occupant current_occupant = c.rooms[current_room].display();


            // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ game logic ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
            if (num_arrows == 0)
            {
                std::cout << "You ran out of arrows! Now you can't defeat the wumpus!" << std::endl;
                break;
            }

            else if (current_occupant == Room::Occupant::EMPTY)
            {
                Turn player_turn;

                // MOVE
                if (player_turn.move == 'm')
                {
                    if (c.rooms[current_room].is_connected_to(player_turn.targets[0]))
                    {
                        current_room = player_turn.targets[0];
                        std::cout << "Moving to room " << player_turn.targets[0] << std::endl;
                    }
                    else
                        std::cout << "This room isn't connected to that one." << std::endl;
                }

                // FIRE ARROW
                else if (player_turn.move == 'f')
                {
                    for (int target : player_turn.targets)
                    {
                        if (c.rooms[current_room].is_connected_to(target))
                        {
                            std::cout << "You fired an arrow into room " << target << std::endl;

                            if (c.rooms[target].occ == Room::Occupant::WUMPUS)
                            {
                                std::cout << "You hit the wumpus! You win!" << std::endl;
                                std::cout << std::endl;
                                gameon = false;
                                break;
                            }
                            else
                            {
                                std::cout << "The arrow flew off into the dark, hitting nothing." << std::endl;
                                std::cout << std::endl;

                                c.move_the_wumpus();

                                std::cout << "The sound of the arrow stirred the wumpus from its sleep!" << std::endl;
                                std::cout << "The wumpus has moved to a new room!" << std::endl;
                                std::cout << std::endl;

                                --num_arrows;
                            }

                        }
                        else
                            std::cout << "This room isn't connected to that one." << std::endl;
                    }

                }

                // DEBUG
                else if (player_turn.move == 'd')
                {
                    c.display_debug();
                }
            }

            // EVENT
            else if (current_occupant == Room::Occupant::BAT)
            {
                std::cout << "You were picked up by the bat!" << std::endl;
                current_room = c.find_empty_room();
                std::cout << "It picked you up and flew you to room " << current_room << "!" << std::endl;
            }

            else if (current_occupant == Room::Occupant::WUMPUS)
            {
                std::cout << "The wumpus ate you!! Ahh!" << std::endl;
                gameon = false;
            }

            else if (current_occupant == Room::Occupant::PIT)
            {
                std::cout << "You fell into the pit!" << std::endl;
                gameon = false;
            }

            system("PAUSE");
            system("CLS");
        }

        std::cout << "Game Over" << std::endl;
        std::cin.get();

        std::cout << "Play again? (y/n): ";
        char input;
        std::cin >> input;
        if (input == 'n') break;

        system("CLS");
    }
}

\$\endgroup\$
10
\$\begingroup\$

Many of the member functions of Room can be const since they don't modify the object data (e.g., bool needs_connections() const).

all_rooms_conneted (which has a typo in the name) should take its parameter by const std::vector<Room> &rooms to avoid making a copy of the vector. Likewise for compare with its list parameter. compare can simply short circuit with return true rather than using result.

In Room::display, you have a series of if () { return; } else if () { return; } else statements that could do without the else. And since the conditions are checking one variable for certain values, you could use a switch. In addition, if occ is not one of the four values you check for, you don't return a value from this function resulting in Undefined Behavior. If you compile with the warning level cranked up you should get a warning from the compiler for this.

Cave::population can potentially end up in an infinite loop if the initial value for population is too large and all rooms get populated while still needing to add more.

When initializing rooms in the Cave constructor, you can use rooms.emplace_back(i); instead of creating a temporary room and copying it in.

The loop in Cave::move_the_wumpus can be rewritten using a do/while loop:

do {
    new_room = find_empty_room();
} while (new_room != wumpus_location);

putback_string is flawed. You can only put back one character, and the putback will fail on an input-only stream.

In the Turn constructor, you can avoid using is.putback of a single character (known to be a digit) by replacing those four lines with targets.push_back(c - '0');.

There are better random number sources in the <random> header that you should look in to.

In main, you can again replace a series of if/else with a switch on current_occupant.

You use gameon in some instances to end the game, but in another use a break. You should stick one for consistency. Also, rather than bool gameon = true; while (gameon), you can use a bool gameon = true; do { ... } while (gameon); loop.

Calling system("PAUSE"); is a bit nonportable. You can print a message and get a character, or just ask the player for their next move. The clear screen (again, using a system call) is unnecessary. Text games have a long history of being scrolling windows.

| improve this answer | |
\$\endgroup\$
10
\$\begingroup\$

Legacy C-style headers

These:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

are deprecated. Instead, use the C++-style headers:

#include <cstdio>
#include <cstdlib>
#include <ctime>

And prefix all the calls to the C functions with std::.

Possibility for scoped enum usage

You can use enum class Occupant to scope your enum. This way you can possibly reuse the names of the enum's members elsewhere (not that you would want to do that). You will be able to access the enum using Room::Occupant::<member>, like you already do in a lot of cases.

Inlining

You have made a lot of the relatively simple functions that aren't one-liners external. For performance reasons, you may consider defining these inside the class, where they will be inlined into the code.

Use range-for loops consistently, and with auto

In some places I see you not using range-for loops at all:

for (int i = 0; i < rooms.size(); ++i)
    if (rooms[i].connections.size() < 3) return false;

while in others you're not using auto and instead being explicit:

for (Room* connection : connections)
    if (connection->rn == i) return true;

Both of these should work great with range-for loops and auto &:

for (auto& r : rooms)
   if (r.connections.size() < 3) return false;

and:

for (auto &connection : connections)
    if (connection->rn == i) return true;

Don't use std::endl when you don't need to

There's no reason for you to explicitly flush the streams. A simple newline (\n) would do.

Portability

These:

system("PAUSE");
system("CLS");

are not portable. Instead, you can use this for system("PAUSE"):

std::cout << "Press any key to continue . . .\n";
std::cin.get();

and just remove system("CLS"); altogether. I'd rather not have my screen cleared when playing a game.

| improve this answer | |
\$\endgroup\$
5
\$\begingroup\$

A comment on the actual game logic rather than coding style: It looks like it's possible the cave might not be connected, meaning there would be no way for the player to ever reach the wumpus.

Generating a random but connected graph is a bit tricky, but there are various things you could try.

I think many traditional implementations always have 12 rooms, with the structure of the rooms and edges matching the corners and edges of a dodecahedron. The room numbers can be shuffled so that exploration is still tricky no matter how well a player knows the game.

You could first add rooms one by one with an initial connection to some previous random room, forming a tree graph. Then add more random connections like your current cave-building. This might still cause caves where, for example, two different sections are somewhat easy to get around but only two paths connect between them. But that doesn't sound terrible as long as the total size is reasonable.

| improve this answer | |
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.