|
17.x — Chapter 17 comprehensive quiz
By Alex on October 29th, 2016 | last modified by Alex on December 21st, 2020 Summary
Inheritance allows us to model an is-a relationship between two objects. The object being inherited from is called the parent class, base class, or superclass. The object doing the inheriting is called the child class, derived class, or subclass.
When a derived class inherits from a base class, the derived class acquires all of the members of the base class.
When a derived class is constructed, the base portion of the class is constructed first, and then the derived portion is constructed. In more detail:
- Memory for the derived class is set aside (enough for both the base and derived portions).
- The appropriate derived class constructor is called.
- The base class object is constructed first using the appropriate base class constructor. If no base class constructor is specified, the default constructor will be used.
- The initialization list of the derived class initializes members of the derived class.
- The body of the derived class constructor executes.
- Control is returned to the caller.
Destruction happens in the opposite order, from most-derived to most-base class.
C++ has 3 access specifiers: public, private, and protected. The protected access specifier allows the class the member belongs to, friends, and derived classes to access the protected member, but not the public.
Classes can inherit from another class publicly, privately, or protectedly. Classes almost always inherit publicly.
Here’s a table of all of the access specifier and inheritance types combinations:
Access specifier in base class |
Access specifier when inherited publicly |
Access specifier when inherited privately |
Access specifier when inherited protectedly |
Public |
Public |
Private |
Protected |
Private |
Inaccessible |
Inaccessible |
Inaccessible |
Protected |
Protected |
Private |
Protected |
Derived classes can add new functions, change the way functions that exist in the base class work in the derived class, change an inherited member’s access level, or hide functionality.
Multiple inheritance enables a derived class to inherit members from more than one parent. You should avoid multiple inheritance as much as possible.
Quiz time
For each of the following programs, determine what they output, or if they would not compile, indicate why. This exercise is meant to be done by inspection, so do not compile these (otherwise the answers are trivial).
a)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
#include <iostream> class Base { public: Base() { std::cout << "Base()\n"; } ~Base() { std::cout << "~Base()\n"; } }; class Derived: public Base { public: Derived() { std::cout << "Derived()\n"; } ~Derived() { std::cout << "~Derived()\n"; } }; int main() { Derived d{}; return 0; } |
Show Solution
Construction happens in order from most-Parent to most-Child. Destruction happens in the opposite order.
Base()
Derived()
~Derived()
~Base()
b)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
#include <iostream> class Base { public: Base() { std::cout << "Base()\n"; } ~Base() { std::cout << "~Base()\n"; } }; class Derived: public Base { public: Derived() { std::cout << "Derived()\n"; } ~Derived() { std::cout << "~Derived()\n"; } }; int main() { Derived d; Base b; return 0; } |
Hint: Local variables are destroyed in the opposite order of definition.
Show Solution
First we construct d, which prints:
Base()
Derived()
Then we construct b, which prints:
Base()
Then we destruct b, which prints:
~Base()
Then we destruct d, which prints:
~Derived()
~Base()
c)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
#include <iostream> class Base { private: int m_x; public: Base(int x): m_x{ x } { std::cout << "Base()\n"; } ~Base() { std::cout << "~Base()\n"; } void print() const { std::cout << "Base: " << m_x << '\n'; } }; class Derived: public Base { public: Derived(int y): Base{ y } { std::cout << "Derived()\n"; } ~Derived() { std::cout << "~Derived()\n"; } void print() const { std::cout << "Derived: " << m_x << '\n'; } }; int main() { Derived d{ 5 }; d.print(); return 0; } |
Show Solution
Doesn't compile, Derived::print() can't access private member m_x
d)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
#include <iostream> class Base { protected: int m_x; public: Base(int x): m_x{ x } { std::cout << "Base()\n"; } ~Base() { std::cout << "~Base()\n"; } void print() const { std::cout << "Base: " << m_x << '\n'; } }; class Derived: public Base { public: Derived(int y): Base{ y } { std::cout << "Derived()\n"; } ~Derived() { std::cout << "~Derived()\n"; } void print() const { std::cout << "Derived: " << m_x << '\n'; } }; int main() { Derived d{ 5 }; d.print(); return 0; } |
Show Solution
Base()
Derived()
Derived: 5
~Derived()
~Base()
e)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
#include <iostream> class Base { protected: int m_x; public: Base(int x): m_x{ x } { std::cout << "Base()\n"; } ~Base() { std::cout << "~Base()\n"; } void print() const { std::cout << "Base: " << m_x << '\n'; } }; class Derived: public Base { public: Derived(int y): Base{ y } { std::cout << "Derived()\n"; } ~Derived() { std::cout << "~Derived()\n"; } void print() { std::cout << "Derived: " << m_x << '\n'; } }; class D2 : public Derived { public: D2(int z): Derived{ z } { std::cout << "D2()\n"; } ~D2() { std::cout << "~D2()\n"; } // note: no print() function here }; int main() { D2 d{ 5 }; d.print(); return 0; } |
Show Solution
Base()
Derived()
D2()
Derived: 5
~D2()
~Derived()
~Base()
a) Write an Apple class and a Banana class that are derived from a common Fruit class. Fruit should have two members: a name, and a color.
The following program should run:
|
int main() { Apple a{ "red" }; Banana b{}; std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n"; std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n"; return 0; } |
And produce the result:
My apple is red.
My banana is yellow.
Show Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
#include <iostream> #include <string> class Fruit { private: std::string m_name; std::string m_color; public: Fruit(const std::string& name, const std::string& color) : m_name{ name }, m_color{ color } { } const std::string& getName() const { return m_name; } const std::string& getColor() const { return m_color; } }; class Apple: public Fruit { public: Apple(const std::string& color="red") : Fruit{ "apple", color } { } }; class Banana : public Fruit { public: Banana() : Fruit{ "banana", "yellow" } { } }; int main() { Apple a{ "red" }; Banana b; std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n"; std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n"; return 0; } |
b) Add a new class to the previous program called GrannySmith that inherits from Apple.
The following program should run:
|
int main() { Apple a{ "red" }; Banana b; GrannySmith c; std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n"; std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n"; std::cout << "My " << c.getName() << " is " << c.getColor() << ".\n"; return 0; } |
And produce the result:
My apple is red.
My banana is yellow.
My granny smith apple is green.
Show Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
#include <iostream> #include <string> class Fruit { private: std::string m_name; std::string m_color; public: Fruit(const std::string& name, const std::string& color) : m_name{ name }, m_color{ color } { } const std::string& getName() const { return m_name; } const std::string& getColor() const { return m_color; } }; class Apple: public Fruit { // The previous constructor we used for Apple had a fixed name ("apple"). // We need a new constructor for GrannySmith to use to set the name of the fruit protected: // protected so only derived classes can access Apple(const std::string& name, const std::string& color) : Fruit{ name, color } { } public: Apple(const std::string& color="red") : Fruit{ "apple", color } { } }; class Banana : public Fruit { public: Banana() : Fruit{ "banana", "yellow" } { } }; class GrannySmith : public Apple { public: GrannySmith() : Apple{ "granny smith apple", "green" } { } }; int main() { Apple a{ "red" }; Banana b; GrannySmith c; std::cout << "My " << a.getName() << " is " << a.getColor() << ".\n"; std::cout << "My " << b.getName() << " is " << b.getColor() << ".\n"; std::cout << "My " << c.getName() << " is " << c.getColor() << ".\n"; return 0; } |
Challenge time! The following quiz question is more difficult and lengthy. We're going to write a simple game where you fight monsters. The goal of the game is to collect as much gold as you can before you die or get to level 20.
Our program is going to consist of 3 classes: A Creature class, a Player class, and a Monster class. Player and Monster both inherit from Creature.
a) First create the Creature class. Creatures have 5 attributes: A name (std::string), a symbol (a char), an amount of health (int), the amount of damage they do per attack (int), and the amount of gold they are carrying (int). Implement these as class members. Write a full set of getters (a get function for each member). Add three other functions: void reduceHealth(int) reduces the Creature's health by an integer amount. bool isDead() returns true when the Creature's health is 0 or less. void addGold(int) adds gold to the Creature.
The following program should run:
|
#include <iostream> #include <string> int main() { Creature o{ "orc", 'o', 4, 2, 10 }; o.addGold(5); o.reduceHealth(1); std::cout << "The " << o.getName() << " has " << o.getHealth() << " health and is carrying " << o.getGold() << " gold.\n"; return 0; } |
And produce the result:
The orc has 3 health and is carrying 15 gold.
Show Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
#include <iostream> #include <string> #include <string_view> // Requires C++17 class Creature { protected: std::string m_name; char m_symbol; int m_health; int m_damage; int m_gold; public: Creature(std::string_view name, char symbol, int health, int damage, int gold): m_name{ name }, m_symbol{ symbol }, m_health{ health }, m_damage{ damage }, m_gold{ gold } { } const std::string& getName() const { return m_name; } char getSymbol() const { return m_symbol; } int getHealth() const { return m_health; } int getDamage() const { return m_damage; } int getGold() const { return m_gold; } void reduceHealth(int health) { m_health -= health; } bool isDead() const { return m_health <= 0; } void addGold(int gold) { m_gold += gold; } }; int main() { Creature o{ "orc", 'o', 4, 2, 10 }; o.addGold(5); o.reduceHealth(1); std::cout << "The " << o.getName() << " has " << o.getHealth() << " health and is carrying " << o.getGold() << " gold.\n"; return 0; } |
b) Now we're going to create the Player class. The Player class inherits from Creature. Player has one additional member, the player's level, which starts at 1. The player has a custom name (entered by the user), uses symbol '@', has 10 health, does 1 damage to start, and has no gold. Write a function called levelUp() that increases the player's level and damage by 1. Also write a getter for the level member. Finally, write a function called hasWon() that returns true if the player has reached level 20.
Write a new main() function that asks the user for their name and produces the output as follows:
Enter your name: Alex
Welcome, Alex.
You have 10 health and are carrying 0 gold.
Show Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
|
#include <iostream> #include <string> class Creature { protected: std::string m_name; char m_symbol; int m_health; int m_damage; int m_gold; public: Creature(std::string_view name, char symbol, int health, int damage, int gold): m_name{ name }, m_symbol{ symbol }, m_health{ health }, m_damage{ damage }, m_gold{ gold } { } const std::string& getName() const { return m_name; } char getSymbol() const { return m_symbol; } int getHealth() const { return m_health; } int getDamage() const { return m_damage; } int getGold() const { return m_gold; } void reduceHealth(int health) { m_health -= health; } bool isDead() const { return m_health <= 0; } void addGold(int gold) { m_gold += gold; } }; class Player : public Creature { int m_level{ 1 }; public: Player(std::string_view name) : Creature{ name, '@', 10, 1, 0 } { } void levelUp() { ++m_level; ++m_damage; } int getLevel() const { return m_level; } bool hasWon() const { return m_level >= 20; } }; int main() { std::cout << "Enter your name: "; std::string playerName; std::cin >> playerName; Player p{ playerName }; std::cout << "Welcome, " << p.getName() << ".\n"; std::cout << "You have " << p.getHealth() << " health and are carrying " << p.getGold() << " gold.\n"; return 0; } |
c) Next up is the Monster class. Monster also inherits from Creature. Monsters have no non-inherited member variables.
First, write an empty Monster class inheriting from Creature, and then add an enum inside the Monster class named Type that contains enumerators for the 3 monsters that we'll have in this game: DRAGON, ORC, and SLIME (you'll also want a max_types enumerator, as that will come in handy in a bit).
Show Solution
|
class Monster : public Creature { public: enum class Type { dragon, orc, slime, max_types }; }; |
d) Each Monster type will have a different name, symbol, starting health, gold, and damage. Here is a table of stats for each monster Type:
Type |
Name |
Symbol |
Health |
Damage |
Gold |
dragon |
dragon |
D |
20 |
4 |
100 |
orc |
orc |
o |
4 |
2 |
25 |
slime |
slime |
s |
1 |
1 |
10 |
Next step is to write a Monster constructor, so we can create monsters. The Monster constructor should take a Type enum as a parameter, and then create a Monster with the appropriate stats for that kind of monster.
There are a number of different ways to implement this (some better, some worse). However in this case, because all of our monster attributes are predefined (not random), we'll use a lookup table. A lookup table is an array that holds all of the predefined attributes. We can use the lookup table to look up the attributes for a given monster as needed.
So how do we implement this lookup table? It's not hard. We just need an array that contains an element for each monster Type. Each array element will contain a Creature that contains all of the predefined attribute values for that Type of Monster. We place this array inside of a static member function of Monster so that we can get a default Creature for a given Monster::Type.
The definition of the lookup table is as follows:
|
// As a private member of Monster static const Creature& getDefaultCreature(Type type) { static std::array<Creature, static_cast<std::size_t>(Type::max_types)> monsterData{ { { "dragon", 'D', 20, 4, 100 }, { "orc", 'o', 4, 2, 25 }, { "slime", 's', 1, 1, 10 } } }; return monsterData.at(static_cast<std::size_t>(type)); } |
Now we can call this function to lookup any values we need! For example, to get a Dragon's gold, we can call getDefaultCreature(Type::dragon).getGold() .
Use this function and delegating constructors to implement your Monster constructor.
The following program should compile:
|
#include <iostream> #include <string> int main() { Monster m{ Monster::Type::orc }; std::cout << "A " << m.getName() << " (" << m.getSymbol() << ") was created.\n"; return 0; } |
and print:
A orc (o) was created.
Show Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
|
#include <array> #include <iostream> #include <string> #include <string_view> // std::string_view requires C++17 class Creature { protected: std::string m_name; char m_symbol; int m_health; int m_damage; int m_gold; public: Creature(std::string_view name, char symbol, int health, int damage, int gold) : m_name{ name }, m_symbol{ symbol }, m_health{ health }, m_damage{ damage }, m_gold{ gold } { } const std::string& getName() const { return m_name; } char getSymbol() const { return m_symbol; } int getHealth() const { return m_health; } int getDamage() const { return m_damage; } int getGold() const { return m_gold; } void reduceHealth(int health) { m_health -= health; } bool isDead() const { return m_health <= 0; } void addGold(int gold) { m_gold += gold; } }; class Player : public Creature { int m_level{ 1 }; public: Player(std::string_view name) : Creature{ name, '@', 10, 1, 0 } { } void levelUp() { ++m_level; ++m_damage; } int getLevel() const { return m_level; } }; class Monster : public Creature { public: enum class Type { dragon, orc, slime, max_types }; private: static const Creature& getDefaultCreature(Type type) { static std::array<Creature, static_cast<std::size_t>(Type::max_types)> monsterData{ { { "dragon", 'D', 20, 4, 100 }, { "orc", 'o', 4, 2, 25 }, { "slime", 's', 1, 1, 10 } } }; return monsterData.at(static_cast<std::size_t>(type)); } public: Monster(Type type) : Creature{ getDefaultCreature(type) } { } }; int main() { Monster m{ Monster::Type::orc }; std::cout << "A " << m.getName() << " (" << m.getSymbol() << ") was created.\n"; return 0; } |
e) Finally, add a static function to Monster named getRandomMonster() . This function should pick a random number from 0 to max_types-1 and return a monster (by value) with that Type (you'll need to static_cast the int to a Type to pass it to the Monster constructor).
Lesson 8.5 -- Random number generation contains code you can use to pick a random number.
The following main function should run:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
#include <iostream> #include <string> #include <cstdlib> // for rand() and srand() #include <ctime> // for time() int main() { std::srand(static_cast<unsigned int>(std::time(nullptr))); // set initial seed value to system clock std::rand(); // get rid of first result for (int i{ 0 }; i < 10; ++i) { Monster m{ Monster::getRandomMonster() }; std::cout << "A " << m.getName() << " (" << m.getSymbol() << ") was created.\n"; } return 0; } |
The results of this program should be randomized.
Show Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
|
#include <array> #include <cstdlib> // for rand() and srand() #include <ctime> // for time() #include <iostream> #include <string> #include <string_view> // Generate a random number between min and max (inclusive) // Assumes std::srand() has already been called // Assumes max - min <= RAND_MAX int getRandomNumber(int min, int max) { static constexpr double fraction{ 1.0 / (RAND_MAX + 1.0) }; // static used for efficiency, so we only calculate this value once // evenly distribute the random number across our range return min + static_cast<int>((max - min + 1) * (std::rand() * fraction)); } class Creature { protected: std::string m_name; char m_symbol; int m_health; int m_damage; int m_gold; public: Creature(const std::string& name, char symbol, int health, int damage, int gold) : m_name{ name }, m_symbol{ symbol }, m_health{ health }, m_damage{ damage }, m_gold{ gold } { } char getSymbol() const { return m_symbol; } const std::string& getName() const { return m_name; } bool isDead() const { return m_health <= 0; } int getGold() const { return m_gold; } void addGold(int gold) { m_gold += gold; } void reduceHealth(int health) { m_health -= health; } int getHealth() const { return m_health; } int getDamage() const { return m_damage; } }; class Player : public Creature { int m_level{ 1 }; public: Player(const std::string& name) : Creature{ name, '@', 10, 1, 0 } { } void levelUp() { ++m_level; ++m_damage; } int getLevel() const { return m_level; } bool hasWon() const { return m_level >= 20; } }; class Monster : public Creature { public: enum class Type { dragon, orc, slime, max_types }; private: static const Creature& getDefaultCreature(Type type) { static std::array<Creature, static_cast<std::size_t>(Type::max_types)> monsterData{ { { "dragon", 'D', 20, 4, 100 }, { "orc", 'o', 4, 2, 25 }, { "slime", 's', 1, 1, 10 } } }; return monsterData.at(static_cast<std::size_t>(type)); } public: Monster(Type type) : Creature{ getDefaultCreature(type) } { } static Monster getRandomMonster() { int num{ getRandomNumber(0, static_cast<int>(Type::max_types) - 1) }; return Monster{ static_cast<Type>(num) }; } }; int main() { std::srand(static_cast<unsigned int>(std::time(nullptr))); // set initial seed value to system clock std::rand(); // get rid of first result for (int i{ 0 }; i < 10; ++i) { Monster m{ Monster::getRandomMonster() }; std::cout << "A " << m.getName() << " (" << m.getSymbol() << ") was created.\n"; } return 0; } |
f) We're finally set to write our game logic!
Here are the rules for the game:
The player encounters one randomly generated monster at a time.
For each monster, the player has two choices: (R)un or (F)ight.
If the player decides to Run, they have a 50% chance of escaping.
If the player escapes, they move to the next encounter with no ill effects.
If the player does not escape, the monster gets a free attack, and the player chooses their next action.
If the player chooses to fight, the player attacks first. The monster's health is reduced by the player's damage.
If the monster dies, the player takes any gold the monster is carrying. The player also levels up, increasing their level and damage by 1.
If the monster does not die, the monster attacks the player back. The player's health is reduced by the monster's damage.
The game ends when the player has died (loss) or reached level 20 (win)
If the player dies, the game should tell the player what level they were and how much gold they had.
If the player wins, the game should tell the player they won, and how much gold they had
Here's a sample game session:
Enter your name: Alex
Welcome, Alex
You have encountered a slime (s).
(R)un or (F)ight: f
You hit the slime for 1 damage.
You killed the slime.
You are now level 2.
You found 10 gold.
You have encountered a dragon (D).
(R)un or (F)ight: r
You failed to flee.
The dragon hit you for 4 damage.
(R)un or (F)ight: r
You successfully fled.
You have encountered a orc (o).
(R)un or (F)ight: f
You hit the orc for 2 damage.
The orc hit you for 2 damage.
(R)un or (F)ight: f
You hit the orc for 2 damage.
You killed the orc.
You are now level 3.
You found 25 gold.
You have encountered a dragon (D).
(R)un or (F)ight: r
You failed to flee.
The dragon hit you for 4 damage.
You died at level 3 and with 35 gold.
Too bad you can't take it with you!
Hint: Create 4 functions:
The main() function should handle game setup (creating the Player) and the main game loop.
fightMonster() handles the fight between the Player and a single Monster, including asking the player what they want to do, handling the run or fight cases.
attackMonster() handles the player attacking the monster, including leveling up.
attackPlayer() handles the monster attacking the player.
Show Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
|
#include <array> #include <cstdlib> // for rand() and srand() #include <ctime> // for time() #include <iostream> #include <string> #include <string_view> // Generate a random number between min and max (inclusive) // Assumes std::srand() has already been called // Assumes max - min <= RAND_MAX int getRandomNumber(int min, int max) { static constexpr double fraction{ 1.0 / (RAND_MAX + 1.0) }; // static used for efficiency, so we only calculate this value once // evenly distribute the random number across our range return min + static_cast<int>((max - min + 1) * (std::rand() * fraction)); } class Creature { protected: std::string m_name; char m_symbol; int m_health; int m_damage; int m_gold; public: Creature(std::string_view name, char symbol, int health, int damage, int gold) : m_name{ name }, m_symbol{ symbol }, m_health{ health }, m_damage{ damage }, m_gold{ gold } { } char getSymbol() const { return m_symbol; } const std::string& getName() const { return m_name; } bool isDead() const { return m_health <= 0; } int getGold() const { return m_gold; } void addGold(int gold) { m_gold += gold; } void reduceHealth(int health) { m_health -= health; } int getHealth() const { return m_health; } int getDamage() const { return m_damage; } }; class Player : public Creature { int m_level{ 1 }; public: Player(std::string_view name) : Creature{ name, '@', 10, 1, 0 } { } void levelUp() { ++m_level; ++m_damage; } int getLevel() const { return m_level; } bool hasWon() const { return m_level >= 20; } }; class Monster : public Creature { public: enum class Type { dragon, orc, slime, max_types }; private: static const Creature& getDefaultCreature(Type type) { static std::array<Creature, static_cast<std::size_t>(Type::max_types)> monsterData{ { { "dragon", 'D', 20, 4, 100 }, { "orc", 'o', 4, 2, 25 }, { "slime", 's', 1, 1, 10 } } }; return monsterData.at(static_cast<std::size_t>(type)); } public: Monster(Type type) : Creature{ getDefaultCreature(type) } { } static Monster getRandomMonster() { int num{ getRandomNumber(0, static_cast<int>(Type::max_types) - 1) }; return Monster{ static_cast<Type>(num) }; } }; // This function handles the player attacking the monster void attackMonster(Player& player, Monster& monster) { // If the player is dead, we can't attack the monster if (player.isDead()) return; std::cout << "You hit the " << monster.getName() << " for " << player.getDamage() << " damage.\n"; // Reduce the monster's health by the player's damage monster.reduceHealth(player.getDamage()); // If the monster is now dead, level the player up if (monster.isDead()) { std::cout << "You killed the " << monster.getName() << ".\n"; player.levelUp(); std::cout << "You are now level " << player.getLevel() << ".\n"; std::cout << "You found " << monster.getGold() << " gold.\n"; player.addGold(monster.getGold()); } } // This function handles the monster attacking the player void attackPlayer(const Monster& monster, Player& player) { // If the monster is dead, it can't attack the player if (monster.isDead()) return; // Reduce the player's health by the monster's damage player.reduceHealth(monster.getDamage()); std::cout << "The " << monster.getName() << " hit you for " << monster.getDamage() << " damage.\n"; } // This function handles the entire fight between a player and a randomly generated monster void fightMonster(Player& player) { // First randomly generate a monster Monster monster{ Monster::getRandomMonster() }; std::cout << "You have encountered a " << monster.getName() << " (" << monster.getSymbol() << ").\n"; // While the monster isn't dead and the player isn't dead, the fight continues while (!monster.isDead() && !player.isDead()) { std::cout << "(R)un or (F)ight: "; char input{}; std::cin >> input; if (input == 'R' || input == 'r') { // 50% chance of fleeing successfully if (getRandomNumber(1, 2) == 1) { std::cout << "You successfully fled.\n"; return; // success ends the encounter } else { // Failure to flee gives the monster a free attack on the player std::cout << "You failed to flee.\n"; attackPlayer(monster, player); continue; } } if (input == 'F' || input == 'f') { // Player attacks first, monster attacks second attackMonster(player, monster); attackPlayer(monster, player); } } } int main() { std::srand(static_cast<unsigned int>(std::time(nullptr))); // set initial seed value to system clock std::rand(); // get rid of first result std::cout << "Enter your name: "; std::string playerName; std::cin >> playerName; Player player{ playerName }; std::cout << "Welcome, " << player.getName() << '\n'; // If the player isn't dead and hasn't won yet, the game continues while (!player.isDead() && !player.hasWon()) fightMonster(player); // At this point, the player is either dead or has won if (player.isDead()) { std::cout << "You died at level " << player.getLevel() << " and with " << player.getGold() << " gold.\n"; std::cout << "Too bad you can't take it with you!\n"; } else { std::cout << "You won the game with " << player.getGold() << " gold!\n"; } return 0; } |
g) Extra credit:
Reader Tom didn't sharpen his sword enough to defeat the mighty dragon. Help him by implementing the following potions in different sizes:
Type |
Effect (Small) |
Effect (Medium) |
Effect (Large) |
Health |
+2 Health |
+2 Health |
+5 Health |
Strength |
+1 Damage |
+1 Damage |
+1 Damage |
Poison |
-1 Health |
-1 Health |
-1 Health |
Feel free to get creative and add more potions or change their effects!
The player has a 30% chance of finding a potion after every won fight and has the choice between drinking or not drinking it. If the player doesn't drink the potion, it disappears. The player doesn't know what type of potion was found until the player drinks it, at which point the type and size of the potion is revealed and the effect is applied.
In the following example, the player found a poison potion and died from drinking it (Poison was much more damaging in this example)
You have encountered a slime (s).
(R)un or (F)ight: f
You hit the slime for 1 damage.
You killed the slime.
You are now level 2.
You found 10 gold.
You found a mythical potion! Do you want to drink it? [y/n]: y
You drank a Medium potion of Poison
You died at level 2 and with 10 gold.
Too bad you can't take it with you!
Show Hint
Add a Potion class that has a type and size member variable, along with a member function that returns its name and a static member function the creates a random Potion, similar to the getRandomMonster() function.
In the Player class, add a drinkPotion() member function the applies the potion's effect.
Show Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
|
#include <array> #include <cstdlib> #include <ctime> #include <iostream> #include <sstream> // for std::stringstream #include <string> #include <string_view> int getRandomNumber(int min, int max) { static constexpr double fraction{ 1.0 / (RAND_MAX + 1.0) }; return min + static_cast<int>((max - min + 1) * (std::rand() * fraction)); } // All possible types of potions enum class PotionType { health, strength, poison, // For random potion generation max_type }; enum class PotionSize { small, medium, large, max_size }; // The names of potions are compile-time literals, we can // return a std::string_view. std::string_view getPotionTypeName(PotionType type) { static constexpr std::array names{ "Health", "Strength", "Poison" }; return names.at(static_cast<std::size_t>(type)); } std::string_view getPotionSizeName(PotionSize size) { static constexpr std::array names{ "Small", "Medium", "Large" }; return names.at(static_cast<std::size_t>(size)); } class Potion { private: PotionType m_type{}; PotionSize m_size{}; public: Potion(PotionType type, PotionSize size) : m_type{ type }, m_size{ size } { } PotionType getType() const { return m_type; } PotionSize getSize() const { return m_size; } std::string getName() const { // We use a std::stringstream, but this could also be solved using // std::string. // We first used std::stringstream in lesson 7.13. std::stringstream result{}; result << getPotionSizeName(getSize()) << " potion of " << getPotionTypeName(getType()); // We can extract the string from an std::stringstream by using the str() // member function. return result.str(); } static Potion getRandomPotion() { return { static_cast<PotionType>(getRandomNumber(0, static_cast<int>(PotionType::max_type) - 1)), static_cast<PotionSize>(getRandomNumber(0, static_cast<int>(PotionSize::max_size) - 1)) }; } }; class Creature { protected: std::string m_name; char m_symbol; int m_health; int m_damage; int m_gold; public: Creature(std::string_view name, char symbol, int health, int damage, int gold) : m_name{ name }, m_symbol{ symbol }, m_health{ health }, m_damage{ damage }, m_gold{ gold } { } char getSymbol() const { return m_symbol; } const std::string& getName() const { return m_name; } bool isDead() const { return m_health <= 0; } int getGold() const { return m_gold; } void addGold(int gold) { m_gold += gold; } void reduceHealth(int health) { m_health -= health; } int getHealth() const { return m_health; } int getDamage() const { return m_damage; } }; class Player : public Creature { int m_level{ 1 }; public: Player(std::string_view name) : Creature{ name, '@', 10, 1, 0 } { } void levelUp() { ++m_level; ++m_damage; } int getLevel() const { return m_level; } bool hasWon() const { return m_level >= 20; } // Applies a potion's effect to the player void drinkPotion(const Potion& potion) { switch (potion.getType()) { case PotionType::health: // Only a health potion's size affects its power. All other // potions are independent of size. m_health += ((potion.getSize() == PotionSize::large) ? 5 : 2); break; case PotionType::strength: ++m_damage; break; case PotionType::poison: reduceHealth(1); break; // Handle max_type to silence the compiler warning. Don't use default: // because we want the compiler to warn us if we add a new potion but // forget to implement its effect. case PotionType::max_type: break; } } }; class Monster : public Creature { public: enum class Type { dragon, orc, slime, max_types }; private: static const Creature& getDefaultCreature(Type type) { static std::array<Creature, static_cast<std::size_t>(Type::max_types)> monsterData{ { { "dragon", 'D', 20, 4, 100 }, { "orc", 'o', 4, 2, 25 }, { "slime", 's', 1, 1, 10 } } }; return monsterData.at(static_cast<std::size_t>(type)); } public: Monster(Type type) : Creature{ getDefaultCreature(type) } { } static Monster getRandomMonster() { int num{ getRandomNumber(0, static_cast<int>(Type::max_types) - 1) }; return Monster{ static_cast<Type>(num) }; } }; // We moved this out of attackMonster() to keep the function shorter. void onMonsterKilled(Player& player, const Monster& monster) { std::cout << "You killed the " << monster.getName() << ".\n"; player.levelUp(); std::cout << "You are now level " << player.getLevel() << ".\n"; std::cout << "You found " << monster.getGold() << " gold.\n"; player.addGold(monster.getGold()); // 30% chance of finding a potion constexpr int potionChance{ 30 }; if (getRandomNumber(1, 100) <= potionChance) { // Generate a random potion auto potion{ Potion::getRandomPotion() }; std::cout << "You found a mythical potion! Do you want to drink it? [y/n]: "; char choice{}; std::cin >> choice; if (choice == 'y') { // Apply the effect player.drinkPotion(potion); // Reveal the potion type and size std::cout << "You drank a " << potion.getName() << '\n'; } } } void attackMonster(Player& player, Monster& monster) { if (player.isDead()) return; std::cout << "You hit the " << monster.getName() << " for " << player.getDamage() << " damage.\n"; monster.reduceHealth(player.getDamage()); if (monster.isDead()) { // Reward the player onMonsterKilled(player, monster); } } void attackPlayer(const Monster& monster, Player& player) { if (monster.isDead()) return; player.reduceHealth(monster.getDamage()); std::cout << "The " << monster.getName() << " hit you for " << monster.getDamage() << " damage.\n"; } void fightMonster(Player& player) { Monster monster{ Monster::getRandomMonster() }; std::cout << "You have encountered a " << monster.getName() << " (" << monster.getSymbol() << ").\n"; while (!monster.isDead() && !player.isDead()) { std::cout << "(R)un or (F)ight: "; char input; std::cin >> input; if (input == 'R' || input == 'r') { if (getRandomNumber(1, 2) == 1) { std::cout << "You successfully fled.\n"; return; } else { std::cout << "You failed to flee.\n"; attackPlayer(monster, player); continue; } } if (input == 'F' || input == 'f') { attackMonster(player, monster); attackPlayer(monster, player); } } } int main() { std::srand(static_cast<unsigned int>(std::time(nullptr))); std::rand(); std::cout << "Enter your name: "; std::string playerName; std::cin >> playerName; Player player{ playerName }; std::cout << "Welcome, " << player.getName() << '\n'; while (!player.isDead() && !player.hasWon()) fightMonster(player); if (player.isDead()) { std::cout << "You died at level " << player.getLevel() << " and with " << player.getGold() << " gold.\n"; std::cout << "Too bad you can't take it with you!\n"; } else { std::cout << "You won the game with " << player.getGold() << " gold!\n"; } return 0; } |
|
|
Hi, I was wondering if the static in the std::array is so that the array is only initialized once. Missing this keyword generated no compiler error but I get abort debug message.
thanks.
can i get more clarification on Question #3
Classes have a copy constructor by default. They create a copy of the value you give them.
for class Player
is it not advisable to use default parameter values so that instances of Player class can also be initiated with values like in Creature class ?
in answer to question #3 i got confused a bit.
some part where i thought a const must be used it wasn't
and some part where i thought its not necessary, it was.
Hi sir, Under 3-d)
Are we making the lookup table "static" to make sure the reference dosen't go out of scope when the function returns ?
Also in line 3 we have static_cast "Type::max_types" to std::size_t. Is it because we use std::array or because it is a user defined type?
And again in line 10, we static_cast "type" to std::size_t . Why is that ?
> Are we making the lookup table "static" to make sure the reference dosen't go out of scope when the function returns
That, and creating the lookup table with every call to the function would be unnecessary expensive
> cast
`std::array` wants a `std::size_t`, you have an `enum class`, you have to cast
> cast
Yes, but in line 83 itself we have casted to std::size_t, So why should we need to cast it again while returning it in line 89/90 ?
Why doesn't the C++ remember that we mean to return the already casted type from line 83 ?
A cast doesn't affect the casted-from variable. Casts produce a new variable. Types can never change.
Thank you. And 1 final doubt. And in d function "getDefaultCreature" we return Creature by reference. Creature is able to accept the type "std::size_t" from the return statement. Is it because "std::size_t" is a global type ? Will "Creature" be also able to accept any other type like "int","char" etc., if a situation demands so ?
The `std::size_t` in the return-statement is unrelated to `Creature`. It's used to index the array.
Hi there!
Some questions about the very last exercise solution:
1) For the construciton of the name of the potion, wouldn't be better to use a switch instead of two static array of names?
2) Shouldn't be better that the functions that handle the attack to the player and to the monster wouldn't care about the fatct that the player and the monster are dead? I mean the initial check in both the functions, I think that thouse should be conceptually managed by the function that calls them...