• Advertisement
Sign in to follow this  

Text Based RPG help

This topic is 2283 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Ive been working through the Game Institute's C++ Module I and have completed almost all of the exercises except for adding multiple enemies to combat. Most of the features I did get operational I implemented in unorthodox ways(i.e. Store, Save Game, Load Game, and Random Encounter during rest)

What I'm looking for is some constructive criticism on how to improve the code and any tips on how to implement multiple enemies into the combat scenario. It advises to return a pointer to an array of enemies from Map::checkRandomCounter() then to update the player's attack function to accept an array of monsters. I can't seem to figure it out.

Also, attached is the code for the project.

EDIT: Went through the code again and fixed some bugs. I updated the .zip archive.

Share this post


Link to post
Share on other sites
Advertisement
Can someone please give me some tips on how to clean this up and make it more object-oriented?

Initially I tried to create an enter function within the store(i.e. store.enter(mainPlayer)) and pass a pointer to player into the function but that didn't work out as I am still struggling to grasp the concept of pointers.

Also random encounter during rest is pretty messy. I would have liked to create the the random encounter within the rest function but failed as I'm sure using pointers is the solution.

I'm not asking for someone to just hand me over a solution I would just like some tips on improvement and hopefully help grasping pointers and their mechanics. Thank you in advance.

Share this post


Link to post
Share on other sites
I'm a newbie or I would try to help you.

However, I've found using pastebin.com will make it easier for others to answer your question. Also, paste code here using the code tag. That might get you some answers! :)


EDIT: I'm playing your game now, by the way! :) Edited by Sharpe

Share this post


Link to post
Share on other sites
Sorry it took me so long to respond. I don't mean to bring up an old thread. I would just like to say thank you for your very educational post. It took me at least 5 reads to grasp it but I finally understand pointers and implemented them in my game loop, the result is a much clearer and more optimized one that I'm very grateful for. Thanks again.

Share this post


Link to post
Share on other sites
Went through the code today and cleaned it up a lot and commented it as much as I could to explain how everything works. Got the main source file game.cpp down to 96 lines.


// game.cpp
#include "Map.h"
#include "Player.h"
#include "Store.h"
#include "Display.h"
#include <cstdlib>
#include <iostream>
#include <fstream>
using namespace std;

int main()
{
////////////INITIALIZE GLOBAL VARIABLES////////////////////
//Initialize the game map, the player, and the display which contains functions used to display the various menus throughout the game..
//Also initialize an input variable to navigate the different menus of the game.
Map gameMap;
Player mainPlayer;
Display display;
int selection = 0;
////////////////////BEGIN START MENU/////////////////////
//Display the Start menu and prompt the user to load a previous game or start a new game.
display.Splash();
display.StartMenu();
cin >> selection;
cin.ignore();
if(selection == 2) {
//Load Game.
mainPlayer.loadGame();
gameMap.loadPlayerPos();
}
else {
//Create New Player
mainPlayer.createClass();
}
/////////////////////END START MENU /////////////////////////
/////////////////////////////////////////////////////////////
////////////////////BEGIN GAME LOOP//////////////////////////
/*Initialize control variable for main game loop.
If this variable is set true the game loop will exit.*/
bool done = false;
while( !done ) {
//Clear the screen.
system("CLS");
selection = 0;
// Each loop cycle we output the player position and a selection menu.
display.MainMenu(gameMap);
cin >> selection;
// Test the selection. If Selection == valid. Game Loop keeps going.
// If Selection != valid. Game loop restarts and prompts user for another selection.
if(!display.testSelection(selection, 1, 6))
continue;
//Initialize monster and set it to zero, in case the combat simulation is ran.
Monster* monster = 0;
switch( selection ) {
case 1://Player Moves
//Update players position on the game map.
gameMap.movePlayer();
/*If the Player has moved to the stores location on the game map they enter it.
Here the player can purchase weapons, items, and spells.
When the player is finished at the store he exits and returns to the game loop.*/
if( gameMap.getPlayerXPos() == 2 && gameMap.getPlayerYPos() == 3)
{
//Initialize store and enter it.
Store store;
store.enter(mainPlayer);
}
//If the player isnt at the stores location then test for a combat encounter.
else {
if(mainPlayer.combatSimulation( monster, mainPlayer, gameMap) >= 1)
done = true;
}
break;
case 2://Player Rests
if(mainPlayer.rest(monster, mainPlayer, gameMap) >= 1)
done = true;
break;
case 3://View Stats
mainPlayer.viewStats();
break;
case 4://View Map Landmarks.
mainPlayer.viewLandmarks();
break;
case 5://Save Game
mainPlayer.saveGame(gameMap.getPlayerXPos(), gameMap.getPlayerYPos());
break;
case 6://Quit
done = true;
break;
}
}
}
/////////////////END GAME LOOP///////////////////////////


