Skip to content

Commit

Permalink
Implemented the tests for GameEngine, load_hitbox(), `get_factori…
Browse files Browse the repository at this point in the history
…es()`, and `MapGenerator::get_enemy_limit()`.

Fixed and improved the Python tests so that they work with the new `GameEngine` class. This required moving to a more mock-based design as the Python tests cannot initialise the components any more.

`PhysicsSystem::add_bullet()` now uses the factories to initialise a bullet allowing all of them to be defined in one place.

`load_hitbox()` now returns whether the load was successful or not.
  • Loading branch information
JackAshwell11 committed Oct 14, 2024
1 parent accfb95 commit 7754439
Show file tree
Hide file tree
Showing 15 changed files with 374 additions and 315 deletions.
2 changes: 1 addition & 1 deletion hades_extensions/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ from hades_extensions.ecs import GameObjectType, Registry
def load_hitbox(
game_object_type: GameObjectType,
hitbox: Sequence[tuple[float, float]],
) -> None: ...
) -> bool: ...

class GameEngine:
def __init__(self: GameEngine, level: int, seed: int | None = None) -> None: ...
Expand Down
6 changes: 5 additions & 1 deletion src/hades_extensions/include/factories.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ using ComponentFactory = std::function<std::vector<std::shared_ptr<ComponentBase
///
/// @param game_object_type - The game object type.
/// @param hitbox - The hitbox to load.
void load_hitbox(GameObjectType game_object_type, const std::vector<std::pair<double, double>> &hitbox);
/// @return Whether the hitbox was loaded or not.
auto load_hitbox(GameObjectType game_object_type, const std::vector<std::pair<double, double>> &hitbox) -> bool;

/// Clear all hitboxes.
void clear_hitboxes();

