@* Wumpus Noir. \vskip 20pt Jessica Lloyd \vskip 12pt 2780847 \vskip 12pt jkl290 \vskip 12pt CSCI337 Assignment 2 - Literate programming \vskip 40pt I've decided to take some liberties with the narrative in the original {\it Hunt the Wumpus} game. It'll be a bit of fun for me and it won't make any true difference to the game itself. So I thought, why not? @* Introduction. The first thing I'm going to do is just outline the very basic layout of any C++ programme, much like I would in any other programming assignment. The beautiful thing here is that this is actually part of the literate paradigm. Excellent! @c @@; @@; @@; @ Very simple. The next thing I always do is set apart a place for any libraries I need to include, use the standard namespace, plus create a space for the function declarations. @= @@; using namespace std; @@; @ Since the game will need to use i/o, I'm going to include {\tt iostream}. @= #include @ And then I create the skeleton of the main programme, which I will fill in as we go. The while loop will keep replaying the game until the user chooses to quit. @= int main() { @@; @@; while(1) { @@; @@; } return 0; } @ I know that this is a very minimalistic starting point, but it always works for me. @* Welcome and instructions. I always like to start my programmes with a welcome message and since {\it Hunt the Wumpus} also has to enquire about whether the player would like the read the instructions, I think I'll take care of that part first. We need a character variable to hold their {\tt response}: @= char response; @ Now we need to greet the user, followed by prompting them for a response regarding whether they want to read the instructions. @= cout << "Welcome to Wumpus Noir" << endl << endl << "Do you want to read the instructions? (Y/N) "; @ It'll make life a bit easier if we ensure that the response character is in lower case, so we'll convert it before we do any work. To use the {\tt tolower()} function we have to include another library @= #include @ To interpret the user's input, we'll need a function. So let's make the function first. Let's have it return a boolean, T for yes and F for no. Also, we have to include Ian's data dump, (although this won't work unless we're in the game loop - otherwise there's be no data to dump) so I'll need to be able to access the hunter and hazards. First job is to read in the first character of their response and ignore any other rubbish they might have typed. @= bool YesOrNo(char response, int hunter, int hazard[5]) { cin >> response; cin.ignore(50, '\n'); response = tolower(response); if(response == 'd') { @@; } while(!(response == 'y' || response == 'n')) { cout << endl << "Incorrect input. Please enter Y or N: "; cin >> response; cin.ignore(50, '\n'); response = tolower(response); } if(response == 'y') return true; return false; } @ And we must not forget to declare it in the header or C++ will get upset with us. @= bool YesOrNo(char response, int hunter, int hazard[5]); @ Groovy. Now that we have {\tt YesOrNo()} defined we can go back and use it to check whether the user wanted to read the instructions and print them if necessary. @= if(YesOrNo(response, hunter, hazard)) { @@; } cout << endl; @ And now the final step for this section: writing up the actual instructions. This is going to be a bit long, a bit verbose, but bear with me. It'll explain the new narrative for the game - it's educational! @= cout << endl << "Yobtown used to be the prettiest city a man could dream of. That was before " << "the mafia had established itself. Before the boss, known only as 'the Wumpus', gained " << "his reputation as a man-eater. Colours seemed to fade as his influence extended; " << "nowadays there are only a handful of men at the Yobtown PD that aren't in the Wumpus' " << "pocket. You, Detective Hunter, are one of the few good guys left. You've managed to " << "keep a low enough profile to stay off the boss' hitlist and you still believe that " << "Yobtown could be restored to its former glory if only it could be rid of the Wumpus' " << "influence." << endl << endl << "Recent intel has led you to a labarynth of old warehouses in the seediest part of " << "Yobtown - your source says that the Wumpus fancies these abandoned structures for " << "dealing personally with individuals who have... fallen from his favour. However, the " << "Wumpus is not the only danger in this district: violent abductions are commonplace, " << "and one cannot forget the simple danger of testing the strength of decaying " << "floorboards in these ancient buildings. A man who always keeps his wits about him, " << "paying attention to warning signs, may make it out of this part of town alive." << endl << endl << "There are 20 warehouses, each of which has three alleys leading to three other " << "warehouses. You have just received a tip that the Wumpus is taking care of some " << "'personal' business tonight, and speed out to the labarynth of warehouses. When you " << "arrive, you flip the cylinder from your revolver and examine it: you are carrying five " << "rounds. You are an experienced marksman with a peculiar talent - you can control the " << "path of your bullet for up to four ricochets, meaning that if you focus you should be " << "able to fire into five neighbouring warehouses with a single bullet. However, you will " << "need to be frugal - here, an unarmed man is doomed." << endl << endl << "With some skill and a lot of luck, maybe you will be able to rid Yobtown of the Wumpus." << endl; @* Initialisation. Okay, whenever the game starts up, we will need the information initialised. This includes seeding a random number generator from the current time, creating the labarynth array, randomly placing the hunter and the hazards. Also, if I find the time I'll make it randomise the labarynth layout for each game, but since that's not pivotal I'll let it slide for now. First we need to include {\tt cstdlib} for the pseudorandom number generator and {\tt ctime} so that we can seed it from the current time. @= #include #include @ Now let's create the variables we need for the items I mentioned above. I'm going to use a 20x3 array to represent the labarynth, a much more efficient method than the 20x20 one I used in the fortran assignment. Actually, since this is global data I will make it a global constant in the header instead of a variable. @= const int LABARYNTH[3][20] = {{1, 0, 1, 2, 3, 0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 15}, {4, 2, 3, 4, 0, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 18}, {5, 13, 11, 9, 7, 14, 16, 4, 17, 3, 18, 2, 19, 1, 5, 19, 6, 8, 10, 12}}; @ Next, variables for the hunter, the hazards and the hunter's arrows. Also, we need something to store the last seed used for the random number generator so that we can replay the last game if we want to. These can all be integers. @= int hunter=-1, hazard[5], quiver = 5, seed; @ I set {\tt hunter} to -1 in the above so that I would have a signal for {\it Dump current data} to know that the data has yet to be initialised. @ Then we need to initialise all these. We will take the seed for the random number generator from the current time and seed the random number generator from it to set up the game. Unfortunately, during testing I realised that if I actually initialised these inside {\it Initialise} as I planned, then I would not be able to replay a game as {\it Initialise} recurs each time we start a new game and would get a new random number even if we didn't want to. For this reason, I'm going to initialise {\tt seed} and {\tt srand()} at the end of {\it Welcome} so it's got a value for the first game played. @= seed = time(0); srand(seed); @ The hunter should be placed first - that is, we need to assign it a number from 0 to 19. @= hunter = rand()%20; @ The hazards get placed next. So that the hunter has some kind of fighting chance, we won't allow the hazards to be placed in the same cave as the hunter. @= for (int i = 0; i < 5; i++) { hazard[i] = rand()%20; while(hazard[i] == hunter) hazard[i] = rand()%20; } @ It's going to make life a lot easier for people reading my code if I don't refer to the wumpus as {\tt hazard[0]} but {\tt hazard[WUMPUS]}, so let's set up some global constants for that. @= const int WUMPUS = 0; const int BAT1 = 1; const int BAT2 = 2; const int PIT1 = 3; const int PIT2 = 4; @ Now that we've established the format of the game data, we can dump it on command. @= cout << endl; if(hunter == -1) cout << "Data has not been initialised yet." << endl; else { cout << "Cave adjacencies:" << endl << " 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20" << endl << "--------------------------------------------------------------------------------" << endl; for (int i = 0; i < 3; i++) { for (int j = 0; j < 20; j++) { cout << setw(4) << LABARYNTH[i][j] + 1; } cout << endl; } cout << "The hunter is located in " << hunter+1 << endl << "The Wumpus is located in " << hazard[WUMPUS]+1 << endl << "The first bat is located in " << hazard[BAT1]+1 << endl << "The second bat is located in " << hazard[BAT2]+1 << endl << "The first pit is located in " << hazard[PIT1]+1 << endl << "The second pit is located in " << hazard[PIT2]+1 << endl; } @ For the above to work we'll need to be able to format the output, which requires another library. @= #include @* Taking a turn. Wow, I'm really enjoying this ability to write everything out of order. It's a good feeling. The next job at hand is to let the user take a turn. The user is going to keep taking turns while they're alive, so let's set up a variable to keep track of that little tidbit, and set it to true when the user starts the game. @= bool alive = true; @ Now, the game keeps running as long as the player is still alive. @= while (alive) { @@; } @ At the beginning of every turn, the user will receive warnings about any hazards that they might hear nearby. Then they will be asked whether they want to shoot or move. @= @@; @@; @@; cout << endl; @ First I want to deal with the {\it S or M} part of the turn. This will require a modified version of {\tt YesOrNo()}. This time the meaning of the bool is T for Shoot or F for Move. And once again, we need to be able to dump the game info upon request. The hunter and hazards will have to be arguments to this function since the dump procedure needs that information. @= bool ShootOrMove(char response, int hunter, int hazard[5]) { cin >> response; cin.ignore(50, '\n'); response = tolower(response); if(response == 'd') { @@; } while(!(response == 's' || response == 'm')) { cout << endl << "Incorrect input. Please enter S to shoot or M to move: "; cin >> response; cin.ignore(50, '\n'); response = tolower(response); } if(response == 's') return true; return false; } @ And of course the function declaration. @= bool ShootOrMove(char response, int hunter, int hazard[5]); @ Now with that stuff out of the way we can get into the important part of {\it Shoot or move}. Here we just need to prompt the user, get their response, and shoot or move accordingly. @= cout << "Do you take a shot (S) or move to an adjacent warehouse (M)? "; if(ShootOrMove(response, hunter, hazard)) { @@; } else { @@; } @ Printing the location is really easy with the 20x3 labarynth. Let's do that now. @= cout << "You are in warehouse " << hunter+1 << '.' << endl << "Alleys lead to warehouses " << LABARYNTH[0][hunter]+1 << ", " << LABARYNTH[1][hunter]+1 << ", and " << LABARYNTH[2][hunter]+1 << '.' << endl; @ The warnings require a bit more work. We'll need a loop to go through the three adjacent caves to the hunter, and then we'll have to check each one for all of the hazards. @= for (int i = 0; i < 3; i++) { @@; } @ So we have to check for the Wumpus first. @= if (LABARYNTH[i][hunter] == hazard[WUMPUS]) cout << "You can hear voices echoing up the alley from one of the adjacent warehouses: " << "a man begging, a gun cocking, cruel laughter." << endl; @ Then the superbats. @= if ((LABARYNTH[i][hunter] == hazard[BAT1]) || (LABARYNTH[i][hunter] == hazard[BAT2])) cout << "Squealing tyres and burning rubber come from one of the alleys nearby." << endl; @ And finally the bottomless pits. @= if ((LABARYNTH[i][hunter] == hazard[PIT1]) || (LABARYNTH[i][hunter] == hazard[PIT2])) cout << "The buildings around you groan and creak eerily." << endl; @* Shoot. When a player shoots, we need to ask them how far the shot goes. That means we need a variable to hold it. I'll make it a cstring so that it's easier to error-check. @= char distance[3]; @ Next, we have to iterate through {\tt distance} loops and ask where to shoot to each time. We'll need a cstring variable to hold the destination since it could be more than a single character. @= char destination[3]; @ We'll also need a variable to keep track of the location of the arrow as it travels. This will be set to the the hunter's location at the beginning of the procedure. @= int arrow; @ So let's build {\it Shoot}. @= arrow = hunter; cout << "Flipping open your revolver's cylinder, you double check - yes, you have " << quiver << " rounds left. You cock your weapon and take careful aim." << endl; quiver--; cout << "To how many warehouses will you shoot (1-5)? "; cin >> distance; cin.ignore(50, '\n'); @@; @ If the cave the player chooses is adjacent to the arrow's current position, the arrow successfully moves to the specified cave. If the cave the player chooses is not adjacent to the cave the arrow currently occupies, it must go into a random cave that {\it is} adjacent. In every leg of the arrow's travels we must check if the hunter has hit himself or the Wumpus. @= for(int i = 0; i < atoi(distance); i++) { if(i == 0) cout << "Shoot to warehouse: "; else cout << "Richochet to warehouse: "; cin >> destination; cin.ignore(50, '\n'); if (((atoi(destination)-1) == LABARYNTH[0][arrow]) || ((atoi(destination)-1) == LABARYNTH[1][arrow]) || ((atoi(destination)-1) == LABARYNTH[2][arrow])) { arrow = atoi(destination)-1; cout << "You've still got it! Your bullet zips right into warehouse " << arrow+1 << '.' << endl; @@; } else { arrow = LABARYNTH[rand()%3][arrow]; cout << "The darkness must have messed you up - do you know where you are? " << "Your bullet richochets from a wall and ends up in warehouse " << arrow+1 << '.' << endl; @@; } } @ We have to check on the results r each part of the shot. If the hunter and the arrow share a cave after this part of the shot, then the hunter dies. If the Wumpus and the arrow share a cave, then the hunter wins (and dies to end the game). @= if(hunter == arrow) { cout << "You explode in a kaleidoscope of pain as your own bullet hits you in the back! " << "Slumping to the ground, your extremeties start to numb as your blood funnels " << "from your wound, staining the decaying floorboards around you. Your eyes close " << "and you are relieved - you didn't want to live in Yobtown if the Wumpus " << "prevailed, anyway." << endl; alive = false; break; } else if (hazard[WUMPUS] == arrow) { cout << "A wet thunk echoes through the labarynth of warehouses as your shot hits home. " << "Gravelley shouting and a volley of gunfire follow. Creeping " << "to a window you see the Wumpus' men running towards their cars. After they " << "pull away, you go to the warehouse they just abandoned. The acrid scent of " << "spent gunpowder fills your nose. Splayed on the rotten " << "floorboards is the Wumpus in his rich italian suit, haloed in maroon ichor. " << "Surveying the room, you note an assortment of bodies, the last men to die " << "because of this boss. Lighting a cigraette, you walk back to your car. Even in " << "the dead of night, the colours of the city seem to be returning. The future " << "looks brighter." << endl; alive = false; break; } @ If the arrow doesn't do these things by the time the full shot is complete then it is a miss, and the Wumpus has a 75\% chance of waking up and moving to an adjacent cave - if the hunter is in it, he gets eaten. However, if the hunter is out of arrows, the wumpus will be garanteed to come and eat him! This is because Ian thought it was cruel of me to leave the unarmed hunter wandering the caves waiting for death in assignment 1. @= if (alive) { if (quiver < 1) { @@; } else if(rand()%4 < 1) { cout << "Your bullet richochets from the wall of the empty warehouse and buries " << "itself in the floorboards. The echo of the shot dies away without " << "incident; the Wumpus mustn't have noticed the sound." << endl; } else { cout << "Your bullet richochets from the wall of an empty warehouse and buries " << "itself in the floorboards. The echo of your shot rings through the " << "labarynth and you hear the Wumpus barking instructions to " << "his men to move their victim to one of their adjacent warehouses. " << "You must have spooked him." << endl; hazard[WUMPUS] = LABARYNTH[rand()%3][hazard[WUMPUS]]; if(hunter == hazard[WUMPUS]) { @@; } } } @ Okay, in the event that the hunter ends up in the same cave as the Wumpus(or if he's out of arrows), he gets dead. Let's write that part now. @= cout << "You hear some shuffling behind you and spin around. Are your eyes playing tricks on you? " << "No. You can definitely make out some shapes in the thick darkness of this warehouse. " << "Your stomach drops when you feel the cold kiss of a gun's muzzle at the nape of your " << "neck. " << endl << "\"Still seeking glory, Detective Hunter?\"" << endl << "The Wumpus steps forward from the gloom at the same time as his lackey latches onto your " << "shoulder with a vice-like grip. As you are shoved to your knees you bite your lips " << "closed; you're not going to give him the satisfaction of a response. " << endl << "You glare up at the man who single-handedly demolished all the good you had ever known. " << "He smiles down at you calmly, raising a finger to the man behind you. " << endl << "\"Ciao, Hunter.\"" << endl << "You've been Wumped." << endl; alive = false; @ In order to error-check the distance input, we'll need a little loop. @= while(!((atoi(distance) > 0) && (atoi(distance) < 6))) { cout << "You may be good, but there's no point taking stupid risks tonight. Choose a " << "distance between 1 and 5: "; cin >> distance; cin.ignore(50, '\n'); } @* Move. All right. We're moving forward slowly but surely (hah, get it?). Now it's time to let the hunter move. To do this, we need to prompt the player for the place to move to. We will need to error-check the response and once it's correct, we can allow the player to make the move. Then we have to deal with the consequences of moving. @= cout << "Which warehouse do you move to? "; cin >> destination; cin.ignore(50, '\n'); @@; hunter = atoi(destination)-1; @@; @ To error-check the destination, we need to interpret it and make sure it's one of the three adjacent caves to the hunter. If it's not, prompt the user for a new response. We'll also allow a data dump here. @= while(!(((atoi(destination)-1) == LABARYNTH[0][hunter]) || ((atoi(destination)-1) == LABARYNTH[1][hunter]) || ((atoi(destination)-1) == LABARYNTH[2][hunter]) )) { if(tolower(destination[0]) == 'd') { @@; } cout << "You must have become disoriented in the darkness." << endl; @@; cout << "Which warehouse do you move to? "; cin >> destination; cin.ignore(50, '\n'); } @ Now for the fun part: the consequences! After the hunter moves, we need to check whether he now shares a cave with the Wumpus, a superbat or a bottomless pit. If he does... well, he suffers the consequences. I'll do the superbats first and they'll be in a loop since they can theoretically drop you in the cave with another superbat. @= while((hunter == hazard[BAT1]) || (hunter == hazard[BAT2])) { cout << "As you make your way down the alley a black van, headlights out, screeches up " << "next to you. You're grabbed and dragged inside; a bag is shoved over your head " << "and men are yelling at you; something about heroin and whose turf you're on. " << "The van starts moving and they're beating you with what could only be " << "baseball bats. You try to yell, to tell them you're after bigger fish tonight, " << "but they don't hear. Suddenly the vehicle jerks to a halt and you are thrust " << "back out into the street outside another warehouse. You pick yourself back up, " << "slowly stretching your back up straight and testing your body after the beating. " << "It aches; you'll have to see the doctor; but that will have to wait until after " << "tonight. You move to go inside." << endl; if (hunter == hazard[BAT1]) hazard[BAT1] = rand()%20; else hazard[BAT2] = rand()%20; hunter = rand()%20; } @ Now we'll check if the hunter has fallen into a bottomless pit. @= if((hunter == hazard[PIT1]) || (hunter == hazard[PIT2])) { cout << "As you step into the warehouse, the floorboards whine beneath you. Too late you " << "realise what's happening - the ancient wood beneath your feet splinters and " << "breaks, and you fall through the floor. You land on your back, and on something " << "sharp. Something big and sharp. Something big enough to now be protruding from " << "your abdomen, glossy with your blood. Shuddering, you try to lift yourself up " << "off this makeshift manskewer. You cannot. Your sight clouds red and eventually " << "you relax; this was one way to escape the hell the Wumpus created in Yobtown." << endl; alive = false; } @ And last but not least, let's see if the hunter was stupid enough to walk in on the Wumpus and get eaten. @= else if(hunter == hazard[WUMPUS]) { @@; } @* Replay. Now that we've organised all of the actual game play, it's time to deal with replay. That is, when the game ends we have to ask the user if they would like to quit, and if they don't want to quit we have to ask whether they would like to replay the last game. So let's check if they want to quit first. @= cout << "Game over. Would you like to quit? (Y/N) "; if(YesOrNo(response, hunter, hazard)) { cout << endl << "See you next time, Detective Hunter." << endl; return 0; } @ Okay. If they say no, we'll ask them whether to replay the last game. If they say no, we need to get a new time seed so that we can seed the random number generator with a different value. Either way, we have to reseed the random number generator. We also mustn't forget to bring them back to life! @= cout << endl << "Would you like to replay the last game? (Y/N) "; if(!YesOrNo(response, hunter, hazard)) seed = time(0); srand(seed); alive = true; cout << endl; @ Note: I tried to randomise the cave structure for ach game and it was too hard - then when I tried to save my attempt to work on over the weekend, I accidentally deleted it! So the cave structure is fixed again, and I'm lucky to have this working backup to submit. Happy days. And that's all she wrote! @* Index.