I've already made a version of this game which was supposed to be played on the console. I got it reviewed and applied almost all of the recommendations I received in that topic. Thus you can consider this topic a follow-up from this one. This would be the first time I got in touch with SFML. I must mention that all the user interactions are still going through the console.
Most of the files remained the same from the original question (with the recommendations applied), so I will post only the new files/parts that have changed as those concern me the most. However, I've got the project hosted on GitHub if you're interested in other aspects.
CardsTexturesHolder.hpp
#pragma once
#include <unordered_map>
#include <filesystem>
#include <SFML/Graphics/Texture.hpp>
#include "Exceptions.hpp"
#include "Card.hpp"
namespace std
{
template <>
struct hash<Card>
{
constexpr std::size_t operator()(Card const& card) const noexcept
{
return (static_cast<int>(card.rank()) + static_cast<int>(card.suit()));
}
};
}
class CardsTexturesHolder
{
public:
template <typename Container>
CardsTexturesHolder(Container const& cardIdentifiers, std::string_view path);
template <typename Iterator>
CardsTexturesHolder(Iterator first, Iterator last, std::string_view path);
const sf::Texture& operator[](Card const& card) const noexcept;
private:
std::unordered_map<Card, sf::Texture> _textures;
template <typename Container>
void load(Container const& cardIdentifiers, std::string_view path);
template <typename Iterator>
void load(Iterator first, Iterator last, std::string_view path);
};
template <typename Container>
CardsTexturesHolder::CardsTexturesHolder(Container const& cardIdentifiers, std::string_view path)
{
load(cardIdentifiers, path);
}
template <typename Iterator>
CardsTexturesHolder::CardsTexturesHolder(Iterator first, Iterator last, std::string_view path)
{
load(first, last, path);
}
template <typename Iterator>
void CardsTexturesHolder::load(Iterator first, Iterator last, std::string_view path)
{
sf::Texture texture;
auto constexpr iteratorAdvanceOffset{ 1 };
for (auto const& file : std::filesystem::directory_iterator(path))
{
if (texture.loadFromFile(file.path().string()))
{
_textures[*first] = texture;
std::advance(first, iteratorAdvanceOffset);
}
else
throw CouldNotLoadTextureException("A certain card texture could not be loaded. Consider checking the textures directory.");
}
}
template <typename Container>
void CardsTexturesHolder::load(Container const& cardIdentifiers, std::string_view path)
{
sf::Texture texture;
auto currentCardIterator = std::begin(cardIdentifiers);
auto constexpr iteratorAdvanceOffset{ 1 };
for (auto const& file : std::filesystem::directory_iterator(path))
{
if (texture.loadFromFile(file.path().string()))
{
_textures[*currentCardIterator] = texture;
std::advance(currentCardIterator, iteratorAdvanceOffset);
}
else
throw CouldNotLoadTextureException("A certain card texture could not be loaded. Consider checking the textures directory.");
}
}
CardsTexturesHolder.cpp
#include "CardsTexturesHolder.hpp"
const sf::Texture& CardsTexturesHolder::operator[](Card const& card) const noexcept
{
auto const it = _textures.find(card);
return it->second;
}
There are 2 aspects of this class that I don't like.
- The Cards in the
cardIdentifiers
container must have a strict ordering that matches the one in the Textures folder (see it on GitHub). If one changes any card's place in that container, the textures won't be corresponding to the actual card anymore. - I had to name all the textures in such a way that the folder will be traversed in alphabetical order. If one changes the name of a texture file, everything will be messed up.
FontsHolder.hpp
#pragma once
#include <SFML/Graphics/Font.hpp>
#include <unordered_map>
class FontsHolder
{
public:
explicit FontsHolder(std::string_view path);
const sf::Font& operator[](std::string const& fontName) const;
private:
const std::string extractFileName(std::string const& filePath) const noexcept;
void loadFonts(std::string_view path);
std::unordered_map<std::string, sf::Font> _fonts;
};
FontsHolder.cpp
#include "FontsHolder.hpp"
#include "Exceptions.hpp"
#include <algorithm>
#include <filesystem>
FontsHolder::FontsHolder(std::string_view path)
{
loadFonts(path);
}
const sf::Font& FontsHolder::operator[](std::string const& fontName) const
{
auto const it = _fonts.find(fontName);
if (it != _fonts.cend())
return it->second;
else
throw FontNotFoundException("The requested font was not found.");
}
const std::string FontsHolder::extractFileName(std::string const& filePath) const noexcept
{
auto constexpr newDirectoryIdentifier = '\\';
auto constexpr fileIdentifier = '.';
auto const lastPositionOfDirectoryIdentifier = filePath.find_last_of(newDirectoryIdentifier);
auto const firstPositionOfFileIdentifier = filePath.find_first_of(fileIdentifier);
return std::string(std::next(filePath.cbegin(),lastPositionOfDirectoryIdentifier + 1), std::next(filePath.cbegin(), firstPositionOfFileIdentifier));
}
void FontsHolder::loadFonts(std::string_view path)
{
sf::Font font;
std::string filePath;
for (auto const& file : std::filesystem::directory_iterator(path))
{
filePath = file.path().string();
if (font.loadFromFile(filePath))
_fonts[extractFileName(filePath)] = font;
else
throw CouldNotLoadFontException("A certain font could not be loaded. Consider checking the textures directory.");
}
}
Renderer.hpp
#pragma once
#include <SFML/Graphics/Drawable.hpp>
#include <SFML/Graphics/RenderTarget.hpp>
#include <SFML/Graphics/Sprite.hpp>
#include "FontsHolder.hpp"
#include "CardsTexturesHolder.hpp"
#include "Deck.hpp"
#include <vector>
class Renderer
{
public:
template <typename Container>
Renderer(Container const& cardsIdentifiers, std::string_view pathToTextures, std::string_view pathToFonts);
template <typename Iterator>
Renderer(Iterator first, Iterator last, std::string_view pathToTextures, std::string_view pathToFonts);
template <typename Container>
void drawPlayerCards(sf::RenderTarget& target, Container const& cards) const;
void drawPileTopCard(sf::RenderTarget& target, Card const& card) const;
void drawEscapeText(sf::RenderTarget& target, std::string const& style) const;
void drawPlayerName(sf::RenderTarget& target, std::string const& name, sf::Vector2f position, std::string const& style) const;
void drawWinner(sf::RenderTarget& target, std::string const& outputData, std::string const& style) const;
void drawInformationAboutSpecialCard(sf::RenderTarget& target, std::string const& information, sf::Vector2f position, std::string const& style) const;
private:
CardsTexturesHolder _textures;
FontsHolder _fonts;
};
template<typename Container>
inline Renderer::Renderer(Container const & cardsIdentifiers, std::string_view pathToTextures, std::string_view pathToFonts)
: _textures(cardsIdentifiers, pathToTextures),
_fonts(pathToFonts)
{}
template<typename Iterator>
inline Renderer::Renderer(Iterator first, Iterator last, std::string_view pathToTextures, std::string_view pathToFonts)
: _textures(first, last, pathToTextures),
_fonts(pathToFonts)
{}
template <typename Container>
void Renderer::drawPlayerCards(sf::RenderTarget & target, Container const & cards) const
{
auto const defaultScaleValues = sf::Vector2f(0.2f, 0.2f);
auto constexpr xDirectionOffset{ 135 };
auto constexpr yDirectionOffset{ 220 };
sf::Sprite cardSprite;
auto constexpr initialValueForX{ 0 };
auto constexpr initialValueForY{ 250 };
float x{ initialValueForX };
float y{ initialValueForY };
for (auto const& card : cards)
{
cardSprite.setTexture(_textures[card]);
cardSprite.setPosition(x, y);
cardSprite.setScale(defaultScaleValues);
target.draw(cardSprite);
x += xDirectionOffset;
if (x + cardSprite.getGlobalBounds().width > target.getSize().x)
{
x = initialValueForX;
y += yDirectionOffset;
}
}
}
Renderer.cpp
#include "Renderer.hpp"
#include <SFML/Graphics/Text.hpp>
void Renderer::drawPileTopCard(sf::RenderTarget& target, Card const& card) const
{
sf::Vector2f const defaultScaleValues(0.2f, 0.2f);
sf::Sprite cardSprite{ _textures[card] };
cardSprite.setScale(defaultScaleValues);
auto constexpr yDirectionOffset{ 1 };
cardSprite.setPosition(sf::Vector2f(target.getSize().x / 2, yDirectionOffset));
target.draw(cardSprite);
}
void Renderer::drawPlayerName(sf::RenderTarget & target, std::string const& name, sf::Vector2f position, std::string const& style) const
{
sf::Text playerName;
playerName.setFont(_fonts[style]);
playerName.setString(name);
playerName.setPosition(position);
target.draw(playerName);
}
void Renderer::drawEscapeText(sf::RenderTarget & target, std::string const & style) const
{
static std::string const outputData{ "Press Enter to continue..." };
sf::Text output;
output.setFont(_fonts[style]);
output.setString(outputData);
static const auto targetSize = target.getSize();
static const auto point = sf::Vector2f(10, 10);
output.setPosition(point);
target.draw(output);
}
void Renderer::drawWinner(sf::RenderTarget & target, std::string const & outputData, std::string const & style) const
{
int constexpr characterSize{ 60 };
sf::Text output;
output.setFont(_fonts[style]);
output.setString(outputData);
output.setCharacterSize(characterSize);
static auto const targetSize = target.getSize();
static auto const middlePoint = sf::Vector2f(targetSize.x / 2, targetSize.y / 2);
output.setPosition(middlePoint);
target.draw(output);
}
void Renderer::drawInformationAboutSpecialCard(sf::RenderTarget& target, std::string const& information, sf::Vector2f position, std::string const& style) const
{
sf::Text specialCardInformation;
specialCardInformation.setFont(_fonts[style]);
specialCardInformation.setString(information);
specialCardInformation.setPosition(position);
target.draw(specialCardInformation);
}
VisualGame.hpp
#pragma once
#include <SFML/Graphics/RenderWindow.hpp>
#include "Player.hpp"
#include "FontsHolder.hpp"
#include "Renderer.hpp"
#include <vector>
std::string const defaultWindowName{ "MacaoGame" };
class VisualGame
{
public:
template <typename InputContainer>
VisualGame(InputContainer&& players, std::string_view pathToCardTextures
, std::string_view pathToFonts, std::size_t width, std::size_t height);
template <typename InputIterator>
VisualGame(InputIterator first, InputIterator last,
std::string_view pathToCardTextures, std::size_t width, std::size_t height);
void run();
private:
constexpr void validateNumberOfPlayers() const;
constexpr bool areCompatible(Card const& left, Card const& right) const noexcept;
void prepareGame() noexcept;
void dealInitialCardsToEachPlayer() noexcept;
void receiveCardsFromDeck(Player& player, std::size_t numberOfCards);
void putCardToPile(Player& player, std::size_t cardNumber) noexcept;
bool isSpecial(Card const& card) const noexcept;
void printInformationAboutThePlayerAndTheTopCardFromPile(Player const& player) noexcept;
void printInformationAboutThePlayerAndTheTopCardFromPile(Player const& player, Rank rank) noexcept;
void normalCardPlayerTurn(Player& player);
void specialCardPlayerTurn(Player& player);
std::unique_ptr<Player> findWinner() const noexcept;
auto receiveAndValidateInput() const noexcept->int;
auto checkBounds(Player const& player, int cardNumber) const noexcept->int;
void validatePlayerCardCompatibilityWithPileTopCard(Player const& player, int& cardNumber) const noexcept;
sf::RenderWindow _mainWindow;
Renderer _renderer;
std::vector<std::unique_ptr<Player>> _players;
Deck _deck;
Pile _pile;
bool _gameOver;
};
inline constexpr void VisualGame::validateNumberOfPlayers() const
{
static constexpr auto minimumNumberOfPlayers{ 2 };
static constexpr auto maximumNumberOfPlayers{ 9 };
auto const numberOfPlayers = std::distance(_players.cbegin(), _players.cend());
if (numberOfPlayers < minimumNumberOfPlayers || numberOfPlayers > maximumNumberOfPlayers)
throw InvalidNumberOfPlayersException("The minimum number of players is 2, while the maximum is 9.");
}
template<typename InputContainer>
inline VisualGame::VisualGame(InputContainer && players, std::string_view pathToCardTextures,
std::string_view pathToFonts, std::size_t width, std::size_t height)
: _players(std::move(players)),
_renderer(defaultDeck, pathToCardTextures, pathToFonts),
_mainWindow(sf::VideoMode(width, height), defaultWindowName),
_gameOver(false)
{
validateNumberOfPlayers();
}
template<typename InputIterator>
inline VisualGame::VisualGame(InputIterator first, InputIterator last,
std::string_view pathToCardTextures, std::size_t width, std::size_t height)
:_players(std::make_move_iterator(std::begin(players)), std::make_move_iterator(std::end(players))),
_renderer(vectorDefaultDeck, pathToCardTextures, pathToFonts),
_mainWindow(sf::VideoMode(width, height), defaultWindowName),
_gameOver(false)
{
validateNumberOfPlayers();
}
VisualGame.cpp
#include <SFML/Graphics.hpp>
#include "VisualGame.hpp"
#include "Exceptions.hpp"
#include <iostream>
#include <cctype>
auto const defaultBackgroundColor = sf::Color::Color(51, 51, 255);
std::string const defaultFont{ "FredokaOne" };
namespace
{
constexpr auto specialCards = std::array
{
Card{ Rank::Two, Suit::Clubs },
Card{ Rank::Two, Suit::Diamonds },
Card{ Rank::Two, Suit::Spades },
Card{ Rank::Two, Suit::Hearts },
Card{ Rank::Three, Suit::Clubs },
Card{ Rank::Three, Suit::Diamonds },
Card{ Rank::Three, Suit::Spades },
Card{ Rank::Three, Suit::Hearts },
Card{ Rank::Ace, Suit::Diamonds },
Card{ Rank::Ace, Suit::Clubs },
Card{ Rank::Ace, Suit::Spades },
Card{ Rank::Ace, Suit::Hearts },
};
}
constexpr bool VisualGame::areCompatible(Card const& left, Card const& right) const noexcept
{
return left.rank() == right.rank() || left.suit() == right.suit();
}
void VisualGame::prepareGame() noexcept
{
_deck.shuffle();
dealInitialCardsToEachPlayer();
_pile.add(_deck.deal());
while (isSpecial(_pile.topCard()))
{
_deck.add(_pile.deal());
_deck.shuffle();
_pile.add(_deck.deal());
}
}
void VisualGame::dealInitialCardsToEachPlayer() noexcept
{
static constexpr auto numberOfCards{ 5 };
static std::vector<Card> playerCards;
for (auto const& player : _players)
{
playerCards = _deck.deal(numberOfCards);
player->_hand.add(playerCards.cbegin(), playerCards.cend());
}
}
void VisualGame::printInformationAboutThePlayerAndTheTopCardFromPile(const Player& player) noexcept
{
_mainWindow.clear(defaultBackgroundColor);
_renderer.drawPileTopCard(_mainWindow, _pile.topCard());
_renderer.drawPlayerName(_mainWindow, player.name(), sf::Vector2f(10, 10), defaultFont);
_renderer.drawPlayerCards(_mainWindow, player._hand.cards());
_mainWindow.display();
}
void VisualGame::printInformationAboutThePlayerAndTheTopCardFromPile(Player const & player, Rank rank) noexcept
{
static auto const cardInformationPosition = sf::Vector2f(500, 1);
static auto const playerNamePosition = sf::Vector2f(10, 10);
static auto const specialCardInformationPosition = sf::Vector2f(0, _mainWindow.getSize().x / 2);
switch (rank)
{
case Rank::Two:
{
_mainWindow.clear(defaultBackgroundColor);
_renderer.drawPileTopCard(_mainWindow, _pile.topCard());
_renderer.drawPlayerName(_mainWindow, player.name(), playerNamePosition, defaultFont);
static const std::string cardInformation{ "This is 2 card and you will need to receive 2 cards from the deck.\n"
"If you have a 4 card that has the same suit, then you can stop it.\n" };
_renderer.drawInformationAboutSpecialCard(_mainWindow, cardInformation, specialCardInformationPosition, defaultFont);
_renderer.drawPlayerCards(_mainWindow, player._hand.cards());
_mainWindow.display();
break;
}
case Rank::Three:
{
_mainWindow.clear(defaultBackgroundColor);
_renderer.drawPileTopCard(_mainWindow, _pile.topCard());
_renderer.drawPlayerName(_mainWindow, player.name(), playerNamePosition, defaultFont);
static const std::string cardInformation{ "This is 3 card and you will need to receive 3 cards from the deck.\n"
"If you have a 4 card that has the same suit, then you can stop it.\n" };
_renderer.drawInformationAboutSpecialCard(_mainWindow, cardInformation, specialCardInformationPosition, defaultFont);
_renderer.drawPlayerCards(_mainWindow, player._hand.cards());
_mainWindow.display();
break;
}
//case Rank::Seven:
//{
// _renderer.draw(_mainWindow, _pile.topCard());
// _renderer.draw(_mainWindow, player._hand.cards());
// std::cout << "\nTop card from pile: " << _pile.top() << "\nThis is a 7 card and you will need to put down a card that has the specified suit. If you have a Joker, then"
// << " you can put it. Enter 0 if you don't have such a compatible card.\n" << player << "\n";
// break;
//}
case Rank::Ace:
{
_mainWindow.clear(defaultBackgroundColor);
_renderer.drawPileTopCard(_mainWindow, _pile.topCard());
_renderer.drawPlayerName(_mainWindow, player.name(), sf::Vector2f(10, 10), "FredokaOne");
_renderer.drawPlayerCards(_mainWindow, player._hand.cards());
_mainWindow.display();
break;
}
}
}
void VisualGame::validatePlayerCardCompatibilityWithPileTopCard(Player const& player, int& cardNumber) const noexcept
{
while (!areCompatible(player._hand.get(cardNumber), _pile.topCard()))
{
std::cout << "This card is incompatible.\nEnter another card or enter 0 to skip your turn.\n";
cardNumber = receiveAndValidateInput();
if (!cardNumber)
break;
}
}
std::unique_ptr<Player> VisualGame::findWinner() const noexcept
{
auto const it = std::find_if(_players.cbegin(), _players.cend(),
[](auto const& player) { return !player->_hand.numberOfCards(); });
if (it == _players.cend())
return std::make_unique<Player>(nullptr);
return std::make_unique<Player>(**it);
}
auto VisualGame::checkBounds(Player const& player, int cardNumber) const noexcept->int
{
static constexpr auto lowerBound{ 0 };
while (cardNumber < lowerBound || cardNumber > player._hand.numberOfCards())
{
std::cout << "That card doesn't exist. You can enter 0 to skip your turn.\n";
std::cin >> cardNumber;
}
return cardNumber;
}
auto VisualGame::receiveAndValidateInput() const noexcept->int
{
std::string cardNumber{};
std::getline(std::cin, cardNumber);
while (!std::all_of(cardNumber.cbegin(), cardNumber.cend(), [](auto const character) {return std::isdigit(character); })
|| cardNumber.empty())
{
std::cout << "Empty input/Not an integer.\n";
std::getline(std::cin, cardNumber);
}
return std::stoi(cardNumber);
}
void VisualGame::specialCardPlayerTurn(Player& player)
{
static constexpr auto neededCardsToPickForTwo{ 2 };
static constexpr auto neededCardsToPickForThree{ 3 };
int cardNumber{ 0 };
switch (_pile.topCard().rank())
{
case Rank::Two:
{
printInformationAboutThePlayerAndTheTopCardFromPile(player, Rank::Two);
int cardNumber = receiveAndValidateInput();
cardNumber = checkBounds(player, cardNumber);
if (!cardNumber)
receiveCardsFromDeck(player, neededCardsToPickForTwo);
else if (player._hand.get(cardNumber).rank() == Rank::Four && areCompatible(player._hand.get(cardNumber), _pile.topCard()))
putCardToPile(player, cardNumber);
else
{
receiveCardsFromDeck(player, neededCardsToPickForTwo);
/* validatePlayerCardCompatibilityWithPileTopCard(player, cardNumber);
if (!cardNumber)
else
putCardToPile(player, cardNumber);*/
}
break;
}
case Rank::Three:
{
printInformationAboutThePlayerAndTheTopCardFromPile(player, Rank::Three);
int cardNumber = receiveAndValidateInput();
cardNumber = checkBounds(player, cardNumber);
if (!cardNumber)
receiveCardsFromDeck(player, neededCardsToPickForThree);
else if (player._hand.get(cardNumber).rank() == Rank::Four && areCompatible(player._hand.get(cardNumber), _pile.topCard()))
putCardToPile(player, cardNumber);
else
{
receiveCardsFromDeck(player, neededCardsToPickForThree);
/* validatePlayerCardCompatibilityWithPileTopCard(player, cardNumber);
if (!cardNumber)
else
putCardToPile(player, cardNumber);*/
}
break;
}
case Rank::Ace:
{
printInformationAboutThePlayerAndTheTopCardFromPile(player, Rank::Ace);
break;
}
}
if (!player._hand.numberOfCards())
_gameOver = true;
}
void VisualGame::normalCardPlayerTurn(Player& player)
{
static constexpr auto defaultNumberOfCardsToPick{ 1 };
printInformationAboutThePlayerAndTheTopCardFromPile(player);
int cardNumber = receiveAndValidateInput();
cardNumber = checkBounds(player, cardNumber);
if (!cardNumber)
receiveCardsFromDeck(player, defaultNumberOfCardsToPick);
else
{
validatePlayerCardCompatibilityWithPileTopCard(player, cardNumber);
cardNumber = checkBounds(player, cardNumber);
if (!cardNumber)
receiveCardsFromDeck(player, defaultNumberOfCardsToPick);
else
putCardToPile(player, cardNumber);
}
if (!player._hand.numberOfCards())
_gameOver = true;
}
void VisualGame::run()
{
prepareGame();
Card lastCard{ _pile.topCard() };
bool specialCardHadEffect{ false };
while (_mainWindow.isOpen())
{
sf::Event event;
while (_mainWindow.pollEvent(event))
{
if (event.type == sf::Event::Closed)
_mainWindow.close();
}
for (auto& currentPlayer : _players)
{
if (!isSpecial(_pile.topCard()))
normalCardPlayerTurn(*currentPlayer);
else
{
if (isSpecial(_pile.topCard()) && !specialCardHadEffect)
{
specialCardPlayerTurn(*currentPlayer);
specialCardHadEffect = true;
}
else
normalCardPlayerTurn(*currentPlayer);
}
if (_pile.topCard() != lastCard)
{
specialCardHadEffect = false;
lastCard = _pile.topCard();
}
if (_gameOver)
break;
}
while (_gameOver)
{
_mainWindow.clear(defaultBackgroundColor);
_renderer.drawWinner(_mainWindow, findWinner()->name() + " wins!", defaultFont);
_renderer.drawEscapeText(_mainWindow, defaultFont);
_mainWindow.display();
sf::Event event;
if (_mainWindow.waitEvent(event))
{
if(sf::Keyboard::isKeyPressed(sf::Keyboard::Enter))
_mainWindow.close();
}
}
}
}
And the driver code:
SFML.cpp
#include "VisualGame.hpp"
#include <iostream>
using vectorOfPlayers = std::vector<std::unique_ptr<Player>>;
using paths = std::pair<std::string, std::string>;
vectorOfPlayers getPlayers() noexcept
{
vectorOfPlayers players;
std::cout << "Enter players.\n"
"\t* Enter an empty name when done adding players.\n";
for (auto name = std::string{}; std::getline(std::cin, name); )
{
if (name == "")
break;
players.push_back(std::make_unique<Player>(std::move(name)));
}
return players;
}
paths getPaths() noexcept
{
std::cout << "Enter the path to the textures folder.\n";
std::string texturesPath;
std::getline(std::cin, texturesPath);
std::cout << "Enter the path to the fonts folder.\n";
std::string fontsPath;
std::getline(std::cin, fontsPath);
return std::make_pair(texturesPath, fontsPath);
}
int main()
{
while (true)
{
try
{
int const monitorWidth = sf::VideoMode::getDesktopMode().width;
int const monitorHeight = sf::VideoMode::getDesktopMode().height;
vectorOfPlayers players = getPlayers();
paths resourcePaths = getPaths();
//constexpr std::string_view firstPath("G:/Visual Studio projects/MacaoGame/Debug/Textures");
//constexpr std::string_view secondPath("G:/Visual Studio projects/MacaoGame/Debug/Fonts");
VisualGame myVisualGame(players, resourcePaths.first, resourcePaths.second, monitorWidth, monitorHeight);
myVisualGame.run();
break;
}
catch (CouldNotLoadTextureException const& e)
{
std::cerr << e.what() << '\n';
return -1;
}
catch (CouldNotLoadFontException const& e)
{
std::cerr << e.what() << '\n';
return -1;
}
catch (InvalidNumberOfPlayersException const& e)
{
std::cerr << e.what() << '\n';
return -1;
}
}
return 0;
}
Also, what I don't like here is that I need to give absolute paths, otherwise std::filesystem::directory_iterator
will throw an exception.