|
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,
why 'Cat cat1 {cat}' would call Animal constructor??
It's obvious why 'Cat cat {"aa"}' calls the Animal constructor. But what about 'Cat cat1 {cat}' ? should not it call just the copy constructor of 'Cat'?
Result:
Animal constructor
cat constructor
Animal constructor
cat copy constructor
aa
`Cat` is an `Animal`. Every `Cat` object has an `Animal` subobject. That subobject is created using the `Animal` constructor.
but in line 48 we don't need to create a new Cat object since we already have one "cat1", so why Animal constructor ?
You have 2 cats, each has an `Animal` part, so 2 `Animal` constructor calls are expected.
Hello for question 3d,line 87, the data() member function is part of the string_view class and it returns a pointer to the underlying array, so is the argument passed to Creature constructor const std::string &name=pointer?
If my above question is unclear, my question is what arguments does the const std::string &name parameter take? and also for e.g const std::string& str="Hello"; ,is the string literal converted to a anonymous std::string object and str is a const ref to it?
Hello! This is my attempt at the final quiz question. Hope it's all good, took me quite a while cause I mucked up the first time (it was an absolute mess). Thanks and have a nice day! :)
// main.cpp
// Creature.h
// Creature.cpp
// Game.h
// Game.cpp
// Helper.h
// Helper.cpp
// Monster.h
// Monster.cpp
// Player.h
Even got some `noexcept` in there, nice :)
- Include as few files as possible in headers. More includes means slower compilation and higher risk of circular dependencies.
- Initialize variables with list initialization for higher type-safety.
- There's no need to explicitly default `Game::Game`. It's implicitly-declared, because you have no other constructors.
- Only use pointers if `nullptr` is handled. Your functions cause undefined behavior when a `nullptr` is passed to them. Use references instead.
- `Game::start` would've benefited from being separated into more functions. This would also spare you the `break`, which could be replaced by `return` (Which is easier to understand, because the reader doesn't have to search for the end of the loop).
- `std::system` is platform-specific, your program won't behave well on anything other than Windows. If you have to use ugly code, hide it inside function to make porting the code easier. eg. `clearScreen`, `pause`, and so on, then call those functions instead of using `std::system` directly.
- Magic values: 20, 'r', 'f'
- If you're using a standard function without the `std::` prefix, you're using a C function. In C++, everything was moved into the `std` namespace.
- It's been found now :)
- Strings are more expensive than characters. Use single quotes for characters.
- You never need
These can and should always be returned with
Pretty nice code overall, keep it up!
Thanks for the advice & tips ! Will be sure to implement them into the code and future code I write from now on, apart from the uniform/list initialization for variables (just hate the way it looks lol).
About the usage of std::system(), I am still yet to find a platform independent function that will let me change text colour inside of the console. Would I be best just using a 3rd-party library for doing it, that deals with all of the platform specific details? Thanks!
There is no platform-independent way, because it depends on the console/terminal used, and those are plentiful. You're best of using a library.
Alright, thanks a lot! I'll search around, see if I can find one.
Getting some very strange errors. Here is my code for the Creature and Monster classes:
When attempting to build this program, I receive the following errors:
If I remove 'static const' from the line in which the array is declared, my compiler tells me that there is an unhandled exception in the Creature constructor... (???)
You cannot initialize `static` members inside the class unless they're const and have an integral type or are constexpr. `std::array` isn't an integral type, so you can't initialize it in the class with `const`. You could initialize it in the class if it were `constexpr`, but you use `std::string` to store the name and `std::string` can't be `constexpr`.
If you use `std::string_view` for the name and make `monsterData` `constexpr`, your code works. If you're using an old compiler, you can't do that. In that case, initialize `monsterData` outside of the class.
I just read up on this, and it seems to simply be a rule in C++ to prevent multiple declarations/definitions of a single variable, particularly in the case that it is const and cannot be altered after-the-fact.
The main issue, it seems, is that if I remove ``const`` from the following code segment, I will be presented with the error that in-class initializers must be ``const``, but if I make the std::array static, the compiler will complain that the static member is either not inline, or is not an integral type / constant enumerated type. I can see that using ``constexpr`` with std::string is not possible, because std::string is not a literal type.
Does this mean that this array can only be implemented using constexpr, and using a literal implementation of strings, such as std::string_view?
Also, thank you for the help!
Thank you nascardriver! You are truly diligent, helping all of these people for so long, with such consistency! I hope to be as great as you guys some day!
A type that has a member variable of non-literal type, is a non-literal type. So your `MondayData` cannot be `constexpr`. If you remove the `std::string` and replace it with `std::string_view` (Which is a literal type), your entire `MonotoneData` becomes a literal type as well, so you can use it with `constexpr`, and `constexpr` member variables can be initialized inside the class.
3f)
Things got a little bit messy...
I hope you can provide some feedback.
Also, I have a few question..
1 - When you said "escape without ill effects", you meant escaping without getting hit, not restoring player's health right?
2 - At the beginning of the source code, I tried to forward declare the global functions, such as fightMonster(), but I got an error that says "illegal use of void". What's the issue here?
3 - Regarding the chance of fleeing... I reused the getRandomNumber() function to set a flag's value (either 1: success, 0: fail). Does that compensate for having to do some math/statistics maybe?
1 - I didn't write this quiz, but I think that's what it means.
2 - That's an odd error message. You need to forward declare `Player` and `Monster` before you can use them in forward declarations of the functions.
3 - Nothing wrong with that. If you have a function, you should use it.
- Returning by `const` value has no benefit but prevents fast returns.
- `isDead` and `hasWon` still aren't `const`. That's what the function in my last reply should've shown you.
- Line 118, 189, 190,
- Don't seed random number generators more than once. You're not generating random numbers.
- Lack of `const` on function parameters.
- Line 219 is misleading. Are you using an auto-formatter?
Thanks for the feedback, the second point is interesting though... I don't know if this error was pointed out in previous lessons. It's the first time I encounter it. Maybe you guys can add it as a side note if it's not there already (just a suggestion :$).
I clearly need to pay more attention to 'const' for parameters, I keep forgetting to include it.
Well, VS automatically adjust things when I type ';', but that 'break' is misleading because of my spacing probably. It's for the case 'r'.
3b)
`player` should be initialized with list initialization.
Why doesn't this work?
I really didn't understand what you were trying to say here :$...
Was this reply meant for me? Because this is still 3b.
Yes, all as intended. If you try to add the function I posted to your code, it won't work. I could tell you why it doesn't work, but if you figure it out yourself, you'll remember better.
Why can't I write the following code for the quiz 2(b)?