Anymore comments or suggestions would be great. I'm still having some trouble with implementing the multiple enemies.
I'm not sure how I should go about making it balanced. If the player faces 3 enemies, should he have 3 attack turns? I'm just not sure.

An updated version of the game is attached to this post.
NOTE: The dungeon is not operational. It is part of a quest line I'm going to be implementing.

Share this post


Link to post
Share on other sites
NPHshreddar, is it possible for you to put your project somewhere else? Because I can't download it for some reason (the first 5 downloads were my unsuccessful attempts).
With regards to the game.cpp you put on the forum, you probably shouldn't put srand in the game loop.

Share this post


Link to post
Share on other sites

NPHshreddar, is it possible for you to put your project somewhere else? Because I can't download it for some reason (the first 5 downloads were my unsuccessful attempts).
With regards to the game.cpp you put on the forum, you probably shouldn't put srand in the game loop.


External Download Link

There ya go. Its hosted on fileswap.

Also, I removed the srand() function from the game loop and put it into the random function so it is called every time random is. Next feature is a quest line with some side quests. I'll be adding more map landmarks as well. I think I'm gonna be doing a redesign of the view map landmarks function to make more accurate and dynamic map. Ive also been considering consolidating all of the display functions for the store into a single function withtin the display class. We'll see.

Share this post


Link to post
Share on other sites
You should not call srand inside the while loop. That will mess up the logic behind the random functions. Just call it once at the beginning of main.

If the player should get three turns against three enemies doesn't really make sense to me. If he got himself into a gang fight, he must deal with it biggrin.gif

You could give the player a bonus turn if he does something smart or something, but these things is entirely up to you, the game creator

Share this post


Link to post
Share on other sites

Also, I removed the srand() function from the game loop and put it into the random function so it is called every time random is.


Don't do this.
It should be called only once before you start pulling values from the rand function

Share this post


Link to post
Share on other sites
You can reduce the repeated lines of code in the Map::checkRandomEncounter() function



Monster* Map::checkRandomEncounter()
{
int roll = Random(0, 20);

Monster* monster = 0;

if( roll <= 5 )
return 0;
else if(roll >= 6 && roll <= 10)
monster = new Monster("Orc", 20, 8, 200, 1, "Short Sword", 2, 7 , 20);
else if(roll >= 11 && roll <= 15)
monster = new Monster("Goblin", 12, 6, 100, 0, "Dagger", 1, 8, 10);
else if(roll >= 16 && roll <= 19)
monster = new Monster("Ogre", 28, 12, 500, 2, "Club", 3, 8, 45);
else if(roll == 20)
monster = new Monster("Orc Lord", 35, 15, 2000, 5, "Two Handed Sword", 5, 10, 200);

cout << "You encountered an " << monster->getName() << "!" << endl;
cout << "Prepare for battle!" << endl << endl;

return monster;
}


Maybe not a big deal but it reduces the chance of spelling errors smile.gif

Share this post


