In working on this answer it occurred to me that it might be interesting to further expand the idea.
The game
This is an extremely simple (and boring!) text-based game that creates a few monsters which attack the player and which the player can attack. The goal is for player to defeats all of the monsters before dying. To fight monsters, the player types the generic name of the monster, e.g "Orc" and if there are multiple Orcs, the program will register a hit on each of them. The game is very boring, to play but it was intended as a proof-of-concept rather than a fully fleshed out game. With that said, it is complete and runs without error.
Features
Some features of the game are that it's multi-threaded with the monsters being driven in one thread and the user I/O in another. It uses a std::priority_queue
to keep track of the timing of the monster attacks on the player. That is, the monsters act in real time and autonomously, independent of the player.
It can use the C++20 std::osyncstream
if available, but has a substitute for C++11 or above which is why it has both tags.
Questions
I'm particularly interested in:
- Is the
Game
object design easy to understand? - Does the scheduling mechanism make sense?
- I could have used
std::initializer_list<Monster>
as the argument instead ofstd::initializer_list<std::pair<std::string, unsigned>>
for the constructor. I chose not to because I didn't want temporary copies ofMonster
s made. What do you think of that choice? - Are there any flaws in the multithreading?
- The game currently ends via an
exit
call to kill all threads. Is there a more elegant way to approach this?
Game.h
#ifndef GAME_H
#define GAME_H
#include <atomic>
#include <initializer_list>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
class Game {
struct Monster {
static unsigned serial;
Monster(std::string name, unsigned interval);
Monster(const Monster &other) = delete;
std::string name;
unsigned interval;
unsigned deadline;
int health = 4;
unsigned id;
};
std::vector<std::shared_ptr<Monster>> Monster_list;
std::mutex monster_lock;
std::atomic_uint enemy_count{0};
std::atomic_uint player_health{10};
void listEnemies();
unsigned hit(std::shared_ptr<Monster> victim);
public:
void monsters();
void player();
Game(std::initializer_list<std::pair<std::string, unsigned>> init);
};
#endif // GAME_H
Game.cpp
#include "Game.h"
#include <atomic>
#include <chrono>
#include <initializer_list>
#include <iostream>
#include <memory>
#include <mutex>
#include <queue>
#include <string>
#include <thread>
#include <vector>
#if __has_include(<syncstream>)
#include <syncstream>
std::osyncstream sync_out{std::cout};
#else
class syncstr {
std::mutex cout_lock;
std::ostream& out;
public:
syncstr(std::ostream& out) : out{out} {}
syncstr& operator<<(const char *item) {
std::lock_guard<std::mutex> lock(cout_lock);
out << item;
out.flush();
return *this;
}
syncstr& operator<<(const std::string &item) {
std::lock_guard<std::mutex> lock(cout_lock);
out << item;
out.flush();
return *this;
}
syncstr& operator<<(char item) {
std::lock_guard<std::mutex> lock(cout_lock);
out << item;
out.flush();
return *this;
}
syncstr& operator<<(unsigned item) {
std::lock_guard<std::mutex> lock(cout_lock);
out << item;
out.flush();
return *this;
}
syncstr& operator<<(int item) {
std::lock_guard<std::mutex> lock(cout_lock);
out << item;
out.flush();
return *this;
}
syncstr& operator<<(long int item) {
std::lock_guard<std::mutex> lock(cout_lock);
out << item;
out.flush();
return *this;
}
};
syncstr sync_out{std::cout};
#endif
Game::Monster::Monster(std::string name, unsigned interval) :
name{name},
interval{interval},
deadline{interval},
id{++serial}
{ }
Game::Game(std::initializer_list<std::pair<std::string, unsigned>> init) {
std::cin.tie(nullptr);
for (auto &temp : init) {
Monster_list.emplace_back(std::make_shared<Monster>(temp.first, temp.second));
++enemy_count;
}
}
void Game::listEnemies() {
std::lock_guard<std::mutex> mlock(monster_lock);
sync_out << "Surrounding you are " << enemy_count << " enemies:\n";
for (const auto &m: Monster_list) {
if (m->health) {
sync_out << m->name << m->id << '\n';
}
}
sync_out << "What would you like to attack? ";
}
unsigned Game::hit(std::shared_ptr<Monster> victim) {
if (victim->health) {
if (--victim->health == 0) {
sync_out << victim->name << victim->id << " defeated!\n";
--enemy_count;
}
}
return victim->health;
}
void Game::monsters() {
static auto compare_deadlines = [](std::shared_ptr<Monster> a, std::shared_ptr<Monster> b){
return a->deadline > b->deadline;
};
std::priority_queue<std::shared_ptr<Monster>, std::vector<std::shared_ptr<Monster>>, decltype(compare_deadlines)> monster{compare_deadlines};
for (auto m : Monster_list) {
monster.push(m);
}
auto start = std::chrono::system_clock::now();
while (!monster.empty()) {
std::this_thread::sleep_for(std::chrono::seconds(monster.top()->deadline));
std::unique_lock<std::mutex> mlock(monster_lock);
// if the moster is already dead, don't let it attack
if (hit(monster.top())) {
sync_out << std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - start).count() << " "
<< monster.top()->name << monster.top()->id << " attacks! Health = " << monster.top()->health << '\n'
<< "Your health = " << --player_health << '\n';
}
// is the game over?
if (player_health == 0 || enemy_count == 0) {
mlock.unlock();
if (enemy_count) {
sync_out << "You have died -- Game over!\n";
} else {
sync_out << "All enemies are defeated!!\n";
}
// this also kills the other thread
exit(0);
}
// adjust the priority queue
auto elapsed = monster.top()->deadline;
decltype(monster) m2{std::move(monster)};
while (!m2.empty()) {
auto current = m2.top();
m2.pop();
if (current->deadline > elapsed) {
current->deadline -= elapsed;
} else {
current->deadline = current->interval;
}
if (current->health) {
monster.push(current);
}
}
}
}
void Game::player() {
while (enemy_count) {
listEnemies();
std::string enemy;
std::cin >> enemy;
unsigned hitcount{0};
for (auto &m: Monster_list) {
std::lock_guard<std::mutex> mlock(monster_lock);
if (m->name == enemy && m->health) {
sync_out << "Hacking away at " << m->name << m->id << '\n';
hit(m);
++hitcount;
}
}
if (hitcount == 0) {
sync_out << "No living enemy named " << enemy << '\n';
}
}
}
unsigned Game::Monster::serial{0};
main.cpp
#include "Game.h"
#include <thread>
#include <functional>
int main() {
Game game{ {"Dragon",8}, {"Orc", 3}, {"Wumpus", 5}, {"Edward", 7}, {"Orc", 4} };
auto m = std::thread(&Game::monsters, std::ref(game));
game.player();
m.join();
}