diff --git a/.gitignore b/.gitignore index 7c0bc40..a42eae4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ build/ compile_commands.json .cache .vscode - diff --git a/cc-sim.Dockerfile b/cc-sim.Dockerfile index 0722541..fff5f80 100644 --- a/cc-sim.Dockerfile +++ b/cc-sim.Dockerfile @@ -4,12 +4,12 @@ RUN apt-get update # RUN apt-get install -y clang-11 RUN apt-get install -y cmake ninja-build python3 python3-pip RUN apt-get install -y clang-format vim git -RUN pip install cmake-format compdb conan +RUN pip install cmake-format compdb conan --break-system-packages WORKDIR /simulator ENV CONAN_USER_HOME=/simulator/build COPY requirements.txt . -RUN pip install -r requirements.txt +RUN pip install -r requirements.txt --break-system-packages # COPY . . # RUN bash setup_build.sh --build diff --git a/src/attacker/CMakeLists.txt b/src/attacker/CMakeLists.txt index d935eb0..67bd529 100644 --- a/src/attacker/CMakeLists.txt +++ b/src/attacker/CMakeLists.txt @@ -1 +1,2 @@ +add_subdirectory(pvpattacker) target_sources(cc_simulator PRIVATE attacker.cpp) diff --git a/src/attacker/attacker.cpp b/src/attacker/attacker.cpp index 82079b0..1172e89 100644 --- a/src/attacker/attacker.cpp +++ b/src/attacker/attacker.cpp @@ -58,10 +58,12 @@ void Attacker::update_state() { if (this->get_hp() == 0) { this->set_state(State::DEAD); - Logger::log_dead('A', this->_id); + this->log_death(); } } +void Attacker::log_death() const { Logger::log_dead('A', this->get_id()); } + [[nodiscard]] std::optional Attacker::get_nearest_defender_index( const std::vector &defenders) const { if (defenders.empty()) { @@ -100,14 +102,46 @@ void Attacker::move(Position position) { (int)Map::no_of_rows - 1); this->_position = Position(new_x, new_y); - Logger::log_move(this->_id, this->_position.get_x(), this->_position.get_y()); + this->log_move(this->_position); +} + +void Attacker::log_move(Position &p) const { + Logger::log_move(this->get_id(), p.get_x(), p.get_y()); } void Attacker::attack(Actor &opponent) const { opponent.take_damage(this->get_attack_power()); - Logger::log_shoot('D', this->_id, opponent.get_id(), opponent.get_hp()); + this->log_shoot(opponent); +} + +void Attacker::log_shoot(Actor &opponent) const { + Logger::log_shoot('D', this->get_id(), opponent.get_id(), opponent.get_hp()); } void Attacker::set_state(State s) { this->_state = s; } Attacker::State Attacker::get_state() const { return this->_state; } + +void Attacker::activate_ability(size_t turn) { + // Already activated, skip. + if (this->_ability_activated_turn.has_value()) + return; + + this->_ability_activated_turn = turn; +} + +void Attacker::check_ability(size_t turn) { + this->_is_ability_active = this->_ability_activated_turn.has_value() && + turn <= this->_ability_activated_turn.value() + + this->get_num_ability_turns(); +} + +bool Attacker::is_ability_active() const { return this->_is_ability_active; } + +unsigned Attacker::get_ability_activation_cost() { + return this->attribute_dictionary[this->_type].ability_activation_cost; +} + +unsigned Attacker::get_num_ability_turns() { + return this->attribute_dictionary[this->_type].num_ability_turns; +} diff --git a/src/attacker/attacker.hpp b/src/attacker/attacker.hpp index 504d7c2..ede8ebf 100644 --- a/src/attacker/attacker.hpp +++ b/src/attacker/attacker.hpp @@ -58,8 +58,21 @@ class Attacker : public Actor { void update_state() final; + virtual void log_death() const; + + virtual void log_move(Position &p) const; + + virtual void log_shoot(Actor &opponent) const; + [[nodiscard]] State get_state() const; + void activate_ability(size_t turn); + bool is_ability_active() const; + void check_ability(size_t turn); + + unsigned get_ability_activation_cost(); + unsigned get_num_ability_turns(); + private: void set_state(State s); static inline size_t _id_counter = 0; @@ -71,4 +84,7 @@ class Attacker : public Actor { Position _destination; bool _is_target_set_by_player; size_t _target_id; + + std::optional _ability_activated_turn; + bool _is_ability_active = false; }; diff --git a/src/attacker/pvpattacker/CMakeLists.txt b/src/attacker/pvpattacker/CMakeLists.txt new file mode 100644 index 0000000..6c90e26 --- /dev/null +++ b/src/attacker/pvpattacker/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(cc_simulator PRIVATE pvpattacker.cpp) diff --git a/src/attacker/pvpattacker/pvpattacker.cpp b/src/attacker/pvpattacker/pvpattacker.cpp new file mode 100644 index 0000000..59c7b5b --- /dev/null +++ b/src/attacker/pvpattacker/pvpattacker.cpp @@ -0,0 +1,70 @@ +#include "attacker/pvpattacker/pvpattacker.hpp" +#include "logger/pvplogger.hpp" +#include "utils/attributes.hpp" + +#include + +PvPAttacker PvPAttacker::construct(AttackerType type, Position p, Owner owner) { + Attributes attr = Attacker::attribute_dictionary[type]; + return {type, p, + attr.hp, attr.range, + attr.speed, attr.attack_power, + attr.price, attr.is_aerial, + owner, PvPAttacker::State::SPAWNED}; +} + +Owner PvPAttacker::get_owner() const { return this->_owner; } + +[[nodiscard]] std::optional PvPAttacker::get_nearest_defender_index( + const std::vector &defenders) const { + if (defenders.empty()) { + return std::nullopt; + } + if (this->is_aerial_type()) { + auto nearest_attacker = std::min_element( + defenders.begin(), defenders.end(), + [this](const PvPAttacker a, const PvPAttacker b) { + if (a.is_aerial_type() && !b.is_aerial_type() && + this->is_in_range(a)) { + return true; + } + if (b.is_aerial_type() && !a.is_aerial_type() && + this->is_in_range(b)) { + return false; + } + return this->get_position().distance_to(a.get_position()) < + this->get_position().distance_to(b.get_position()); + }); + return std::distance(defenders.begin(), nearest_attacker); + } + auto nearest_attacker = std::min_element( + defenders.begin(), defenders.end(), + [this](const PvPAttacker a, const PvPAttacker b) { + if (a.is_aerial_type() && !b.is_aerial_type()) { + return false; + } + if (b.is_aerial_type() && !a.is_aerial_type()) { + return true; + } + return this->get_position().distance_to(a.get_position()) < + this->get_position().distance_to(b.get_position()); + }); + if (!nearest_attacker->is_aerial_type()) { + return std::distance(defenders.begin(), nearest_attacker); + } + + return std::nullopt; +} + +void PvPAttacker::log_death() const { + PvPLogger::log_dead(this->get_id(), this->_owner); +} + +void PvPAttacker::log_move(Position &p) const { + PvPLogger::log_move(this->get_id(), p.get_x(), p.get_y(), this->_owner); +} + +void PvPAttacker::log_shoot(Actor &opponent) const { + PvPLogger::log_shoot(this->get_id(), opponent.get_id(), opponent.get_hp(), + this->_owner); +} diff --git a/src/attacker/pvpattacker/pvpattacker.hpp b/src/attacker/pvpattacker/pvpattacker.hpp new file mode 100644 index 0000000..777e57b --- /dev/null +++ b/src/attacker/pvpattacker/pvpattacker.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "attacker/attacker.hpp" +#include "utils/attributes.hpp" + +#include +#include +#include + +enum class Owner { PLAYER1, PLAYER2 }; + +class PvPAttacker : public Attacker { +public: + static inline std::unordered_map + pvp_weight_dictionary; + + PvPAttacker(AttackerType type, Position position, unsigned hp, unsigned range, + unsigned speed, unsigned attack_power, unsigned price, + bool is_aerial, Owner owner, State state = State::SPAWNED) + : Attacker{type, position, hp, range, speed, + attack_power, price, is_aerial, state}, + _owner(owner) {} + + [[nodiscard]] Owner get_owner() const; + + [[nodiscard]] static PvPAttacker construct(AttackerType type, Position p, + Owner owner); + + // TODO: make this function properly follow the rules of polymorphism + [[nodiscard]] std::optional + get_nearest_defender_index(const std::vector &defenders) const; + + void log_death() const override final; + + void log_move(Position &p) const override final; + + void log_shoot(Actor &opponent) const override final; + +private: + Owner _owner; +}; diff --git a/src/defender/defender.cpp b/src/defender/defender.cpp index 5fd096f..2598a91 100644 --- a/src/defender/defender.cpp +++ b/src/defender/defender.cpp @@ -37,8 +37,7 @@ std::optional Defender::get_nearest_attacker_index( } if (this->is_aerial_type()) { auto nearest_attacker = std::min_element( - attackers.begin(), attackers.end(), - [this](const Attacker a, const Attacker b) { + attackers.begin(), attackers.end(), [this](Attacker a, Attacker b) { if (a.is_aerial_type() && !b.is_aerial_type() && this->is_in_range(a)) { return true; @@ -47,20 +46,30 @@ std::optional Defender::get_nearest_attacker_index( this->is_in_range(b)) { return false; } + + if (a.is_ability_active()) + return false; + if (b.is_ability_active()) + return true; + return this->get_position().distance_to(a.get_position()) < this->get_position().distance_to(b.get_position()); }); return std::distance(attackers.begin(), nearest_attacker); } auto nearest_attacker = std::min_element( - attackers.begin(), attackers.end(), - [this](const Attacker a, const Attacker b) { + attackers.begin(), attackers.end(), [this](Attacker a, Attacker b) { if (a.is_aerial_type() && !b.is_aerial_type()) { return false; } if (b.is_aerial_type() && !a.is_aerial_type()) { return true; } + if (a.is_ability_active()) + return false; + if (b.is_ability_active()) + return true; + return this->get_position().distance_to(a.get_position()) < this->get_position().distance_to(b.get_position()); }); diff --git a/src/game/CMakeLists.txt b/src/game/CMakeLists.txt index 668cec1..69986ee 100644 --- a/src/game/CMakeLists.txt +++ b/src/game/CMakeLists.txt @@ -1 +1,2 @@ target_sources(cc_simulator PRIVATE game.cpp) +target_sources(cc_simulator PRIVATE pvpgame.cpp) diff --git a/src/game/game.cpp b/src/game/game.cpp index b854ea0..6e07b37 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -23,19 +23,24 @@ Game::Game(std::vector attackers, std::vector defenders, } Game Game::simulate( + const Game::turn_t turn, const std::unordered_map &player_set_targets, - const std::vector> &spawn_positions) - const { + const std::vector> &spawn_positions, + const std::vector &ability_activations) { const auto &prev_state_attackers = this->get_attackers(); const auto &prev_state_defenders = this->get_defenders(); + auto coins_left = this->get_coins(); + std::vector attackers(prev_state_attackers.begin(), prev_state_attackers.end()); std::vector defenders(prev_state_defenders.begin(), prev_state_defenders.end()); - ranges::for_each(attackers, - [](Attacker &attacker) { attacker.clear_destination(); }); + ranges::for_each(attackers, [turn](Attacker &attacker) { + attacker.clear_destination(); + attacker.check_ability(turn); + }); ranges::for_each( player_set_targets, @@ -50,6 +55,23 @@ Game Game::simulate( } }); + ranges::for_each(ability_activations, [&](Game::attacker_id id) { + auto attacker_index = this->get_attacker_index_by_id(id); + unsigned ability_activation_cost = + attackers[*attacker_index].get_ability_activation_cost(); + + if (coins_left < ability_activation_cost) { + return; + } + coins_left -= ability_activation_cost; + // In case the ability is already active, + // the user would have paid the cost but the ability would not be activated + // This is the penalty for trying to activate an already active ability. + if (attacker_index.has_value()) { + attackers[*attacker_index].activate_ability(turn); + } + }); + // Attacker Loop ranges::for_each(attackers, [&](Attacker &attacker) mutable { std::optional defender_index{std::nullopt}; @@ -77,9 +99,15 @@ Game Game::simulate( ranges::for_each(defenders, [&](Defender &defender) mutable { if (auto attacker_index = defender.get_nearest_attacker_index(prev_state_attackers)) { - if ((defender.is_in_range(attackers[*attacker_index])) && - ((defender.is_aerial_type()) || - (!attackers[*attacker_index].is_aerial_type()))) { + + bool should_attack = (defender.is_in_range(attackers[*attacker_index])) && + ((defender.is_aerial_type()) || + (!attackers[*attacker_index].is_aerial_type())); + + should_attack = + should_attack && !attackers[*attacker_index].is_ability_active(); + + if (should_attack) { defender.attack(attackers[*attacker_index]); // set defender's state to ATTACKING defender.set_state(Defender::State::ATTACKING); @@ -131,8 +159,6 @@ Game Game::simulate( }), defenders.end()); - auto coins_left = this->get_coins(); - // new attackers are spawned here auto positions = std::set{}; ranges::for_each(spawn_positions, [&](const auto &spawn_details) { diff --git a/src/game/game.hpp b/src/game/game.hpp index 8acccdd..3a2b155 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -5,26 +5,30 @@ #include "utils/position.hpp" #include +#include +#include #include class Game { public: - Game(std::vector attackers, std::vector defenders, - unsigned coins); - using defender_id = size_t; using attacker_id = size_t; using index_t = size_t; + using turn_t = size_t; + + Game(std::vector attackers, std::vector defenders, + unsigned coins); [[nodiscard]] const std::vector &get_attackers() const; [[nodiscard]] const std::vector &get_defenders() const; [[nodiscard]] Game simulate( + const turn_t turn, const std::unordered_map &player_set_targets, - const std::vector> &spawn_positions) - const; + const std::vector> &spawn_positions, + const std::vector &ability_activations); [[nodiscard]] unsigned get_coins() const; diff --git a/src/game/pvpgame.cpp b/src/game/pvpgame.cpp new file mode 100644 index 0000000..cb2947f --- /dev/null +++ b/src/game/pvpgame.cpp @@ -0,0 +1,296 @@ +#include "game/pvpgame.hpp" +#include "logger/logger.hpp" +#include "logger/pvplogger.hpp" + +#include +#include +#include +#include + +#include + +PvPGame::PvPGame(std::vector player_1_attackers, + std::vector player_2_attackers) + : _player_1_attackers(std::move(player_1_attackers)), + _player_2_attackers(std::move(player_2_attackers)) { + // make all the id to index maps in constructor itself + std::ranges::for_each( + this->_player_1_attackers, + [&, index = 0](const PvPAttacker &attacker) mutable { + this->_player_1_attacker_id_to_index[attacker.get_id()] = index++; + }); + std::ranges::for_each( + this->_player_2_attackers, + [&, index = 0](const PvPAttacker &attacker) mutable { + this->_player_2_attacker_id_to_index[attacker.get_id()] = index++; + }); +} + +PvPGame PvPGame::simulate( + const turn_t turn, + const std::unordered_map + &player_1_set_targets, + const std::vector> + &player_1_spawn_positions, + const std::unordered_map + &player_2_set_targets, + const std::vector> + &player_2_spawn_positions, + const std::vector &player_1_ability_activations, + const std::vector &player_2_ability_activations) + const { + + const auto &prev_state_player_1_attackers = this->get_player_1_attackers(); + const auto &prev_state_player_2_attackers = this->get_player_2_attackers(); + + std::vector player_1_attackers( + prev_state_player_1_attackers.begin(), + prev_state_player_1_attackers.end()); + std::vector player_2_attackers( + prev_state_player_2_attackers.begin(), + prev_state_player_2_attackers.end()); + + std::ranges::for_each(player_1_attackers, [turn](PvPAttacker &attacker) { + attacker.clear_destination(); + attacker.check_ability(turn); + }); + + std::ranges::for_each(player_2_attackers, [turn](PvPAttacker &attacker) { + attacker.clear_destination(); + attacker.check_ability(turn); + }); + + unsigned player_1_coins = PvPGame::FIXED_COINS_PER_TURN; + unsigned player_2_coins = PvPGame::FIXED_COINS_PER_TURN; + + std::ranges::for_each( + player_1_set_targets, + [&](const std::pair &entry) { + auto player_1_attacker_index = + this->get_player_1_attacker_index_by_id(entry.first); + auto player_2_attacker_index = + this->get_player_2_attacker_index_by_id(entry.second); + if (player_1_attacker_index.has_value() && + player_2_attacker_index.has_value()) { + player_1_attackers[*player_1_attacker_index].set_target( + player_2_attackers[*player_2_attacker_index].get_id()); + } + }); + + std::ranges::for_each( + player_2_set_targets, + [&](const std::pair &entry) { + auto player_2_attacker_index = + this->get_player_2_attacker_index_by_id(entry.first); + auto player_1_attacker_index = + this->get_player_1_attacker_index_by_id(entry.second); + if (player_2_attacker_index.has_value() && + player_1_attacker_index.has_value()) { + player_2_attackers[*player_2_attacker_index].set_target( + player_1_attackers[*player_1_attacker_index].get_id()); + } + }); + + std::ranges::for_each( + player_1_ability_activations, [&](player_1_attacker_id id) { + auto player_1_attacker_index = + this->get_player_1_attacker_index_by_id(id); + unsigned ability_activation_cost = + player_1_attackers[*player_1_attacker_index] + .get_ability_activation_cost(); + + if (player_1_coins < ability_activation_cost) { + return; + } + player_1_coins -= ability_activation_cost; + // In case the ability is already active, + // the user would have paid the cost but the ability would not be + // activated This is the penalty for trying to activate an already + // active ability. + if (player_1_attacker_index.has_value()) { + player_1_attackers[*player_1_attacker_index].activate_ability(turn); + } + }); + + std::ranges::for_each( + player_2_ability_activations, [&](player_2_attacker_id id) { + auto player_2_attacker_index = + this->get_player_2_attacker_index_by_id(id); + unsigned ability_activation_cost = + player_2_attackers[*player_2_attacker_index] + .get_ability_activation_cost(); + + if (player_2_coins < ability_activation_cost) { + return; + } + player_2_coins -= ability_activation_cost; + // In case the ability is already active, + // the user would have paid the cost but the ability would not be + // activated This is the penalty for trying to activate an already + // active ability. + if (player_2_attacker_index.has_value()) { + player_2_attackers[*player_2_attacker_index].activate_ability(turn); + } + }); + + std::ranges::for_each(player_1_attackers, [&](PvPAttacker &attacker) mutable { + std::optional player_2_attacker_index{std::nullopt}; + if (attacker.is_target_set_by_player() && + this->get_player_2_attacker_index_by_id(attacker.get_target_id())) { + player_2_attacker_index = + this->get_player_2_attacker_index_by_id(attacker.get_target_id()); + } else { + attacker.clear_target(); + player_2_attacker_index = + attacker.get_nearest_defender_index(player_2_attackers); + } + + if (player_2_attacker_index.has_value()) { + if ((attacker.is_in_range( + player_2_attackers[*player_2_attacker_index]))) { + attacker.attack(player_2_attackers[*player_2_attacker_index]); + } else { + attacker.set_destination( + player_2_attackers[*player_2_attacker_index].get_position()); + } + } + }); + + std::ranges::for_each(player_2_attackers, [&](PvPAttacker &attacker) mutable { + std::optional player_1_attacker_index{std::nullopt}; + if (attacker.is_target_set_by_player() && + this->get_player_1_attacker_index_by_id(attacker.get_target_id())) { + player_1_attacker_index = + this->get_player_1_attacker_index_by_id(attacker.get_target_id()); + } else { + attacker.clear_target(); + player_1_attacker_index = + attacker.get_nearest_defender_index(player_1_attackers); + } + + if (player_1_attacker_index.has_value()) { + if ((attacker.is_in_range( + player_1_attackers[*player_1_attacker_index]))) { + attacker.attack(player_1_attackers[*player_1_attacker_index]); + } else { + attacker.set_destination( + player_1_attackers[*player_1_attacker_index].get_position()); + } + } + }); + + std::ranges::for_each(player_1_attackers, + [](PvPAttacker &attacker) { attacker.update_state(); }); + std::ranges::for_each(player_2_attackers, + [](PvPAttacker &attacker) { attacker.update_state(); }); + + std::ranges::for_each(player_1_attackers, [&](PvPAttacker &attacker) { + if (attacker.is_target_set_by_player()) { + auto opponent_attacker = + this->get_player_2_attacker_index_by_id(attacker.get_target_id()); + if (opponent_attacker.has_value() && + player_2_attackers[*opponent_attacker].get_state() == + Attacker::State::DEAD) { + attacker.clear_target(); + } + } + }); + + std::ranges::for_each(player_2_attackers, [&](PvPAttacker &attacker) { + if (attacker.is_target_set_by_player()) { + auto opponent_attacker = + this->get_player_1_attacker_index_by_id(attacker.get_target_id()); + if (opponent_attacker.has_value() && + player_1_attackers[*opponent_attacker].get_state() == + Attacker::State::DEAD) { + attacker.clear_target(); + } + } + }); + + player_1_attackers.erase( + std::remove_if(player_1_attackers.begin(), player_1_attackers.end(), + [](const PvPAttacker &attacker) { + return attacker.get_state() == Attacker::State::DEAD; + }), + player_1_attackers.end()); + + player_2_attackers.erase( + std::remove_if(player_2_attackers.begin(), player_2_attackers.end(), + [](const PvPAttacker &attacker) { + return attacker.get_state() == Attacker::State::DEAD; + }), + player_2_attackers.end()); + + auto player_1_positions = std::set{}; + auto player_2_positions = std::set{}; + + std::ranges::for_each( + player_1_spawn_positions, [&](const auto &spawn_details) { + const auto &[position, type] = spawn_details; + const unsigned price = Attacker::attribute_dictionary[type].price; + if (price > player_1_coins) { + return; + } + player_1_coins -= price; + if (Position::is_valid_spawn_position(position.get_x(), + position.get_y()) && + !player_1_positions.contains(position)) { + player_1_positions.insert(position); + // TODO find a way to log in constructor itself + auto new_attacker = + PvPAttacker::construct(type, position, Owner::PLAYER1); + PvPLogger::log_spawn(new_attacker.get_id(), type, position.get_x(), + position.get_y(), Owner::PLAYER1); + player_1_attackers.push_back(std::move(new_attacker)); + } + }); + + std::ranges::for_each( + player_2_spawn_positions, [&](const auto &spawn_details) { + const auto &[position, type] = spawn_details; + const unsigned price = Attacker::attribute_dictionary[type].price; + if (price > player_2_coins) { + return; + } + player_2_coins -= price; + if (Position::is_valid_spawn_position(position.get_x(), + position.get_y()) && + !player_2_positions.contains(position)) { + player_2_positions.insert(position); + auto new_attacker = + PvPAttacker::construct(type, position, Owner::PLAYER2); + PvPLogger::log_spawn(new_attacker.get_id(), type, position.get_x(), + position.get_y(), Owner::PLAYER2); + player_2_attackers.push_back(std::move(new_attacker)); + } + }); + + return {std::move(player_1_attackers), std::move(player_2_attackers)}; +} + +const std::vector &PvPGame::get_player_1_attackers() const { + return this->_player_1_attackers; +} + +const std::vector &PvPGame::get_player_2_attackers() const { + return this->_player_2_attackers; +} + +std::optional +PvPGame::get_player_1_attacker_index_by_id(player_1_attacker_id id) const { + if (this->_player_1_attacker_id_to_index.contains(id)) { + return this->_player_1_attacker_id_to_index.at(id); + } else { + return std::nullopt; + } +} + +std::optional +PvPGame::get_player_2_attacker_index_by_id(player_2_attacker_id id) const { + if (this->_player_2_attacker_id_to_index.contains(id)) { + return this->_player_2_attacker_id_to_index.at(id); + } else { + return std::nullopt; + } +} \ No newline at end of file diff --git a/src/game/pvpgame.hpp b/src/game/pvpgame.hpp new file mode 100644 index 0000000..d48d743 --- /dev/null +++ b/src/game/pvpgame.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "attacker/pvpattacker/pvpattacker.hpp" +#include "utils/position.hpp" + +#include +#include + +class PvPGame { + +public: + PvPGame(std::vector player_1_attackers, + std::vector player_2_attackers); + using player_1_attacker_id = size_t; + using player_2_attacker_id = size_t; + using index_t = size_t; + using turn_t = size_t; + + static inline unsigned int FIXED_COINS_PER_TURN; + + [[nodiscard]] const std::vector &get_player_1_attackers() const; + + [[nodiscard]] const std::vector &get_player_2_attackers() const; + + [[nodiscard]] PvPGame simulate( + const turn_t turn, + const std::unordered_map + &player_1_set_targets, + const std::vector> + &player_1_spawn_positions, + const std::unordered_map + &player_2_set_targets, + const std::vector> + &player_2_spawn_positions, + const std::vector &player_1_ability_activations, + const std::vector &player_2_ability_activations) + const; + +private: + std::optional + get_player_1_attacker_index_by_id(player_1_attacker_id id) const; + std::optional + get_player_2_attacker_index_by_id(player_2_attacker_id id) const; + + std::unordered_map + _player_1_attacker_id_to_index; + std::unordered_map + _player_2_attacker_id_to_index; + std::vector _player_1_attackers; + std::vector _player_2_attackers; +}; diff --git a/src/logger/CMakeLists.txt b/src/logger/CMakeLists.txt index 5ac092e..4dbe10e 100644 --- a/src/logger/CMakeLists.txt +++ b/src/logger/CMakeLists.txt @@ -1 +1,2 @@ target_sources(cc_simulator PRIVATE logger.cpp) +target_sources(cc_simulator PRIVATE pvplogger.cpp) diff --git a/src/logger/logger.cpp b/src/logger/logger.cpp index 93f8781..3c1173d 100644 --- a/src/logger/logger.cpp +++ b/src/logger/logger.cpp @@ -30,6 +30,8 @@ void Logger::log_turn(unsigned turn) { void Logger::log_move(size_t attacker_id, int x, int y) { Logger::get_stream() << "MOVE" + << ", " + << "A" << ", " << attacker_id << ", " << x << ", " << y << "\n"; } @@ -42,6 +44,8 @@ void Logger::log_shoot(char opponent_actor_type, size_t attacker_id, void Logger::log_spawn(size_t attacker_id, AttackerType type, int x, int y) { Logger::get_stream() << "SPAWN" + << ", " + << "A" << ", " << attacker_id << ", " << (int)type << ", " << x << ", " << y << "\n"; } diff --git a/src/logger/pvplogger.cpp b/src/logger/pvplogger.cpp new file mode 100644 index 0000000..c88b9fb --- /dev/null +++ b/src/logger/pvplogger.cpp @@ -0,0 +1,149 @@ +#include "logger/pvplogger.hpp" + +#include +#include +#include + +void PvPLogger::log_init(void) { + PvPLogger::get_p1_stream() << "INIT" + << "\n"; + PvPLogger::get_p2_stream() << "INIT" + << "\n"; +} + +void PvPLogger::log_turn(unsigned turn) { + PvPLogger::get_p1_stream() << "TURN" + << ", " << turn << "\n"; + PvPLogger::get_p2_stream() << "TURN" + << ", " << turn << "\n"; +} + +void PvPLogger::log_move(size_t attacker_id, int x, int y, Owner owner) { + if (owner == Owner::PLAYER1) { + PvPLogger::get_p1_stream() + << "MOVE" + << ", " + << "A" + << ", " << attacker_id << ", " << x << ", " << y << "\n"; + PvPLogger::get_p2_stream() + << "MOVE" + << ", " + << "D" + << ", " << attacker_id << ", " << x << ", " << y << "\n"; + } else { + PvPLogger::get_p2_stream() + << "MOVE" + << ", " + << "A" + << ", " << attacker_id << ", " << x << ", " << y << "\n"; + PvPLogger::get_p1_stream() + << "MOVE" + << ", " + << "D" + << ", " << attacker_id << ", " << x << ", " << y << "\n"; + } +} + +void PvPLogger::log_shoot(size_t attacker_id, size_t opponent_id, + unsigned target_hp, Owner owner) { + if (owner == Owner::PLAYER1) { + PvPLogger::get_p1_stream() << "SHOOT" + << ", " + << "D" + << ", " << attacker_id << ", " << opponent_id + << ", " << target_hp << "\n"; + PvPLogger::get_p2_stream() << "SHOOT" + << ", " + << "A" + << ", " << attacker_id << ", " << opponent_id + << ", " << target_hp << "\n"; + } else { + PvPLogger::get_p2_stream() << "SHOOT" + << ", " + << "D" + << ", " << attacker_id << ", " << opponent_id + << ", " << target_hp << "\n"; + PvPLogger::get_p1_stream() << "SHOOT" + << ", " + << "A" + << ", " << attacker_id << ", " << opponent_id + << ", " << target_hp << "\n"; + } +} + +void PvPLogger::log_spawn(size_t attacker_id, AttackerType type, int x, int y, + Owner owner) { + // attacker for owner is A, defender for opponent is D + if (owner == Owner::PLAYER1) { + PvPLogger::get_p1_stream() << "SPAWN" + << ", " + << "A" + << ", " << attacker_id << ", " << (int)type + << ", " << x << ", " << y << "\n"; + PvPLogger::get_p2_stream() << "SPAWN" + << ", " + << "D" + << ", " << attacker_id << ", " << (int)type + << ", " << x << ", " << y << "\n"; + } else { + PvPLogger::get_p2_stream() << "SPAWN" + << ", " + << "A" + << ", " << attacker_id << ", " << (int)type + << ", " << x << ", " << y << "\n"; + + PvPLogger::get_p1_stream() << "SPAWN" + << ", " + << "D" + << ", " << attacker_id << ", " << (int)type + << ", " << x << ", " << y << "\n"; + } +} + +void PvPLogger::log_dead(size_t actor_id, Owner owner) { + // if player1's attacker died player 2's defender died and vice versa + if (owner == Owner::PLAYER1) { + PvPLogger::get_p1_stream() << "DEAD" + << ", " + << "A" + << ", " << actor_id << "\n"; + PvPLogger::get_p2_stream() << "DEAD" + << ", " + << "D" + << ", " << actor_id << "\n"; + } else { + PvPLogger::get_p2_stream() << "DEAD" + << ", " + << "A" + << ", " << actor_id << "\n"; + PvPLogger::get_p1_stream() << "DEAD" + << ", " + << "D" + << ", " << actor_id << "\n"; + } +} + +void PvPLogger::log_score(unsigned score, Owner owner) { + if (owner == Owner::PLAYER1) { + PvPLogger::get_p1_stream() << "SCORE" + << ", " << score << "\n"; + } else { + PvPLogger::get_p2_stream() << "SCORE" + << ", " << score << "\n"; + } +} + +void PvPLogger::log_end() { + PvPLogger::get_p1_stream() << "END" + << "\n"; + PvPLogger::get_p2_stream() << "END" + << "\n"; +} + +[[nodiscard]] std::string PvPLogger::get_p1_log() { + return PvPLogger::get_p1_stream().str(); +} + +[[nodiscard]] std::string PvPLogger::get_p2_log() { + return PvPLogger::get_p2_stream().str(); +} \ No newline at end of file diff --git a/src/logger/pvplogger.hpp b/src/logger/pvplogger.hpp new file mode 100644 index 0000000..2331265 --- /dev/null +++ b/src/logger/pvplogger.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "attacker/pvpattacker/pvpattacker.hpp" + +#include +#include + +/* + * A singleton class that handles logging for pvp + */ +class PvPLogger { + +public: + static PvPLogger &get() { + static PvPLogger logger; + return logger; + } + PvPLogger(PvPLogger const &) = delete; + PvPLogger(PvPLogger &&) = delete; + PvPLogger &operator=(PvPLogger const &) = delete; + PvPLogger &operator=(PvPLogger &&) = delete; + static void log_init(void); + static std::ostringstream &get_p1_stream() { return get()._p1_stream; } + static std::ostringstream &get_p2_stream() { return get()._p2_stream; } + static void log_turn(unsigned turn); + static void log_move(size_t attacker_id, int x, int y, Owner owner); + static void log_shoot(size_t attacker_id, size_t opponent_id, + unsigned target_hp, Owner owner); + static void log_spawn(size_t attacker_id, AttackerType type, int x, int y, + Owner owner); + static void log_dead(size_t actor_id, Owner owner); + static void log_score(unsigned score, Owner owner); + static void log_end(); + [[nodiscard]] static std::string get_p1_log(); + [[nodiscard]] static std::string get_p2_log(); + +private: + std::ostringstream _p1_stream; + std::ostringstream _p2_stream; + PvPLogger() = default; + ~PvPLogger() = default; +}; diff --git a/src/main.cpp b/src/main.cpp index dbb8981..7b89103 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,15 +1,22 @@ #include "defender/defender.hpp" #include "game/game.hpp" +#include "game/pvpgame.hpp" #include "logger/logger.hpp" +#include "logger/pvplogger.hpp" +#include "utils/argument_reader.hpp" #include "utils/attributes.hpp" +#include "utils/file_descriptor.hpp" #include "utils/game_map.hpp" +#include "utils/game_type.hpp" #include +#include +#include +#include #include #include -int main() { - std::cout.setf(std::ios::unitbuf); +void NormalGame() { unsigned turns, coins; std::cin >> turns >> coins; @@ -17,12 +24,16 @@ int main() { std::cin >> n_attacker_types; for (unsigned attacker_type_id = 1; attacker_type_id <= n_attacker_types; ++attacker_type_id) { - unsigned hp, range, attack_power, speed, price; + // for normal game weight is not required so we just input and ignore it + unsigned hp, range, attack_power, speed, price, weight, num_ability_turns, + ability_activation_cost; bool is_aerial; - std::cin >> hp >> range >> attack_power >> speed >> price >> is_aerial; + std::cin >> hp >> range >> attack_power >> speed >> price >> is_aerial >> + weight >> num_ability_turns >> ability_activation_cost; Attacker::attribute_dictionary.insert(std::make_pair( AttackerType(attacker_type_id), - Attributes(hp, range, attack_power, speed, price, is_aerial))); + Attributes(hp, range, attack_power, speed, price, is_aerial, + num_ability_turns, ability_activation_cost))); } unsigned n_defender_types; @@ -34,7 +45,7 @@ int main() { std::cin >> hp >> range >> attack_power >> speed >> price >> is_aerial; Defender::attribute_dictionary.insert(std::make_pair( DefenderType(defender_type_id), - Attributes(hp, range, attack_power, speed, price, is_aerial))); + Attributes(hp, range, attack_power, speed, price, is_aerial, 0, 0))); } auto map = Map::get(std::cin); @@ -49,7 +60,7 @@ int main() { Game game({}, defenders, coins); - for (size_t turn = 0; turn < turns; ++turn) { + for (Game::turn_t turn = 0; turn < turns; ++turn) { Logger::log_turn(turn); unsigned n_attackers; @@ -66,8 +77,9 @@ int main() { // Get all the manually to be set targets as input, we need attacker's id // and targetted defender id std::unordered_map player_set_targets; - int no_of_player_set_targets = 0; + int no_of_player_set_targets = -1; std::cin >> no_of_player_set_targets; + assert(no_of_player_set_targets >= 0); while ((no_of_player_set_targets--) > 0) { Game::attacker_id att_id = 0; Game::defender_id def_id = 0; @@ -75,14 +87,28 @@ int main() { player_set_targets[att_id] = def_id; } - game = game.simulate(player_set_targets, spawn_positions); + // TODO: Update the runner format to include ability activations + std::vector ability_activations; + int no_of_ability_activations = -1; + std::cin >> no_of_ability_activations; + assert(no_of_ability_activations >= 0); + + while ((no_of_ability_activations--) > 0) { + Game::attacker_id att_id = 0; + std::cin >> att_id; + ability_activations.push_back(att_id); + } + + game = game.simulate(turn, player_set_targets, spawn_positions, + ability_activations); auto active_attackers = game.get_attackers(); std::cout << active_attackers.size() << "\n"; std::ranges::for_each(active_attackers, [](const Attacker &attacker) { std::cout << attacker.get_id() << " " << attacker.get_position().get_x() << " " << attacker.get_position().get_y() << " " - << (int)attacker.get_type() << " " << attacker.get_hp() << "\n"; + << (int)attacker.get_type() << " " << attacker.get_hp() << " " + << (int)attacker.is_ability_active() << "\n"; }); auto active_defenders = game.get_defenders(); @@ -101,12 +127,235 @@ int main() { return acc + defender.get_hp(); }); - Logger::log_destruction( - initial_hp == 0 ? 0 : (initial_hp - current_hp) * 100.0 / initial_hp); + Logger::log_destruction(initial_hp == 0 ? 100.0 + : (initial_hp - current_hp) * + 100.0 / initial_hp); Logger::log_coins(game.get_coins()); } Logger::log_end(); - // TODO: Figure out a way to save and extract logs from driver std::cerr << Logger::get_log(); } + +void PVPGame(std::string p1_in, std::string p1_out, std::string p2_in, + std::string p2_out) { + + FileDescriptorInput player1_stream(p1_in, p1_out); + FileDescriptorInput player2_stream(p2_in, p2_out); + + unsigned turns; + std::cin >> turns >> PvPGame::FIXED_COINS_PER_TURN; + + unsigned n_attacker_types; + std::cin >> n_attacker_types; + for (unsigned attacker_type_id = 1; attacker_type_id <= n_attacker_types; + ++attacker_type_id) { + unsigned hp, range, attack_power, speed, price, weight, num_ability_turns, + ability_activation_cost; + bool is_aerial; + std::cin >> hp >> range >> attack_power >> speed >> price >> is_aerial >> + weight >> num_ability_turns >> ability_activation_cost; + Attacker::attribute_dictionary.insert(std::make_pair( + AttackerType(attacker_type_id), + Attributes(hp, range, attack_power, speed, price, is_aerial, + num_ability_turns, ability_activation_cost))); + PvPAttacker::pvp_weight_dictionary.insert( + std::make_pair(AttackerType(attacker_type_id), weight)); + } + + unsigned n_defender_types; + std::cin >> n_defender_types; + for (unsigned defender_type_id = 1; defender_type_id <= n_defender_types; + ++defender_type_id) { + unsigned hp, range, attack_power, speed, price; + bool is_aerial; + std::cin >> hp >> range >> attack_power >> speed >> price >> is_aerial; + Defender::attribute_dictionary.insert(std::make_pair( + DefenderType(defender_type_id), + Attributes(hp, range, attack_power, speed, price, is_aerial, 0, 0))); + } + + PvPLogger::log_init(); + + PvPGame game({}, {}); + for (size_t turn = 0; turn < turns; ++turn) { + + PvPLogger::log_turn(turn); + + unsigned n_player_1_attackers; + player1_stream.get() >> n_player_1_attackers; + + auto player_1_spawn_positions = + std::vector>(); + while (n_player_1_attackers-- > 0) { + unsigned type_id, x, y; + player1_stream.get() >> type_id >> x >> y; + player_1_spawn_positions.emplace_back( + std::make_pair(Position(x, y), AttackerType(type_id))); + } + + unsigned n_player_2_attackers; + player2_stream.get() >> n_player_2_attackers; + + auto player_2_spawn_positions = + std::vector>(); + + while (n_player_2_attackers-- > 0) { + unsigned type_id, x, y; + player2_stream.get() >> type_id >> x >> y; + player_2_spawn_positions.emplace_back( + std::make_pair(Position(x, y), AttackerType(type_id))); + } + + std::unordered_map + player_1_set_targets; + int no_of_player_1_set_targets = 0; + player1_stream.get() >> no_of_player_1_set_targets; + while ((no_of_player_1_set_targets--) > 0) { + PvPGame::player_1_attacker_id att_id = 0; + PvPGame::player_2_attacker_id def_id = 0; + player1_stream.get() >> att_id >> def_id; + player_1_set_targets[att_id] = def_id; + } + + int no_of_player_2_set_targets = 0; + player2_stream.get() >> no_of_player_2_set_targets; + std::unordered_map + player_2_set_targets; + while ((no_of_player_2_set_targets--) > 0) { + PvPGame::player_2_attacker_id att_id = 0; + PvPGame::player_1_attacker_id def_id = 0; + player2_stream.get() >> att_id >> def_id; + player_2_set_targets[att_id] = def_id; + } + + int no_of_player_1_ability_activations = 0; + player1_stream.get() >> no_of_player_1_ability_activations; + std::vector player_1_ability_activations; + while ((no_of_player_1_ability_activations--) > 0) { + PvPGame::player_1_attacker_id att_id = 0; + player1_stream.get() >> att_id; + player_1_ability_activations.push_back(att_id); + } + + int no_of_player_2_ability_activations = 0; + player2_stream.get() >> no_of_player_2_ability_activations; + std::vector player_2_ability_activations; + while ((no_of_player_2_ability_activations--) > 0) { + PvPGame::player_2_attacker_id att_id = 0; + player2_stream.get() >> att_id; + player_2_ability_activations.push_back(att_id); + } + + game = game.simulate(turn, player_1_set_targets, player_1_spawn_positions, + player_2_set_targets, player_2_spawn_positions, + player_1_ability_activations, + player_2_ability_activations); + + auto player1_active_attackers = game.get_player_1_attackers(); + player1_stream.put() << player1_active_attackers.size() << "\n"; + + std::ranges::for_each( + player1_active_attackers, [&player1_stream](const Attacker &attacker) { + player1_stream.put() + << attacker.get_id() << " " << attacker.get_position().get_x() + << " " << attacker.get_position().get_y() << " " + << (int)attacker.get_type() << " " << attacker.get_hp() << " " + << (int)attacker.is_ability_active() << "\n"; + player1_stream.put().flush(); + }); + + player1_stream.put().flush(); + + auto player2_active_attackers = game.get_player_2_attackers(); + player1_stream.put() << player2_active_attackers.size() << "\n"; + + std::ranges::for_each( + player2_active_attackers, [&player1_stream](const Attacker &attacker) { + player1_stream.put() + << attacker.get_id() << " " << attacker.get_position().get_x() + << " " << attacker.get_position().get_y() << " " + << (int)attacker.get_type() << " " << attacker.get_hp() << " " + << (int)attacker.is_ability_active() << "\n"; + player1_stream.put().flush(); + }); + player1_stream.put().flush(); + + player2_stream.put() << player2_active_attackers.size() << "\n"; + std::ranges::for_each( + player2_active_attackers, [&player2_stream](const Attacker &attacker) { + player2_stream.put() + << attacker.get_id() << " " << attacker.get_position().get_x() + << " " << attacker.get_position().get_y() << " " + << (int)attacker.get_type() << " " << attacker.get_hp() << " " + << (int)attacker.is_ability_active() << "\n"; + player2_stream.put().flush(); + }); + + player2_stream.put().flush(); + + player2_stream.put() << player1_active_attackers.size() << "\n"; + std::ranges::for_each( + player1_active_attackers, [&player2_stream](const Attacker &attacker) { + player2_stream.put() + << attacker.get_id() << " " << attacker.get_position().get_x() + << " " << attacker.get_position().get_y() << " " + << (int)attacker.get_type() << " " << attacker.get_hp() << " " + << (int)attacker.is_ability_active() << "\n"; + player2_stream.put().flush(); + }); + + player2_stream.put().flush(); + player1_stream.put().flush(); + } + + auto player1_weighted_score = std::accumulate( + game.get_player_1_attackers().begin(), + game.get_player_1_attackers().end(), 0, + [](unsigned acc, const Attacker &attacker) { + return acc + PvPAttacker::pvp_weight_dictionary[attacker.get_type()]; + }); + + PvPLogger::log_score(player1_weighted_score, Owner::PLAYER1); + + auto player2_weighted_score = std::accumulate( + game.get_player_2_attackers().begin(), + game.get_player_2_attackers().end(), 0, + [](unsigned acc, const Attacker &attacker) { + return acc + PvPAttacker::pvp_weight_dictionary[attacker.get_type()]; + }); + + PvPLogger::log_score(player2_weighted_score, Owner::PLAYER2); + + PvPLogger::log_end(); + + // TODO find a seperator for the logs + std::cerr << PvPLogger::get_p1_log() << std::endl; + std::cerr << "DELIMITER" << std::endl; + std::cerr << PvPLogger::get_p2_log() << std::endl; +} + +int main(int argc, char *argv[]) { + std::cout.setf(std::ios::unitbuf); + + ArgumentReader commandLineReader = ArgumentReader(argc, argv); + GameType gameType = + stringToGameType(commandLineReader.get("--type", "normal")); + + switch (gameType) { + case GameType::Normal: + NormalGame(); + break; + + case GameType::PvP: + std::string p1_in = commandLineReader.get("p1_in"); + std::string p2_in = commandLineReader.get("p2_in"); + std::string p1_out = commandLineReader.get("p1_out"); + std::string p2_out = commandLineReader.get("p2_out"); + + PVPGame(p1_in, p1_out, p2_in, p2_out); + break; + } +} diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index 6c607f5..4f5c12d 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -1 +1,8 @@ -target_sources(cc_simulator PRIVATE position.cpp attributes.cpp game_map.cpp) +target_sources( + cc_simulator + PRIVATE position.cpp + attributes.cpp + game_map.cpp + argument_reader.cpp + file_descriptor.cpp + game_type.cpp) diff --git a/src/utils/argument_reader.cpp b/src/utils/argument_reader.cpp new file mode 100644 index 0000000..0c7d79a --- /dev/null +++ b/src/utils/argument_reader.cpp @@ -0,0 +1,39 @@ +#include "argument_reader.hpp" + +#include +#include +#include +#include + +ArgumentReader::ArgumentReader(int argc, char **argv) { + for (int i = 0; i < argc; i++) { + std::string cur(argv[i]); + _args.push_back(cur); + } + ArgumentReader::process(); +}; + +void ArgumentReader::process(void) { + std::for_each(_args.begin(), _args.end(), [&](std::string &arg) { + size_t found = arg.find(SepChar); + + if (found == std::string::npos) { + return; + } + + std::string key = arg.substr(0, found); + std::string value = arg.substr(found + 1); + _dict[key] = value; + }); +} + +std::string ArgumentReader::get(std::string key) { return this->get(key, ""); } + +std::string ArgumentReader::get(std::string key, + std::string default_value = "") { + if (_dict.count(key) > 0) { + return _dict[key]; + } + + return default_value; +} diff --git a/src/utils/argument_reader.hpp b/src/utils/argument_reader.hpp new file mode 100644 index 0000000..1bbeaff --- /dev/null +++ b/src/utils/argument_reader.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +class ArgumentReader { +public: + ArgumentReader(int, char **); + [[nodiscard]] std::string get(std::string, std::string); + [[nodiscard]] std::string get(std::string); + +private: + const char SepChar = '='; + std::vector _args; + std::map _dict; + void process(void); +}; diff --git a/src/utils/attributes.cpp b/src/utils/attributes.cpp index 4ee7e50..2f00ba2 100644 --- a/src/utils/attributes.cpp +++ b/src/utils/attributes.cpp @@ -1,9 +1,13 @@ #include "utils/attributes.hpp" Attributes::Attributes(unsigned hp, unsigned range, unsigned attack_power, - unsigned speed, unsigned price, bool is_aerial) + unsigned speed, unsigned price, bool is_aerial, + unsigned num_ability_turns, + unsigned ability_activation_cost) : hp(hp), range(range), attack_power(attack_power), speed(speed), - price(price), is_aerial(is_aerial) {} + price(price), is_aerial(is_aerial), num_ability_turns(num_ability_turns), + ability_activation_cost(ability_activation_cost) {} Attributes::Attributes() - : hp(0), range(0), attack_power(0), speed(0), price(0), is_aerial(false) {} + : hp(0), range(0), attack_power(0), speed(0), price(0), is_aerial(false), + num_ability_turns(0), ability_activation_cost(0) {} diff --git a/src/utils/attributes.hpp b/src/utils/attributes.hpp index 4ba8a62..a585a84 100644 --- a/src/utils/attributes.hpp +++ b/src/utils/attributes.hpp @@ -7,8 +7,12 @@ struct Attributes { const unsigned speed; const unsigned price; const bool is_aerial; + const unsigned num_ability_turns; + const unsigned ability_activation_cost; + Attributes(unsigned hp, unsigned range, unsigned attack_power, unsigned speed, - unsigned price, bool is_aerial); + unsigned price, bool is_aerial, unsigned num_ability_turns, + unsigned ability_activation_cost); /** * A default constructor is to be added for this, solely for the reason of diff --git a/src/utils/file_descriptor.cpp b/src/utils/file_descriptor.cpp new file mode 100644 index 0000000..125b1d5 --- /dev/null +++ b/src/utils/file_descriptor.cpp @@ -0,0 +1,37 @@ +#include "utils/file_descriptor.hpp" +#include +#include +#include +#include + +FileDescriptorInput::FileDescriptorInput(std::string input_path, + std::string output_path) + : _input_path(input_path), _output_path(output_path) { + + std::ifstream &istream_file = this->get(); + std::ofstream &ostream_file = this->put(); + istream_file.open(_input_path); + ostream_file.open(_output_path); + + if (!istream_file) { + std::cerr << "Failed in opening the input file" + << " " << _input_path << "with error no - " << errno << std::endl; + istream_file.close(); + } + if (!ostream_file) { + std::cerr << "Failed in opening the input file" + << " " << _output_path << "with error no - " << errno + << std::endl; + ostream_file.close(); + } +} + +FileDescriptorInput::~FileDescriptorInput() { this->close_stream(); } +std::ifstream &FileDescriptorInput ::get() { return this->_ifs; } + +std::ofstream &FileDescriptorInput ::put() { return this->_ofs; } + +void FileDescriptorInput::close_stream() { + this->get().close(); + this->put().close(); +} diff --git a/src/utils/file_descriptor.hpp b/src/utils/file_descriptor.hpp new file mode 100644 index 0000000..b7b084e --- /dev/null +++ b/src/utils/file_descriptor.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class FileDescriptorInput { +public: + FileDescriptorInput(std::string input_fd, std::string output_fd); + ~FileDescriptorInput(); + std::ifstream &get(); + std::ofstream &put(); + void close_stream(); + +private: + std::string _input_path, _output_path; + std::ifstream _ifs; + std::ofstream _ofs; +}; diff --git a/src/utils/game_type.cpp b/src/utils/game_type.cpp new file mode 100644 index 0000000..ea8af66 --- /dev/null +++ b/src/utils/game_type.cpp @@ -0,0 +1,16 @@ +#include "game_type.hpp" + +#include +#include + +GameType stringToGameType(std::string gameType) { + std::transform(gameType.begin(), gameType.end(), gameType.begin(), ::tolower); + + if (gameType == "normal") + return GameType::Normal; + + if (gameType == "pvp") + return GameType::PvP; + + throw std::invalid_argument("Unable to convert string to GameType enum"); +} diff --git a/src/utils/game_type.hpp b/src/utils/game_type.hpp new file mode 100644 index 0000000..317d5ae --- /dev/null +++ b/src/utils/game_type.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +enum GameType { Normal, PvP }; + +[[nodiscard]] GameType stringToGameType(std::string gameType); diff --git a/test/argument_reader_tests.cpp b/test/argument_reader_tests.cpp new file mode 100644 index 0000000..91f15fe --- /dev/null +++ b/test/argument_reader_tests.cpp @@ -0,0 +1,79 @@ +#include "utils/argument_reader.hpp" +#include "utils/game_type.hpp" + +#include + +SCENARIO("Single Key") { + GIVEN("One key-value pair") { + int argc = 1; + char **argv = new char *[1]; + argv[0] = (char *)"--type=Normal"; + + ArgumentReader args = ArgumentReader(argc, argv); + + THEN("Getting args") { REQUIRE(args.get("--type") == "Normal"); } + } +} + +SCENARIO("Key not present") { + GIVEN("No args") { + int argc = 0; + char **argv = nullptr; + + ArgumentReader args = ArgumentReader(argc, argv); + + THEN("Getting args") { REQUIRE(args.get("--type") == ""); } + } +} + +SCENARIO("Single key present multiple time") { + GIVEN("Multiple pairs with same keys") { + int argc = 2; + char **argv = new char *[2]; + + argv[0] = (char *)"--type=Normal"; + argv[1] = (char *)"--type=PvP"; + + ArgumentReader args = ArgumentReader(argc, argv); + + THEN("Getting args") { REQUIRE(args.get("--type") == "PvP"); } + } +} + +SCENARIO("Cases of keys don't matter") { + GIVEN("All keys in lower case") { + int argc = 1; + char **argv = new char *[1]; + argv[0] = (char *)"--type=normal"; + + ArgumentReader args = ArgumentReader(argc, argv); + + GameType gameType = stringToGameType(args.get("--type")); + + THEN("Getting args") { REQUIRE(gameType == GameType::Normal); } + } + + GIVEN("All keys in upper case") { + int argc = 1; + char **argv = new char *[1]; + argv[0] = (char *)"--type=PVP"; + + ArgumentReader args = ArgumentReader(argc, argv); + + GameType gameType = stringToGameType(args.get("--type")); + + THEN("Getting args") { REQUIRE(gameType == GameType::PvP); } + } + + GIVEN("All keys in mixed case") { + int argc = 1; + char **argv = new char *[1]; + argv[0] = (char *)"--type=NoRmAl"; + + ArgumentReader args = ArgumentReader(argc, argv); + + GameType gameType = stringToGameType(args.get("--type")); + + THEN("Getting args") { REQUIRE(gameType == GameType::Normal); } + } +} diff --git a/test/attacker_tests.cpp b/test/attacker_tests.cpp index 3eb4923..a3dadda 100644 --- a/test/attacker_tests.cpp +++ b/test/attacker_tests.cpp @@ -12,7 +12,7 @@ SCENARIO("Attacker::move") { Position initial_position(0, 0); Attacker::attribute_dictionary.clear(); Attacker::attribute_dictionary.insert(std::make_pair( - AttackerType::A1, Attributes(0, range, 0, speed, 0, true))); + AttackerType::A1, Attributes(0, range, 0, speed, 0, true, 0, 0))); Attacker attacker = Attacker::construct(AttackerType::A1, initial_position); WHEN("distance is more than range + speed") { @@ -56,7 +56,7 @@ SCENARIO("Attacker::move") { Position initial_position(0, 0); Attacker::attribute_dictionary.clear(); Attacker::attribute_dictionary.insert(std::make_pair( - AttackerType::A1, Attributes(0, range, 0, speed, 0, true))); + AttackerType::A1, Attributes(0, range, 0, speed, 0, true, 0, 0))); Attacker attacker = Attacker::construct(AttackerType::A1, initial_position); WHEN("distance is more than range + speed") { @@ -100,7 +100,7 @@ SCENARIO("Attacker::move") { Position initial_position(0, 0); Attacker::attribute_dictionary.clear(); Attacker::attribute_dictionary.insert(std::make_pair( - AttackerType::A1, Attributes(0, range, 0, speed, 0, true))); + AttackerType::A1, Attributes(0, range, 0, speed, 0, true, 0, 0))); Attacker attacker = Attacker::construct(AttackerType::A1, initial_position); WHEN("distance is more than range + speed") { @@ -144,8 +144,8 @@ SCENARIO("Attacker::move") { SCENARIO("Aerial Attacker::get_nearest_defender_index") { GIVEN("a list of defenders of varying length") { Attacker::attribute_dictionary.clear(); - Attacker::attribute_dictionary.insert( - std::make_pair(AttackerType::A1, Attributes(0, 0, 0, 0, 0, true))); + Attacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A1, Attributes(0, 0, 0, 0, 0, true, 0, 0))); Attacker attacker = Attacker::construct(AttackerType::A1, {0, 0}); WHEN("the given list is empty") { @@ -182,10 +182,10 @@ SCENARIO("Ground Attacker::get_nearest_defender_index") { GIVEN("List of different types of defenders around") { WHEN("The list has both ground defenders and aerial defenders") { Defender::attribute_dictionary.clear(); - Defender::attribute_dictionary.insert( - std::make_pair(DefenderType::D1, Attributes(0, 0, 0, 0, 0, true))); - Defender::attribute_dictionary.insert( - std::make_pair(DefenderType::D2, Attributes(0, 0, 0, 0, 0, false))); + Defender::attribute_dictionary.insert(std::make_pair( + DefenderType::D1, Attributes(0, 0, 0, 0, 0, true, 0, 0))); + Defender::attribute_dictionary.insert(std::make_pair( + DefenderType::D2, Attributes(0, 0, 0, 0, 0, false, 0, 0))); std::vector defenders{ Defender::construct(DefenderType::D1, {0, 1}), Defender::construct(DefenderType::D1, {0, 2}), @@ -196,8 +196,8 @@ SCENARIO("Ground Attacker::get_nearest_defender_index") { Defender::construct(DefenderType::D2, {0, 7})}; Attacker::attribute_dictionary.clear(); - Attacker::attribute_dictionary.insert( - std::make_pair(AttackerType::A1, Attributes(0, 0, 0, 0, 0, false))); + Attacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A1, Attributes(0, 0, 0, 0, 0, false, 0, 0))); Attacker attacker = Attacker::construct(AttackerType::A1, {0, 0}); auto nearest_defender = attacker.get_nearest_defender_index(defenders); diff --git a/test/defender_tests.cpp b/test/defender_tests.cpp index a8f69ee..d86783f 100644 --- a/test/defender_tests.cpp +++ b/test/defender_tests.cpp @@ -6,8 +6,8 @@ SCENARIO("Defender::get_nearest_attacker_index") { GIVEN("a list of attackers of varying length") { Defender::attribute_dictionary.clear(); - Defender::attribute_dictionary.insert( - std::make_pair(DefenderType::D1, Attributes(0, 0, 0, 0, 0, true))); + Defender::attribute_dictionary.insert(std::make_pair( + DefenderType::D1, Attributes(0, 0, 0, 0, 0, true, 0, 0))); Defender defender = Defender::construct(DefenderType::D1, {0, 0}); WHEN("the given list is empty") { @@ -44,10 +44,10 @@ SCENARIO("Ground Defender::get_nearest_attacker_index") { GIVEN("List of different types of attackers around") { WHEN("The list has both ground attackers and aerial attackers") { Attacker::attribute_dictionary.clear(); - Attacker::attribute_dictionary.insert( - std::make_pair(AttackerType::A1, Attributes(0, 0, 0, 0, 0, true))); - Attacker::attribute_dictionary.insert( - std::make_pair(AttackerType::A2, Attributes(0, 0, 0, 0, 0, false))); + Attacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A1, Attributes(0, 0, 0, 0, 0, true, 0, 0))); + Attacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A2, Attributes(0, 0, 0, 0, 0, false, 0, 0))); std::vector attackers{ Attacker::construct(AttackerType::A1, {0, 1}), Attacker::construct(AttackerType::A1, {0, 2}), @@ -58,8 +58,8 @@ SCENARIO("Ground Defender::get_nearest_attacker_index") { Attacker::construct(AttackerType::A2, {0, 7})}; Defender::attribute_dictionary.clear(); - Defender::attribute_dictionary.insert( - std::make_pair(DefenderType::D1, Attributes(0, 0, 0, 0, 0, false))); + Defender::attribute_dictionary.insert(std::make_pair( + DefenderType::D1, Attributes(0, 0, 0, 0, 0, false, 0, 0))); Defender defender = Defender::construct(DefenderType::D1, {0, 0}); auto nearest_attacker = defender.get_nearest_attacker_index(attackers); @@ -72,8 +72,8 @@ SCENARIO("Ground Defender::get_nearest_attacker_index") { WHEN("The list has only aerial attackers") { Attacker::attribute_dictionary.clear(); - Attacker::attribute_dictionary.insert( - std::make_pair(AttackerType::A1, Attributes(0, 0, 0, 0, 0, true))); + Attacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A1, Attributes(0, 0, 0, 0, 0, true, 0, 0))); std::vector attackers{ Attacker::construct(AttackerType::A1, {0, 1}), Attacker::construct(AttackerType::A1, {0, 2}), @@ -84,8 +84,8 @@ SCENARIO("Ground Defender::get_nearest_attacker_index") { Attacker::construct(AttackerType::A1, {0, 7})}; Defender::attribute_dictionary.clear(); - Defender::attribute_dictionary.insert( - std::make_pair(DefenderType::D1, Attributes(0, 0, 0, 0, 0, false))); + Defender::attribute_dictionary.insert(std::make_pair( + DefenderType::D1, Attributes(0, 0, 0, 0, 0, false, 0, 0))); Defender defender = Defender::construct(DefenderType::D1, {0, 0}); auto nearest_attacker = defender.get_nearest_attacker_index(attackers); @@ -97,8 +97,8 @@ SCENARIO("Ground Defender::get_nearest_attacker_index") { WHEN("The list has only ground attackers") { Attacker::attribute_dictionary.clear(); - Attacker::attribute_dictionary.insert( - std::make_pair(AttackerType::A2, Attributes(0, 0, 0, 0, 0, false))); + Attacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A2, Attributes(0, 0, 0, 0, 0, false, 0, 0))); std::vector attackers{ Attacker::construct(AttackerType::A2, {0, 1}), Attacker::construct(AttackerType::A2, {0, 2}), @@ -109,8 +109,8 @@ SCENARIO("Ground Defender::get_nearest_attacker_index") { Attacker::construct(AttackerType::A2, {0, 7})}; Defender::attribute_dictionary.clear(); - Defender::attribute_dictionary.insert( - std::make_pair(DefenderType::D1, Attributes(0, 0, 0, 0, 0, false))); + Defender::attribute_dictionary.insert(std::make_pair( + DefenderType::D1, Attributes(0, 0, 0, 0, 0, false, 0, 0))); Defender defender = Defender::construct(DefenderType::D1, {0, 0}); auto nearest_attacker = defender.get_nearest_attacker_index(attackers); @@ -128,10 +128,10 @@ SCENARIO("Aerial Defender::get_nearest_attacker_index") { WHEN("The list has both ground attackers and aerial attackers with aerial " "attacker in range") { Attacker::attribute_dictionary.clear(); - Attacker::attribute_dictionary.insert( - std::make_pair(AttackerType::A1, Attributes(0, 0, 0, 0, 0, true))); - Attacker::attribute_dictionary.insert( - std::make_pair(AttackerType::A2, Attributes(0, 0, 0, 0, 0, false))); + Attacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A1, Attributes(0, 0, 0, 0, 0, true, 0, 0))); + Attacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A2, Attributes(0, 0, 0, 0, 0, false, 0, 0))); std::vector attackers{ Attacker::construct(AttackerType::A1, {0, 1}), Attacker::construct(AttackerType::A1, {0, 2}), @@ -142,8 +142,8 @@ SCENARIO("Aerial Defender::get_nearest_attacker_index") { Attacker::construct(AttackerType::A2, {0, 0})}; Defender::attribute_dictionary.clear(); - Defender::attribute_dictionary.insert( - std::make_pair(DefenderType::D1, Attributes(0, 3, 0, 0, 0, true))); + Defender::attribute_dictionary.insert(std::make_pair( + DefenderType::D1, Attributes(0, 3, 0, 0, 0, true, 0, 0))); Defender defender = Defender::construct(DefenderType::D1, {0, 0}); auto nearest_attacker = defender.get_nearest_attacker_index(attackers); @@ -157,10 +157,10 @@ SCENARIO("Aerial Defender::get_nearest_attacker_index") { WHEN("The list has both ground attackers and aerial attackers with aerial " "attacker not in range") { Attacker::attribute_dictionary.clear(); - Attacker::attribute_dictionary.insert( - std::make_pair(AttackerType::A1, Attributes(0, 0, 0, 0, 0, true))); - Attacker::attribute_dictionary.insert( - std::make_pair(AttackerType::A2, Attributes(0, 0, 0, 0, 0, false))); + Attacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A1, Attributes(0, 0, 0, 0, 0, true, 0, 0))); + Attacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A2, Attributes(0, 0, 0, 0, 0, false, 0, 0))); std::vector attackers{ Attacker::construct(AttackerType::A1, {0, 5}), Attacker::construct(AttackerType::A1, {0, 6}), @@ -171,8 +171,8 @@ SCENARIO("Aerial Defender::get_nearest_attacker_index") { Attacker::construct(AttackerType::A2, {0, 0})}; Defender::attribute_dictionary.clear(); - Defender::attribute_dictionary.insert( - std::make_pair(DefenderType::D1, Attributes(0, 3, 0, 0, 0, true))); + Defender::attribute_dictionary.insert(std::make_pair( + DefenderType::D1, Attributes(0, 3, 0, 0, 0, true, 0, 0))); Defender defender = Defender::construct(DefenderType::D1, {0, 0}); auto nearest_attacker = defender.get_nearest_attacker_index(attackers); diff --git a/test/game_tests.cpp b/test/game_tests.cpp index 2618fae..531843a 100644 --- a/test/game_tests.cpp +++ b/test/game_tests.cpp @@ -4,7 +4,8 @@ #include -template concept ActorBased = std::is_base_of::value; +template +concept ActorBased = std::is_base_of::value; SCENARIO("Game::simulate") { @@ -46,16 +47,16 @@ SCENARIO("Game::simulate") { 50, // attack_power 0, // speed 0, - true // price - ))); + true, // price + 10, 5))); Defender::attribute_dictionary.insert( std::make_pair(DefenderType::D2, Attributes(200, // hp 3, // range 100, // attack_power 0, // speed 0, - false // price - ))); + false, // price + 10, 5))); Attacker::attribute_dictionary.clear(); Attacker::attribute_dictionary.insert( @@ -64,8 +65,8 @@ SCENARIO("Game::simulate") { 50, // attack_power 2, // speed 100, - true // price - ))); + true, // price, + 10, 5))); Attacker::attribute_dictionary.insert( std::make_pair(AttackerType::A2, Attributes(150, // hp @@ -73,12 +74,12 @@ SCENARIO("Game::simulate") { 75, // attack_power 3, // speed 150, - false // price - ))); + false, // price, + 0, 0))); std::vector defenders_initial_state = game_map.spawn_defenders(); Game game(std::vector{}, defenders_initial_state, - 1200 // number of coins + 1200 // number of coins, ); std::vector> initial_spawn_positions{ {{4, 0}, AttackerType::A1}, // cost 100 @@ -101,8 +102,9 @@ SCENARIO("Game::simulate") { std::vector> fifth_turn_spawn_pos; + Game::turn_t turn = 0; + std::vector activations; // FIRST TURN - // First turn, no attackers would be there so no point in adding a target // If lets say somehow there's some requests for targets by the player in // first turn itself, that means it is invalid. @@ -111,8 +113,8 @@ SCENARIO("Game::simulate") { {1, 1}, {2, 1}}; // attacker_id 1 and 2 both targetting defender_id 1 - Game first_turn_state = - game.simulate(first_turn_player_set_targets, initial_spawn_positions); + Game first_turn_state = game.simulate(turn++, first_turn_player_set_targets, + initial_spawn_positions, activations); // SECOND TURN @@ -127,15 +129,17 @@ SCENARIO("Game::simulate") { second_turn_player_set_targets = { {manually_attacking_attackers_id, targetted_defenders_id}}; - Game second_turn_state = first_turn_state.simulate( - second_turn_player_set_targets, second_turn_spawn_pos); + Game second_turn_state = + first_turn_state.simulate(turn++, second_turn_player_set_targets, + second_turn_spawn_pos, activations); // THIRD TURN std::unordered_map third_turn_player_set_targets; - Game third_turn_state = second_turn_state.simulate( - third_turn_player_set_targets, third_turn_spawn_pos); + Game third_turn_state = + second_turn_state.simulate(turn++, third_turn_player_set_targets, + third_turn_spawn_pos, activations); // FOURTH TURN const Attacker attacker_at_0_0 = *find_actor_by_position( @@ -145,15 +149,17 @@ SCENARIO("Game::simulate") { std::unordered_map fourth_turn_player_set_targets = { {attacker_at_0_0.get_id(), defender_at_4_5.get_id()}}; - Game fourth_turn_state = third_turn_state.simulate( - fourth_turn_player_set_targets, fourth_turn_spawn_pos); + Game fourth_turn_state = + third_turn_state.simulate(turn++, fourth_turn_player_set_targets, + fourth_turn_spawn_pos, activations); // FIFTH TURN std::unordered_map fifth_turn_player_set_targets = { {attacker_at_0_0.get_id(), defender_at_4_5.get_id()}}; - Game fifth_turn_state = fourth_turn_state.simulate( - fourth_turn_player_set_targets, fourth_turn_spawn_pos); + Game fifth_turn_state = + fourth_turn_state.simulate(turn++, fourth_turn_player_set_targets, + fourth_turn_spawn_pos, activations); WHEN("FIRST TURN") { THEN("ONLY NEW ATTACKERS WILL BE SPAWNED IN THE GAME, ALL DEFENDERS ARE " @@ -410,4 +416,85 @@ SCENARIO("Game::simulate") { } } } + + // ABILITY TESTS + + const unsigned ACTIVATION_TURNS = 3; + const unsigned INITIAL_HP = 10000; + + // We're making the HP very high so that no one dies. + // We just want to check wether they take damage or not. + // The range is also very high so that the attacker is always + // be able to be attacked. + Defender::attribute_dictionary.clear(); + Defender::attribute_dictionary.insert( + std::make_pair(DefenderType::D1, Attributes(INITIAL_HP, // hp + 2000000, // range + 50, // attack_power + 0, // speed + 0, + true, // price + 0, 0))); + + Attacker::attribute_dictionary.clear(); + Attacker::attribute_dictionary.insert( + std::make_pair(AttackerType::A1, Attributes(INITIAL_HP, // hp + 2000000, // range 2 + 50, // attack_power + 2, // speed + 100, + true, // price, + ACTIVATION_TURNS, 0))); + + std::vector ability_defenders_initial_state = { + Defender::construct(DefenderType::D1, {0, 0}), + }; + + Game abilitygame(std::vector{}, ability_defenders_initial_state, + 100000 // number of coins, + ); + + std::vector> ability_test_spawn_positions{ + {{0, 0}, AttackerType::A1}, + }; + + Game::turn_t turn = 0; + + abilitygame = + abilitygame.simulate(turn++, {}, ability_test_spawn_positions, {}); + + // auto attackers = abilitygame.get_attackers(); + WHEN("INITIAL SINGLE ATTACKER") { + THEN("There is only one single attacker on the map right now.") { + auto cur_attackers = abilitygame.get_attackers(); + REQUIRE(cur_attackers.size() == 1); + + abilitygame = abilitygame.simulate(turn++, {}, {}, {}); + + // The attacker should have taken damage. + REQUIRE(abilitygame.get_attackers().at(0).get_hp() < INITIAL_HP); + + auto prev_hp = abilitygame.get_attackers().at(0).get_hp(); + abilitygame = abilitygame.simulate(turn++, {}, {}, {}); + + // The attacker should continuously be taking damage. + REQUIRE(abilitygame.get_attackers().at(0).get_hp() < prev_hp); + auto id = abilitygame.get_attackers().at(0).get_id(); + + // Now we're gonna activate the ability. + abilitygame = abilitygame.simulate(turn++, {}, {}, {id}); + + // For the next couple of turns, no damage should be taken. + for (unsigned i = 0; i < ACTIVATION_TURNS; ++i) { + prev_hp = abilitygame.get_attackers().at(0).get_hp(); + abilitygame = abilitygame.simulate(turn++, {}, {}, {}); + REQUIRE(abilitygame.get_attackers().at(0).get_hp() == prev_hp); + } + + // Now that the ability is over, we should start taking damage again. + prev_hp = abilitygame.get_attackers().at(0).get_hp(); + abilitygame = abilitygame.simulate(turn++, {}, {}, {}); + REQUIRE(abilitygame.get_attackers().at(0).get_hp() < prev_hp); + } + } } diff --git a/test/pvpattacker_tests.cpp b/test/pvpattacker_tests.cpp new file mode 100644 index 0000000..20e3c17 --- /dev/null +++ b/test/pvpattacker_tests.cpp @@ -0,0 +1,115 @@ +#include "attacker/pvpattacker/pvpattacker.hpp" + +#include +#include +#include + +SCENARIO("PvPAttacker::get_nearest_defender_index") { + GIVEN("a list of defenders") { + PvPAttacker::attribute_dictionary.clear(); + PvPAttacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A1, Attributes(0, 2, 0, 4, 0, false, 1, 1))); + PvPAttacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A2, Attributes(0, 4, 0, 2, 0, false, 1, 1))); + PvPAttacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A3, Attributes(0, 4, 0, 4, 0, true, 1, 1))); + std::vector attackers{ + PvPAttacker::construct(AttackerType::A1, {0, 1}, Owner::PLAYER1), + PvPAttacker::construct(AttackerType::A2, {0, 2}, Owner::PLAYER1), + PvPAttacker::construct(AttackerType::A3, {0, 4}, Owner::PLAYER1), + }; + WHEN("the list is empty") { + std::vector defenders; + + PvPAttacker currentAttacker = attackers[0]; + + auto nearest_defender_index = + currentAttacker.get_nearest_defender_index(defenders); + + THEN("nearest defender does not exist") { + REQUIRE(nearest_defender_index.has_value() == false); + } + } + + WHEN("list has defenders") { + WHEN("the attacker is aerial") { + PvPAttacker currentAttacker = attackers[2]; + WHEN("the defender is aerial") { + std::vector defenders{ + PvPAttacker::construct(AttackerType::A3, {17, 3}, Owner::PLAYER2), + PvPAttacker::construct(AttackerType::A3, {10, 2}, Owner::PLAYER2), + }; + auto nearest_defender_index = + currentAttacker.get_nearest_defender_index(defenders); + THEN("nearest defender is the closest defender") { + REQUIRE(nearest_defender_index.has_value() == true); + REQUIRE(nearest_defender_index.value() == 1); + } + } + WHEN("the defender is ground") { + std::vector defenders{ + PvPAttacker::construct(AttackerType::A1, {4, 4}, Owner::PLAYER2), + PvPAttacker::construct(AttackerType::A2, {0, 9}, Owner::PLAYER2), + }; + auto nearest_defender_index = + currentAttacker.get_nearest_defender_index(defenders); + THEN("nearest defender is the closest defender") { + REQUIRE(nearest_defender_index.has_value() == true); + REQUIRE(nearest_defender_index.value() == 0); + } + } + WHEN("the defender can be either aerial or ground") { + std::vector defenders{ + PvPAttacker::construct(AttackerType::A1, {3, 0}, Owner::PLAYER2), + PvPAttacker::construct(AttackerType::A3, {1, 4}, Owner::PLAYER2), + }; + auto nearest_defender_index = + currentAttacker.get_nearest_defender_index(defenders); + THEN("nearest defender is the aerial defender in range") { + REQUIRE(nearest_defender_index.has_value() == true); + REQUIRE(nearest_defender_index.value() == 1); + } + } + } + + WHEN("the attacker is ground") { + PvPAttacker currentAttacker = attackers[1]; + WHEN("the defender is aerial") { + std::vector defenders{ + PvPAttacker::construct(AttackerType::A3, {1, 2}, Owner::PLAYER2), + PvPAttacker::construct(AttackerType::A3, {7, 3}, Owner::PLAYER2), + }; + auto nearest_defender_index = + currentAttacker.get_nearest_defender_index(defenders); + THEN("nearest defender does not exist") { + REQUIRE(nearest_defender_index.has_value() == false); + } + } + WHEN("the defender is ground") { + std::vector defenders{ + PvPAttacker::construct(AttackerType::A1, {0, 9}, Owner::PLAYER2), + PvPAttacker::construct(AttackerType::A2, {4, 4}, Owner::PLAYER2), + }; + auto nearest_defender_index = + currentAttacker.get_nearest_defender_index(defenders); + THEN("nearest defender is the closest defender") { + REQUIRE(nearest_defender_index.has_value() == true); + REQUIRE(nearest_defender_index.value() == 1); + } + } + WHEN("the defender can be either aerial or ground") { + std::vector defenders{ + PvPAttacker::construct(AttackerType::A1, {6, 1}, Owner::PLAYER2), + PvPAttacker::construct(AttackerType::A3, {3, 0}, Owner::PLAYER2), + }; + auto nearest_defender_index = + currentAttacker.get_nearest_defender_index(defenders); + THEN("nearest defender is the ground defender") { + REQUIRE(nearest_defender_index.has_value() == true); + REQUIRE(nearest_defender_index.value() == 0); + } + } + } + } + } +} \ No newline at end of file diff --git a/test/pvpgame_tests.cpp b/test/pvpgame_tests.cpp new file mode 100644 index 0000000..472e226 --- /dev/null +++ b/test/pvpgame_tests.cpp @@ -0,0 +1,231 @@ +#include "attacker/pvpattacker/pvpattacker.hpp" +#include "game/pvpgame.hpp" +#include "utils/game_map.hpp" + +#include + +SCENARIO("PVPGame::simulate") { + + GIVEN("a game with 2 attackers each") { + Map::no_of_rows = 10; + Map::no_of_cols = 10; + Map game_map({ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }); + PvPAttacker::attribute_dictionary.clear(); + PvPAttacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A1, + Attributes(10, 1, 5, 4, 50, false, 1, + 1))); // hp = 10, range = 1, damage = 5, speed = 4, cost = 50 + PvPAttacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A2, + Attributes( + 20, 1, 5, 4, 100, false, 1, + 1))); // hp = 20, range = 1, damage = 5, speed = 4, cost = 100 + PvPAttacker::attribute_dictionary.insert(std::make_pair( + AttackerType::A3, + Attributes( + 20, 1, 5, 4, 150, true, 1, + 1))); // hp = 20, range = 1, damage = 5, speed = 4, cost = 150 + std::vector> first_turn_attackers{ + {Position(0, 2), AttackerType::A1}, + {Position(0, 4), AttackerType::A3}, + }; + std::vector> first_turn_defenders{ + {Position(9, 2), AttackerType::A2}, + {Position(9, 4), AttackerType::A3}, + }; + std::vector> second_turn_attackers{ + {Position(0, 3), AttackerType::A2}, + }; + std::vector> second_turn_defenders{ + {Position(9, 6), AttackerType::A1}, + }; + std::vector> third_turn_attackers{ + {Position(3, 3), AttackerType::A2}, // won't spawn as invalid position + }; + std::vector> third_turn_defenders; + std::vector> fourth_turn_attackers{ + {Position(0, 7), AttackerType::A2}, + }; + std::vector> fourth_turn_defenders{ + {Position(9, 9), AttackerType::A2}, + }; + std::vector> fifth_turn_attackers; + std::vector> fifth_turn_defenders; + std::vector> sixth_turn_attackers; + std::vector> sixth_turn_defenders; + // TODO: Add tests for the case of abilities in pvp game + std::vector attackers; + std::vector defenders; + PvPGame pvpgame(attackers, defenders); + pvpgame.FIXED_COINS_PER_TURN = 500; + + // FIRST TURN + PvPGame first_turn_state = + pvpgame.simulate(0, {{0, 0}, {1, 1}}, first_turn_attackers, + {{0, 0}, {1, 1}}, first_turn_defenders, {}, {}); + + first_turn_state.FIXED_COINS_PER_TURN = 500; + // SECOND TURN + PvPGame second_turn_state = first_turn_state.simulate( + 1, {}, second_turn_attackers, {}, second_turn_defenders, {}, {}); + second_turn_state.FIXED_COINS_PER_TURN = 500; + // THIRD TURN + PvPGame third_turn_state = second_turn_state.simulate( + 2, {}, third_turn_attackers, {}, third_turn_defenders, {}, {}); + third_turn_state.FIXED_COINS_PER_TURN = + 60; // won't spawn as insufficient coins + // FOURTH TURN + PvPGame fourth_turn_state = third_turn_state.simulate( + 3, {}, fourth_turn_attackers, {}, fourth_turn_defenders, {}, {}); + + // FIFTH TURN + PvPGame fifth_turn_state = fourth_turn_state.simulate( + 4, {}, fifth_turn_attackers, {}, fifth_turn_defenders, {}, {}); + + // SIXTH TURN + PvPGame sixth_turn_state = fifth_turn_state.simulate( + 5, {}, sixth_turn_attackers, {}, sixth_turn_defenders, {}, {}); + + WHEN("first turn is simulated") { + THEN("all troops are in their respective positions") { + REQUIRE(first_turn_state.get_player_1_attackers() == + std::vector{ + PvPAttacker::construct(AttackerType::A1, {0, 2}, + Owner::PLAYER1), + PvPAttacker::construct(AttackerType::A3, {0, 4}, + Owner::PLAYER1), + }); + } + THEN("set targets are invalid") { + REQUIRE( + std::ranges::none_of(first_turn_state.get_player_1_attackers(), + [](const PvPAttacker &pvpattacker) { + return pvpattacker.is_target_set_by_player(); + })); + REQUIRE( + std::ranges::none_of(first_turn_state.get_player_2_attackers(), + [](const PvPAttacker &pvpattacker) { + return pvpattacker.is_target_set_by_player(); + })); + } + THEN("nearest defender is identified correctly") { + REQUIRE(first_turn_state.get_player_1_attackers()[0] + .get_nearest_defender_index( + first_turn_state.get_player_2_attackers()) + .value() == 0); + REQUIRE(first_turn_state.get_player_1_attackers()[1] + .get_nearest_defender_index( + first_turn_state.get_player_2_attackers()) + .value() == 1); + } + } + WHEN("second turn is simulated") { + THEN("all troops of both players are alive and new troops are spawned") { + REQUIRE(second_turn_state.get_player_1_attackers() == + std::vector{ + PvPAttacker::construct(AttackerType::A1, {4, 2}, + Owner::PLAYER1), + PvPAttacker::construct(AttackerType::A3, {4, 4}, + Owner::PLAYER1), + PvPAttacker::construct(AttackerType::A2, {0, 3}, + Owner::PLAYER1), + }); + REQUIRE(second_turn_state.get_player_2_attackers() == + std::vector{ + PvPAttacker::construct(AttackerType::A2, {5, 2}, + Owner::PLAYER2), + PvPAttacker::construct(AttackerType::A3, {5, 4}, + Owner::PLAYER2), + PvPAttacker::construct(AttackerType::A1, {9, 6}, + Owner::PLAYER2), + }); + } + THEN("no troops have attacked yet") { + for (int i = 0; i < 2; i++) { + REQUIRE(second_turn_state.get_player_1_attackers()[i].get_hp() == + first_turn_state.get_player_1_attackers()[i].get_hp()); + REQUIRE(second_turn_state.get_player_2_attackers()[i].get_hp() == + first_turn_state.get_player_2_attackers()[i].get_hp()); + } + } + THEN("troops spawned in the previous turn are in the MOVING state and " + "troops spawned in this turn are in the SPAWNED state") { + REQUIRE(second_turn_state.get_player_1_attackers()[2].get_state() == + Attacker::State::SPAWNED); + REQUIRE(second_turn_state.get_player_2_attackers()[2].get_state() == + Attacker::State::SPAWNED); + for (int index = 0; index < 2; index++) { + REQUIRE( + second_turn_state.get_player_1_attackers()[index].get_state() == + Attacker::State::MOVING); + REQUIRE( + second_turn_state.get_player_2_attackers()[index].get_state() == + Attacker::State::MOVING); + } + } + } + WHEN("third turn is simulated") { + THEN("all troops of both players are alive but no new troops are " + "spawned") { + REQUIRE(third_turn_state.get_player_1_attackers().size() == 3); + REQUIRE(third_turn_state.get_player_2_attackers().size() == 3); + } + THEN("troops spawned in first turn have started attacking and " + "troops spawned in second turn have not started attacking yet") { + for (int i = 0; i < 2; i++) { + REQUIRE(second_turn_state.get_player_1_attackers()[i].get_hp() > + third_turn_state.get_player_1_attackers()[i].get_hp()); + REQUIRE(second_turn_state.get_player_2_attackers()[i].get_hp() > + third_turn_state.get_player_2_attackers()[i].get_hp()); + } + REQUIRE(second_turn_state.get_player_1_attackers()[2].get_hp() == + third_turn_state.get_player_1_attackers()[2].get_hp()); + REQUIRE(second_turn_state.get_player_2_attackers()[2].get_hp() == + third_turn_state.get_player_2_attackers()[2].get_hp()); + } + } + WHEN("fourth turn is simulated") { + THEN("some troops are gone and no new troops are spawned") { + REQUIRE(fourth_turn_state.get_player_1_attackers().size() == 2); + REQUIRE(fourth_turn_state.get_player_2_attackers().size() == 3); + } + THEN("first attacker is dead for player 1") { + REQUIRE(fourth_turn_state.get_player_1_attackers()[0].get_id() != + third_turn_state.get_player_1_attackers()[0].get_id()); + } + } + WHEN("fifth turn is simulated") { + THEN("some troops are gone") { + REQUIRE(fifth_turn_state.get_player_1_attackers().size() == 2); + REQUIRE(fifth_turn_state.get_player_2_attackers().size() == 2); + } + THEN("first attacker is dead for player 2") { + REQUIRE(fifth_turn_state.get_player_2_attackers()[0].get_id() != + fourth_turn_state.get_player_2_attackers()[0].get_id()); + } + } + WHEN("sixth turn is simulated") { + THEN("some troops are gone") { + REQUIRE(sixth_turn_state.get_player_1_attackers().size() == 1); + REQUIRE(sixth_turn_state.get_player_2_attackers().size() == 1); + } + THEN("only the last spawned attacker is aliive for both players") { + REQUIRE(sixth_turn_state.get_player_1_attackers()[0].get_id() == + second_turn_state.get_player_1_attackers()[2].get_id()); + REQUIRE(sixth_turn_state.get_player_2_attackers()[0].get_id() == + second_turn_state.get_player_2_attackers()[2].get_id()); + } + } + } +} \ No newline at end of file