Link to post
Share on other sites
Your Player::rest function contains exactly the same logic as the combatSimulation. You should be able to reduce it to something like this:



int Player::rest(Monster* monster, Player& player, Map& map)
{
system("CLS");
cout << "Resting..." << endl;

mHitPoints = mMaxHitPoints;

return combatSimulation(monster, player, map);
}


That way, if you want to make some changes to the combatSimulation code, you have one place to do it, not two

Share this post


Link to post
Share on other sites
If you add constructors to the Range and Weapon structs



struct Range
{
Range(int low=0, int high=0)
: mLow(low), mHigh(high) {}

int mLow;
int mHigh;
};



struct Weapon
{
Weapon() // Default constructor
: mName("Empty"), mDamageRange(Range()), mPrice(0) {}

Weapon(const std::string& name, const Range& range, int price) // Parameterized constructor
: mName(name), mDamageRange(range), mPrice(price) {}

std::string mName;
Range mDamageRange;
int mPrice;
};


Then you should be able to get away with this in the Store constructor



Store::Store()
{
//Initialize Weapons.
mWeapons.push_back(Weapon("ShortBow", Range(4, 6), 200));
mWeapons.push_back(Weapon("Scimitar", Range(3, 8), 250));
mWeapons.push_back(Weapon("BattleAxe", Range(2, 9), 300));
mWeapons.push_back(Weapon("Halberd", Range(1, 10), 350));
mWeapons.push_back(Weapon("BastardSword", Range(5, 10), 500));
....
}


You should be able to do the same thing with Items and Spells

Share this post


Link to post
Share on other sites
You have a couple of these


cout << "ERROR: You don't have enough gold for that item!" << endl;


Not having enough gold is really not an error... unsure.gif

Share this post


Link to post
Share on other sites
You do this a lot:


int input = 0;
cin >> input;


This is a ticking bomb. If we type something invalid at the console this will halt our program. Very annoying for the game player.

I would make a bullet proof function specifically for this purpose


int getUserInput()
{
int input;
std::string line;
while(true) // Continue until we have a valid integer...
{
std::getline(std::cin, line); // Read a string. That way we can handle anything without crashing
std::stringstream ss(line); // Move the line into a stringstream object
if(ss >> input) // Convert the line into a integer
break; // The conversion was a success, end the loop
else
std::cout << "Invalid input: " << line << ". Try again" << std::endl;
}
return input;
}


I haven't tested this function but something along those lines should work. It requires the string, iostream and sstream headers.





The class name Player is a bit ambiguous. Is a player the guy playing the game or is it the character he is playing?
Maybe Character is a better name for this class.





Finally, I would consider making the different character races into separate classes inheriting from the character class.
This is a somewhat advanced feature though so I won't recommend you to do this unless you are familiar with inheritance and virtual functions.

Share this post


Link to post
Share on other sites
Wow, a lot of really great suggestions there, very much appreciated.
I've gone through and made quite a few large changes to the code, ill post an updated version when it's completed.

I'm a little stuck on implementing the getUserInput function though. First of all the program still behaves incorrectly on an invalid value. Say the menu has 4 options, I enter 5 as my input it passes 5 back and something unexpected happens like moving in a random direction, the wrong weapon is bought, or the wrong item is used, etc.

Secondly, whenever I go to the battle menu and use this function it immediately without any input displays invalid input . try again. I think it has to do with the newline character left in the input buffer from the previous menu after we hit enter. Having a hell of time trying tot fix this though.

Basically I need Input function for integers that takes two parameters. A low value and a high value. The users input can not be lower than low or higher than high, if it is then the function should re-prompt them for input.

Then I will need a second input function for Strings like the players name.

Share this post


Link to post
Share on other sites

Wow, a lot of really great suggestions there, very much appreciated.
I've gone through and made quite a few large changes to the code, ill post an updated version when it's completed.