/// Get the map of game object types to their respective component factories.
///
Expand Down
4 changes: 3 additions & 1 deletion src/hades_extensions/src/binding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ PYBIND11_MODULE(hades_extensions, module) { // NOLINT
"Load a hitbox for a game object type.\n\n"
"Args:\n"
" game_object_type: The type of game object to load the hitbox for.\n"
" hitbox: The hitbox to load for the game object type.");
" hitbox: The hitbox to load for the game object type.\n\n"
"Returns:\n"
" Whether the hitbox was loaded or not.");
pybind11::class_<GameEngine>(module, "GameEngine", "Manages the game objects and systems.")
.def(pybind11::init<int, std::optional<unsigned int>>(), pybind11::arg("level"),
pybind11::arg("seed") = pybind11::none(),
Expand Down
10 changes: 6 additions & 4 deletions src/hades_extensions/src/ecs/systems/physics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "ecs/systems/attacks.hpp"
#include "ecs/systems/movements.hpp"
#include "ecs/systems/sprite.hpp"
#include "factories.hpp"

void PhysicsSystem::add_force(const GameObjectID game_object_id, const cpVect &force) const {
cpBodyApplyForceAtLocalPoint(
Expand All @@ -20,10 +21,11 @@ void PhysicsSystem::add_force(const GameObjectID game_object_id, const cpVect &f

void PhysicsSystem::add_bullet(const std::pair<cpVect, cpVect> &bullet, const double damage,
const GameObjectType source) const {
const auto bullet_id{
get_registry()->create_game_object(GameObjectType::Bullet, get<0>(bullet),
{std::make_shared<Damage>(damage, -1), std::make_shared<KinematicComponent>(),
std::make_shared<PythonSprite>()})};
const auto bullet_id{get_registry()->create_game_object(GameObjectType::Bullet, get<0>(bullet),
get_factories().at(GameObjectType::Bullet)())};
const auto damage_component{get_registry()->get_component<Damage>(bullet_id)};
damage_component->add_to_max_value(damage - damage_component->get_value());
damage_component->set_value(damage);
cpShapeSetFilter(
*get_registry()->get_component<KinematicComponent>(bullet_id)->shape,
{static_cast<cpGroup>(source), static_cast<cpBitmask>(GameObjectType::Bullet), ~static_cast<cpBitmask>(source)});
Expand Down
32 changes: 25 additions & 7 deletions src/hades_extensions/src/factories.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ namespace {
// The hitboxes for the game objects.
std::unordered_map<GameObjectType, std::vector<cpVect>> hitboxes;

/// The bullet factory.
///
/// @return The components for the bullet.
const auto bullet_factory{[] {
return std::vector<std::shared_ptr<ComponentBase>>{
std::make_shared<Damage>(10, -1),
std::make_shared<KinematicComponent>(),
std::make_shared<PythonSprite>(),
};
}};

/// The enemy factory.
///
/// @return The components for the enemy.
Expand All @@ -30,7 +41,7 @@ const auto enemy_factory{[] {
std::make_shared<AttackRange>(3 * SPRITE_SIZE, 3),
std::make_shared<Damage>(10, 3),
std::make_shared<Health>(100, 5),
std::make_shared<KinematicComponent>(hitboxes[GameObjectType::Enemy]),
std::make_shared<KinematicComponent>(hitboxes.at(GameObjectType::Enemy)),
std::make_shared<MovementForce>(1000, 5),
std::make_shared<PythonSprite>(),
std::make_shared<SteeringMovement>(std::unordered_map<SteeringMovementState, std::vector<SteeringBehaviours>>{
Expand Down Expand Up @@ -93,7 +104,7 @@ const auto player_factory{[] {
std::make_shared<Money>(),
std::make_shared<MovementForce>(5000, 5),
std::make_shared<PythonSprite>(),
std::make_shared<KinematicComponent>(hitboxes[GameObjectType::Player]),
std::make_shared<KinematicComponent>(hitboxes.at(GameObjectType::Player)),
std::make_shared<StatusEffect>(),
std::make_shared<Upgrades>(std::unordered_map<std::type_index, std::pair<ActionFunction, ActionFunction>>{
{typeid(Health), std::make_pair([](const int level) { return std::pow(2, level) + 10; },
Expand Down Expand Up @@ -124,22 +135,29 @@ const auto chest_factory{[] {
} // namespace
// NOLINTEND(cppcoreguidelines-avoid-magic-numbers)

void load_hitbox(const GameObjectType game_object_type, const std::vector<std::pair<double, double>> &hitbox) {
auto load_hitbox(const GameObjectType game_object_type, const std::vector<std::pair<double, double>> &hitbox) -> bool {
if (hitboxes.contains(game_object_type)) {
return;
return false;
}
std::vector<cpVect> hitbox_points;
for (const auto &[x, y] : hitbox) {
hitbox_points.push_back(cpv(x, y) * SPRITE_SCALE);
}
hitboxes[game_object_type] = hitbox_points;
return true;
}

void clear_hitboxes() { hitboxes.clear(); }

auto get_factories() -> const std::unordered_map<GameObjectType, ComponentFactory> & {
static const std::unordered_map<GameObjectType, ComponentFactory> factories{
{GameObjectType::Enemy, enemy_factory}, {GameObjectType::Floor, floor_factory},
{GameObjectType::Player, player_factory}, {GameObjectType::Wall, wall_factory},
{GameObjectType::Goal, goal_factory}, {GameObjectType::HealthPotion, health_potion_factory},
{GameObjectType::Bullet, bullet_factory},
{GameObjectType::Enemy, enemy_factory},
{GameObjectType::Floor, floor_factory},
{GameObjectType::Player, player_factory},
{GameObjectType::Wall, wall_factory},
{GameObjectType::Goal, goal_factory},
{GameObjectType::HealthPotion, health_potion_factory},
{GameObjectType::Chest, chest_factory},
};
return factories;
Expand Down
2 changes: 2 additions & 0 deletions src/hades_extensions/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ add_executable(${TEST_MODULE}
${CMAKE_SOURCE_DIR}/tests/generation/test_dijkstra.cpp
${CMAKE_SOURCE_DIR}/tests/generation/test_map.cpp
${CMAKE_SOURCE_DIR}/tests/generation/test_primitives.cpp
${CMAKE_SOURCE_DIR}/tests/test_factories.cpp
${CMAKE_SOURCE_DIR}/tests/test_game_engine.cpp
)
target_include_directories(${TEST_MODULE} PRIVATE ${CMAKE_SOURCE_DIR}/tests)
target_link_libraries(${TEST_MODULE}
Expand Down
3 changes: 2 additions & 1 deletion src/hades_extensions/tests/ecs/systems/test_attacks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,12 @@ TEST_F(AttackSystemFixture, TestAttackSystemDoAttackMelee) {
TEST_F(AttackSystemFixture, TestAttackSystemDoAttackRanged) {
auto game_object_created{-1};
auto game_object_creation_callback{[&](const GameObjectID game_object_id) {
// The velocity for the bullet is set after the game object is created
const auto *bullet{*registry.get_component<KinematicComponent>(game_object_id)->body};
ASSERT_NEAR(bullet->p.x, 32, 1e-13);
ASSERT_NEAR(bullet->p.y, -32, 1e-13);
ASSERT_NEAR(bullet->v.x, 0, 1e-13);
ASSERT_NEAR(bullet->v.y, -1000, 1e-13);
ASSERT_NEAR(bullet->v.y, 0, 1e-13);
game_object_created = game_object_id;
}};
create_attack_component({AttackAlgorithm::Ranged});
Expand Down
41 changes: 8 additions & 33 deletions src/hades_extensions/tests/generation/test_map.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,6 @@ class MapFixture : public testing::Test { // NOLINT

/// Set up the fixture for the tests.
void SetUp() override { random_generator.seed(0); }

// /// Place a rect made up of walls and floors in the grid for use in testing.
// void place_covered_box() const {
// very_large_grid.place_rect({{.x = 2, .y = 2}, {.x = 20, .y = 20}});
// for (int y = 1; y <= 21; y++) {
// for (int x = 1; x <= 21; x++) {
// if (very_large_grid.get_value({.x = x, .y = y}) != TileType::Floor) {
// very_large_grid.set_value({.x = x, .y = y}, TileType::Wall);
// }
// }
// }
// }
};

namespace {
Expand Down Expand Up @@ -476,24 +464,11 @@ TEST_F(MapFixture, TestMapGeneratorPlaceGoalPlayer) {
assert_min_distance(grid, TileType::Goal);
}

// /// Test that creating a map with a valid level and seed works correctly.
// TEST_F(MapFixture, TestMapGeneratorCreateMapValidLevelSeed) {
// const auto [create_map_valid_grid, create_map_valid_constants]{create_map(0, 10)};
// ASSERT_EQ(create_map_valid_constants.width, 30);
// ASSERT_EQ(create_map_valid_constants.height, 20);
// ASSERT_EQ(std::ranges::count(create_map_valid_grid.begin(), create_map_valid_grid.end(), TileType::Player), 1);
// ASSERT_GE(std::ranges::count(create_map_valid_grid.begin(), create_map_valid_grid.end(), TileType::HealthPotion),
// 1); ASSERT_GE(std::ranges::count(create_map_valid_grid.begin(), create_map_valid_grid.end(), TileType::Chest), 1);
// ASSERT_GE(std::ranges::count(create_map_valid_grid.begin(), create_map_valid_grid.end(), TileType::Goal), 1);
// }
//
// /// Test that creating a map without a seed works correctly.
// TEST_F(MapFixture, TestMapGeneratorCreateMapEmptySeed) {
// const auto [create_map_empty_seed_grid, _]{create_map(0)};
// ASSERT_NE(create_map_empty_seed_grid, create_map(0).first);
// }
//
// /// Test that creating a map with a negative level throws an exception.
// TEST_F(MapFixture, TestMapGeneratorCreateMapNegativeLevel) {
// ASSERT_THROW_MESSAGE(create_map(-1, 5), std::length_error, "Level must be bigger than or equal to 0.")
// }
/// Test that getting the enemy limit works correctly for a positive level.
TEST_F(MapFixture, TestMapGeneratorGetEnemyLimitPositiveLevel) { ASSERT_EQ(small_map.get_enemy_limit(), 2); }

/// Test that getting the enemy limit works correctly for a zero level.
TEST_F(MapFixture, TestMapGeneratorGetEnemyLimitZeroLevel) { ASSERT_EQ(large_map.get_enemy_limit(), 5); }

/// Test that getting the enemy limit works correctly for a negative level.
TEST_F(MapFixture, TestMapGeneratorGetEnemyLimitNegativeLevel) { ASSERT_EQ(empty_map.get_enemy_limit(), 0); }
35 changes: 35 additions & 0 deletions src/hades_extensions/tests/test_factories.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Local headers
#include "factories.hpp"
#include "macros.hpp"

/// Implements the fixture for the factories.hpp tests.
class FactoriesFixture : public testing::Test { // NOLINT
protected:
void TearDown() override { clear_hitboxes(); }
};

/// Test that loading a hitbox for a game object type works.
TEST_F(FactoriesFixture, TestLoadHitboxOnce) { ASSERT_TRUE(load_hitbox(GameObjectType::Player, {{0.0, 0.0}})); }

/// Test that loading two hitboxes for the same game object type doesn't do anything.
TEST_F(FactoriesFixture, TestLoadHitboxTwice) {
load_hitbox(GameObjectType::Player, {{0.0, 0.0}});
ASSERT_FALSE(load_hitbox(GameObjectType::Player, {{1.0, 1.0}}));
}

/// Test that loading a factory that doesn't require a hitbox works.
TEST_F(FactoriesFixture, TestGetFactoryNoHitboxRequired) {
ASSERT_NO_THROW(get_factories().at(GameObjectType::Floor)());
}

/// Test that loading a factory that requires a hitbox works when the hitbox is loaded.
TEST_F(FactoriesFixture, TestGetFactoryHitboxLoaded) {
load_hitbox(GameObjectType::Player, {{0.0, 0.0}});
ASSERT_NO_THROW(get_factories().at(GameObjectType::Player)());
}

/// Test that loading a factory that requires a hitbox which isn't loaded throws an exception.
TEST_F(FactoriesFixture, TestGetFactoriesHitboxNotLoaded) {
ASSERT_THROW_MESSAGE(get_factories().at(GameObjectType::Player)(), std::out_of_range,
"invalid unordered_map<K, T> key");
}
80 changes: 80 additions & 0 deletions src/hades_extensions/tests/test_game_engine.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Local headers
#include "factories.hpp"
#include "game_engine.hpp"
#include "macros.hpp"

/// Implements the fixture for the game_engine.hpp tests.
class GameEngineFixture : public testing::Test { // NOLINT
protected:
/// The game engine object.
GameEngine game_engine{0, 10};

void SetUp() override {
load_hitbox(GameObjectType::Player, {{0, 0}});
load_hitbox(GameObjectType::Enemy, {{0, 0}});
}
};

/// Test that the game engine is created correctly.
TEST_F(GameEngineFixture, TestGameEngineZeroLevel) {
ASSERT_NE(game_engine.get_registry(), nullptr);
ASSERT_EQ(game_engine.get_player_id(), -1);
}

/// Test that the game engine throws an exception when given a negative level.
TEST_F(GameEngineFixture, TestGameEngineNegativeLevel) {
ASSERT_THROW_MESSAGE(GameEngine{-1}, std::length_error, "Level must be bigger than or equal to 0.");
}

/// Test that the game engine creates game objects correctly.
TEST_F(GameEngineFixture, TestGameEngineCreateGameObjects) {
game_engine.create_game_objects();
ASSERT_NE(game_engine.get_player_id(), -1);
}

/// Test that the game engine creates game objects correctly given no seed.
TEST_F(GameEngineFixture, TestGameEngineCreateGameObjectsNoSeed) {
GameEngine game_engine_no_seed{0};
game_engine.create_game_objects();
game_engine_no_seed.create_game_objects();
ASSERT_NE(game_engine.get_player_id(), game_engine_no_seed.get_player_id());
}

/// Test that the game engine generates an enemy correctly.
TEST_F(GameEngineFixture, TestGameEngineGenerateEnemy) {
auto enemy_created{-1};
auto enemy_creation{[&](const GameObjectID enemy_id) { enemy_created = enemy_id; }};
game_engine.create_game_objects();
game_engine.get_registry()->add_callback(EventType::GameObjectCreation, enemy_creation);
game_engine.generate_enemy();
ASSERT_NE(enemy_created, -1);
}

/// Test that the game engine throws an exception if the game objects haven't been created yet.
TEST_F(GameEngineFixture, TestGameEngineGenerateEnemyNoGameObjects) {
ASSERT_THROW_MESSAGE(
game_engine.generate_enemy(), RegistryError,
"The component `KinematicComponent` for the game object ID `-1` is not registered with the registry.");
}

/// Test that the game engine throws an exception if the player is dead.
TEST_F(GameEngineFixture, TestGameEngineGenerateEnemyPlayerDead) {
game_engine.create_game_objects();
game_engine.get_registry()->delete_game_object(game_engine.get_player_id());
ASSERT_THROW_MESSAGE(
game_engine.generate_enemy(), RegistryError,
"The component `KinematicComponent` for the game object ID `291` is not registered with the registry.");
}

/// Test that the game engine doesn't generate an enemy correctly if the enemy limit has been reached.
TEST_F(GameEngineFixture, TestGameEngineGenerateEnemyLimit) {
game_engine.create_game_objects();
for (auto i{0}; i < 10; i++) {
game_engine.generate_enemy();
}
auto enemy_created{-1};
auto enemy_creation{[&](const GameObjectID enemy_id) { enemy_created = enemy_id; }};
game_engine.get_registry()->add_callback(EventType::GameObjectCreation, enemy_creation);
game_engine.generate_enemy();
ASSERT_EQ(enemy_created, -1);
}
15 changes: 0 additions & 15 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
from arcade import Window
from arcade.texture import default_texture_cache

# Custom
from hades_extensions.ecs import Registry

if TYPE_CHECKING:
from collections.abc import Generator

Expand All @@ -23,18 +20,6 @@
__all__ = ()


@pytest.fixture
def registry() -> Registry:
"""Get the registry for testing.
Returns:
Registry: The registry for testing.
"""
registry = Registry()
registry.add_systems()
return registry


@pytest.fixture(scope="session")
def session_window() -> Generator[Window]:
"""Create a window for the session.
Expand Down
Loading

0 comments on commit 7754439

Please sign in to comment.