I have been writing Hunt the Wumpus game in C++, and i think it's finished. It's first non-trivial project, and it DOES work properly (at least 20 hours of debugging showed so). How can the code be more clean or simply better ? Rules are in the game.cpp.
Any criticism or suggestions are welcome.
game.cpp
#pragma once
#include "heart.h"
#include <iostream>
#include <ctime>
void rules()
{
{
std::cout << "Rules:" << std::endl
<< "1. You are in a cave system(a dodecahedron) such that each cave connects to three other caves.\n"
<< "2. Somewhere in the cave lives a dreaful monster - Wumpus.\nHe has sucker feet to cling to the walls of bottomless pits and is too heavy to be picked up by super - bats.\nHe also has has big teet thand will eat you if you are in the same room with him.\n"
<< "3. In the cave system, two caves are bottomless pits. Step in cave with one = you are scattered into pieces (Game Over).\n"
<< "4. Also, two caves contain super - bats, which will carry you off to a random cave.\nBats can carry you to any room, including Pit room and Wampus room, which will result in death (Game Over).\n"
<< "5. If the adjacent room to your position contains any of three events (wampus, bat, pit), you will be notified.\n"
<< "6. Only one instance of event can spawn at a room, but Wampus can move to both bat and pit rooms and remain there.\n"
<< "7. You have 5 arrows.\nWhen you think you know where the Wumpus is(or even if you don't) shoot an arrow into a room.\nIf you hit the Wumpus, he will die and you win the game (Game WON).\n"
<< "8. If you miss, the Wumpus, who is also a very light sleeper:\n1)Will wakeup and move(75%) to one of his adjucent rooms \n2) Will continue sleeping (25%). \nWampus can move in your room, so you would become a delicious snack (Game Over).\n"
<< "9. The arrows are magical, so you choose three destinations, knowing where they would fly.\n"
<< "10. If all arrows are gone, you are defencelence and therefore die of panic (Game Over).\n" << endl
<< " Notes: " << std::endl
<< "1. If Wumpus moved to a bat room, then player also moved to this room (containing both bat and wampus already) -\nbat logic activates, so you are saved...probably.\n"
<< "GOOD LUCK, YUMMY HUMAN...\n" << std::endl;
}
}
int main()
{
rules();
while (true)
{
//srand(time(0));
Heart heart;
heart.play();
std::cout << "Want to play again? y / n" << std::endl;
char answer;
cin >> answer;
if (answer == 'y') continue;
else if (answer == 'n') return static_cast<int>(Heart::EXIT);
}
}
heart.h
#pragma once
#include "Room.h"
#include "Player.h"
class Heart
{
public:
enum CONFIG { SHUT_DOWN = 1, EXIT, LOST, VICTORY, CONTINUE };
vector<vector<int> >roomsInitializeArray
{
//Room 1 Room 2 Room 3 Room 4 Room 5
{2,5,8}, {1,3,10}, {2,4,12}, {3,5,14}, {1,4,6},
//Room 6 Room 7 Room 8 Room 9 Room 10
{5,7,15}, {6,8,17}, {1,7,9}, {8,10,18}, {2,9,11},
// Room 11 Room 12 Room 13 Room 14 Room 15
{10,12,19}, {3,11,13}, {12,14,20}, {4,13,15}, {6,14,16},
// Room 16 Room 17 Room 18 Room 19 Room 20
{15,17,20}, {7,16,18}, {9,17,19}, {11,18,20}, {13,16,19},
};
Heart();
Player p;
void initializeRooms();
int validRoom();
void fillRooms();
void moveWumpus();
void batEncounter();
CONFIG playerMove(int);
CONFIG moveResult();
CONFIG playerShoot(int, int, int);
bool flyCheck(int, int);
CONFIG input();
CONFIG inputError();
void hints();
void debug();
CONFIG play();
};
Room.h
#pragma once
#include <vector>
using namespace std;
const int ROOMS = 20;
class Room
{
private:
static int roomCount;
int roomNumber;
bool player;
bool bat;
bool pit;
bool wumpus;
public:
vector<int>adjRooms = vector<int>(3);
Room(vector<int>);
bool hasBats();
bool hasPit();
bool hasWumpus();
bool hasPlayer();
int getRoomNumber();
bool empty();
void setBats(bool);
void setPit();
void setWumpus(bool);
void setPlayer(bool);
};
Player.h
#pragma once
class Player
{
int arrows = 5;
int currRoom;
public:
Player();
int currPlayer() const;
void setLocation(int);
void fired();
int getArrows();
};
Heart.cpp
#include "heart.h"
#include <random>
#include <iostream>
using namespace std;
vector<Room> labyrinth;
Heart::Heart()
{
initializeRooms();
fillRooms();
}
void Heart::initializeRooms()
{
//copy from hardcoded vector
for (int i = 0; i < ROOMS; i++)
{
// labyrinth[i] = roomsInitializeArray[i]; either way
Room newRoom = roomsInitializeArray[i];
labyrinth.push_back(newRoom);
}
}
int Heart::validRoom()
{
// checks random room for emptiness; if empty = return index, else recursion
int room = rand() % ROOMS;
if (labyrinth[room].empty())
return room;
else validRoom();
}
void Heart::fillRooms()
{
// set up everything (bool true in valid rooms)
p.setLocation(validRoom());
labyrinth[p.currPlayer()].setPlayer(true);
labyrinth[validRoom()].setBats(true);
labyrinth[validRoom()].setBats(true);
labyrinth[validRoom()].setPit();
labyrinth[validRoom()].setPit();
labyrinth[validRoom()].setWumpus(true);
}
void Heart::moveWumpus()
{
// 25% chance to move to one of three rooms or remain in the same
int tmp = rand() % 4;
int currWumpus;
for (int i = 0; i < ROOMS; ++i)
{
if (labyrinth[i].hasWumpus())
{
currWumpus = i;
break;
}
}
labyrinth[currWumpus].setWumpus(false);
if (tmp == 0 or tmp == 1 or tmp == 2)
{
labyrinth[labyrinth[currWumpus].adjRooms[tmp]].setWumpus(true);
cout << "Wampus moved to another room..." << endl;
}
else if (tmp == 3)
{
labyrinth[currWumpus].setWumpus(true);
cout << "Wampus felt no danger & didn't move to another room" << endl;
}
}
Heart::CONFIG Heart::inputError()
{
// Called if input is invalid. Clears buffer and passes continue instruction to play() loop (simply re-calls input()).
cout << "Wrong input" << endl;
cin.clear();
cin.ignore(32767, '\n');
return CONTINUE;
};
bool Heart::flyCheck(int target, int start)
{
// returns true if destination can be chosen
// may be odd, however used to simplify shootArrow()
return(target == labyrinth[start].adjRooms[0] || target == labyrinth[start].adjRooms[1] || target == labyrinth[start].adjRooms[2]);
}
Heart::CONFIG Heart::input()
{
// might be rewrited
// asks for input, checks if input is valid, finally laucnhes logic according to input ; if wrong - calls inputError()
cout << "Move(m) or Shoot(s) ?" << endl;
cout << "To terminate the game enter (T)" << endl;
char action;
cin >> action;
if (action == 'T') return SHUT_DOWN;
int room;
int target1;
int target2;
int target3;
if (action == 'm')
{
cout << "Where to ?" << endl;
cin >> room;
if (cin.fail()) inputError();
else return playerMove(--room); // f.e. player inputs room 1, but program works with it as room[0], therefore room is decremented by 1
}
else if (action == 's')
{
cout << "Where at ?" << endl;
cin >> target1;
if (cin.fail()) inputError();
--target1; // lowering by one just as the 'm' case
if (flyCheck(target1, p.currPlayer()))
{
cout << "Arrow in room # " << target1 + 1
<< ". Enter next destination room: " << labyrinth[target1].adjRooms[0] + 1 << ", " //+1 is made for proper cout (logic works with [index]
<< labyrinth[target1].adjRooms[1] + 1 << ", " // cout is index[1] (Room[0] = Room#1)
<< labyrinth[target1].adjRooms[2] + 1 << endl;
}
else inputError();
cin >> target2; // we get there only after we do flyCheck() with previous target
if (cin.fail()) inputError();
--target2; // do it for each cin
if (flyCheck(target2, target1))
{
cout << "Arrow in room # " << target2 + 1
<< ". Enter final destination room: " << labyrinth[target2].adjRooms[0] + 1 << ", "
<< labyrinth[target2].adjRooms[1] + 1 << ", "
<< labyrinth[target2].adjRooms[2] + 1 << endl;
}
else inputError();
cin >> target3;
if (cin.fail()) inputError();
--target3;
if (flyCheck(target3, target2)) return playerShoot(target1, target2, target3); // if all 3 targets are valid, perform a shot.
else inputError();
}
else inputError();
}
void Heart::hints()
{
// +1 is used for proper cout, Room[0] represented as Room # 1.
cout << "You are in the room # " << p.currPlayer() + 1 << endl;
cout << "Adjacent rooms are: " << labyrinth[p.currPlayer()].adjRooms[0] + 1 << " , " << labyrinth[p.currPlayer()].adjRooms[1] + 1 << " , " << labyrinth[p.currPlayer()].adjRooms[2] + 1 << endl;
cout << "You have " << p.getArrows() << " arrows left" << endl << endl;
// if more than two same objects, received only one message (f.e. two pit rooms = only one message)
if (labyrinth[labyrinth[p.currPlayer()].adjRooms[0]].hasBats() ||
labyrinth[labyrinth[p.currPlayer()].adjRooms[1]].hasBats() ||
labyrinth[labyrinth[p.currPlayer()].adjRooms[2]].hasBats())
cout << "You hear flapping wings..." << endl << endl;
if (labyrinth[labyrinth[p.currPlayer()].adjRooms[0]].hasPit() ||
labyrinth[labyrinth[p.currPlayer()].adjRooms[1]].hasPit() ||
labyrinth[labyrinth[p.currPlayer()].adjRooms[2]].hasPit())
cout << "You feel wind..." << endl << endl;
if (labyrinth[labyrinth[p.currPlayer()].adjRooms[0]].hasWumpus() ||
labyrinth[labyrinth[p.currPlayer()].adjRooms[1]].hasWumpus() ||
labyrinth[labyrinth[p.currPlayer()].adjRooms[2]].hasWumpus())
cout << "You feel the dread within..." << endl << endl;
}
Heart::CONFIG Heart::playerMove(int newLocation)
{
// This ?can? be somehow made into Player member function.
if (newLocation != labyrinth[p.currPlayer()].adjRooms[0] && newLocation != labyrinth[p.currPlayer()].adjRooms[1] && newLocation != labyrinth[p.currPlayer()].adjRooms[2])
{
cout << "Wrong movement!";// << labyrinth[p.currPlayer()].adjRooms[0] << ', ' << labyrinth[p.currPlayer()].adjRooms[1] << ', ' << labyrinth[p.currPlayer()].adjRooms[2] << endl;
return CONTINUE;
}
labyrinth[p.currPlayer()].setPlayer(false);
labyrinth[newLocation].setPlayer(true);
p.setLocation(newLocation);
moveResult();
}
void Heart::batEncounter()
{
// change player position randomly (no emptyness check)
int r = rand() % ROOMS;
labyrinth[p.currPlayer()].setPlayer(false);
labyrinth[p.currPlayer()].setBats(false);
labyrinth[r].setPlayer(true);
p.setLocation(r);
debug();
moveResult();
}
Heart::CONFIG Heart::moveResult()
{
// event reaction
if (labyrinth[p.currPlayer()].hasBats())
{
cout << "You moved to a room with Huge Bat.\nYou are flying! But where...\n" << endl;
batEncounter();
}
if (labyrinth[p.currPlayer()].hasPit())
{
cout << "You moved to a room with Pit.\nVZUH!\nYou are now many pieces.\n" << endl;
return LOST;
}
if (labyrinth[p.currPlayer()].hasWumpus())
{
cout << "You moved to a room with Wumpus.\nMhm... Yummy Human...\nYOU ARE EATEN\n" << endl;
return LOST;
}
return CONTINUE;
}
Heart::CONFIG Heart::playerShoot(int target1, int target2, int target3)
{
// shoots an arrow, checks if wampus was hit, WIN if true, else check if Wampus moved to player, LOST if true, else check remaining arrows, LOST if == 0, else continue play() loop
p.fired();
if (labyrinth[target1].hasWumpus() || labyrinth[target2].hasWumpus() || labyrinth[target3].hasWumpus())
{
cout << "Target practice!\nWampus dead!\nCongratulations!" << endl;
return VICTORY;
}
else
{
cout << "A miss... what a pity." << endl;
moveWumpus();
if (labyrinth[p.currPlayer()].hasWumpus())
{
cout << "YOUR ROOM!\nToo late to run, yummy human" << endl;
return LOST;
}
if (p.getArrows() == 0)
{
cout << "No more arrows...\nYou are defenceless!" << endl;
return LOST;
}
return CONTINUE;
}
}
void Heart::debug()
{
// only for debug, unused logic
for (int i = 0; i < ROOMS; ++i)
{
cout << "Room# " << i << ":\n";
cout << "Bats: " << labyrinth[i].hasBats() << endl;
cout << "Wumpus: " << labyrinth[i].hasWumpus() << endl;
cout << "Pits: " << labyrinth[i].hasPit() << endl;
cout << "Player: " << labyrinth[i].hasPlayer() << endl;
cout << endl << endl;
}
}
Heart::CONFIG Heart::play()
{
// check input results due to Heart::CONFIG enum, finishes game in LOST / VICTORY / SHUT_DOWN cases
while (true)
{
hints();
CONFIG result = input();
if (result == CONTINUE) continue;
else if (result == LOST)
{
cout << "YOU LOST" << endl << endl;
break;
}
else if (result == VICTORY)
{
cout << "YOU WON" << endl << endl;
break;
}
else if (result == SHUT_DOWN) return EXIT;
}
}
Room.cpp
#include "Room.h"
#include <vector>
using namespace std;
int Room::roomCount = 1;
Room::Room(vector<int> v) // accept vector in case of adjucent changes, however only works with dodecahedrone hardcode;
:player(false), bat(false), wumpus(false), pit(false),
adjRooms{v[0]-1, v[1]-1, v[2]-1}, roomNumber(roomCount++)//Off by one problem sollution //(initializing vector begins with Room#1 but [0])
{}
bool Room::hasBats()
{
return bat;
}
bool Room::hasPit()
{
return pit;
}
bool Room::hasWumpus()
{
return wumpus;
}
bool Room::hasPlayer()
{
return player;
}
int Room::getRoomNumber()
{
return roomNumber;
}
bool Room::empty()
{
return (!hasBats() && !hasPit() && !hasPlayer() && !hasWumpus());
}
void Room::setBats(bool status)
{
bat = status;
}
void Room::setPit()
{
pit = true;
}
void Room::setWumpus(bool status)
{
wumpus = status;
}
void Room::setPlayer(bool status)
{
player = status;
}
Player.cpp
#include "Player.h"
Player::Player()
:arrows(5), currRoom(-1) {}
int Player::currPlayer() const
{
return currRoom;
}
void Player::setLocation(int newLocation)
{
currRoom = newLocation;
}
void Player::fired()
{
arrows--;
}
int Player::getArrows()
{
return arrows;
}
As I see the improvements, they might be:
- Make labyrinth[x].method() into a function with accepted x as room number and method as method. However [x] is sometimes a function, and method() is often an adjRooms[], what is basically and integer.
- CONST, references& and pointers*, everywhere. Everything is being copied over and over again, it's a long work, but can be done (if it's better).
- As far as I know, the Interface should be separate with game logic, and input() function should NOT std::cout, however I have no idea how to do it (if it's needed of course).
BOTH CRITICISM AND RECOMMENDATIONS ARE HIGLY APPRECIATED