I'm a little stuck on implementing the getUserInput function though. First of all the program still behaves incorrectly on an invalid value. Say the menu has 4 options, I enter 5 as my input it passes 5 back and something unexpected happens like moving in a random direction, the wrong weapon is bought, or the wrong item is used, etc.

Secondly, whenever I go to the battle menu and use this function it immediately without any input displays invalid input . try again. I think it has to do with the newline character left in the input buffer from the previous menu after we hit enter. Having a hell of time trying tot fix this though.

Basically I need Input function for integers that takes two parameters. A low value and a high value. The users input can not be lower than low or higher than high, if it is then the function should re-prompt them for input.

Then I will need a second input function for Strings like the players name.


Yes you are right. Mixing cin >>, and getline(cin, ...) causes all kinds of problems.

I managed to make a function that might do better in this situation, but more testing is needed (I don't trust it yet)
Also, reading a string from user had to become a separate function because of the arguments



template<class T>
T getUserNumber(T low, T high)
{
T t;
std::cin.sync();
while(!(std::cin >> t) || t < low || t > high)
{
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(),'\n');
std::cout << "Invalid input. Try again" << std::endl;
}
return t;
}

std::string getUserString()
{
std::string s;
std::cin.sync();
std::getline(std::cin, s);
return s;
}


I made the number function a template so it can be used to read doubles. You can remove this and make it a int only function if you want

Here is my test



int main()
{
using namespace std;
int i;
double d;
string s;

cin >> i;
cout << i << endl;

cout << "Enter a string: ";
s = getUserString();
cout << s << endl;

cout << "Enter a integer: ";
i = getUserNumber<int>(1, 3);
cout << i << endl;

cout << "Enter a decimal: ";
d = getUserNumber<double>(1.1, 3.3);
cout << d << endl;
}

Share this post


Link to post
Share on other sites
I showed you a Range constructor earlier taking two arguments (low, high)
This constructor allows us to provide 0, 1, or 2 arguments:


Range() // 0 -> 0
Range(5) // 5 -> 0
Range(1, 5) // 1 -> 5


As you can see, the constructor taking only one argument always results in a useless range.
We can fix this by giving the Range class three constructors instead



struct Range
{
Range() : mLow(0), mHigh(0) {}
Range(int high) : mLow(0), mHigh(high) {}
Range(int low, int high) : mLow(low), mHigh(high) {}

int mLow;
int mHigh;
};




Range() // 0 -> 0
Range(5) // 0 -> 5
Range(1, 5) // 1 -> 5


Just a little nitpicking...

Share this post


Link to post
Share on other sites
I spent quite a few hours working out some major bugs yesterday.. Finally arrived at this for the input of integers. It gets the job done and is relatively simple. Had to give it unsigned int for the types so I could compare the input integer to the size of a vector. Your string input function works so far, although i haven't used it more than once in the program. The info about the Range constructor has been noted.


unsigned int Player::getUserInput(unsigned int low, unsigned int high)
{
unsigned int input = 0;
while(true) // Continue until we have a valid integer...
{
cout &lt;&lt; "Selection: ";
cin &gt;&gt; input; // Read a string. That way we can handle anything without crashing
cin.ignore(); // Clear input buffer.
if(input &gt;= low &amp;&amp; input &lt;= high) // Test if integer is within the bounds.
break; // The integer is valid, end the loop.
cout &lt;&lt; "Invalid input: " &lt;&lt; input &lt;&lt; ". Try again" &lt;&lt; endl;
}
return input;
}


Also thanks to your suggestions the store code is a lot more simplified and easier to manage.


// Store.cpp
#include "Store.h"

Store::Store()
{
//Initialize Weapons.
mWeapons.push_back(Weapon("ShortBow", Range(4, 6), 200));
mWeapons.push_back(Weapon("Scimitar", Range(3, 8), 250));
mWeapons.push_back(Weapon("BattleAxe", Range(2, 9), 300));
mWeapons.push_back(Weapon("Halberd", Range(1, 10), 350));
mWeapons.push_back(Weapon("WarHammer", Range(5, 7), 450));
mWeapons.push_back(Weapon("BastardSword", Range(5, 10), 500));
//Initalize Items.
mItems.push_back(Item("HealingPotion", "Healing", Range(0, 0), Range(6, 8), Range(0, 0), 20));
mItems.push_back(Item("MagicPotion", "MagicRestore", Range(6, 8), Range(0, 0), Range(0, 0), 20));
mItems.push_back(Item("FireBallPotion", "Damage", Range(0, 0), Range(0, 0), Range(6, 8), 30));
//Initalize Spells.
mSpells.push_back(Spell("MagicMissle", "damage", Range(3, 6), Range(0, 0), 4, 200));
mSpells.push_back(Spell("MinorHeal", "heal", Range(0, 0), Range(4, 7), 4, 250));
mSpells.push_back(Spell("Absorb", "hybrid", Range(5, 5), Range(5, 5), 5, 350));
mSpells.push_back(Spell("ChainLightning", "damage", Range(6, 12), Range(0, 0), 6, 450));
mSpells.push_back(Spell("MeteorSwarm", "damage", Range(8, 10), Range(0, 0), 6, 500));
mSpells.push_back(Spell("FireBall", "damage", Range(4, 5), Range(0, 0), 6, 500));
mSpells.push_back(Spell("MajorHeal", "damage", Range(0, 0), Range(8, 10), 5, 750));
}

//Display the store and its contents and allow the player to browse the store and purchase items until they choose to exit.
void Store::enter(Player&amp; player, Disp&amp; disp)
{
//intialize variables for use in the store function.
bool done = false;
//Continue to display the store until the player is done.
while(!done)
{
//Clear the screen.
system("CLS");
//Display the main store menu.
disp.StoreMainMenu();
//Test for invalid selection.
int selection = player.getUserInput(1, 4);
//Display sub-menu based on user's selection.
switch(selection)
{
case 1://Buy Weapons.
{
disp.StoreWeaponMenu(mWeapons, player);
int s = player.getUserInput(0, mWeapons.size());
//Player buys the weapon if the selection was valid and the player has enough gold.
if(mWeapons[s].mPrice &lt;= player.getGold())
player.buyWeapon(mWeapons[s]);
//Player doesnt have enough gold and returns to the main store menu where they may exit.
else
cout &lt;&lt; "You dont have enough gold for that weapon!" &lt;&lt; endl; system("PAUSE");
//function has completed return to store main menu.
break;
}
case 2://Buy Items.
{
disp.StoreItemMenu(mItems, player);
//Test for invalid selection.
int s = player.getUserInput(0, 2);
//Player buys the item if the selection was valid and the player has enough gold.
if(mItems[s].mPrice &lt;= player.getGold())
player.buyItem(mItems[s]);
//Player doesnt have enough gold and returns to the main store menu where they may exit.
else
cout &lt;&lt; "You don't have enough gold for that item!" &lt;&lt; endl; system("PAUSE");
//function has completed return to store main menu.
break;
}
case 3://Buy Spells.
{
disp.StoreSpellMenu(mSpells, player);
//Test for invalid selection.
int s = player.getUserInput(0, mSpells.size());
//Player buys the spell if the selection is valid and they have enough gold.
if(mSpells[s].mPrice &lt;= player.getGold())
player.buySpell(mSpells[s]);
//Player doesn't have enough gold and is returned to the main store menu where they may exit.
else
cout &lt;&lt; "You dont have enough gold for that spell!" &lt;&lt; endl; system("PAUSE");
//function has completed return to store main menu.
break;
}
case 4://Exit Store.
{
//Set done to true. This will cause you to return out fo the store function back to the main game menu.
done = true;
break;
}
}
}
}


I'm going to try to get the player class looking this good. Its going to be quite the task though as it is almost 1,000 lines of code.

Once again, thanks for your suggestions, I'll be posting an updated version with more changes very soon.

Share this post


Link to post
Share on other sites
Thats great biggrin.gif

edit:
I posted a different version of the input function above, if you didn't notice.
If you get any problems you could check them out

Share this post


Link to post
Share on other sites
Here's an updated version with quite a few changes.

Some Changes:

New Start Menu.
Created a new data directory to store all text documents associated with the game.
Menus are now stored in vectors so I can add new menu options without having to edit anything. Also simplified the display function for them.
Some new weapons and spells.
Re-Wrote a ton of the player functions, too many to count. :lol:
Cleaned up the functions pertaining to item, spell, and weapon purchasing and use.
Got rid of Item-types and Spell-types as they were no longer necessary.
Too many changes to list. Find the rest in the source. :wink:

RPG Version 3: External Download Link

Sorry for the large file size. It has more than doubled since the last version. :huh:

Share this post


Link to post
Share on other sites
That's nice.
BTW you have a huge file called rpg.sdf in the source. It looks like a SQL database file. You can probably delete that since you are not using a database

Share this post


Link to post
Share on other sites
Why do you have a Monster pointer in game.cpp that you pass into the combatSimulation and rest methods? It's not used anywhere in that game loop and it's value never changes from 0. The new call within the method doesn't change the value of the pointer in game.cpp. When you pass in the Monster pointer it's making a copy of the pointer and putting it on the method's stack and that is what you are manipulating. You might as well just declare it in the combatSimulation method as a local.

Share this post


Link to post
Share on other sites

Why do you have a Monster pointer in game.cpp that you pass into the combatSimulation and rest methods? It's not used anywhere in that game loop and it's value never changes from 0. The new call within the method doesn't change the value of the pointer in game.cpp. When you pass in the Monster pointer it's making a copy of the pointer and putting it on the method's stack and that is what you are manipulating. You might as well just declare it in the combatSimulation method as a local.


Thank you for pointing that, It has been fixed. Monster is now a local variable inside the combat simulation.

@pulpfist

I deleted the database file and as soon as I opened the project in visual studio 2010 it created a new one and it gave the dialogue "Parsing include files..."

There was another inside the rpg sub-directory as well. I deleted it and I've only opened the project from the main directory and it hasn't created another in the rpg sub-directory yet.


On another note, I am working on implementing quests the player can receive from npc's. The player will have a quest log that he can check in the main menu similar to stats. The player will be able to receive quests from certain npc's they meet and will gain exp and and other possible rewards based on the difficulty of the quest. I'm working on the main quest line that will progress through the game. Ive already setup an inn where the player meets the first character in the main quest line. There will be additional side quests the player can pick up as well for extra exp and some hidden items.It will all come in good time.

I'm still haven't come up with a good way to adapt the battle system to handle multiple enemies and be more balanced. Any advice on this would be greatly appreciated. Thanks again.

Share this post


Link to post
Share on other sites

[color=#1C2837][size=2]I deleted the database file and as soon as I opened the project in visual studio 2010 it created a new one and it gave the dialogue "Parsing include files..."<br style="color: rgb(28, 40, 55); font-size: 13px; line-height: 16px; text-align: left; background-color: rgb(250, 251, 252); "><br style="color: rgb(28, 40, 55); font-size: 13px; line-height: 16px; text-align: left; background-color: rgb(250, 251, 252); ">[color=#1C2837][size=2]

There was another inside the rpg sub-directory as well. I deleted it and I've only opened the project from the main directory and it hasn't created another in the rpg sub-directory yet.

<br style="color: rgb(28, 40, 55); font-size: 13px; line-height: 16px; text-align: left; background-color: rgb(250, 251, 252); ">
[color=#1C2837][size=2]

[/quote]


[color=#1C2837][size=2]



[color=#1C2837][size=2]

Yea it's probably the database files created by intellisense.


[color=#1C2837][size=2]

You can safely delete those before you zip up the source code

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement