diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 48388479..5bcb2761 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -47,7 +47,7 @@ jobs: if: matrix.language == 'cpp' uses: lukka/run-cmake@v3 with: - cmakeListsTxtPath: "${{ github.workspace }}/hades_extensions/CMakeLists.txt" + cmakeListsTxtPath: "${{ github.workspace }}/src/hades_extensions/CMakeLists.txt" buildDirectory: "${{ github.workspace }}/build" - name: Perform CodeQL analysis uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5fef8afb..4aeddfa2 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -50,7 +50,7 @@ jobs: - name: Configure and build CMake uses: lukka/run-cmake@v3 with: - cmakeListsTxtPath: "${{ github.workspace }}/hades_extensions/CMakeLists.txt" + cmakeListsTxtPath: "${{ github.workspace }}/src/hades_extensions/CMakeLists.txt" buildDirectory: "${{ github.workspace }}/build" - name: Run CTest - run: ctest --output-on-failure --test-dir ${{ github.workspace }}/build/test + run: ctest --output-on-failure --test-dir ${{ github.workspace }}/build/tests diff --git a/.gitignore b/.gitignore index c7727e25..8c297d32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,164 +1,77 @@ -## Pre-generated Stuff - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.pyd -*.so - -# Distribution / packaging +# Build/distribution/packaging +**/build*/ +*.egg +*.egg-info/ .Python -build/ +.eggs/ +.installed.cfg +MANIFEST +__pypackages__/ develop-eggs/ dist/ downloads/ eggs/ -.eggs/ lib/ lib64/ parts/ sdist/ +share/python-wheels/ var/ wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -coverage.lcov -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal +window.build/ +window.dist/ -# Flask stuff: -instance/ -.webassets-cache +# Byte-compiled/optimized/DLL files +*$py.class +*.py[cod] +__pycache__/ -# Scrapy stuff: -.scrapy +# C/C++ extensions +*.pyd +*.so -# Sphinx documentation +# Documentation +/site docs/_build/ -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py +# Editor files +.idea/ +.vscode/ # Environments .env .venv -env/ -venv/ ENV/ env.bak/ +env/ venv.bak/ +venv/ -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site +# Logs/saves +logs/ +pip-delete-this-directory.txt +pip-log.txt +saves/ -# mypy -.mypy_cache/ +# Type checking .dmypy.json -dmypy.json - -# Pyre type checker +.mypy_cache/ .pyre/ - -# pytype static type analyzer .pytype/ +dmypy.json -# Cython debug symbols -cython_debug/ - - -## Custom stuff - -# Editor files -.idea/ -.vscode/ - -# Log and save files -logs/ -saves/ - -# CMake files -cmake-build-debug/ -cmake-build-release/ - -# Nuitka files -window.build/ -window.dist/ - -# Aseprite project -aseprite/ +# Unit test/coverage reports +*.cover +*.py,cover +.cache +.coverage +.coverage.* +.hypothesis/ +.nox/ +.pytest_cache/ +.tox/ +cover/ +coverage.lcov +coverage.xml +htmlcov/ +nosetests.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 671d95a2..f8a9b376 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ default_language_version: repos: - repo: "https://github.com/PyCQA/pylint" - rev: v3.0.0a6 + rev: v3.0.1 hooks: - id: pylint - repo: "https://github.com/bwhmather/ssort" @@ -11,17 +11,17 @@ repos: hooks: - id: ssort - repo: "https://github.com/charliermarsh/ruff-pre-commit" - rev: v0.0.284 + rev: v0.1.5 hooks: - id: ruff args: ["--fix", "--exit-non-zero-on-fix"] - repo: "https://github.com/pre-commit/mirrors-mypy" - rev: v1.5.0 + rev: v1.7.0 hooks: - id: mypy additional_dependencies: ["arcade==3.0.0.dev24", "pytest==7.4.0"] - repo: "https://github.com/pre-commit/pre-commit-hooks" - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-json - id: check-toml @@ -33,6 +33,6 @@ repos: args: ["--autofix"] - id: trailing-whitespace - repo: "https://github.com/psf/black" - rev: 23.7.0 + rev: 23.11.0 hooks: - id: black diff --git a/aseprite/armour_boost_potion.aseprite b/aseprite/armour_boost_potion.aseprite new file mode 100644 index 00000000..22e5195d Binary files /dev/null and b/aseprite/armour_boost_potion.aseprite differ diff --git a/aseprite/armour_potion.aseprite b/aseprite/armour_potion.aseprite new file mode 100644 index 00000000..53a528a1 Binary files /dev/null and b/aseprite/armour_potion.aseprite differ diff --git a/aseprite/enemy.aseprite b/aseprite/enemy.aseprite new file mode 100644 index 00000000..f648a3e1 Binary files /dev/null and b/aseprite/enemy.aseprite differ diff --git a/aseprite/fire_rate_boost_potion.aseprite b/aseprite/fire_rate_boost_potion.aseprite new file mode 100644 index 00000000..5fbdb884 Binary files /dev/null and b/aseprite/fire_rate_boost_potion.aseprite differ diff --git a/aseprite/floor.aseprite b/aseprite/floor.aseprite new file mode 100644 index 00000000..bbd29973 Binary files /dev/null and b/aseprite/floor.aseprite differ diff --git a/aseprite/health_boost_potion.aseprite b/aseprite/health_boost_potion.aseprite new file mode 100644 index 00000000..cb3425c3 Binary files /dev/null and b/aseprite/health_boost_potion.aseprite differ diff --git a/aseprite/health_potion.aseprite b/aseprite/health_potion.aseprite new file mode 100644 index 00000000..4ed09971 Binary files /dev/null and b/aseprite/health_potion.aseprite differ diff --git a/aseprite/player.aseprite b/aseprite/player.aseprite new file mode 100644 index 00000000..197e16b4 Binary files /dev/null and b/aseprite/player.aseprite differ diff --git a/aseprite/shop.aseprite b/aseprite/shop.aseprite new file mode 100644 index 00000000..74b2dd7f Binary files /dev/null and b/aseprite/shop.aseprite differ diff --git a/aseprite/speed_boost_potion.aseprite b/aseprite/speed_boost_potion.aseprite new file mode 100644 index 00000000..69146ada Binary files /dev/null and b/aseprite/speed_boost_potion.aseprite differ diff --git a/aseprite/wall.aseprite b/aseprite/wall.aseprite new file mode 100644 index 00000000..638b643a Binary files /dev/null and b/aseprite/wall.aseprite differ diff --git a/build.py b/build.py index 652d9d2a..b669358a 100644 --- a/build.py +++ b/build.py @@ -1,4 +1,5 @@ """Manages various building/compiling operations on the game.""" + from __future__ import annotations # Builtin @@ -47,13 +48,14 @@ def build_extension(self: CMakeBuild, ext: Extension) -> None: build_temp.mkdir(parents=True, exist_ok=True) # Compile and build the CMake extension + # TODO: Add CMake presets here subprocess.run( " ".join( [ "cmake", str(current_dir.joinpath(ext.sources[0])), - "-DDO_PYTHON=true", f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE={build_dir}", + "-DDO_TESTS=OFF", ], ), cwd=build_temp, @@ -103,7 +105,7 @@ def cpp() -> None: result_path = Path(__file__).parent.joinpath( setup( name="hades_extensions", - ext_modules=[Extension("hades_extensions", ["hades_extensions"])], + ext_modules=[Extension("hades_extensions", ["src/hades_extensions"])], script_args=["bdist_wheel"], cmdclass={"build_ext": CMakeBuild}, ).dist_files[0][2], diff --git a/hades_extensions/CMakeLists.txt b/hades_extensions/CMakeLists.txt deleted file mode 100644 index 9397c2e7..00000000 --- a/hades_extensions/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -# Define the minimum CMake version -cmake_minimum_required(VERSION 3.18.2) - -# Define an environment variable to determine if we want to make a Python extension or not -SET(DO_PYTHON false CACHE BOOL "Creates a Python extension for the C++ library") - -# Define some module names which are used by the C++ library, the tests, and the -# Python module -set(PY_MODULE hades_extensions) -set(CPP_LIB hades_extensions_lib) -set(TEST_MODULE hades_extensions_tests) - -# Initialise some environment variables -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Define the project -project(${PY_MODULE} LANGUAGES CXX) - -# Initialise some environment variables -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Add the src and test directories to the project -add_subdirectory(src) -add_subdirectory(test) diff --git a/hades_extensions/game_objects/__init__.pyi b/hades_extensions/game_objects/__init__.pyi new file mode 100644 index 00000000..f56a7431 --- /dev/null +++ b/hades_extensions/game_objects/__init__.pyi @@ -0,0 +1,112 @@ +# Builtin +from abc import abstractmethod +from enum import Enum +from typing import Callable, Final, Self, TypeVar + +# Define some type vars for the registry +_C = TypeVar("_C") +_S = TypeVar("_S") + +# Define the action function for callables +ActionFunction: Callable[[int], float] + +# Define the global variables +SPRITE_SCALE: Final[int] = ... +SPRITE_SIZE: Final[int] = ... + +class RegistryError(Exception): ... +class InventorySpaceError(Exception): ... + +class AttackAlgorithm(Enum): + AreaOfEffect = ... + Melee = ... + Ranged = ... + +class StatusEffectType(Enum): + TEMP = ... + TEMP2 = ... + +class SteeringBehaviours(Enum): + Arrive = ... + Evade = ... + Flee = ... + FollowPath = ... + ObstacleAvoidance = ... + Pursue = ... + Seek = ... + Wander = ... + +class SteeringMovementState(Enum): + Default = ... + Footprint = ... + Target = ... + +class ComponentBase: ... + +class SystemBase: + def __init__(self: SystemBase, registry: Registry) -> None: ... + def get_registry(self: SystemBase) -> Registry: ... + @abstractmethod + def update(self: SystemBase, delta_time: float) -> None: ... + +class Vec2d: + x: float + y: float + + def __init__(self: Vec2d, x: float, y: float) -> None: ... + def magnitude(self: Vec2d) -> float: ... + def normalised(self: Vec2d) -> Vec2d: ... + def rotated(self: Vec2d, angle: float) -> Vec2d: ... + def angle_between(self: Vec2d, other: Vec2d) -> float: ... + def distance_to(self: Vec2d, other: Vec2d) -> float: ... + def __add__(self: Vec2d, other: Vec2d) -> Vec2d: ... + def __iadd__(self: Self, other: Vec2d) -> Self: ... + def __sub__(self: Vec2d, other: Vec2d) -> Vec2d: ... + def __mul__(self: Vec2d, other: Vec2d) -> Vec2d: ... + def __truediv__(self: Vec2d, other: Vec2d) -> Vec2d: ... + def __eq__(self: Vec2d, other: Vec2d) -> bool: ... + def __ne__(self: Vec2d, other: Vec2d) -> bool: ... + def __hash__(self: Vec2d) -> int: ... + +class KinematicObject: + position: Vec2d + velocity: Vec2d + rotation: float + +class Registry: + def __init__(self: Registry) -> None: ... + def create_game_object( + self: Registry, + components: list[ComponentBase], + *, + kinematic: bool = False, + ) -> int: ... + def delete_game_object(self: Registry, game_object_id: int) -> None: ... + def has_component( + self: Registry, + game_object_id: int, + component: type[ComponentBase], + ) -> bool: ... + def get_component( + self: Registry, + game_object_id: int, + component: type[_C], + ) -> _C: ... + def update(self: Registry, delta_time: float) -> None: ... + def get_kinematic_object( + self: Registry, + game_object_id: int, + ) -> KinematicObject: ... + def add_wall(self: Registry, wall: Vec2d) -> None: ... + def add_systems(self: Registry) -> None: ... + def get_system(self: Registry, system: type[_S]) -> _S: ... + +class Stat(ComponentBase): + def __init__(self: Stat, value: float, maximum_level: float) -> None: ... + def get_value(self: Stat) -> float: ... + def set_value(self: Stat, new_value: float) -> None: ... + def get_max_value(self: Stat) -> float: ... + def add_to_max_value(self: Stat, value: float) -> None: ... + def get_current_level(self: Stat) -> float: ... + def increment_current_level(self: Stat) -> None: ... + def get_max_level(self: Stat) -> float: ... diff --git a/hades_extensions/game_objects/components/__init__.pyi b/hades_extensions/game_objects/components/__init__.pyi new file mode 100644 index 00000000..8934f472 --- /dev/null +++ b/hades_extensions/game_objects/components/__init__.pyi @@ -0,0 +1,89 @@ +# Custom +from hades_extensions.game_objects import ( + ActionFunction, + AttackAlgorithm, + ComponentBase, + Stat, + StatusEffectType, + SteeringBehaviours, + SteeringMovementState, + Vec2d, +) + +class Armour(Stat): ... +class ArmourRegen(Stat): ... + +class Attacks(ComponentBase): + attack_algorithms: list[AttackAlgorithm] + attack_state: int + def __init__(self: Attacks, attack_algorithms: list[AttackAlgorithm]) -> None: ... + +class StatusEffectData(ComponentBase): + status_effect_type: StatusEffectType + increase: ActionFunction + def __init__( + self: StatusEffectData, + status_effect_type: StatusEffectType, + increase: ActionFunction, + duration: ActionFunction, + interval: ActionFunction, + ) -> None: ... + +class EffectApplier(ComponentBase): + def __init__( + self: EffectApplier, + instant_effects: dict[type[ComponentBase], ActionFunction], + status_effects: dict[type[ComponentBase], StatusEffectData], + ) -> None: ... + +class Footprints(ComponentBase): + footprints: list[int] + time_since_last_footprint: float + def __init__(self: Footprints) -> None: ... + +class Health(Stat): ... + +class Inventory(ComponentBase): + width: int + height: int + items: list[int] + def __init__(self: Inventory, width: int, height: int) -> None: ... + def get_capacity(self: Inventory) -> int: ... + +class KeyboardMovement(ComponentBase): + moving_north: bool + moving_east: bool + moving_south: bool + moving_west: bool + def __init__(self: KeyboardMovement) -> None: ... + +class Money(ComponentBase): + money: int + def __init__(self: Money, money: int) -> None: ... + +class MovementForce(Stat): ... + +class StatusEffect: + value: float + duration: float + interval: float + target_component: type[ComponentBase] + +class StatusEffects(ComponentBase): + def __init__(self: StatusEffects) -> None: ... + +class SteeringMovement(ComponentBase): + behaviours: dict[SteeringMovementState, list[SteeringBehaviours]] + movement_state: SteeringMovementState + target_id: int + path_list: list[Vec2d] + def __init__( + self: SteeringMovement, + behaviours: dict[SteeringMovementState, list[SteeringBehaviours]], + ) -> None: ... + +class Upgrades(ComponentBase): + def __init__( + self: Upgrades, + upgrades: dict[type[ComponentBase], ActionFunction], + ) -> None: ... diff --git a/hades_extensions/game_objects/systems/__init__.pyi b/hades_extensions/game_objects/systems/__init__.pyi new file mode 100644 index 00000000..856d33a2 --- /dev/null +++ b/hades_extensions/game_objects/systems/__init__.pyi @@ -0,0 +1,87 @@ +# Builtin +from abc import abstractmethod + +# Custom +from hades_extensions.game_objects import ( + ActionFunction, + ComponentBase, + SystemBase, + Vec2d, +) +from hades_extensions.game_objects.components import StatusEffectData + +class ArmourRegenSystem(SystemBase): + def update(self: ArmourRegenSystem, delta_time: float) -> None: ... + +class AttackSystem(SystemBase): + def do_attack( + self: AttackSystem, + game_object_id: int, + targets: list[int], + ) -> None: ... + def previous_attack(self: AttackSystem, game_object_id: int) -> None: ... + def next_attack(self: AttackSystem, game_object_id: int) -> None: ... + @abstractmethod + def update(self: SystemBase, delta_time: float) -> None: ... + +class DamageSystem(SystemBase): + def deal_damage(self: DamageSystem, game_object_id: int, damage: int) -> None: ... + @abstractmethod + def update(self: SystemBase, delta_time: float) -> None: ... + +class EffectSystem(SystemBase): + def apply_instant_effect( + self: EffectSystem, + game_object_id: int, + target_component: type[ComponentBase], + increase: ActionFunction, + level: int, + ) -> None: ... + def apply_status_effect( + self: EffectSystem, + game_object_id: int, + target_component: type[ComponentBase], + status_effect_data: StatusEffectData, + level: int, + ) -> None: ... + def update(self: SystemBase, delta_time: float) -> None: ... + +class InventorySystem(SystemBase): + def add_item_to_inventory( + self: InventorySystem, + game_object_id: int, + item: int, + ) -> None: ... + def remove_item_from_inventory( + self: InventorySystem, + game_object_id: int, + index: int, + ) -> int: ... + def update(self: SystemBase, delta_time: float) -> None: ... + +class FootprintSystem(SystemBase): + def update(self: SystemBase, delta_time: float) -> None: ... + +class KeyboardMovementSystem(SystemBase): + def calculate_force(self: KeyboardMovementSystem, game_object_id: int) -> Vec2d: ... + @abstractmethod + def update(self: SystemBase, delta_time: float) -> None: ... + +class SteeringMovementSystem(SystemBase): + def calculate_force(self: SteeringMovementSystem, game_object_id: int) -> Vec2d: ... + def update_path_list( + self: SteeringMovementSystem, + game_object_id: int, + path_list: list[Vec2d], + ) -> None: ... + @abstractmethod + def update(self: SystemBase, delta_time: float) -> None: ... + +class UpgradeSystem(SystemBase): + def upgrade_component( + self: UpgradeSystem, + game_object_id: int, + target_component: type[ComponentBase], + ) -> None: ... + @abstractmethod + def update(self: SystemBase, delta_time: float) -> None: ... diff --git a/hades_extensions/__init__.pyi b/hades_extensions/generation/__init__.pyi similarity index 100% rename from hades_extensions/__init__.pyi rename to hades_extensions/generation/__init__.pyi diff --git a/hades_extensions/src/CMakeLists.txt b/hades_extensions/src/CMakeLists.txt deleted file mode 100644 index 95c39ce7..00000000 --- a/hades_extensions/src/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -# Create the shared C++ library which will be used by pybind11 and the tests -add_library(${CPP_LIB} STATIC astar.cpp bsp.cpp map.cpp primitives.cpp) -target_include_directories(${CPP_LIB} PUBLIC include) - -if (DO_PYTHON) - # Find the Python interpreter - find_package(PythonInterp REQUIRED) - - # Fetch the pybind11 repository and initialise it - include(FetchContent) - FetchContent_Declare( - pybind11_source - GIT_REPOSITORY https://github.com/pybind/pybind11.git - GIT_TAG v2.10.3 - ) - FetchContent_GetProperties(pybind11_source) - FetchContent_Populate(pybind11_source) - add_subdirectory( - ${pybind11_source_SOURCE_DIR} - ${pybind11_source_BINARY_DIR} - ) - - # Initialise the module with pybind11 - pybind11_add_module(${PY_MODULE} binding.cpp) - target_link_libraries(${PY_MODULE} PRIVATE ${CPP_LIB}) -endif () diff --git a/hades_extensions/src/astar.cpp b/hades_extensions/src/astar.cpp deleted file mode 100644 index 224bf9b9..00000000 --- a/hades_extensions/src/astar.cpp +++ /dev/null @@ -1,103 +0,0 @@ -// Std includes -#include -#include -#include -#include - -// Custom includes -#include "astar.hpp" -#include "primitives.hpp" - -// ----- STRUCTURES ------------------------------ -/// Represents a grid position and its costs from the start position -/// -/// @param cost - The cost to traverse to this neighbour. -/// @param destination - The destination point in the grid. -struct Neighbour { - int cost; - Point destination; - - inline bool operator<(const Neighbour nghbr) const { - // The priority_queue data structure gets the maximum priority, so we need - // to override that functionality to get the minimum priority - return cost > nghbr.cost; - } -}; - -// ----- CONSTANTS ------------------------------ -// Represents the north, south, east, west, north-east, north-west, south-east -// and south-west directions on a compass -const std::array INTERCARDINAL_OFFSETS = { - Point{-1, -1}, Point{0, -1}, Point{1, -1}, Point{-1, 0}, Point{1, 0}, Point{-1, 1}, Point{0, 1}, Point{1, 1}, -}; - -// ----- FUNCTIONS ------------------------------ -std::vector calculate_astar_path(Grid &grid, const Point start, const Point end) { - // Check if the grid size is not zero, if not, set up a few variables needed - // for the pathfinding - if (!grid.width) { - throw std::length_error("Grid size must be bigger than 0."); - } - std::vector result; - std::priority_queue queue; - std::unordered_map neighbours{{start, {0, start}}}; - queue.push({0, start}); - - // Loop until the priority queue is empty - while (!queue.empty()) { - // Get the lowest cost pair from the priority queue - Point current = queue.top().destination; - queue.pop(); - - // Check if we've reached our target - if (current == end) { - // Backtrack through neighbours to get the path - while (!(neighbours[current].destination == current)) { - // Add the current pair to the result list - result.push_back(current); - - // Get the next pair in the path - current = neighbours[current].destination; - } - - // Add the start point and exit out of the loop - result.push_back(start); - break; - } - - // Add all the neighbours to the heap with their cost being f = g + h: - // f - The total cost of traversing the neighbour. - // g - The distance between the start pair and the neighbour pair. - // h - The estimated distance from the neighbour pair to the end pair. - // We're using the Chebyshev distance for this. - for (Point offset : INTERCARDINAL_OFFSETS) { - // Calculate the neighbour's position and check if its valid excluding - // the boundaries as that produces weird paths - Point neighbour = current + offset; - if (neighbour.x < 1 || neighbour.x >= grid.width - 1 || neighbour.y < 1 || neighbour.y >= grid.height - 1) { - continue; - } - - // Test if the neighbour is an obstacle or not. If so, skip to the next - // neighbour as we want to move around it - if (grid.get_value(neighbour) == TileType::Obstacle) { - continue; - } - - // Calculate the distance from the start - int distance = neighbours[current].cost + 1; - - // Check if we need to add a new neighbour to the heap - if ((!neighbours.contains(neighbour)) || distance < neighbours[neighbour].cost) { - neighbours[neighbour] = {distance, current}; - - // Add the neighbour to the priority queue - Point diff = end - neighbour; - queue.emplace(distance + std::max(diff.x, diff.y), neighbour); - } - } - } - - // Return result - return result; -} diff --git a/hades_extensions/src/binding.cpp b/hades_extensions/src/binding.cpp deleted file mode 100644 index a54387ed..00000000 --- a/hades_extensions/src/binding.cpp +++ /dev/null @@ -1,37 +0,0 @@ -// Std includes -#include - -// External includes -#include -#include - -// Custom includes -#include "include/map.hpp" - -// ----- PYTHON MODULE CREATION ------------------------------ -PYBIND11_MODULE(hades_extensions, m) { - // Add the module docstring - m.doc() = "Generates the dungeon and places game objects in it."; - - // Add the create_map function to the module - m.def("create_map", - &create_map, - pybind11::arg("level"), - pybind11::arg("seed") = - pybind11::none(), - ("Generate the game map for a given game level.\n\n" - "Args:\n" - " level: The game level to generate a map for.\n" - " seed: The seed to initialise the random generator.\n\n" - "Returns:\n" - " A tuple containing the generated map and the level constants.\n\n")); - - // Add the TileType enum to the module - pybind11::enum_(m, "TileType") - .value("Empty", TileType::Empty) - .value("Floor", TileType::Floor) - .value("Wall", TileType::Wall) - .value("Obstacle", TileType::Obstacle) - .value("Player", TileType::Player) - .value("Potion", TileType::Potion); -} diff --git a/hades_extensions/src/bsp.cpp b/hades_extensions/src/bsp.cpp deleted file mode 100644 index 096320b3..00000000 --- a/hades_extensions/src/bsp.cpp +++ /dev/null @@ -1,106 +0,0 @@ -// Std includes -#include - -// Custom includes -#include "bsp.hpp" -#include "primitives.hpp" - -// ----- CONSTANTS ------------------------------ -#define CONTAINER_RATIO 1.25 -#define MIN_CONTAINER_SIZE 5 -#define MIN_ROOM_SIZE 4 -#define ROOM_RATIO 0.625 - -// ----- FUNCTIONS ------------------------------ -bool split(Leaf &leaf, std::mt19937 &random_generator) { - // Check if this leaf is already split or not - if (leaf.left && leaf.right) { - return false; - } - - // To determine the direction of split, we test if the width is 25% larger - // than the height, if so we split vertically. However, if the height is 25% - // larger than the width, we split horizontally. Otherwise, we split randomly - bool split_vertical; - if (leaf.container->width > leaf.container->height - && (leaf.container->width >= CONTAINER_RATIO * leaf.container->height)) { - split_vertical = true; - } else if (leaf.container->height > leaf.container->width - && (leaf.container->height >= CONTAINER_RATIO * leaf.container->width)) { - split_vertical = false; - } else { - std::uniform_int_distribution<> split_vertical_distribution{0, 1}; - split_vertical = split_vertical_distribution(random_generator); - } - - // To determine the range of values that we could split on, we need to find - // out if the container is too small. Once we've done that, we can use the - // x1, y1, x2, and y2 coordinates to specify the range of values - int max_size = - (split_vertical) ? leaf.container->width - MIN_CONTAINER_SIZE : leaf.container->height - MIN_CONTAINER_SIZE; - if (max_size <= MIN_CONTAINER_SIZE) { - // Container too small to split - return false; - } - - // Create the split position. This ensures that there will be - // MIN_CONTAINER_SIZE on each side - std::uniform_int_distribution<> pos_distribution{MIN_CONTAINER_SIZE, max_size}; - int pos = pos_distribution(random_generator); - int split_pos = (split_vertical) ? leaf.container->top_left.x + pos : leaf.container->top_left.y + pos; - - // Split the container - if (split_vertical) { - leaf.left = std::make_unique(Rect{{leaf.container->top_left.x, leaf.container->top_left.y}, - {split_pos - 1, leaf.container->bottom_right.y}}); - leaf.right = std::make_unique(Rect{{split_pos + 1, leaf.container->top_left.y}, - {leaf.container->bottom_right.x, leaf.container->bottom_right.y}}); - } else { - leaf.left = std::make_unique(Rect{{leaf.container->top_left.x, leaf.container->top_left.y}, - {leaf.container->bottom_right.x, split_pos - 1}}); - leaf.right = std::make_unique(Rect{{leaf.container->top_left.x, split_pos + 1}, - {leaf.container->bottom_right.x, leaf.container->bottom_right.y}}); - } - - // Successful split - return true; -} - -bool create_room(Leaf &leaf, Grid &grid, std::mt19937 &random_generator) { - // Test if this container is already split or not. If it is, we do not want - // to create a room inside it otherwise it will overwrite other rooms - if (leaf.left && leaf.right) { - return false; - } - - // Pick a random width and height making sure it is at least min_room_size - // but doesn't exceed the container - std::uniform_int_distribution<> width_distribution{MIN_ROOM_SIZE, leaf.container->width}; - int width = width_distribution(random_generator); - std::uniform_int_distribution<> height_distribution{MIN_ROOM_SIZE, leaf.container->height}; - int height = height_distribution(random_generator); - - // Use the width and height to find a suitable x and y position which can - // create the room - std::uniform_int_distribution<> - x_pos_distribution{leaf.container->top_left.x, leaf.container->bottom_right.x - width}; - int x_pos = x_pos_distribution(random_generator); - std::uniform_int_distribution<> - y_pos_distribution{leaf.container->top_left.y, leaf.container->bottom_right.y - height}; - int y_pos = y_pos_distribution(random_generator); - - // Create the room rect and test if its width to height ratio will make an - // oddly-shaped room - Rect rect{{x_pos, y_pos}, {x_pos + width - 1, y_pos + height - 1}}; - if ((static_cast(std::min(rect.width, rect.height)) / std::max(rect.width, rect.height)) < ROOM_RATIO) { - return false; - } - - // Width to height ratio is fine so place the rect in the 2D grid and store - // it - rect.place_rect(grid); - leaf.room = std::make_unique(rect); - - // Successful room creation - return true; -} diff --git a/hades_extensions/src/include/astar.hpp b/hades_extensions/src/include/astar.hpp deleted file mode 100644 index a1104305..00000000 --- a/hades_extensions/src/include/astar.hpp +++ /dev/null @@ -1,18 +0,0 @@ -// Ensure this file is only included once -#pragma once - -// Custom includes -#include "primitives.hpp" - -// ----- DEFINITIONS ------------------------------ -/// Calculate the shortest path in a grid from one pair to another using the A* -/// algorithm. -/// -/// @details https://en.wikipedia.org/wiki/A*_search_algorithm -/// -/// @param grid - The 2D grid which represents the dungeon. -/// @param start - The start point for the algorithm. -/// @param end - The end point for the algorithm. -/// @throws std::length_error - Grid size must be bigger than 0. -/// @return A vector of points mapping out the shortest path from start to end. -std::vector calculate_astar_path(Grid &grid, Point start, Point end); diff --git a/hades_extensions/src/include/bsp.hpp b/hades_extensions/src/include/bsp.hpp deleted file mode 100644 index 325096ba..00000000 --- a/hades_extensions/src/include/bsp.hpp +++ /dev/null @@ -1,48 +0,0 @@ -// Ensure this file is only included once -#pragma once - -// Std includes -#include - -// Custom includes -#include "primitives.hpp" - -// ----- STRUCTURES ------------------------------ -/// A binary spaced partition leaf used to generate the dungeon's rooms. -/// -/// @param container - The rect object that represents this leaf. -/// @details left - The left container of this leaf. If this is null, we have -/// reached the end of the branch. -/// @details right - The right container of this leaf. If this is null, we have -/// reached the end of the branch. -/// @details room - The rect object for representing the room inside this leaf. -struct Leaf { - // Parameters - std::unique_ptr container; - - // Attributes - std::unique_ptr left, right; - std::unique_ptr room; - - inline bool operator==(const Leaf &lef) const { - return container == lef.container && left == lef.left && right == lef.right; - } - - explicit Leaf(Rect container_val) : container(std::make_unique(container_val)) {} -}; - -// ----- FUNCTIONS ------------------------------- -/// Split a container either horizontally or vertically. -/// -/// @param leaf - The leaf to split. -/// @param random_generator - The random generator used to generate the bsp. -/// @return Whether the split was successful or not. -bool split(Leaf &leaf, std::mt19937 &random_generator); - -/// Create a random sized room inside a container. -/// -/// @param leaf - The leaf to create a room inside of. -/// @param grid - The 2D grid which represents the dungeon. -/// @param random_generator - The random generator used to generate the bsp. -/// @return Whether the room creation was successful or not. -bool create_room(Leaf &leaf, Grid &grid, std::mt19937 &random_generator); diff --git a/hades_extensions/src/include/map.hpp b/hades_extensions/src/include/map.hpp deleted file mode 100644 index 0a1841fe..00000000 --- a/hades_extensions/src/include/map.hpp +++ /dev/null @@ -1,110 +0,0 @@ -// Std includes -#include -#include - -// Custom includes -#include "bsp.hpp" -#include "primitives.hpp" - -// ----- STRUCTURES ------------------------------ -/// Represents an undirected weighted edge in a graph. -/// @param cost - The cost of the edge. -/// @param source - The source rect. -/// @param destination - The destination rect. -struct Edge { - int cost; - Rect source, destination; - - Edge(int cost_val, Rect source_val, Rect destination_val) - : cost(cost_val), source(source_val), destination(destination_val) {} - - inline bool operator<(const Edge edg) const { - // The priority_queue data structure gets the maximum priority, so we need - // to override that functionality to get the minimum priority - return cost > edg.cost; - } - - inline bool operator==(const Edge edg) const { - return cost == edg.cost && source == edg.source && - destination == edg.destination; - } -}; - -// ----- FUNCTIONS ------------------------------ -template<> -struct std::hash { - size_t operator()(const Edge &edg) const { - size_t res = 0; - hash_combine(res, edg.cost); - hash_combine(res, edg.source); - hash_combine(res, edg.destination); - return res; - } -}; - -// ----- DEFINITIONS ------------------------------ -/// Collect all points in a given grid that match the target. -/// -/// @param grid - The 2D grid which represents the dungeon. -/// @param target - The TileType to test for. -/// @return A vector of points which match the target. -std::vector collect_positions(Grid &grid, TileType target); - -/// Split the bsp based on the generated constants. -/// -/// @param bsp - The root leaf for the binary space partition. -/// @param grid - The 2D grid which represents the dungeon. -/// @param random_generator - The random generator used to generate the bsp. -/// @param split_iteration - The number of splits to perform. -void split_bsp(Leaf &bsp, Grid &grid, std::mt19937 &random_generator, int split_iteration); - -/// Generate the rooms for a given game level using the bsp. -/// -/// @param bsp - The root leaf for the binary space partition. -/// @param grid - The 2D grid which represents the dungeon. -/// @param random_generator - The random generator used to generate the bsp. -/// @return The generated rooms. -std::vector generate_rooms(Leaf &bsp, Grid &grid, std::mt19937 &random_generator); - -/// Create a set of connections between all the rects ensuring that every rect -/// is reachable. -/// -/// @details https://en.wikipedia.org/wiki/Prim's_algorithm -/// -/// @param complete_graph - An adjacency list which represents a complete -/// graph. -/// @throws std::length_error - Complete graph size must be bigger than 0. -/// @return A set of edges which form the connections between rects. -std::unordered_set create_connections(std::unordered_map> &complete_graph); - -/// Places a given tile in the 2D grid. -/// -/// @param grid - The 2D grid which represents the dungeon. -/// @param random_generator - The random generator used to pick the position. -/// @param target_tile - The tile to place in the 2D grid. -/// @param possible_tiles - The possible tiles that the tile can be placed -/// into. -/// @throws std::length_error - Possible tiles size must be bigger than 0. -void place_tile(Grid &grid, std::mt19937 &random_generator, TileType target_tile, std::vector &possible_tiles); - -/// Create the hallways by placing random obstacles and pathfinding around -/// them. -/// -/// @param grid - The 2D grid which represents the dungeon. -/// @param random_generator - The random generator used to pick the obstacle -/// positions. -/// @param connections - The connections to pathfind using the A* algorithm. -/// @param obstacle_count - The number of obstacles to place in the 2D grid. -void create_hallways(Grid &grid, - std::mt19937 &random_generator, - std::unordered_set &connections, - int obstacle_count); - -/// Generate the game map for a given game level. -/// -/// @param level - The game level to generate a map for. -/// @param seed - The seed to initialise the random generator. If this is -/// empty, then one will be generated. -/// @return A tuple containing the generated map and the level constants. -std::pair, std::tuple> create_map(int level, - std::optional seed = std::nullopt); diff --git a/hades_extensions/src/include/primitives.hpp b/hades_extensions/src/include/primitives.hpp deleted file mode 100644 index 70b2ba5e..00000000 --- a/hades_extensions/src/include/primitives.hpp +++ /dev/null @@ -1,168 +0,0 @@ -// Ensure this file is only included once -#pragma once - -// Std includes -#include -#include -#include - -// ----- ENUMS ------------------------------ -/// Stores the different types of tiles in the game map. -enum class TileType { - Empty, - Floor, - Wall, - Obstacle, - Player, - Potion, -}; - -// ----- STRUCTURES ------------------------------ -/// Represents a point in the grid. -/// -/// @param x - The x position of the point. -/// @param y - The y position of the point. -struct Point { - int x, y; - - inline bool operator==(const Point pnt) const { - return x == pnt.x && y == pnt.y; - } - - inline bool operator!=(const Point pnt) const { - return x != pnt.x || y != pnt.y; - } - - inline Point operator+(const Point pnt) const { - return {x + pnt.x, y + pnt.y}; - } - - inline Point operator-(const Point pnt) const { - return {abs(x - pnt.x), abs(y - pnt.y)}; - } - - Point() = default; - - Point(int x_val, int y_val) : x(x_val), y(y_val) {} -}; - -/// Represents a 2D grid with a set width and height through a 1D vector. -/// -/// @param width - The width of the 2D grid. -/// @param height - The height of the 2D grid. -/// @details grid - The vector which represents the 2D grid. -struct Grid { - // Parameters - int width{}, height{}; - - // Attributes - std::unique_ptr> grid; - - Grid() = default; - - Grid(int width_val, int height_val) - : width(width_val), - height(height_val), - grid(std::make_unique>(width * height, TileType::Empty)) {} - - /// Convert a 2D grid position to a 1D grid position. - /// - /// @param pos - The position to convert. - /// @throws std::out_of_range - Position must be within range. - /// @return The 1D grid position. - [[nodiscard]] int convert_position(const Point &pos) const; - - /// Get a value in the 2D grid from a given position. - /// - /// @param pos - The position to get the value for. - /// @throws std::out_of_range - Position must be within range. - /// @return The value at the given position. - [[nodiscard]] TileType get_value(const Point &pos) const; - - /// Set a value in the 2D grid from a given position. - /// - /// @param pos - The position to set. - /// @throws std::out_of_range - Position must be within range. - void set_value(const Point &pos, TileType target) const; -}; - -/// Represents a rectangle of any size useful for the interacting with the 2D -/// grid. -/// -/// When creating a container, the split wall is included in the rect size, -/// whereas, rooms don't so MIN_CONTAINER_SIZE must be bigger than -/// MIN_ROOM_SIZE. -/// -/// @param top_left - The top left position of the rect. -/// @param bottom_right - The bottom right position of the rect. -/// @details centre - The centre position of the rect. -/// @details width - The width of the rect. -/// @details height - The height of the rect. -struct Rect { - // Parameters - Point top_left, bottom_right; - - // Attributes - Point centre; - int width, height; - - inline bool operator==(const Rect &rct) const { - return top_left == rct.top_left && bottom_right == rct.bottom_right; - } - - inline bool operator!=(const Rect &rct) const { - return top_left != rct.top_left || bottom_right != rct.bottom_right; - } - - Rect() = default; - - Rect(Point top_left_val, Point bottom_right_val) - : top_left(top_left_val), - bottom_right(bottom_right_val), - centre(static_cast(std::round((top_left_val + bottom_right_val).x / 2.0)), - static_cast(std::round((top_left_val + bottom_right_val).y / 2.0))), - width((top_left_val - bottom_right_val).x), - height((top_left_val - bottom_right_val).y) {} - - /// Get the Chebyshev distance to another rect. - /// - /// @param other - The rect to find the distance to. - /// @return The Chebyshev distance between this rect and the given rect. - [[nodiscard]] int get_distance_to(const Rect &other) const; - - /// Place the rect in the 2D grid. - /// - /// @param grid - The 2D grid which represents the dungeon. - void place_rect(Grid &grid) const; -}; - -// ----- FUNCTIONS ------------------------------ -/// Allows multiple hashes to be combined for a struct -/// -/// @param seed - The seed for initialising the hasher. -/// @param v - The value to hash. -template -inline void hash_combine(size_t &seed, const T &v) { - std::hash hasher; - seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); -} - -template<> -struct std::hash { - size_t operator()(const Point &pnt) const { - size_t res = 0; - hash_combine(res, pnt.x); - hash_combine(res, pnt.y); - return res; - } -}; - -template<> -struct std::hash { - size_t operator()(const Rect &rct) const { - size_t res = 0; - hash_combine(res, rct.top_left); - hash_combine(res, rct.bottom_right); - return res; - } -}; diff --git a/hades_extensions/src/map.cpp b/hades_extensions/src/map.cpp deleted file mode 100644 index ee21694f..00000000 --- a/hades_extensions/src/map.cpp +++ /dev/null @@ -1,262 +0,0 @@ -// Std includes -#include -#include -#include -#include -#include -#include - -// Custom includes -#include "astar.hpp" -#include "map.hpp" - -// ----- STRUCTURES ------------------------------ -/// Stores a map generation constant which can be calculated. -/// -/// @param base_value - The base value for the exponential calculation. -/// @param increase - The percentage increase for the constant. -/// @param max_value - The max value for the exponential calculation. -struct MapGenerationConstant { - double base_value, increase, max_value; - - /// Generate a value based on the exponential equation. - /// - /// @param level - The game level to generate a value for. - /// @return The generated value. - [[nodiscard]] inline int generate_value(int level) const { - return (int) std::min(round(base_value * pow(increase, level)), max_value); - } -}; - -/// Stores the map generation constants -/// -/// @param width - The width of the 2D grid. -/// @param height - The height of the 2D grid. -/// @param split_iteration - The amount of splits to perform. -/// @param obstacle_count - The amount of obstacles to place in the 2D grid. -/// @param item_count - The amount of items to place in the 2D grid. -struct MapGenerationConstants { - MapGenerationConstant width, height, split_iteration, obstacle_count, item_count; -}; - -// ----- CONSTANTS ------------------------------ -// Defines constants for hallway and entity generation -#define HALLWAY_SIZE 5 -#define HALF_HALLWAY_SIZE (HALLWAY_SIZE / 2) - -// Defines the constants for the map generation -const MapGenerationConstants MAP_GENERATION_CONSTANTS = { - {30, 1.2, 150}, {20, 1.2, 100}, {5, 1.5, 25}, {20, 1.3, 200}, {5, 1.1, 30}, -}; - -// ----- FUNCTIONS ------------------------------ -std::vector collect_positions(Grid &grid, TileType target) { - // Iterate over grid and check each position - std::vector result; - for (int y = 0; y < grid.height; y++) { - for (int x = 0; x < grid.width; x++) { - if (grid.get_value({x, y}) == target) { - result.emplace_back(x, y); - } - } - } - - // Return result - return result; -} - -void split_bsp(Leaf &bsp, Grid &grid, std::mt19937 &random_generator, int split_iteration) { - // Start the splitting using a queue - std::queue> queue({bsp}); - while (split_iteration > 0 && !queue.empty()) { - // Get the current leaf from the deque object - Leaf ¤t = queue.front().get(); - queue.pop(); - - // Split the bsp if possible - if (split(current, random_generator) && current.left && current.right) { - // Add the child leafs so they can be split - queue.emplace(*current.left); - queue.emplace(*current.right); - - // Decrement the split iteration - split_iteration--; - } - } -} - -std::vector generate_rooms(Leaf &bsp, Grid &grid, std::mt19937 &random_generator) { - // Create the rooms - std::vector rooms; - std::queue> queue({bsp}); - while (!queue.empty()) { - // Get the current leaf from the stack - Leaf ¤t = queue.front().get(); - queue.pop(); - - // Check if a room already exists in this leaf - if (current.room) { - continue; - } - - // Test if we can create a room in the current leaf - if (current.left && current.right) { - // Room creation not successful meaning there are child leafs so try - // again on the child leafs - queue.emplace(*current.left); - queue.emplace(*current.right); - } else { - // Create a room in the current leaf and save the rect. If a room cannot - // be created, the width to height ratio is outside of range so try again - while (!create_room(current, grid, random_generator)) {} - - // Add the created room to the rooms list - rooms.emplace_back(*current.room); - } - } - - // Return all the created rooms - return rooms; -} - -std::unordered_set create_connections(std::unordered_map> &complete_graph) { - // Check if complete_graph is valid - if (complete_graph.empty()) { - throw std::length_error("Complete graph size must be bigger than 0."); - } - - // Use Prim's algorithm to construct a minimum spanning tree from - // complete_graph - Rect start = complete_graph.begin()->first; - std::priority_queue unexplored; - std::unordered_set visited; - std::unordered_set mst; - unexplored.emplace(0, start, start); - while (mst.size() < complete_graph.size() && !unexplored.empty()) { - // Get the neighbour with the lowest cost - Edge lowest = unexplored.top(); - unexplored.pop(); - - // Check if the neighbour is already visited or not - if (visited.contains(lowest.destination)) { - continue; - } - - // Neighbour isn't visited so mark it as visited and add its neighbours to - // the heap - visited.emplace(lowest.destination); - for (const auto &neighbour : complete_graph.at(lowest.destination)) { - unexplored.emplace(lowest.destination.get_distance_to(neighbour), lowest.destination, neighbour); - } - - // Add a new edge towards the lowest cost neighbour onto the mst - if (lowest.source != lowest.destination) { - // Save the connection - mst.emplace(lowest); - } - } - - // Return the constructed minimum spanning tree - return mst; -} - -void place_tile(Grid &grid, std::mt19937 &random_generator, TileType target_tile, std::vector &possible_tiles) { - // Check if at least one tile exists - if (possible_tiles.empty()) { - throw std::length_error("Possible tiles size must be bigger than 0."); - } - - // Get a random floor position and place the target tile - std::uniform_int_distribution possible_tiles_distribution(0, possible_tiles.size() - 1); - std::size_t possible_tile_index = possible_tiles_distribution(random_generator); - Point possible_tile = possible_tiles[possible_tile_index]; - possible_tiles[possible_tile_index] = possible_tiles.back(); - possible_tiles.pop_back(); - grid.set_value(possible_tile, target_tile); -} - -void create_hallways(Grid &grid, - std::mt19937 &random_generator, - std::unordered_set &connections, - int obstacle_count) { - // Place random obstacles in the grid - std::vector obstacle_positions = collect_positions(grid, TileType::Empty); - for (int i = 0; i < obstacle_count; i++) { - place_tile(grid, random_generator, TileType::Obstacle, obstacle_positions); - } - - // Use the A* algorithm with to connect each pair of rooms making sure to - // avoid the obstacles giving us natural looking hallways - std::vector> path_points(connections.size()); - std::transform(std::execution::par, - connections.begin(), - connections.end(), - path_points.begin(), - [&grid](Edge connection) { - return calculate_astar_path(grid, - connection.source.centre, - connection.destination.centre); - }); - for (const std::vector &path : path_points) { - for (const Point &path_point : path) { - // Place a rect box around the path_point using HALLWAY_SIZE to determine - // the width and height - Rect{{path_point.x - HALF_HALLWAY_SIZE, path_point.y - HALF_HALLWAY_SIZE}, - {path_point.x + HALF_HALLWAY_SIZE, path_point.y + HALF_HALLWAY_SIZE}}.place_rect(grid); - } - } -} - -std::pair, std::tuple> create_map(int level, std::optional seed) { - // Check that the level number is valid - if (level < 0) { - throw std::length_error("Level must be bigger than or equal to 0"); - } - - // Create the random generator. If seed is None, then get a random unsigned integer - unsigned int valid_seed; - if (!seed.has_value()) { - std::random_device random_device; - std::mt19937_64 seed_generator{random_device()}; - std::uniform_int_distribution seed_distribution; - valid_seed = seed_distribution(seed_generator); - } else { - valid_seed = seed.value(); - } - std::mt19937 random_generator{valid_seed}; - - // Initialise a few variables needed for the map generation - int grid_width = MAP_GENERATION_CONSTANTS.width.generate_value(level); - int grid_height = MAP_GENERATION_CONSTANTS.height.generate_value(level); - Grid grid{grid_width, grid_height}; - Leaf bsp{{{0, 0}, {grid_width - 1, grid_height - 1}}}; - - // Split the bsp and create the rooms - split_bsp(bsp, grid, random_generator, MAP_GENERATION_CONSTANTS.split_iteration.generate_value(level)); - std::vector rooms = generate_rooms(bsp, grid, random_generator); - - // Create the hallways between the rooms - std::unordered_map> complete_graph; - for (Rect room : rooms) { - std::vector temp; - for (const auto &rect : rooms) { - if (rect != room) { - temp.push_back(rect); - } - } - complete_graph.insert({room, temp}); - } - std::unordered_set connections = create_connections(complete_graph); - create_hallways(grid, random_generator, connections, MAP_GENERATION_CONSTANTS.obstacle_count.generate_value(level)); - - // Place the player and all the items on the tiles which can support items - // being placed on them - std::vector possible_tiles = collect_positions(grid, TileType::Floor); - place_tile(grid, random_generator, TileType::Player, possible_tiles); - for (int item = 0; item < MAP_GENERATION_CONSTANTS.item_count.generate_value(level); item++) { - place_tile(grid, random_generator, TileType::Potion, possible_tiles); - } - - // Return the grid and a LevelConstants object - return std::make_pair(*grid.grid, std::make_tuple(level, grid_width, grid_height)); -} diff --git a/hades_extensions/src/primitives.cpp b/hades_extensions/src/primitives.cpp deleted file mode 100644 index 7b336e2e..00000000 --- a/hades_extensions/src/primitives.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// Std includes -#include -#include -#include - -// Custom includes -#include "primitives.hpp" - -// ----- CONSTANTS ------------------------------ -// Defines constants for hallway and entity generation -const std::unordered_set REPLACEABLE_TILES = {TileType::Empty, TileType::Obstacle}; - -// ----- FUNCTIONS ------------------------------ -int Grid::convert_position(const Point &pos) const { - if (pos.x < 0 || pos.x >= width || pos.y < 0 || pos.y >= height) { - throw std::out_of_range("Position must be within range"); - } - return width * pos.y + pos.x; -} - -TileType Grid::get_value(const Point &pos) const { - return grid->at(convert_position(pos)); -} - -void Grid::set_value(const Point &pos, TileType target) const { - grid->at(convert_position(pos)) = target; -} - -int Rect::get_distance_to(const Rect &other) const { - return std::max(abs(centre.x - other.centre.x), abs(centre.y - other.centre.y)); -} - -void Rect::place_rect(Grid &grid) const { - // Place the walls - for (int y = std::max(top_left.y, 0); y < std::min(bottom_right.y + 1, grid.height); y++) { - for (int x = std::max(top_left.x, 0); x < std::min(bottom_right.x + 1, grid.width); x++) { - if (REPLACEABLE_TILES.contains(grid.get_value({x, y}))) { - grid.set_value({x, y}, TileType::Wall); - } - } - } - - // Place the floors. The ranges must be -1 in all directions since we don't - // want to overwrite the walls keeping the player in, but we still want to - // overwrite walls that block the path for hallways - for (int y = std::max(top_left.y + 1, 1); y < std::min(bottom_right.y, grid.height - 1); y++) { - for (int x = std::max(top_left.x + 1, 1); x < std::min(bottom_right.x, grid.width - 1); x++) { - grid.set_value({x, y}, TileType::Floor); - } - } -} diff --git a/hades_extensions/test/CMakeLists.txt b/hades_extensions/test/CMakeLists.txt deleted file mode 100644 index 0428ece3..00000000 --- a/hades_extensions/test/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Fetch the GoogleTest repository and initialise it -include(FetchContent) -FetchContent_Declare( - googletest - GIT_REPOSITORY https://github.com/google/googletest.git - GIT_TAG v1.13.0 -) -FetchContent_MakeAvailable(googletest) -add_library(GTest::GTest INTERFACE IMPORTED) -target_link_libraries(GTest::GTest INTERFACE gtest_main) - -# Create an executable from our test code linking it against GTest and the C++ -# shared library -add_executable(${TEST_MODULE} test_astar.cpp test_bsp.cpp test_map.cpp test_primitives.cpp) -target_link_libraries( - ${TEST_MODULE} - gtest_main - ${CPP_LIB} -) -include(CTest) -add_test(NAME Tests COMMAND ${TEST_MODULE}) diff --git a/hades_extensions/test/fixtures.hpp b/hades_extensions/test/fixtures.hpp deleted file mode 100644 index 8d7d8a75..00000000 --- a/hades_extensions/test/fixtures.hpp +++ /dev/null @@ -1,33 +0,0 @@ -// External includes -#include "gtest/gtest.h" - -// Custom includes -#include "bsp.hpp" -#include "primitives.hpp" - -// ----- FIXTURES ------------------------------ -class Fixtures : public testing::Test { - /// Hold fixtures relating to the C++ tests. - protected: - Point valid_point_one{3, 5}, valid_point_two{5, 7}, boundary_point{4, 0}, zero_point{0, 0}; - Rect valid_rect_one{valid_point_one, valid_point_two}, valid_rect_two{valid_point_one, boundary_point}, - zero_size_rect{zero_point, zero_point}; - Leaf leaf{{{0, 0}, {19, 19}}}; - Grid empty_grid, grid = {20, 20}, small_grid = {6, 9}, detailed_grid = {6, 9}; - std::mt19937 random_generator; - - void SetUp() override { - random_generator.seed(0); - detailed_grid.grid = std::make_unique>(std::vector{ - TileType::Obstacle, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Obstacle, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Obstacle, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Obstacle, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Obstacle, TileType::Empty, TileType::Obstacle, TileType::Obstacle, TileType::Empty, - TileType::Floor, TileType::Floor, TileType::Floor, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Floor, TileType::Floor, TileType::Floor, TileType::Empty, TileType::Empty, TileType::Obstacle, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Obstacle, TileType::Obstacle, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Obstacle, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Obstacle, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - }); - } -}; diff --git a/hades_extensions/test/test_astar.cpp b/hades_extensions/test/test_astar.cpp deleted file mode 100644 index 59a3badf..00000000 --- a/hades_extensions/test/test_astar.cpp +++ /dev/null @@ -1,39 +0,0 @@ -// Std includes -#include - -// External includes -#include "gtest/gtest.h" - -// Custom includes -#include "astar.hpp" -#include "fixtures.hpp" - -// ----- TESTS ------------------------------ -TEST_F(Fixtures, TestCalculateAstarPathObstaclesMiddleStart) { - // Test A* in a grid with obstacles - std::vector obstacles_result = {{1, 2}, {2, 3}, {2, 4}, {3, 5}}; - ASSERT_EQ(calculate_astar_path(detailed_grid, valid_point_one, {1, 2}), obstacles_result); -} - -TEST_F(Fixtures, TestCalculateAstarPathObstaclesBoundaryStart) { - // Test A* in a grid with obstacles - std::vector obstacles_result = {}; - ASSERT_EQ(calculate_astar_path(detailed_grid, valid_point_one, boundary_point), obstacles_result); -} - -TEST_F(Fixtures, TestCalculateAstarPathNoObstaclesMiddleStart) { - // Test A* in a grid with no obstacles - std::vector no_obstacles_result = {{5, 7}, {4, 6}, {3, 5}}; - ASSERT_EQ(calculate_astar_path(grid, valid_point_one, valid_point_two), no_obstacles_result); -} - -TEST_F(Fixtures, TestCalculateAstarPathBoundaryGoal) { - // Test A* with a goal on the boundaries - std::vector boundary_result = {}; - ASSERT_EQ(calculate_astar_path(small_grid, valid_point_one, valid_point_two), boundary_result); -} - -TEST_F(Fixtures, TestCalculateAstarPathEmptyGrid) { - // Test A* in an empty grid - ASSERT_THROW(calculate_astar_path(empty_grid, valid_point_one, boundary_point), std::length_error); -} diff --git a/hades_extensions/test/test_bsp.cpp b/hades_extensions/test/test_bsp.cpp deleted file mode 100644 index 3d29c916..00000000 --- a/hades_extensions/test/test_bsp.cpp +++ /dev/null @@ -1,49 +0,0 @@ -// External includes -#include "gtest/gtest.h" - -// Custom includes -#include "bsp.hpp" -#include "fixtures.hpp" - -// ----- TESTS ------------------------------ -TEST_F(Fixtures, TestBspSplitCorrect) { - // Split the bsp normally - split(leaf, random_generator); - - // Calculate the difference between the bottom right point of the left leaf - // and the top right point of the right leaf, so we can determine it's split - // direction - Point leaf_diff = leaf.left->container->bottom_right - leaf.right->container->top_left; - Point target_diff = (leaf_diff.x == 2) ? Point{2, 19} : Point{19, 2}; - - // Test if the child leafs border each other - ASSERT_EQ(leaf_diff, target_diff); -} - -TEST_F(Fixtures, TestBspSplitSmallWidthHeight) { - // Make sure we test what happens if the container's width and height are - // both less than MIN_CONTAINER_SIZE - leaf.container = std::make_unique(Point{-1, -1}, Point{-1, -1}); - ASSERT_FALSE(split(leaf, random_generator)); -} - -TEST_F(Fixtures, TestBspSplitNotNullLeftRight) { - // Test what happens if the leaf and right leafs are not null - leaf.left = std::make_unique(Rect{{0, 0}, {0, 0}}); - leaf.right = std::make_unique(Rect{{0, 0}, {0, 0}}); - ASSERT_FALSE(split(leaf, random_generator)); -} - -TEST_F(Fixtures, TestBspCreateRoomChildLeaf) { - // Repeat until a room is created since the ratio may be wrong sometimes then - // test that the room is not null - while (!create_room(leaf, grid, random_generator)) {} - ASSERT_TRUE(leaf.room != nullptr); -} - -TEST_F(Fixtures, TestBspCreateRoomNotNullLeftRight) { - // Test what happens if the leaf and right leafs are not null - leaf.left = std::make_unique(Rect{{0, 0}, {0, 0}}); - leaf.right = std::make_unique(Rect{{0, 0}, {0, 0}}); - ASSERT_FALSE(create_room(leaf, grid, random_generator)); -} diff --git a/hades_extensions/test/test_map.cpp b/hades_extensions/test/test_map.cpp deleted file mode 100644 index 1c18a230..00000000 --- a/hades_extensions/test/test_map.cpp +++ /dev/null @@ -1,248 +0,0 @@ -// Std includes -#include -#include -#include - -// External includes -#include "gtest/gtest.h" - -// Custom includes -#include "map.hpp" -#include "fixtures.hpp" - -// ----- TESTS ------------------------------ -TEST_F(Fixtures, TestMapCollectPositionsExist) { - // Test finding a tile that exists in a grid - std::vector tile_exists_result = {{0, 4}, {1, 4}, {2, 4}, {0, 5}, {1, 5}, {2, 5}}; - ASSERT_EQ(collect_positions(detailed_grid, TileType::Floor), tile_exists_result); -} - -TEST_F(Fixtures, TestMapCollectPositionsNoExist) { - // Test finding a tile that doesn't exist in a grid - std::vector tile_not_exist_result; - ASSERT_EQ(collect_positions(detailed_grid, TileType::Player), tile_not_exist_result); -} - -TEST_F(Fixtures, TestMapCollectPositionsEmptyGrid) { - // Test finding a tile in an empty grid - std::vector tile_not_exist_result; - ASSERT_EQ(collect_positions(empty_grid, TileType::Floor), tile_not_exist_result); -} - -TEST_F(Fixtures, TestMapSplitBspNegativeSplit) { - // Test what happens if split_iteration is -1 - split_bsp(leaf, grid, random_generator, -1); - ASSERT_TRUE(leaf.left == nullptr); - ASSERT_TRUE(leaf.right == nullptr); -} - -TEST_F(Fixtures, TestMapSplitBspZeroSplit) { - // Test what happens if split_iteration is 0 - split_bsp(leaf, grid, random_generator, 0); - ASSERT_TRUE(leaf.left == nullptr); - ASSERT_TRUE(leaf.right == nullptr); -} - -TEST_F(Fixtures, TestMapSplitBspThreeChildren) { - // Split the bsp 2 times. This should generate two child leafs under the left - // child and one child leaf on the right - split_bsp(leaf, grid, random_generator, 2); - - // Test if the child leafs were generated - ASSERT_TRUE(leaf.left->left->left == nullptr); - ASSERT_TRUE(leaf.left->left->right == nullptr); - ASSERT_TRUE(leaf.left->right->left == nullptr); - ASSERT_TRUE(leaf.left->right->right == nullptr); - ASSERT_TRUE(leaf.right->left == nullptr); - ASSERT_TRUE(leaf.right->right == nullptr); -} - -TEST_F(Fixtures, TestMapSplitBspMaxSplit) { - // Split the bsp with a split iteration of 10. We want to keep splitting the - // bsp until it is no longer possible so this value should ensure that - split_bsp(leaf, grid, random_generator, 10); - - // Collect all the child leafs so that we can test them - std::vector> children; - std::queue> split_queue({leaf}); - while (!split_queue.empty()) { - // Get the current leaf - Leaf ¤t = split_queue.front().get(); - split_queue.pop(); - - // Check if current has children. If so, push its children into the queue, - // otherwise, add the child leaf to the children vector - if (current.left && current.right) { - split_queue.emplace(*current.left); - split_queue.emplace(*current.right); - } else { - children.emplace_back(current); - } - } - - // Test that each child leaf cannot be split anymore and that there are - // approximately 4-8 children (this varies based on the toolchain) - for (Leaf &child : children) { - ASSERT_FALSE(split(child, random_generator)); - } - ASSERT_TRUE(children.size() >= 4 || children.size() <= 8); -} - -TEST_F(Fixtures, TestMapGenerateRoomsSetLeaf) { - // Test if at least 1 room is generated - leaf.left = std::make_unique(Rect{{0, 0}, {9, 15}}); - leaf.right = std::make_unique(Rect{{10, 0}, {15, 15}}); - ASSERT_EQ(generate_rooms(leaf, grid, random_generator).size(), 2); -} - -TEST_F(Fixtures, TestMapGenerateRoomsRoomExist) { - // Test if no rooms are generated if a room already exists - leaf.room = std::make_unique(valid_rect_one); - ASSERT_TRUE(generate_rooms(leaf, grid, random_generator).empty()); -} - -TEST_F(Fixtures, TestMapCreateConnectionsGivenConnections) { - // Create a complete graph with 4 nodes and 6 connections - std::unordered_map> complete_graph; - Rect temp_rect_one = {{0, 0}, {3, 3}}; - Rect temp_rect_two = {{10, 10}, {12, 12}}; - complete_graph.emplace(valid_rect_one, std::vector{valid_rect_two, temp_rect_one, temp_rect_two}); - complete_graph.emplace(valid_rect_two, std::vector{valid_rect_one, temp_rect_one, temp_rect_two}); - complete_graph.emplace(temp_rect_one, std::vector{valid_rect_one, valid_rect_two, temp_rect_two}); - complete_graph.emplace(temp_rect_two, std::vector{valid_rect_one, valid_rect_two, temp_rect_one}); - std::unordered_set connections = create_connections(complete_graph); - - // Test if all rects are connected and that there are only 3 connections - std::unordered_set discovered; - for (Edge connection : connections) { - discovered.emplace(connection.source); - discovered.emplace(connection.destination); - } - ASSERT_EQ(discovered.size(), 4); - ASSERT_EQ(connections.size(), 3); -} - -TEST_F(Fixtures, TestMapCreateConnectionsEmpty) { - // Test if no mst is generated if the provided unordered_map is empty - std::unordered_map> empty_map; - ASSERT_THROW(create_connections(empty_map), std::length_error); -} - -TEST_F(Fixtures, TestMapPlaceTileGivenPositions) { - // Test if a tile is correctly placed in the 2D grid - std::vector possible_tiles = {{5, 6}, {4, 2}}; - place_tile(small_grid, random_generator, TileType::Player, possible_tiles); - ASSERT_TRUE(std::find(small_grid.grid->begin(), small_grid.grid->end(), TileType::Player) != small_grid.grid->end()); -} - -TEST_F(Fixtures, TestMapPlaceTileEmpty) { - // Test if a tile is not placed in the 2D grid - std::vector possible_tiles; - ASSERT_THROW(place_tile(grid, random_generator, TileType::Player, possible_tiles), std::length_error); -} - -TEST_F(Fixtures, TestMapCreateHallwaysNoObstacles) { - // Test if a connection is correctly drawn in the 2D grid without obstacles - std::unordered_set connections = {{0, valid_rect_one, valid_rect_two}}; - create_hallways(small_grid, random_generator, connections, 0); - std::vector create_hallways_no_obstacles_result = { - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, - TileType::Empty, TileType::Wall, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Wall, - TileType::Empty, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, - TileType::Empty, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, - TileType::Empty, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, - TileType::Empty, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, - TileType::Empty, TileType::Wall, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Wall, - TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, - }; - ASSERT_EQ(*small_grid.grid, create_hallways_no_obstacles_result); -} - -TEST_F(Fixtures, TestMapCreateHallwaysWithObstacles) { - // Test if a connection is correctly drawn in the 2D grid with obstacles - std::unordered_set connections = {{0, valid_rect_one, valid_rect_two}}; - create_hallways(small_grid, random_generator, connections, 5); - - // Get the first floor tile in the grid - int index = - (int) (std::find(small_grid.grid->begin(), small_grid.grid->end(), TileType::Floor) - small_grid.grid->begin()); - Point start = {index % small_grid.width, index / small_grid.width}; - - // Use a Dijkstra map to count the number of floor tiles reachable - std::unordered_set tiles; - std::deque queue = {start}; - std::vector offsets = {{-1, -1}, {0, -1}, {1, -1}, {-1, 0}, {1, 0}, {-1, 1}, {0, 1}, {1, 1}}; - while (!queue.empty()) { - // Get the current point to explore - Point current = queue.front(); - queue.pop_front(); - - // Get the current tile's neighbours - for (Point offset : offsets) { - // Calculate the neighbour's position and check if its valid excluding - // the boundaries as that produces weird paths - Point neighbour = current + offset; - if (neighbour.x < 0 || neighbour.x >= small_grid.width || neighbour.y < 0 || neighbour.y >= small_grid.height) { - continue; - } else if (small_grid.get_value(neighbour) == TileType::Floor && !tiles.contains(neighbour)) { - queue.push_back(neighbour); - tiles.emplace(neighbour); - } - } - } - - // Determine if the number of floor tiles generated matches the number of - // traversable floor tiles - ASSERT_EQ(std::count(small_grid.grid->begin(), small_grid.grid->end(), TileType::Floor), tiles.size()); -} - -TEST_F(Fixtures, TestMapCreateHallwaysNoConnections) { - // Test if nothing gets drawn in the 2D grid except from obstacles - std::unordered_set connections = {}; - create_hallways(small_grid, random_generator, connections, 5); - std::vector create_hallways_no_connections_result = { - TileType::Empty, TileType::Obstacle, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Obstacle, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Obstacle, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Obstacle, TileType::Empty, TileType::Empty, - TileType::Obstacle, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - }; - - // Determine if there's 5 obstacles and no floor tiles - ASSERT_EQ(std::count(small_grid.grid->begin(), small_grid.grid->end(), TileType::Obstacle), 5); - ASSERT_EQ(std::count(small_grid.grid->begin(), small_grid.grid->end(), TileType::Floor), 0); -} - -TEST_F(Fixtures, TestMapCreateMapCorrect) { - // Test if a map is correctly generated - std::pair, std::tuple> create_map_valid = create_map(0, 5); - ASSERT_EQ(std::count(create_map_valid.first.begin(), create_map_valid.first.end(), TileType::Player), 1); - ASSERT_EQ(std::count(create_map_valid.first.begin(), create_map_valid.first.end(), TileType::Potion), 5); - ASSERT_EQ(create_map_valid.second, std::make_tuple(0, 30, 20)); -} - -TEST_F(Fixtures, TestMapCreateMapNegativeLevel) { - // Test if an exception is thrown on a negative level - ASSERT_THROW(create_map(-1, 5), std::length_error); -} - -TEST_F(Fixtures, TestMapCreateMapEmptySeed) { - // Test if a map is correctly generated without a given seed. We can't test - // it against a set result since the seed is randomly generated - std::optional empty_seed; - std::pair, std::tuple> create_map_empty_seed = create_map(0, empty_seed); - ASSERT_EQ(create_map_empty_seed.second, std::make_tuple(0, 30, 20)); - - // Test if the player exists in the 2D grid - bool has_player = false; - if (std::find(create_map_empty_seed.first.begin(), create_map_empty_seed.first.end(), TileType::Player) - != create_map_empty_seed.first.end()) { - has_player = true; - } - ASSERT_TRUE(has_player); -} diff --git a/hades_extensions/test/test_primitives.cpp b/hades_extensions/test/test_primitives.cpp deleted file mode 100644 index c13c28dd..00000000 --- a/hades_extensions/test/test_primitives.cpp +++ /dev/null @@ -1,108 +0,0 @@ -// External includes -#include "gtest/gtest.h" - -// Custom includes -#include "primitives.hpp" -#include "fixtures.hpp" - -// ----- TESTS ------------------------------ -TEST_F(Fixtures, TestGridConvertPositionMiddle) { - // Test if a position in the middle can be converted correctly - ASSERT_EQ(small_grid.convert_position({3, 4}), 27); -} - -TEST_F(Fixtures, TestGridConvertPositionEdgeTop) { - // Test if a position on the top edge can be converted correctly - ASSERT_EQ(small_grid.convert_position({3, 0}), 3); -} - -TEST_F(Fixtures, TestGridConvertPositionEdgeBottom) { - // Test if a position on the bottom edge can be converted correctly - ASSERT_EQ(small_grid.convert_position({4, 8}), 52); -} - -TEST_F(Fixtures, TestGridConvertPositionEdgeLeft) { - // Test if a position on the left edge can be converted correctly - ASSERT_EQ(small_grid.convert_position({0, 7}), 42); -} - -TEST_F(Fixtures, TestGridConvertPositionEdgeRight) { - // Test if a position on the right edge can be converted correctly - ASSERT_EQ(small_grid.convert_position({2, 8}), 50); -} - -TEST_F(Fixtures, TestGridConvertPositionSmall) { - // Test if converting a position outside the array throws an exception - ASSERT_THROW(small_grid.convert_position({-1, -1}), std::out_of_range); -} - -TEST_F(Fixtures, TestGridConvertPositionLarge) { - // Test if converting a position outside the array throws an exception - ASSERT_THROW(small_grid.convert_position({10, 10}), std::out_of_range); -} - -TEST_F(Fixtures, TestGridGetValueMiddle) { - // Test if a position in the middle can be got correctly - (*small_grid.grid)[47] = TileType::Player; - ASSERT_EQ(small_grid.get_value({5, 7}), TileType::Player); -} - -TEST_F(Fixtures, TestGridGetValueEdge) { - // Test if a position on the edge can be got correctly - (*small_grid.grid)[29] = TileType::Player; - ASSERT_EQ(small_grid.get_value({5, 4}), TileType::Player); -} - -TEST_F(Fixtures, TestGridGetValueLarge) { - // Test if getting a position outside the array throws an exception - ASSERT_THROW(small_grid.get_value({10, 10}), std::out_of_range); -} - -TEST_F(Fixtures, TestGridSetValueMiddle) { - // Test if a position in the middle can be set correctly - small_grid.set_value({1, 7}, TileType::Player); - ASSERT_EQ((*small_grid.grid)[43], TileType::Player); -} - -TEST_F(Fixtures, TestGridSetValueEdge) { - // Test if a position on the edge can be set correctly - small_grid.set_value({5, 2}, TileType::Player); - ASSERT_EQ((*small_grid.grid)[17], TileType::Player); -} - -TEST_F(Fixtures, TestGridSetValueSmall) { - // Test if setting a position outside the array throws an exception - ASSERT_THROW(small_grid.set_value({-1, -1}, TileType::Player), std::out_of_range); -} - -TEST_F(Fixtures, TestRectGetDistanceToValid) { - // Test finding the distance between two valid rects - ASSERT_EQ(valid_rect_one.get_distance_to(valid_rect_two), 3); -} - -TEST_F(Fixtures, TestRectGetDistanceToIdentical) { - // Test finding the distance between two identical rects - ASSERT_EQ(valid_rect_one.get_distance_to(valid_rect_one), 0); -} - -TEST_F(Fixtures, TestRectGetDistanceToZero) { - // Test finding the distance between a valid and zero size rect - ASSERT_EQ(valid_rect_one.get_distance_to(zero_size_rect), 6); -} - -TEST_F(Fixtures, TestRectPlaceRectCorrect) { - // Test if the place_rect function places a rect correctly in the grid - valid_rect_one.place_rect(small_grid); - std::vector target_result = { - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Wall, TileType::Floor, TileType::Wall, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, - TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, - }; - ASSERT_EQ(*small_grid.grid, target_result); -} diff --git a/poetry.lock b/poetry.lock index 2cee18ff..0362957c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,42 +2,38 @@ [[package]] name = "arcade" -version = "3.0.0.dev24" +version = "3.0.0.dev25" description = "Arcade Game Development Library" optional = false python-versions = ">=3.8" files = [ - {file = "arcade-3.0.0.dev24-py3-none-any.whl", hash = "sha256:f666d96de17d1b33ce6b5e61c8e5084a266e004ae67e25874f81d21c54145c63"}, - {file = "arcade-3.0.0.dev24.tar.gz", hash = "sha256:6c0a979f0f3feda002d0859a661095f14228133837d85d64596059d653ac952c"}, + {file = "arcade-3.0.0.dev25-py3-none-any.whl", hash = "sha256:af3daf0a0bbe6bbd3164471238ee41ee54e4cad38ea907606c112c3d8f17f08f"}, + {file = "arcade-3.0.0.dev25.tar.gz", hash = "sha256:1bef787b9f691ed9a687f0b37fad1931521c7cf689a731cae13d103cf2f6be65"}, ] [package.dependencies] -pillow = ">=9.4.0,<9.5.0" -pyglet = ">=2.0.7,<2.1" -pymunk = ">=6.4.0,<6.5.0" +pillow = ">=10.0.0,<10.1.0" +pyglet = ">=2.0.8,<2.1" +pymunk = ">=6.5.1,<6.6.0" pytiled-parser = ">=2.2.3,<2.3.0" [package.extras] -dev = ["coverage", "coveralls", "docutils (==0.19)", "furo", "mypy", "pygments (==2.14.0)", "pyright", "pytest", "pytest-cov", "pytest-mock", "pyyaml (==6.0)", "ruff", "sphinx (==6.2.1)", "sphinx-autobuild (==2021.3.14)", "sphinx-copybutton (==0.5.1)", "sphinx-sitemap (==2.3.0)", "typer[all] (==0.7.0)", "wheel"] +dev = ["coverage", "coveralls", "docutils (==0.20.1)", "furo", "mypy", "pygments (==2.16.1)", "pyright (==1.1.322)", "pytest", "pytest-cov", "pytest-mock", "pyyaml (==6.0.1)", "ruff", "sphinx (==7.2.2)", "sphinx-autobuild (==2021.3.14)", "sphinx-copybutton (==0.5.2)", "sphinx-sitemap (==2.5.1)", "typer[all] (==0.7.0)", "wheel"] +testing-libraries = ["pytest", "pytest-cov", "pytest-mock", "pyyaml (==6.0.1)", "typer[all] (==0.7.0)"] [[package]] name = "astroid" -version = "2.15.6" +version = "3.0.1" description = "An abstract syntax tree for Python with inference support." optional = false -python-versions = ">=3.7.2" +python-versions = ">=3.8.0" files = [ - {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"}, - {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"}, + {file = "astroid-3.0.1-py3-none-any.whl", hash = "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca"}, + {file = "astroid-3.0.1.tar.gz", hash = "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e"}, ] [package.dependencies] -lazy-object-proxy = ">=1.4.0" typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} -wrapt = [ - {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, - {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, -] [[package]] name = "attrs" @@ -59,33 +55,29 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte [[package]] name = "black" -version = "23.7.0" +version = "23.11.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, - {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, - {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, - {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, - {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, - {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, - {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, - {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, - {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, - {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, - {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, - {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, - {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, + {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, + {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, + {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, + {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, + {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, + {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, + {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, + {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, + {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, + {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, + {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, + {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, + {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, + {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, + {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, + {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, + {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, + {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, ] [package.dependencies] @@ -95,6 +87,7 @@ packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -104,86 +97,74 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cachetools" -version = "5.3.1" +version = "5.3.2" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, - {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, + {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, + {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, ] [[package]] name = "cffi" -version = "1.15.1" +version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, ] [package.dependencies] @@ -213,13 +194,13 @@ files = [ [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -238,63 +219,63 @@ files = [ [[package]] name = "coverage" -version = "7.3.0" +version = "7.3.2" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5"}, - {file = "coverage-7.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1"}, - {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977"}, - {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51"}, - {file = "coverage-7.3.0-cp310-cp310-win32.whl", hash = "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527"}, - {file = "coverage-7.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1"}, - {file = "coverage-7.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f"}, - {file = "coverage-7.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7"}, - {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214"}, - {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f"}, - {file = "coverage-7.3.0-cp311-cp311-win32.whl", hash = "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482"}, - {file = "coverage-7.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70"}, - {file = "coverage-7.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b"}, - {file = "coverage-7.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe"}, - {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2"}, - {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b"}, - {file = "coverage-7.3.0-cp312-cp312-win32.whl", hash = "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321"}, - {file = "coverage-7.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479"}, - {file = "coverage-7.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1"}, - {file = "coverage-7.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54"}, - {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84"}, - {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985"}, - {file = "coverage-7.3.0-cp38-cp38-win32.whl", hash = "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9"}, - {file = "coverage-7.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543"}, - {file = "coverage-7.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba"}, - {file = "coverage-7.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95"}, - {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e"}, - {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54"}, - {file = "coverage-7.3.0-cp39-cp39-win32.whl", hash = "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3"}, - {file = "coverage-7.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e"}, - {file = "coverage-7.3.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0"}, - {file = "coverage-7.3.0.tar.gz", hash = "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, + {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, + {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, + {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, + {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, + {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, + {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, + {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, + {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, + {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, + {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, + {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, + {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, ] [package.dependencies] @@ -344,38 +325,40 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.12.2" +version = "3.13.1" description = "A platform independent file lock." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, - {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] [package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] [[package]] name = "icdiff" -version = "2.0.6" +version = "2.0.7" description = "improved colored diff" optional = false python-versions = "*" files = [ - {file = "icdiff-2.0.6.tar.gz", hash = "sha256:a2673b335d671e64fc73c44e1eaa0aa01fd0e68354e58ee17e863ab29912a79a"}, + {file = "icdiff-2.0.7-py3-none-any.whl", hash = "sha256:f05d1b3623223dd1c70f7848da7d699de3d9a2550b902a8234d9026292fb5762"}, + {file = "icdiff-2.0.7.tar.gz", hash = "sha256:f79a318891adbf59a45e3a7694f5e1f18c5407065264637072ac8363b759866f"}, ] [[package]] name = "identify" -version = "2.5.26" +version = "2.5.32" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.26-py2.py3-none-any.whl", hash = "sha256:c22a8ead0d4ca11f1edd6c9418c3220669b3b7533ada0a0ffa6cc0ef85cf9b54"}, - {file = "identify-2.5.26.tar.gz", hash = "sha256:7243800bce2f58404ed41b7c002e53d4d22bcf3ae1b7900c2d7aefd95394bf7f"}, + {file = "identify-2.5.32-py2.py3-none-any.whl", hash = "sha256:0b7656ef6cba81664b783352c73f8c24b39cf82f926f78f4550eda928e5e0545"}, + {file = "identify-2.5.32.tar.gz", hash = "sha256:5d9979348ec1a21c768ae07e0a652924538e8bce67313a73cb0f681cf08ba407"}, ] [package.extras] @@ -409,51 +392,6 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib" plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] -[[package]] -name = "lazy-object-proxy" -version = "1.9.0" -description = "A fast and thorough lazy object proxy." -optional = false -python-versions = ">=3.7" -files = [ - {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, -] - [[package]] name = "mccabe" version = "0.7.0" @@ -467,33 +405,38 @@ files = [ [[package]] name = "mypy" -version = "1.5.0" +version = "1.7.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ad3109bec37cc33654de8db30fe8ff3a1bb57ea65144167d68185e6dced9868d"}, - {file = "mypy-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b4ea3a0241cb005b0ccdbd318fb99619b21ae51bcf1660b95fc22e0e7d3ba4a1"}, - {file = "mypy-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fe816e26e676c1311b9e04fd576543b873576d39439f7c24c8e5c7728391ecf"}, - {file = "mypy-1.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:42170e68adb1603ccdc55a30068f72bcfcde2ce650188e4c1b2a93018b826735"}, - {file = "mypy-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d145b81a8214687cfc1f85c03663a5bbe736777410e5580e54d526e7e904f564"}, - {file = "mypy-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c36011320e452eb30bec38b9fd3ba20569dc9545d7d4540d967f3ea1fab9c374"}, - {file = "mypy-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3940cf5845b2512b3ab95463198b0cdf87975dfd17fdcc6ce9709a9abe09e69"}, - {file = "mypy-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9166186c498170e1ff478a7f540846b2169243feb95bc228d39a67a1a450cdc6"}, - {file = "mypy-1.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:725b57a19b7408ef66a0fd9db59b5d3e528922250fb56e50bded27fea9ff28f0"}, - {file = "mypy-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:eec5c927aa4b3e8b4781840f1550079969926d0a22ce38075f6cfcf4b13e3eb4"}, - {file = "mypy-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79c520aa24f21852206b5ff2cf746dc13020113aa73fa55af504635a96e62718"}, - {file = "mypy-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:769ddb6bfe55c2bd9c7d6d7020885a5ea14289619db7ee650e06b1ef0852c6f4"}, - {file = "mypy-1.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf18f8db7e5f060d61c91e334d3b96d6bb624ddc9ee8a1cde407b737acbca2c"}, - {file = "mypy-1.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a2500ad063413bc873ae102cf655bf49889e0763b260a3a7cf544a0cbbf7e70a"}, - {file = "mypy-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:84cf9f7d8a8a22bb6a36444480f4cbf089c917a4179fbf7eea003ea931944a7f"}, - {file = "mypy-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a551ed0fc02455fe2c1fb0145160df8336b90ab80224739627b15ebe2b45e9dc"}, - {file = "mypy-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:372fd97293ed0076d52695849f59acbbb8461c4ab447858cdaeaf734a396d823"}, - {file = "mypy-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8a7444d6fcac7e2585b10abb91ad900a576da7af8f5cffffbff6065d9115813"}, - {file = "mypy-1.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:35b13335c6c46a386577a51f3d38b2b5d14aa619e9633bb756bd77205e4bd09f"}, - {file = "mypy-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:2c9d570f53908cbea326ad8f96028a673b814d9dca7515bf71d95fa662c3eb6f"}, - {file = "mypy-1.5.0-py3-none-any.whl", hash = "sha256:69b32d0dedd211b80f1b7435644e1ef83033a2af2ac65adcdc87c38db68a86be"}, - {file = "mypy-1.5.0.tar.gz", hash = "sha256:f3460f34b3839b9bc84ee3ed65076eb827cd99ed13ed08d723f9083cada4a212"}, + {file = "mypy-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5da84d7bf257fd8f66b4f759a904fd2c5a765f70d8b52dde62b521972a0a2357"}, + {file = "mypy-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a3637c03f4025f6405737570d6cbfa4f1400eb3c649317634d273687a09ffc2f"}, + {file = "mypy-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b633f188fc5ae1b6edca39dae566974d7ef4e9aaaae00bc36efe1f855e5173ac"}, + {file = "mypy-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d6ed9a3997b90c6f891138e3f83fb8f475c74db4ccaa942a1c7bf99e83a989a1"}, + {file = "mypy-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fe46e96ae319df21359c8db77e1aecac8e5949da4773c0274c0ef3d8d1268a9"}, + {file = "mypy-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:df67fbeb666ee8828f675fee724cc2cbd2e4828cc3df56703e02fe6a421b7401"}, + {file = "mypy-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a79cdc12a02eb526d808a32a934c6fe6df07b05f3573d210e41808020aed8b5d"}, + {file = "mypy-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65f385a6f43211effe8c682e8ec3f55d79391f70a201575def73d08db68ead1"}, + {file = "mypy-1.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e81ffd120ee24959b449b647c4b2fbfcf8acf3465e082b8d58fd6c4c2b27e46"}, + {file = "mypy-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:f29386804c3577c83d76520abf18cfcd7d68264c7e431c5907d250ab502658ee"}, + {file = "mypy-1.7.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:87c076c174e2c7ef8ab416c4e252d94c08cd4980a10967754f91571070bf5fbe"}, + {file = "mypy-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cb8d5f6d0fcd9e708bb190b224089e45902cacef6f6915481806b0c77f7786d"}, + {file = "mypy-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93e76c2256aa50d9c82a88e2f569232e9862c9982095f6d54e13509f01222fc"}, + {file = "mypy-1.7.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cddee95dea7990e2215576fae95f6b78a8c12f4c089d7e4367564704e99118d3"}, + {file = "mypy-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:d01921dbd691c4061a3e2ecdbfbfad029410c5c2b1ee88946bf45c62c6c91210"}, + {file = "mypy-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:185cff9b9a7fec1f9f7d8352dff8a4c713b2e3eea9c6c4b5ff7f0edf46b91e41"}, + {file = "mypy-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7b1e399c47b18feb6f8ad4a3eef3813e28c1e871ea7d4ea5d444b2ac03c418"}, + {file = "mypy-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc9fe455ad58a20ec68599139ed1113b21f977b536a91b42bef3ffed5cce7391"}, + {file = "mypy-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d0fa29919d2e720c8dbaf07d5578f93d7b313c3e9954c8ec05b6d83da592e5d9"}, + {file = "mypy-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b53655a295c1ed1af9e96b462a736bf083adba7b314ae775563e3fb4e6795f5"}, + {file = "mypy-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1b06b4b109e342f7dccc9efda965fc3970a604db70f8560ddfdee7ef19afb05"}, + {file = "mypy-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf7a2f0a6907f231d5e41adba1a82d7d88cf1f61a70335889412dec99feeb0f8"}, + {file = "mypy-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:551d4a0cdcbd1d2cccdcc7cb516bb4ae888794929f5b040bb51aae1846062901"}, + {file = "mypy-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55d28d7963bef00c330cb6461db80b0b72afe2f3c4e2963c99517cf06454e665"}, + {file = "mypy-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:870bd1ffc8a5862e593185a4c169804f2744112b4a7c55b93eb50f48e7a77010"}, + {file = "mypy-1.7.0-py3-none-any.whl", hash = "sha256:96650d9a4c651bc2a4991cf46f100973f656d69edc7faf91844e87fe627f7e96"}, + {file = "mypy-1.7.0.tar.gz", hash = "sha256:1e280b5697202efa698372d2f39e9a6713a0395a756b1c6bd48995f8d72690dc"}, ] [package.dependencies] @@ -504,6 +447,7 @@ typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] @@ -533,52 +477,18 @@ setuptools = "*" [[package]] name = "nuitka" -version = "1.7.10" +version = "1.8.6" description = "Python compiler with full language support and CPython compatibility" optional = false python-versions = "*" files = [ - {file = "Nuitka-1.7.10.tar.gz", hash = "sha256:61c84b4eb7105d20836940ab6134460b690da8aab7a74bdc84ddd05de1e04b16"}, + {file = "Nuitka-1.8.6.tar.gz", hash = "sha256:88e6f436cfeeed1a30d8b44cd51d3a2b157a6a275dd007eba79f915a94ee20d3"}, ] [package.dependencies] ordered-set = ">=4.1.0" zstandard = ">=0.15" -[[package]] -name = "numpy" -version = "1.25.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, - {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, - {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, - {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, - {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, - {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, - {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, - {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, - {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, - {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, - {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, - {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, -] - [[package]] name = "ordered-set" version = "4.1.0" @@ -595,13 +505,13 @@ dev = ["black", "mypy", "pytest"] [[package]] name = "packaging" -version = "23.1" +version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] @@ -617,103 +527,80 @@ files = [ [[package]] name = "pillow" -version = "9.4.0" +version = "10.0.1" description = "Python Imaging Library (Fork)" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, - {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, - {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, - {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, - {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, - {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, - {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, - {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, - {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, - {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, - {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, - {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, - {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, - {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, - {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, - {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, - {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, - {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, - {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, - {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, - {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, - {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, - {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, - {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, - {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, - {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"}, + {file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"}, + {file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"}, + {file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"}, + {file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"}, + {file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"}, + {file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] [[package]] name = "platformdirs" -version = "3.10.0" +version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, ] [package.extras] @@ -722,13 +609,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co [[package]] name = "pluggy" -version = "1.2.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] @@ -748,13 +635,13 @@ files = [ [[package]] name = "pre-commit" -version = "3.3.3" +version = "3.5.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.8" files = [ - {file = "pre_commit-3.3.3-py2.py3-none-any.whl", hash = "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb"}, - {file = "pre_commit-3.3.3.tar.gz", hash = "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023"}, + {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, + {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, ] [package.dependencies] @@ -777,32 +664,33 @@ files = [ [[package]] name = "pyglet" -version = "2.0.9" +version = "2.0.10" description = "Cross-platform windowing and multimedia library" optional = false python-versions = "*" files = [ - {file = "pyglet-2.0.9-py3-none-any.whl", hash = "sha256:8520b22dde75f47167e1fedeed58ac0bb0c890c0dca17d8528427d6b318cd9cc"}, - {file = "pyglet-2.0.9.zip", hash = "sha256:a0922e42f2d258505678e2f4a355c5476c1a6352c3f3a37754042ddb7e7cf72f"}, + {file = "pyglet-2.0.10-py3-none-any.whl", hash = "sha256:e10a1f1a6a2dcfbf23155913746ff6fbf8ea18c5ee813b6d0e79d273bb2b3c18"}, + {file = "pyglet-2.0.10.zip", hash = "sha256:242beb1b3bd67c5bebdfe5ba11ec56b696ad86b50c6e7f2a317f8d783256b9c9"}, ] [[package]] name = "pylint" -version = "2.17.5" +version = "3.0.2" description = "python code static checker" optional = false -python-versions = ">=3.7.2" +python-versions = ">=3.8.0" files = [ - {file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"}, - {file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"}, + {file = "pylint-3.0.2-py3-none-any.whl", hash = "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda"}, + {file = "pylint-3.0.2.tar.gz", hash = "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496"}, ] [package.dependencies] -astroid = ">=2.15.6,<=2.17.0-dev0" +astroid = ">=3.0.1,<=3.1.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, ] isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.8" @@ -816,62 +704,67 @@ testutils = ["gitpython (>3)"] [[package]] name = "pymunk" -version = "6.4.0" +version = "6.5.2" description = "Pymunk is a easy-to-use pythonic 2d physics library" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pymunk-6.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3651706fad57d2ef5be58cccc911e8ddf71c2d22171e28e05624dbf8591a519b"}, - {file = "pymunk-6.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491feebb552e17f81c2b24d7a6558ad7e1c5f59545f0494b5fa3f0174f4fc54d"}, - {file = "pymunk-6.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19051ac3916767b000c8d6b945883769276b82c5e9660f3739b2af534393794b"}, - {file = "pymunk-6.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a01637814a4cd9e356deb3145d54a71cdd256823a0ae32706d8c731b6053c67b"}, - {file = "pymunk-6.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:189b12b71dd938758745892aa9d6ea26c8b9b5fdf4e985638137ce4e02ff9d5f"}, - {file = "pymunk-6.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1e02e45012e788fe828e5ea470049f797688565a4ff06f7b8269f5f22d152ea5"}, - {file = "pymunk-6.4.0-cp310-cp310-win32.whl", hash = "sha256:ad252c1221f201b466984a17e6c61198e6be44691d4f5d46192fb313e8d170fc"}, - {file = "pymunk-6.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b65185404af6a3bc8447d8aa3d0149378323d8d487aa4accf1eb2491f21a568"}, - {file = "pymunk-6.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0063de8563021f47abbca03526f77786caa9c441ed90264566ad59e480688c95"}, - {file = "pymunk-6.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02d4650c677a37d6d4617b4fd74ce1b943da9f916062bf7b60cd849c7da18d75"}, - {file = "pymunk-6.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d6bf81b0d9ffe84adc926e74e3a446c515107d87408b0812ca7fc18378a7e2"}, - {file = "pymunk-6.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7b326b64d2903a17581952c8813e64e8373077481f602420e683e0940e9f00a"}, - {file = "pymunk-6.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f477e920b537e0a1d25a3bfe2672c042b40e2d9da156869c9c6fe208e8ae4668"}, - {file = "pymunk-6.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5d6b19d8bf6394507bb0425f627a30d00ad49c94aaa92abd284594d78a1aa7c3"}, - {file = "pymunk-6.4.0-cp311-cp311-win32.whl", hash = "sha256:1c849e96d9d9b10d46eb9f5ebab7a9e120aba1ff911d055cff896bec519db965"}, - {file = "pymunk-6.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d0747e0074adf8e6997bc3991d712f2476b2abde2328f1cf7b2bc6120753db9"}, - {file = "pymunk-6.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:061c52ff3c1fe8a86a6bea072099f0ef2f68e18d4987a92eb04c727479f9aae5"}, - {file = "pymunk-6.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a942fdce0a8122e4ad2a0ae8c19a2bcad31716d63b57aa43394ebe2a95c650"}, - {file = "pymunk-6.4.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:236188f41b58e00c0d3249ebe09c96ee8853e583558088a809203c663a366876"}, - {file = "pymunk-6.4.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6da38bfce15ea8df39fd11614224500c98e7dfd372a5a6de7745a5599879a88"}, - {file = "pymunk-6.4.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3aff899733251b50f4d762d497cd3b1481b6d16488f0dcb179f73ef9a626cc8"}, - {file = "pymunk-6.4.0-cp36-cp36m-win32.whl", hash = "sha256:486c17603abf92f32aa1640f1f03b8b7637b9acb20750a88e02ac58c95c2be96"}, - {file = "pymunk-6.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c167bcd66f4ff322abef24175b5518b8a9f27798a2b8dbb6f7b632115dc71186"}, - {file = "pymunk-6.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:292494cb438047206c03820b0c41d072992cb028d20a3d920277479dc3011ee7"}, - {file = "pymunk-6.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9763088b1a229ea816adfc672fe3ae0cc8153d39a98da602c98ff190ba584d8"}, - {file = "pymunk-6.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:393b2eea7319b6a841ff597e9ff90ec8ab1fff6c0a8c920d08c5231ed87a1cdc"}, - {file = "pymunk-6.4.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6a59b1fb11a1c9c9b2abac78d97c96a060f77f8be6e0e8815fc25ae6ed5120d9"}, - {file = "pymunk-6.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3bf05beb688f06bc51cb27e56c5f05ceb189ce108e536cdccb6e710ca4975da0"}, - {file = "pymunk-6.4.0-cp37-cp37m-win32.whl", hash = "sha256:ef2c25ecd78883b90f038299ca632cdd55e92148647d7242f7132dd5e8a5ba77"}, - {file = "pymunk-6.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:19798b0a0e98ce84489023b832963aa3f8f05097c3614b3b777885a8dc039030"}, - {file = "pymunk-6.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:853a4839601ab82f4055791afa7284e71ad7c58c68719ee7160c48026589d0ab"}, - {file = "pymunk-6.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:030acf18b2fe6ed0211feab645ce51568b18b182756256b8135d20411ae9b27e"}, - {file = "pymunk-6.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7efb271777bb887451d3cba88110c9d9a21ba231eb4b1605e88b7822a2d2e5d7"}, - {file = "pymunk-6.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32665d4ba65ff4310de0405d8fd0436d3a808a9d1ce61604078054af7d08497a"}, - {file = "pymunk-6.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6043c7031c5f7d8443cd6aa06f774b4bf2389c1164fda33e8ec80848c16b1710"}, - {file = "pymunk-6.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e1bdc6d7c59ceb1bae542e3b3093054e06b96080ab46f6a80def20afe3d268a2"}, - {file = "pymunk-6.4.0-cp38-cp38-win32.whl", hash = "sha256:a1e75d8ae0b38e8c8c87231e8728bfa6c19f59d4006417630dff87c151726e83"}, - {file = "pymunk-6.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:2d58222bbd358f03f3fe29a1878d9d0f78513dd9822f622fa9023b899b3eb42f"}, - {file = "pymunk-6.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:190060a06a98cb06cfe68c190b79c8575460a8ae27b55e62f1aa1ee06a7acf77"}, - {file = "pymunk-6.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0ec63a3796a5c7f1efeb6a40194806bcbc2389cd32e8fc67915daa395991c0c"}, - {file = "pymunk-6.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80998668f87fc696e48fc972a341a787e747cfdd3b23fff0e6a73c6ab882e165"}, - {file = "pymunk-6.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef39069e4ccd90b188102381e8fb2558ecfd61bd04d09370896cb76eafd0773"}, - {file = "pymunk-6.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:83efbe0d23cee31c4fd80c853babd2cb8cafb12cf94b8c2ab88748d9435064d9"}, - {file = "pymunk-6.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1301f9e657c26de9148c6857b3d9005c6356b4a0783d06cdecfd00661b2161e7"}, - {file = "pymunk-6.4.0-cp39-cp39-win32.whl", hash = "sha256:710746dcb65ae543dcfa2f64c19793ecd5d12d70cee10ea2efc6fd6a7a5265a9"}, - {file = "pymunk-6.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a7a1c985a0340785fa70eee8524305f2a329debe542d877dc0f206e18332d528"}, - {file = "pymunk-6.4.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:73f8b3ca9948519dcc96989fbebd458b525f6a3eb5492f96fef64f07b61ad22b"}, - {file = "pymunk-6.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad32a32fe508d5bb1c2c0b6446dba32cfb812688bc2b7cf4780a24e68b9a2d16"}, - {file = "pymunk-6.4.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:098dfc52d8c1ecb4108ef345f3d4526a7adfef8eaa5148b250ad5a1754580675"}, - {file = "pymunk-6.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6453475e46414d2e1d71314dcb565ce51caa195b0f7bf1714453e9f1012ee970"}, - {file = "pymunk-6.4.0.zip", hash = "sha256:60dcd9ff0433e6ce49e5cb577a4368d0592c4685f4deb644d705c882161c724e"}, + {file = "pymunk-6.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:469d07a453ae48ddfbbdd3425fd71fc8c6e5dd9df516adbd37ee6c61095a183b"}, + {file = "pymunk-6.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e73aeaf2954c736768c4e1de2f6a519caf20c8c18f0948107dfb964e43e532e"}, + {file = "pymunk-6.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5f3f3ae2507f7b76616f99e92e84801f862cfbeabfcd60127462166f34dcaf6"}, + {file = "pymunk-6.5.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f3b2e763bb1fe8743b811682b6a61b66275e0cd30e83048c1c6c210475d5abf"}, + {file = "pymunk-6.5.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa2e18c2469190c419af1eda15a4056921a9abe6c1d3ea4e75afbc20122489bb"}, + {file = "pymunk-6.5.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d2b4e183965215fd9dc0c75e1884dbb48d598a8e1b6dfed4c17c9e0ab1aa2109"}, + {file = "pymunk-6.5.2-cp310-cp310-win32.whl", hash = "sha256:72c92796cc0747a203e7120050dd7dd699e09838c09c4e8589edbd5204f11f62"}, + {file = "pymunk-6.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:37356740dc14ae26c117bc70887b0f346bb3a73e79add032bbd32451056ad6b8"}, + {file = "pymunk-6.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff1cf95725362676a24af7c057cab447d620ee7d5f4ab9125e76d4f11ccd11aa"}, + {file = "pymunk-6.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bafea4b86551cbea0bde352a5dc04b5a355a016fbe19c66e250f84e2b6feb60c"}, + {file = "pymunk-6.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3157e5ed99516f3a15e210206ad33b2f9954580e041b0c4f3efe50c8e3e30ea8"}, + {file = "pymunk-6.5.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:225356b1d4851a5e1cf36fe8d0962a43502f89d5bd7b94224726a3c2a70c03d9"}, + {file = "pymunk-6.5.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:22b726a84eb2b5018c6721fd9f2690d7f1348a01492b3f3f6c9e635902774564"}, + {file = "pymunk-6.5.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea9680b19cebf5b955fbd79c15e940b7d134e9afb0999a0bcad7cb46f845fe3"}, + {file = "pymunk-6.5.2-cp311-cp311-win32.whl", hash = "sha256:1944fefa03776c1bef58f9cdec82c70e2b88543fe218220482d442b9e950827a"}, + {file = "pymunk-6.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:f6e8fe43bbee763a4d3ce1d9b1bf237e6219d96ad0fce4dbfd1aa40832ee5a51"}, + {file = "pymunk-6.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b110cbd0f0bfd5b7b5e9e167804da8fe1343221d39bbe5f101a2bf687e2108ac"}, + {file = "pymunk-6.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69fa2c768d55da7885036f99d707ad242ef988e7e62ca18d0b0e458928718a8d"}, + {file = "pymunk-6.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c143738636face5bbf4854d105f8e8d3cdd21b2c580ec1af9a5bbcfde743a8a"}, + {file = "pymunk-6.5.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bddfbb2d4fbff17fd0e3c2cfab7e4b43666693d8fbc554a3782b71299a5b7a0c"}, + {file = "pymunk-6.5.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e09e51f678660c49ec4dc089d04b04561b768805a650c393357e7f6e6ea30a96"}, + {file = "pymunk-6.5.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:938c067fd42e2553013161ab94959d577c90229b1fe52d8bb23b20e3863e04ea"}, + {file = "pymunk-6.5.2-cp312-cp312-win32.whl", hash = "sha256:8a17f5498596e76a5317873cfe4bc0b1e6096ca8d339294ba955e74ae74cb693"}, + {file = "pymunk-6.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:8cebd80bad5c65cd6dfb82a254550fb62415cf76142e7c547b22cbfc6eba7134"}, + {file = "pymunk-6.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3532a07aa0507cef23508ec74d9b58d9b4d4aa35b47f727bd30e73453525bae4"}, + {file = "pymunk-6.5.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2382e926905a6e1454947634ef7ba34c1985bacf4a2e2567ba7c289669a495d0"}, + {file = "pymunk-6.5.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd6db865e3ff7d5f88a8cec16bd4534415ebb27068fc7aba55bee340d017d5cb"}, + {file = "pymunk-6.5.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:69e777fc1e61a48d9655484fa2ea4afc926c45bd281ad0550d784b82f0bccddb"}, + {file = "pymunk-6.5.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae78d8cc75a945142641662f967bde1e1c15d39a84f0147dc389f8d3170216f1"}, + {file = "pymunk-6.5.2-cp37-cp37m-win32.whl", hash = "sha256:f378d60a43728692039258a8e3607dd60f68486cbe91a044ee55eebb57de86a6"}, + {file = "pymunk-6.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:da0579fe2cbb31adde79f03a9475e68cd5a4d65e4d2b801b5bc16e683497aa90"}, + {file = "pymunk-6.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:899d2b10dc764a08d7548c310bac7ecb198985977e7546ddaa75e717aa8bc58b"}, + {file = "pymunk-6.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:063e51eb3b589918482fa2d5fde1df39ca81673a8f48d988fc6684781842cd7f"}, + {file = "pymunk-6.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92f440cfb45361ebaa0ade7c56f3c03d04d36321cdd52d55e6f7e9eb0b535b61"}, + {file = "pymunk-6.5.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a56fa10107bd602a2cd554d2a2b9b2575161154d74417c65a1f180cf4c96802"}, + {file = "pymunk-6.5.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:78fe43f587b7813341a0ec8a54dfc2023881ed1843b7f096f2ee44cbc05e9684"}, + {file = "pymunk-6.5.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5d5ec0159d6210d55e748802224d286f4613de5a739b46023e3914f6385726d0"}, + {file = "pymunk-6.5.2-cp38-cp38-win32.whl", hash = "sha256:7fd6eee1de2b2b99930433e7bd04f5ca16dc01100f742f85c8b0c82104aaa67c"}, + {file = "pymunk-6.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:3e039d935e92030480553d494eccf40cc7024e0833b5968d63875113cf62db0a"}, + {file = "pymunk-6.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1607d57cd35986934dbcd64522b2fe5e5d9c04a1b4a00a92084080d7e76f0b00"}, + {file = "pymunk-6.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91a9a65528d702641d469be80b40f2f35670ac8e987fb4f8230492bb902d8a1e"}, + {file = "pymunk-6.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dfbf57e60b03820d38f8015219cee866ec135cfc10098b2fa35e996067e5ed"}, + {file = "pymunk-6.5.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f3d24c6e3c5c957844e0bea16dbeea0ff2b31f05faf79a5e000d589451fb997"}, + {file = "pymunk-6.5.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6b5a96a8df196498b297f7dd2010e3bb67ecce145395e6d4b286d35f5ebff97f"}, + {file = "pymunk-6.5.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0566e6a5eabfdbaf9e236581608b7db6f3a02eba6126971921b7c9c7ff4c51ea"}, + {file = "pymunk-6.5.2-cp39-cp39-win32.whl", hash = "sha256:feef1266b6c2957e85d9799510a6ba0046553d6af3631ff58d6794c876d550e0"}, + {file = "pymunk-6.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:6949d79091e567480f1788a299c4609014c4ee7d629583442a695a13e122e7ca"}, + {file = "pymunk-6.5.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa54569a2fe4da79f5bc04d0eecc25f59a9d283762cb2a4ae7275d1ece6a31f"}, + {file = "pymunk-6.5.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948a0e6f165d81e59d4d82128a5ddc0bd0958351a86a62ea63b316687abb906d"}, + {file = "pymunk-6.5.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41cbd2a5570544dfab3f2aac0ea2bf9d64020c266f1f0dccc72d862249e2064f"}, + {file = "pymunk-6.5.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1ac469921913ad0f7c961cfeb4e85401b3873055698728b62e6c83e55bf8137"}, + {file = "pymunk-6.5.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:76c63e069193791bd6e6c6e99b439932b86c36f68ebf303e1dd3ccd457c86834"}, + {file = "pymunk-6.5.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98d328aad251028a4b543e75b8ec3d86007a3b23146782c3969ad06a4473223b"}, + {file = "pymunk-6.5.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1c3dbc72993cb50fad322461a5b7bcb9b0855dd0342a55fe567a98e0ce16eb7"}, + {file = "pymunk-6.5.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4a6903765e823fbfbceb976162ec786b7b0f5c60c952b4c91576774ec1af475f"}, + {file = "pymunk-6.5.2.zip", hash = "sha256:015eaea5a65c9db2a6426f6d4c8b5107a2e97246a9a63747bcc1ff6b09ff29e1"}, ] [package.dependencies] @@ -882,13 +775,13 @@ dev = ["aafigure", "matplotlib", "pygame", "pyglet (<2.0.0)", "sphinx", "wheel"] [[package]] name = "pyproject-api" -version = "1.5.3" +version = "1.6.1" description = "API to interact with the python pyproject.toml based projects" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pyproject_api-1.5.3-py3-none-any.whl", hash = "sha256:14cf09828670c7b08842249c1f28c8ee6581b872e893f81b62d5465bec41502f"}, - {file = "pyproject_api-1.5.3.tar.gz", hash = "sha256:ffb5b2d7cad43f5b2688ab490de7c4d3f6f15e0b819cb588c4b771567c9729eb"}, + {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"}, + {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"}, ] [package.dependencies] @@ -896,18 +789,18 @@ packaging = ">=23.1" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} [package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "setuptools (>=67.8)", "wheel (>=0.40)"] +docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"] [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] @@ -941,12 +834,13 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-icdiff" -version = "0.6" +version = "0.8" description = "use icdiff for better error messages in pytest assertions" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pytest-icdiff-0.6.tar.gz", hash = "sha256:e8f1ef4550a893b4f0a0ea7e7a8299b12ded72c086101d7811ddec0d85fd1bad"}, + {file = "pytest-icdiff-0.8.tar.gz", hash = "sha256:4f493ae5ee63c8e90e9f96d4b0b2968b19634dfed8a6e3c9848fcd0d6cadcf7b"}, + {file = "pytest_icdiff-0.8-py3-none-any.whl", hash = "sha256:8fac8667d7042270c23019580b4b5dfd81e1c3e5a9bc9d5df6ac4a49788d42f2"}, ] [package.dependencies] @@ -970,13 +864,13 @@ pytest = ">=5" [[package]] name = "pytest-randomly" -version = "3.13.0" +version = "3.15.0" description = "Pytest plugin to randomly order tests and control random.seed." optional = false python-versions = ">=3.8" files = [ - {file = "pytest_randomly-3.13.0-py3-none-any.whl", hash = "sha256:e78d898ef4066f89744e5075083aa7fb6f0de07ffd70ca9c4435cda590cf1eac"}, - {file = "pytest_randomly-3.13.0.tar.gz", hash = "sha256:079c78b94693189879fbd7304de4e147304f0811fa96249ea5619f2f1cd33df0"}, + {file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"}, + {file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"}, ] [package.dependencies] @@ -1073,99 +967,45 @@ files = [ [[package]] name = "ruff" -version = "0.0.284" -description = "An extremely fast Python linter, written in Rust." +version = "0.1.6" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.0.284-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:8b949084941232e2c27f8d12c78c5a6a010927d712ecff17231ee1a8371c205b"}, - {file = "ruff-0.0.284-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:a3930d66b35e4dc96197422381dff2a4e965e9278b5533e71ae8474ef202fab0"}, - {file = "ruff-0.0.284-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1f7096038961d8bc3b956ee69d73826843eb5b39a5fa4ee717ed473ed69c95"}, - {file = "ruff-0.0.284-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bcaf85907fc905d838f46490ee15f04031927bbea44c478394b0bfdeadc27362"}, - {file = "ruff-0.0.284-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3660b85a9d84162a055f1add334623ae2d8022a84dcd605d61c30a57b436c32"}, - {file = "ruff-0.0.284-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0a3218458b140ea794da72b20ea09cbe13c4c1cdb7ac35e797370354628f4c05"}, - {file = "ruff-0.0.284-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2fe880cff13fffd735387efbcad54ba0ff1272bceea07f86852a33ca71276f4"}, - {file = "ruff-0.0.284-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1d098ea74d0ce31478765d1f8b4fbdbba2efc532397b5c5e8e5ea0c13d7e5ae"}, - {file = "ruff-0.0.284-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c79ae3308e308b94635cd57a369d1e6f146d85019da2fbc63f55da183ee29b"}, - {file = "ruff-0.0.284-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f86b2b1e7033c00de45cc176cf26778650fb8804073a0495aca2f674797becbb"}, - {file = "ruff-0.0.284-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e37e086f4d623c05cd45a6fe5006e77a2b37d57773aad96b7802a6b8ecf9c910"}, - {file = "ruff-0.0.284-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d29dfbe314e1131aa53df213fdfea7ee874dd96ea0dd1471093d93b59498384d"}, - {file = "ruff-0.0.284-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:88295fd649d0aa1f1271441df75bf06266a199497afd239fd392abcfd75acd7e"}, - {file = "ruff-0.0.284-py3-none-win32.whl", hash = "sha256:735cd62fccc577032a367c31f6a9de7c1eb4c01fa9a2e60775067f44f3fc3091"}, - {file = "ruff-0.0.284-py3-none-win_amd64.whl", hash = "sha256:f67ed868d79fbcc61ad0fa034fe6eed2e8d438d32abce9c04b7c4c1464b2cf8e"}, - {file = "ruff-0.0.284-py3-none-win_arm64.whl", hash = "sha256:1292cfc764eeec3cde35b3a31eae3f661d86418b5e220f5d5dba1c27a6eccbb6"}, - {file = "ruff-0.0.284.tar.gz", hash = "sha256:ebd3cc55cd499d326aac17a331deaea29bea206e01c08862f9b5c6e93d77a491"}, + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, + {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, + {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, + {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, + {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, ] [[package]] name = "setuptools" -version = "68.0.0" +version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.7" -files = [ - {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, - {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "shapely" -version = "2.0.1" -description = "Manipulation and analysis of geometric objects" -optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "shapely-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b06d031bc64149e340448fea25eee01360a58936c89985cf584134171e05863f"}, - {file = "shapely-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9a6ac34c16f4d5d3c174c76c9d7614ec8fe735f8f82b6cc97a46b54f386a86bf"}, - {file = "shapely-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:865bc3d7cc0ea63189d11a0b1120d1307ed7a64720a8bfa5be2fde5fc6d0d33f"}, - {file = "shapely-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45b4833235b90bc87ee26c6537438fa77559d994d2d3be5190dd2e54d31b2820"}, - {file = "shapely-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce88ec79df55430e37178a191ad8df45cae90b0f6972d46d867bf6ebbb58cc4d"}, - {file = "shapely-2.0.1-cp310-cp310-win32.whl", hash = "sha256:01224899ff692a62929ef1a3f5fe389043e262698a708ab7569f43a99a48ae82"}, - {file = "shapely-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:da71de5bf552d83dcc21b78cc0020e86f8d0feea43e202110973987ffa781c21"}, - {file = "shapely-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:502e0a607f1dcc6dee0125aeee886379be5242c854500ea5fd2e7ac076b9ce6d"}, - {file = "shapely-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7d3bbeefd8a6a1a1017265d2d36f8ff2d79d0162d8c141aa0d37a87063525656"}, - {file = "shapely-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f470a130d6ddb05b810fc1776d918659407f8d025b7f56d2742a596b6dffa6c7"}, - {file = "shapely-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4641325e065fd3e07d55677849c9ddfd0cf3ee98f96475126942e746d55b17c8"}, - {file = "shapely-2.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90cfa4144ff189a3c3de62e2f3669283c98fb760cfa2e82ff70df40f11cadb39"}, - {file = "shapely-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70a18fc7d6418e5aea76ac55dce33f98e75bd413c6eb39cfed6a1ba36469d7d4"}, - {file = "shapely-2.0.1-cp311-cp311-win32.whl", hash = "sha256:09d6c7763b1bee0d0a2b84bb32a4c25c6359ad1ac582a62d8b211e89de986154"}, - {file = "shapely-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d8f55f355be7821dade839df785a49dc9f16d1af363134d07eb11e9207e0b189"}, - {file = "shapely-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:83a8ec0ee0192b6e3feee9f6a499d1377e9c295af74d7f81ecba5a42a6b195b7"}, - {file = "shapely-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a529218e72a3dbdc83676198e610485fdfa31178f4be5b519a8ae12ea688db14"}, - {file = "shapely-2.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91575d97fd67391b85686573d758896ed2fc7476321c9d2e2b0c398b628b961c"}, - {file = "shapely-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8b0d834b11be97d5ab2b4dceada20ae8e07bcccbc0f55d71df6729965f406ad"}, - {file = "shapely-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:b4f0711cc83734c6fad94fc8d4ec30f3d52c1787b17d9dca261dc841d4731c64"}, - {file = "shapely-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:05c51a29336e604c084fb43ae5dbbfa2c0ef9bd6fedeae0a0d02c7b57a56ba46"}, - {file = "shapely-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b519cf3726ddb6c67f6a951d1bb1d29691111eaa67ea19ddca4d454fbe35949c"}, - {file = "shapely-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:193a398d81c97a62fc3634a1a33798a58fd1dcf4aead254d080b273efbb7e3ff"}, - {file = "shapely-2.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e55698e0ed95a70fe9ff9a23c763acfe0bf335b02df12142f74e4543095e9a9b"}, - {file = "shapely-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f32a748703e7bf6e92dfa3d2936b2fbfe76f8ce5f756e24f49ef72d17d26ad02"}, - {file = "shapely-2.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a34a23d6266ca162499e4a22b79159dc0052f4973d16f16f990baa4d29e58b6"}, - {file = "shapely-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d173d24e85e51510e658fb108513d5bc11e3fd2820db6b1bd0522266ddd11f51"}, - {file = "shapely-2.0.1-cp38-cp38-win32.whl", hash = "sha256:3cb256ae0c01b17f7bc68ee2ffdd45aebf42af8992484ea55c29a6151abe4386"}, - {file = "shapely-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:c7eed1fb3008a8a4a56425334b7eb82651a51f9e9a9c2f72844a2fb394f38a6c"}, - {file = "shapely-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac1dfc397475d1de485e76de0c3c91cc9d79bd39012a84bb0f5e8a199fc17bef"}, - {file = "shapely-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33403b8896e1d98aaa3a52110d828b18985d740cc9f34f198922018b1e0f8afe"}, - {file = "shapely-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2569a4b91caeef54dd5ae9091ae6f63526d8ca0b376b5bb9fd1a3195d047d7d4"}, - {file = "shapely-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a70a614791ff65f5e283feed747e1cc3d9e6c6ba91556e640636bbb0a1e32a71"}, - {file = "shapely-2.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c43755d2c46b75a7b74ac6226d2cc9fa2a76c3263c5ae70c195c6fb4e7b08e79"}, - {file = "shapely-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad81f292fffbd568ae71828e6c387da7eb5384a79db9b4fde14dd9fdeffca9a"}, - {file = "shapely-2.0.1-cp39-cp39-win32.whl", hash = "sha256:b50c401b64883e61556a90b89948297f1714dbac29243d17ed9284a47e6dd731"}, - {file = "shapely-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:bca57b683e3d94d0919e2f31e4d70fdfbb7059650ef1b431d9f4e045690edcd5"}, - {file = "shapely-2.0.1.tar.gz", hash = "sha256:66a6b1a3e72ece97fc85536a281476f9b7794de2e646ca8a4517e2e3c1446893"}, + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, ] -[package.dependencies] -numpy = ">=1.14" - [package.extras] -docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] -test = ["pytest", "pytest-cov"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "ssort" @@ -1207,62 +1047,62 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.1" +version = "0.12.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, - {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, ] [[package]] name = "tox" -version = "4.8.0" +version = "4.11.3" description = "tox is a generic virtualenv management and test command line tool" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tox-4.8.0-py3-none-any.whl", hash = "sha256:4991305a56983d750a0d848a34242be290452aa88d248f1bf976e4036ee8b213"}, - {file = "tox-4.8.0.tar.gz", hash = "sha256:2adacf435b12ccf10b9dfa9975d8ec0afd7cbae44d300463140d2117b968037b"}, + {file = "tox-4.11.3-py3-none-any.whl", hash = "sha256:599af5e5bb0cad0148ac1558a0b66f8fff219ef88363483b8d92a81e4246f28f"}, + {file = "tox-4.11.3.tar.gz", hash = "sha256:5039f68276461fae6a9452a3b2c7295798f00a0e92edcd9a3b78ba1a73577951"}, ] [package.dependencies] cachetools = ">=5.3.1" -chardet = ">=5.1" +chardet = ">=5.2" colorama = ">=0.4.6" -filelock = ">=3.12.2" +filelock = ">=3.12.3" packaging = ">=23.1" -platformdirs = ">=3.9.1" -pluggy = ">=1.2" -pyproject-api = ">=1.5.3" +platformdirs = ">=3.10" +pluggy = ">=1.3" +pyproject-api = ">=1.6.1" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} -virtualenv = ">=20.24.1" +virtualenv = ">=20.24.3" [package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.23.3,!=1.23.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=0.3.1)", "diff-cover (>=7.7)", "distlib (>=0.3.7)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.17.1)", "psutil (>=5.9.5)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "re-assert (>=1.1)", "time-machine (>=2.10)", "wheel (>=0.40)"] +docs = ["furo (>=2023.8.19)", "sphinx (>=7.2.4)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.24)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=1)", "diff-cover (>=7.7)", "distlib (>=0.3.7)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.18)", "psutil (>=5.9.5)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "re-assert (>=1.1)", "time-machine (>=2.12)", "wheel (>=0.41.2)"] [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] [[package]] name = "virtualenv" -version = "20.24.3" +version = "20.24.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.3-py3-none-any.whl", hash = "sha256:95a6e9398b4967fbcb5fef2acec5efaf9aa4972049d9ae41f95e0972a683fd02"}, - {file = "virtualenv-20.24.3.tar.gz", hash = "sha256:e5c3b4ce817b0b328af041506a2a299418c98747c4b1e68cb7527e74ced23efc"}, + {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, + {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, ] [package.dependencies] @@ -1271,143 +1111,62 @@ filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<4" [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] -[[package]] -name = "wrapt" -version = "1.15.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, - {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, - {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, - {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, - {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, - {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, - {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, - {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, - {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, - {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, - {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, -] - [[package]] name = "zstandard" -version = "0.21.0" +version = "0.22.0" description = "Zstandard bindings for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "zstandard-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:649a67643257e3b2cff1c0a73130609679a5673bf389564bc6d4b164d822a7ce"}, - {file = "zstandard-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:144a4fe4be2e747bf9c646deab212666e39048faa4372abb6a250dab0f347a29"}, - {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b72060402524ab91e075881f6b6b3f37ab715663313030d0ce983da44960a86f"}, - {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8257752b97134477fb4e413529edaa04fc0457361d304c1319573de00ba796b1"}, - {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c053b7c4cbf71cc26808ed67ae955836232f7638444d709bfc302d3e499364fa"}, - {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2769730c13638e08b7a983b32cb67775650024632cd0476bf1ba0e6360f5ac7d"}, - {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7d3bc4de588b987f3934ca79140e226785d7b5e47e31756761e48644a45a6766"}, - {file = "zstandard-0.21.0-cp310-cp310-win32.whl", hash = "sha256:67829fdb82e7393ca68e543894cd0581a79243cc4ec74a836c305c70a5943f07"}, - {file = "zstandard-0.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6048a287f8d2d6e8bc67f6b42a766c61923641dd4022b7fd3f7439e17ba5a4d"}, - {file = "zstandard-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7f2afab2c727b6a3d466faee6974a7dad0d9991241c498e7317e5ccf53dbc766"}, - {file = "zstandard-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff0852da2abe86326b20abae912d0367878dd0854b8931897d44cfeb18985472"}, - {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12fa383e315b62630bd407477d750ec96a0f438447d0e6e496ab67b8b451d39"}, - {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1b9703fe2e6b6811886c44052647df7c37478af1b4a1a9078585806f42e5b15"}, - {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df28aa5c241f59a7ab524f8ad8bb75d9a23f7ed9d501b0fed6d40ec3064784e8"}, - {file = "zstandard-0.21.0-cp311-cp311-win32.whl", hash = "sha256:0aad6090ac164a9d237d096c8af241b8dcd015524ac6dbec1330092dba151657"}, - {file = "zstandard-0.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:48b6233b5c4cacb7afb0ee6b4f91820afbb6c0e3ae0fa10abbc20000acdf4f11"}, - {file = "zstandard-0.21.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e7d560ce14fd209db6adacce8908244503a009c6c39eee0c10f138996cd66d3e"}, - {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e6e131a4df2eb6f64961cea6f979cdff22d6e0d5516feb0d09492c8fd36f3bc"}, - {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1e0c62a67ff425927898cf43da2cf6b852289ebcc2054514ea9bf121bec10a5"}, - {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1545fb9cb93e043351d0cb2ee73fa0ab32e61298968667bb924aac166278c3fc"}, - {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe6c821eb6870f81d73bf10e5deed80edcac1e63fbc40610e61f340723fd5f7c"}, - {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ddb086ea3b915e50f6604be93f4f64f168d3fc3cef3585bb9a375d5834392d4f"}, - {file = "zstandard-0.21.0-cp37-cp37m-win32.whl", hash = "sha256:57ac078ad7333c9db7a74804684099c4c77f98971c151cee18d17a12649bc25c"}, - {file = "zstandard-0.21.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1243b01fb7926a5a0417120c57d4c28b25a0200284af0525fddba812d575f605"}, - {file = "zstandard-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea68b1ba4f9678ac3d3e370d96442a6332d431e5050223626bdce748692226ea"}, - {file = "zstandard-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8070c1cdb4587a8aa038638acda3bd97c43c59e1e31705f2766d5576b329e97c"}, - {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af612c96599b17e4930fe58bffd6514e6c25509d120f4eae6031b7595912f85"}, - {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff891e37b167bc477f35562cda1248acc115dbafbea4f3af54ec70821090965"}, - {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9fec02ce2b38e8b2e86079ff0b912445495e8ab0b137f9c0505f88ad0d61296"}, - {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdbe350691dec3078b187b8304e6a9c4d9db3eb2d50ab5b1d748533e746d099"}, - {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b69cccd06a4a0a1d9fb3ec9a97600055cf03030ed7048d4bcb88c574f7895773"}, - {file = "zstandard-0.21.0-cp38-cp38-win32.whl", hash = "sha256:9980489f066a391c5572bc7dc471e903fb134e0b0001ea9b1d3eff85af0a6f1b"}, - {file = "zstandard-0.21.0-cp38-cp38-win_amd64.whl", hash = "sha256:0e1e94a9d9e35dc04bf90055e914077c80b1e0c15454cc5419e82529d3e70728"}, - {file = "zstandard-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2d61675b2a73edcef5e327e38eb62bdfc89009960f0e3991eae5cc3d54718de"}, - {file = "zstandard-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25fbfef672ad798afab12e8fd204d122fca3bc8e2dcb0a2ba73bf0a0ac0f5f07"}, - {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62957069a7c2626ae80023998757e27bd28d933b165c487ab6f83ad3337f773d"}, - {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e10ed461e4807471075d4b7a2af51f5234c8f1e2a0c1d37d5ca49aaaad49e8"}, - {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cff89a036c639a6a9299bf19e16bfb9ac7def9a7634c52c257166db09d950e7"}, - {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52b2b5e3e7670bd25835e0e0730a236f2b0df87672d99d3bf4bf87248aa659fb"}, - {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b1367da0dde8ae5040ef0413fb57b5baeac39d8931c70536d5f013b11d3fc3a5"}, - {file = "zstandard-0.21.0-cp39-cp39-win32.whl", hash = "sha256:db62cbe7a965e68ad2217a056107cc43d41764c66c895be05cf9c8b19578ce9c"}, - {file = "zstandard-0.21.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8d200617d5c876221304b0e3fe43307adde291b4a897e7b0617a61611dfff6a"}, - {file = "zstandard-0.21.0.tar.gz", hash = "sha256:f08e3a10d01a247877e4cb61a82a319ea746c356a3786558bed2481e6c405546"}, + {file = "zstandard-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:275df437ab03f8c033b8a2c181e51716c32d831082d93ce48002a5227ec93019"}, + {file = "zstandard-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ac9957bc6d2403c4772c890916bf181b2653640da98f32e04b96e4d6fb3252a"}, + {file = "zstandard-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe3390c538f12437b859d815040763abc728955a52ca6ff9c5d4ac707c4ad98e"}, + {file = "zstandard-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1958100b8a1cc3f27fa21071a55cb2ed32e9e5df4c3c6e661c193437f171cba2"}, + {file = "zstandard-0.22.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93e1856c8313bc688d5df069e106a4bc962eef3d13372020cc6e3ebf5e045202"}, + {file = "zstandard-0.22.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1a90ba9a4c9c884bb876a14be2b1d216609385efb180393df40e5172e7ecf356"}, + {file = "zstandard-0.22.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3db41c5e49ef73641d5111554e1d1d3af106410a6c1fb52cf68912ba7a343a0d"}, + {file = "zstandard-0.22.0-cp310-cp310-win32.whl", hash = "sha256:d8593f8464fb64d58e8cb0b905b272d40184eac9a18d83cf8c10749c3eafcd7e"}, + {file = "zstandard-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1a4b358947a65b94e2501ce3e078bbc929b039ede4679ddb0460829b12f7375"}, + {file = "zstandard-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:589402548251056878d2e7c8859286eb91bd841af117dbe4ab000e6450987e08"}, + {file = "zstandard-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a97079b955b00b732c6f280d5023e0eefe359045e8b83b08cf0333af9ec78f26"}, + {file = "zstandard-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:445b47bc32de69d990ad0f34da0e20f535914623d1e506e74d6bc5c9dc40bb09"}, + {file = "zstandard-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33591d59f4956c9812f8063eff2e2c0065bc02050837f152574069f5f9f17775"}, + {file = "zstandard-0.22.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:888196c9c8893a1e8ff5e89b8f894e7f4f0e64a5af4d8f3c410f0319128bb2f8"}, + {file = "zstandard-0.22.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:53866a9d8ab363271c9e80c7c2e9441814961d47f88c9bc3b248142c32141d94"}, + {file = "zstandard-0.22.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4ac59d5d6910b220141c1737b79d4a5aa9e57466e7469a012ed42ce2d3995e88"}, + {file = "zstandard-0.22.0-cp311-cp311-win32.whl", hash = "sha256:2b11ea433db22e720758cba584c9d661077121fcf60ab43351950ded20283440"}, + {file = "zstandard-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:11f0d1aab9516a497137b41e3d3ed4bbf7b2ee2abc79e5c8b010ad286d7464bd"}, + {file = "zstandard-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6c25b8eb733d4e741246151d895dd0308137532737f337411160ff69ca24f93a"}, + {file = "zstandard-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f9b2cde1cd1b2a10246dbc143ba49d942d14fb3d2b4bccf4618d475c65464912"}, + {file = "zstandard-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88b7df61a292603e7cd662d92565d915796b094ffb3d206579aaebac6b85d5f"}, + {file = "zstandard-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466e6ad8caefb589ed281c076deb6f0cd330e8bc13c5035854ffb9c2014b118c"}, + {file = "zstandard-0.22.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1d67d0d53d2a138f9e29d8acdabe11310c185e36f0a848efa104d4e40b808e4"}, + {file = "zstandard-0.22.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:39b2853efc9403927f9065cc48c9980649462acbdf81cd4f0cb773af2fd734bc"}, + {file = "zstandard-0.22.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8a1b2effa96a5f019e72874969394edd393e2fbd6414a8208fea363a22803b45"}, + {file = "zstandard-0.22.0-cp312-cp312-win32.whl", hash = "sha256:88c5b4b47a8a138338a07fc94e2ba3b1535f69247670abfe422de4e0b344aae2"}, + {file = "zstandard-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:de20a212ef3d00d609d0b22eb7cc798d5a69035e81839f549b538eff4105d01c"}, + {file = "zstandard-0.22.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d75f693bb4e92c335e0645e8845e553cd09dc91616412d1d4650da835b5449df"}, + {file = "zstandard-0.22.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:36a47636c3de227cd765e25a21dc5dace00539b82ddd99ee36abae38178eff9e"}, + {file = "zstandard-0.22.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68953dc84b244b053c0d5f137a21ae8287ecf51b20872eccf8eaac0302d3e3b0"}, + {file = "zstandard-0.22.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2612e9bb4977381184bb2463150336d0f7e014d6bb5d4a370f9a372d21916f69"}, + {file = "zstandard-0.22.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23d2b3c2b8e7e5a6cb7922f7c27d73a9a615f0a5ab5d0e03dd533c477de23004"}, + {file = "zstandard-0.22.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d43501f5f31e22baf822720d82b5547f8a08f5386a883b32584a185675c8fbf"}, + {file = "zstandard-0.22.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a493d470183ee620a3df1e6e55b3e4de8143c0ba1b16f3ded83208ea8ddfd91d"}, + {file = "zstandard-0.22.0-cp38-cp38-win32.whl", hash = "sha256:7034d381789f45576ec3f1fa0e15d741828146439228dc3f7c59856c5bcd3292"}, + {file = "zstandard-0.22.0-cp38-cp38-win_amd64.whl", hash = "sha256:d8fff0f0c1d8bc5d866762ae95bd99d53282337af1be9dc0d88506b340e74b73"}, + {file = "zstandard-0.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2fdd53b806786bd6112d97c1f1e7841e5e4daa06810ab4b284026a1a0e484c0b"}, + {file = "zstandard-0.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:73a1d6bd01961e9fd447162e137ed949c01bdb830dfca487c4a14e9742dccc93"}, + {file = "zstandard-0.22.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9501f36fac6b875c124243a379267d879262480bf85b1dbda61f5ad4d01b75a3"}, + {file = "zstandard-0.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48f260e4c7294ef275744210a4010f116048e0c95857befb7462e033f09442fe"}, + {file = "zstandard-0.22.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959665072bd60f45c5b6b5d711f15bdefc9849dd5da9fb6c873e35f5d34d8cfb"}, + {file = "zstandard-0.22.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d22fdef58976457c65e2796e6730a3ea4a254f3ba83777ecfc8592ff8d77d303"}, + {file = "zstandard-0.22.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a7ccf5825fd71d4542c8ab28d4d482aace885f5ebe4b40faaa290eed8e095a4c"}, + {file = "zstandard-0.22.0-cp39-cp39-win32.whl", hash = "sha256:f058a77ef0ece4e210bb0450e68408d4223f728b109764676e1a13537d056bb0"}, + {file = "zstandard-0.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:e9e9d4e2e336c529d4c435baad846a181e39a982f823f7e4495ec0b0ec8538d2"}, + {file = "zstandard-0.22.0.tar.gz", hash = "sha256:8226a33c542bcb54cd6bd0a366067b610b41713b64c9abec1bc4533d69f51e70"}, ] [package.dependencies] @@ -1419,4 +1178,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "b577b177644e57cd3e1938968951a0641359a38ebac26f5a89e7bea0ed94ff5b" +content-hash = "0b648f9634063e12553534296b8879b0250d4e9c63bb4aa47724d8b33198f90b" diff --git a/pyproject.toml b/pyproject.toml index 40314de8..502d9cf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,26 +14,24 @@ repository = "https://github.com/Aspect1103/Hades" [tool.poetry.dependencies] python = "^3.10" -arcade = "3.0.0.dev24" -pymunk = "6.4.0" -shapely = "2.0.1" +arcade = "3.0.0.dev25" [tool.poetry.dev-dependencies] -black = "23.7.0" -mypy = "1.5.0" -nuitka = "1.7.10" +black = "23.11.0" +mypy = "1.7.0" +nuitka = "1.8.6" ordered-set = "4.1.0" -pre-commit = "3.3.3" -pylint = "2.17.5" -pytest = "7.4.0" +pre-commit = "3.5.0" +pylint = "3.0.2" +pytest = "7.4.3" pytest-cov = "4.1.0" -pytest-icdiff = "0.6" +pytest-icdiff = "0.8" pytest-instafail = "0.5.0" -pytest-randomly = "3.13.0" +pytest-randomly = "3.15.0" pytest-sugar = "0.9.7" -ruff = "0.0.284" +ruff = "0.1.6" ssort = "v0.11.6" -tox = "4.8.0" +tox = "4.11.3" [tool.black] preview = true @@ -104,7 +102,7 @@ load-plugins = [ "pylint.extensions.consider_refactoring_into_while_condition", "pylint.extensions.consider_ternary_expression", # Disallow spreading multiple assignment statements across if/else blocks "pylint.extensions.dunder", # Disallow mispelled dunder methods - "pylint.extensions.emptystring", # Disallow comparisons to empty strings +# "pylint.extensions.emptystring", # Disallow comparisons to empty strings "pylint.extensions.eq_without_hash", # Disallow implementing __eq__ without __hash__ "pylint.extensions.for_any_all", # Disallow using a for loop to check for a condition and return a bool "pylint.extensions.magic_value", # Disallow using magic values @@ -115,6 +113,7 @@ load-plugins = [ "pylint.extensions.redefined_variable_type", # Disallow redefining variable types "pylint.extensions.set_membership", # Disallow not using sets for membership tests "pylint.extensions.typing", # Disallow bad typing + "pylint.extensions.code_style", # Disallow code that reduces code consistency ] [tool.pylint."MESSAGES CONTROL"] @@ -124,6 +123,7 @@ enable = [ "file-ignored", # Disallow messages to inform that files won't be checked "use-symbolic-message-instead", # Disallow messages enabled/disabled by ID "useless-suppression", # Disallow disabled messages when they aren't triggered + "prefer-typing-namedtuple", # Enforce typing.NamedTuple over collections.namedtuple ] include-naming-hint = true disable = [ @@ -155,8 +155,9 @@ extend-exclude = [ extend-ignore = [ "S311", # Allow use of the random library ] -format = "grouped" +output-format = "grouped" line-length = 88 +preview = true select = [ "A", # Enable flake8-builtins "ANN", # Enable flake8-annotations @@ -218,7 +219,7 @@ src = ["src"] "S607", # Ignore flake8-bandit start process with a partial path rule "T201", # Ignore flake8-print print rule ] -"hades_extensions/__init__.pyi" = [ +"hades_extensions/*.pyi" = [ "D100", # Ignore pydocstyle module docstring rule "D101", # Ignore pydocstyle class docstring rule "D103", # Ignore pydocstyle method docstring rule @@ -246,7 +247,9 @@ python = [testenv] passenv = * deps = pytest-cov -commands = pytest --cov-append +commands = + python -m build -c + pytest --cov-append [testenv:clean] deps = pytest-cov diff --git a/src/hades/__init__.py b/src/hades/__init__.py index e87745de..8eccb64b 100644 --- a/src/hades/__init__.py +++ b/src/hades/__init__.py @@ -1,4 +1,5 @@ """Stores all the functionality which creates the game and makes it playable.""" + from __future__ import annotations # Builtin diff --git a/src/hades/__main__.py b/src/hades/__main__.py index fc50293f..e97952c8 100644 --- a/src/hades/__main__.py +++ b/src/hades/__main__.py @@ -1,4 +1,5 @@ """Allows the game to be run from the command line.""" + from __future__ import annotations # Custom diff --git a/src/hades/constants.py b/src/hades/constants.py index 0d41a96f..30667e81 100644 --- a/src/hades/constants.py +++ b/src/hades/constants.py @@ -1,4 +1,5 @@ """Stores constants relating to the game and its functionality.""" + from __future__ import annotations # Builtin @@ -8,6 +9,9 @@ from pathlib import Path from typing import Final +# Custom +from hades_extensions.game_objects import SPRITE_SIZE + __all__ = ( "ARMOUR_REGEN_AMOUNT", "ATTACK_COOLDOWN", @@ -33,8 +37,6 @@ "OBSTACLE_AVOIDANCE_ANGLE", "PATH_POINT_RADIUS", "SLOWING_RADIUS", - "SPRITE_SCALE", - "SPRITE_SIZE", "TARGET_DISTANCE", "TOTAL_ENEMY_COUNT", "WANDER_CIRCLE_DISTANCE", @@ -102,10 +104,6 @@ class GameObjectType(Enum): }, } -# Sprite sizes -SPRITE_SCALE: Final = 0.5 -SPRITE_SIZE: Final = 128 * SPRITE_SCALE - # Attack constants ATTACK_COOLDOWN: Final = 3 ATTACK_RANGE: Final = 3 * SPRITE_SIZE diff --git a/src/hades/game_objects/constructors.py b/src/hades/constructors.py similarity index 54% rename from src/hades/game_objects/constructors.py rename to src/hades/constructors.py index e6e4c673..4c84aa7c 100644 --- a/src/hades/game_objects/constructors.py +++ b/src/hades/constructors.py @@ -1,4 +1,5 @@ """Stores all the constructors used to make the game objects.""" + from __future__ import annotations # Builtin @@ -6,20 +7,20 @@ # Custom from hades.constants import GameObjectType -from hades.game_objects.attributes import MovementForce -from hades.game_objects.base import ( - ComponentType, - SteeringBehaviours, - SteeringMovementState, -) -from hades.game_objects.components import Footprint, Inventory -from hades.game_objects.movements import KeyboardMovement, SteeringMovement from hades.textures import TextureType +from hades_extensions.game_objects import SteeringBehaviours, SteeringMovementState +from hades_extensions.game_objects.components import ( + Footprints, + Inventory, + KeyboardMovement, + MovementForce, + SteeringMovement, +) if TYPE_CHECKING: from arcade import Texture - from hades.game_objects.base import ComponentData, GameObjectComponent + from hades_extensions.game_objects import ComponentBase __all__ = ( "ENEMY", @@ -45,18 +46,16 @@ class GameObjectConstructor(NamedTuple): game_object_type: The type of this game object. game_object_textures: The collection of textures which relate to this game object. - components: A list of component types that are part of this game object. - component_data: The data for the components. + components: A list of components that are part of this game object. blocking: Whether the game object blocks sprite movement or not. - physics: Whether the game object should have a physics object or not. + kinematic: Whether the game object should have a kinematic object or not. """ game_object_type: GameObjectType game_object_textures: GameObjectTextures - components: ClassVar[list[type[GameObjectComponent]]] = [] - component_data: ClassVar[ComponentData] = {} + components: ClassVar[list[ComponentBase]] = [] blocking: bool = False - physics: bool = False + kinematic: bool = False # Static tiles @@ -74,31 +73,33 @@ class GameObjectConstructor(NamedTuple): PLAYER: Final = GameObjectConstructor( GameObjectType.PLAYER, GameObjectTextures(TextureType.PLAYER_IDLE.value[0]), - components=[Inventory, MovementForce, KeyboardMovement, Footprint], - component_data={ - "attributes": {ComponentType.MOVEMENT_FORCE: (5000, 5)}, - "inventory_size": (6, 5), - }, - physics=True, + components=[ + Inventory(6, 5), + MovementForce(5000, 5), + KeyboardMovement(), + Footprints(), + ], + kinematic=True, ) # Enemy characters ENEMY: Final = GameObjectConstructor( GameObjectType.ENEMY, GameObjectTextures(TextureType.ENEMY_IDLE.value[0]), - components=[MovementForce, SteeringMovement], - component_data={ - "attributes": {ComponentType.MOVEMENT_FORCE: (1000, 5)}, - "steering_behaviours": { - SteeringMovementState.DEFAULT: [ - SteeringBehaviours.OBSTACLE_AVOIDANCE, - SteeringBehaviours.WANDER, - ], - SteeringMovementState.FOOTPRINT: [SteeringBehaviours.FOLLOW_PATH], - SteeringMovementState.TARGET: [SteeringBehaviours.PURSUIT], - }, - }, - physics=True, + components=[ + MovementForce(1000, 5), + SteeringMovement( + { + SteeringMovementState.Default: [ + SteeringBehaviours.ObstacleAvoidance, + SteeringBehaviours.Wander, + ], + SteeringMovementState.Footprint: [SteeringBehaviours.FollowPath], + SteeringMovementState.Target: [SteeringBehaviours.Pursue], + }, + ), + ], + kinematic=True, ) # Potion tiles @@ -106,3 +107,5 @@ class GameObjectConstructor(NamedTuple): GameObjectType.POTION, GameObjectTextures(TextureType.HEALTH_POTION.value), ) + +# TODO: This file needs redoing diff --git a/src/hades/game_objects/__init__.py b/src/hades/game_objects/__init__.py deleted file mode 100644 index 79bdb74c..00000000 --- a/src/hades/game_objects/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Contains the functionality which manages the game objects.""" -from __future__ import annotations diff --git a/src/hades/game_objects/attacks.py b/src/hades/game_objects/attacks.py deleted file mode 100644 index e99b5f52..00000000 --- a/src/hades/game_objects/attacks.py +++ /dev/null @@ -1,187 +0,0 @@ -"""Manages the different attack algorithms available.""" -from __future__ import annotations - -# Builtin -import math -from typing import TYPE_CHECKING, TypedDict - -# Custom -from hades.constants import ( - ATTACK_RANGE, - BULLET_VELOCITY, - DAMAGE, - MELEE_ATTACK_OFFSET_LOWER, - MELEE_ATTACK_OFFSET_UPPER, -) -from hades.game_objects.attributes import deal_damage -from hades.game_objects.base import ( - AttackAlgorithms, - ComponentType, - GameObjectComponent, - Vec2d, -) - -if TYPE_CHECKING: - from collections.abc import Sequence - - from hades.game_objects.base import ComponentData - from hades.game_objects.movements import PhysicsObject - from hades.game_objects.system import ECS - -__all__ = ("Attacks", "AttackResult") - - -class AttackResult(TypedDict, total=False): - """Holds the result of an attack.""" - - ranged_attack: tuple[Vec2d, float, float] - - -class Attacks(GameObjectComponent): - """Allows a game object to attack other game objects.""" - - __slots__ = ("_attacks", "_attack_state", "_steering_owner") - - # Class variables - component_type: ComponentType = ComponentType.ATTACKS - - def __init__( - self: Attacks, - game_object_id: int, - system: ECS, - component_data: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - component_data: The data for the components. - """ - super().__init__(game_object_id, system, component_data) - self._attacks: Sequence[AttackAlgorithms] = component_data["enabled_attacks"] - self._attack_state: int = 0 - self._steering_owner: PhysicsObject = ( - self.system.get_physics_object_for_game_object(self.game_object_id) - ) - - @property - def attack_state(self: Attacks) -> int: - """Get the index of the current attack algorithm. - - Returns: - The index of the current attack algorithm. - """ - return self._attack_state - - def _area_of_effect_attack(self: Attacks, targets: list[int]) -> None: - """Perform an area of effect attack around the game object. - - Args: - targets: The targets to attack. - """ - # Find all targets that are within range and attack them - for target in targets: - if ( - self._steering_owner.position.get_distance_to( - self.system.get_physics_object_for_game_object(target).position, - ) - <= ATTACK_RANGE - ): - deal_damage(target, self.system, DAMAGE) - - def _melee_attack(self: Attacks, targets: list[int]) -> None: - """Perform a melee attack in the direction the game object is facing. - - Args: - targets: The targets to attack. - """ - # Calculate a vector that is perpendicular to the current rotation of the game - # object - physics_object = self.system.get_physics_object_for_game_object( - self.game_object_id, - ) - rotation = Vec2d( - math.sin(math.radians(physics_object.rotation)), - math.cos(math.radians(physics_object.rotation)), - ) - - # Find all targets that can be attacked - for target in targets: - # Calculate the angle between the current rotation of the game object and - # the direction the target is in - target_position = self.system.get_physics_object_for_game_object( - target, - ).position - theta = (target_position - physics_object.position).get_angle_between( - rotation, - ) - - # Test if the target is within range and within the circle's sector - if ( - physics_object.position.get_distance_to(target_position) <= ATTACK_RANGE - and theta <= MELEE_ATTACK_OFFSET_LOWER - or theta >= MELEE_ATTACK_OFFSET_UPPER - ): - deal_damage(target, self.system, DAMAGE) - - def _ranged_attack(self: Attacks) -> AttackResult: - """Perform a ranged attack in the direction the game object is facing. - - Returns: - The result of the attack. - """ - # Calculate the bullet's angle of rotation - physics_object = self.system.get_physics_object_for_game_object( - self.game_object_id, - ) - angle_radians = physics_object.rotation * math.pi / 180 - - # Return the result of the attack - return { - "ranged_attack": ( - physics_object.position, - math.cos(angle_radians) * BULLET_VELOCITY, - math.sin(angle_radians) * BULLET_VELOCITY, - ), - } - - def do_attack(self: Attacks, targets: list[int]) -> AttackResult: - """Perform the currently selected attack algorithm. - - Args: - targets: The targets to attack. - - Returns: - The result of the attack. - """ - # Perform the attack on the targets - match self._attacks[self._attack_state]: - case AttackAlgorithms.AREA_OF_EFFECT_ATTACK: - self._area_of_effect_attack(targets) - case AttackAlgorithms.MELEE_ATTACK: - self._melee_attack(targets) - case AttackAlgorithms.RANGED_ATTACK: - return self._ranged_attack() - case _: # pragma: no cover - # This should never happen as all attacks are covered above - raise ValueError - - # Return an empty result as no ranged attack was performed - return {} - - def previous_attack(self: Attacks) -> None: - """Select the previous attack algorithm.""" - self._attack_state = max(self._attack_state - 1, 0) - - def next_attack(self: Attacks) -> None: - """Select the next attack algorithm.""" - self._attack_state = min(self._attack_state + 1, len(self._attacks) - 1) - - def __repr__(self: Attacks) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return f"" diff --git a/src/hades/game_objects/attributes.py b/src/hades/game_objects/attributes.py deleted file mode 100644 index 83442e5a..00000000 --- a/src/hades/game_objects/attributes.py +++ /dev/null @@ -1,393 +0,0 @@ -"""Manages the different game object attributes available.""" -from __future__ import annotations - -# Builtin -from dataclasses import dataclass, field -from typing import TYPE_CHECKING, TypeVar, cast - -# Custom -from hades.game_objects.base import ComponentData, ComponentType, GameObjectComponent - -if TYPE_CHECKING: - from collections.abc import Callable - - from hades.game_objects.system import ECS - -__all__ = ( - "Armour", - "ArmourRegenCooldown", - "GameObjectAttributeBase", - "GameObjectAttributeError", - "FireRatePenalty", - "Health", - "Money", - "deal_damage", - "MovementForce", - "ViewDistance", -) - - -# Define a generic type for the keyword arguments -KW = TypeVar("KW") - - -class GameObjectAttributeError(Exception): - """Raised when there is an error with a game object attribute.""" - - def __init__(self: GameObjectAttributeError, *, name: str, error: str) -> None: - """Initialise the object. - - Args: - name: The name of the game object attribute. - error: The problem raised by the game object attribute. - """ - super().__init__(f"The game object attribute `{name}` cannot {error}.") - - -@dataclass(slots=True) -class StatusEffect: - """Represents a status effect that can be applied to a game object attribute. - - value: The value that should be applied to the game object temporarily. - duration: The duration the status effect should be applied for. - original: The original value of the game object attribute which is being changed. - original_max_value: The original maximum value of the game object attribute which is - being changed. - time_counter: The time counter for the status effect. - """ - - value: float - duration: float - original_value: float - original_max_value: float - time_counter: float = field(init=False) - - def __post_init__(self: StatusEffect) -> None: - """Initialise the object after its initial initialisation.""" - self.time_counter = 0 - - -class GameObjectAttributeBase(GameObjectComponent): - """The base class for all game object attributes.""" - - __slots__ = ( - "_level_limit", - "_value", - "_max_value", - "_applied_status_effect", - "_current_level", - ) - - # Class variables - instant_effect: bool = True - maximum: bool = True - status_effect: bool = True - upgradable: bool = True - - def __init__( - self: GameObjectAttributeBase, - game_object_id: int, - system: ECS, - component_data: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - component_data: The data for the components. - """ - super().__init__(game_object_id, system, component_data) - initial_value, self._level_limit = component_data["attributes"][ - self.component_type - ] - self._value: float = initial_value - self._max_value: float = initial_value if self.maximum else float("inf") - self._current_level: int = 0 - self._applied_status_effect: StatusEffect | None = None - - @property - def value(self: GameObjectAttributeBase) -> float: - """Get the game object attribute's value. - - Returns: - The game object attribute's value. - """ - return self._value - - @value.setter - def value(self: GameObjectAttributeBase, new_value: float) -> None: - """Set the game object attribute's value. - - Args: - new_value: The new game object attribute's value. - """ - self._value = max(min(new_value, self._max_value), 0) - - @property - def max_value(self: GameObjectAttributeBase) -> float: - """Get the attribute's max value. - - If this is infinity, then the attribute does not have a maximum value - - Returns: - The attribute's max value. - """ - return self._max_value - - @property - def current_level(self: GameObjectAttributeBase) -> int: - """Get the attribute's current level. - - Returns: - The attribute's current level. - """ - return self._current_level - - @property - def level_limit(self: GameObjectAttributeBase) -> int: - """Get the attribute's level limit. - - If this is -1, then the attribute is not upgradable. - - Returns: - The attribute's level limit. - """ - return self._level_limit - - @property - def applied_status_effect(self: GameObjectAttributeBase) -> StatusEffect | None: - """Get the currently applied status effect. - - Returns: - The currently applied status effect. - """ - return self._applied_status_effect - - def upgrade( - self: GameObjectAttributeBase, - increase: Callable[[int], float], - ) -> bool: - """Upgrade the game object attribute to the next level if possible. - - Args: - increase: The exponential lambda function which calculates the next level's - value based on the current level. - - Returns: - Whether the attribute upgrade was successful or not. - - Raises: - GameObjectAttributeError: The game object attribute cannot be upgraded. - """ - # Check if the attribute can be upgraded - if not self.upgradable: - raise GameObjectAttributeError( - name=self.__class__.__name__, - error="be upgraded", - ) - - # Check if the current level is below the level limit - if self.current_level >= self.level_limit: - return False - - # Upgrade the attribute based on the difference between the current level and - # the next - diff = increase(self.current_level + 1) - increase(self.current_level) - self._max_value += diff - self._current_level += 1 - self.value += diff - return True - - def apply_instant_effect( - self: GameObjectAttributeBase, - increase: Callable[[int], float], - level: int, - ) -> bool: - """Apply an instant effect to the game object attribute if possible. - - Args: - increase: The exponential lambda function which calculates the next level's - value based on the current level. - level: The level to initialise the instant effect at. - - Returns: - Whether the instant effect could be applied or not. - - Raises: - GameObjectAttributeError: The game object attribute cannot have an instant - effect. - """ - # Check if the attribute can have an instant effect - if not self.instant_effect: - raise GameObjectAttributeError( - name=self.__class__.__name__, - error="have an instant effect", - ) - - # Check if the attribute's value is already at max - if self.value == self.max_value: - return False - - # Add the instant effect to the attribute - self.value += increase(level) - return True - - def apply_status_effect( - self: GameObjectAttributeBase, - increase: Callable[[int], float], - duration: Callable[[int], float], - level: int, - ) -> bool: - """Apply a status effect to the attribute if possible. - - Args: - increase: The exponential lambda function which calculates the next level's - value based on the current level. - duration: The exponential lambda function which calculates the next level's - duration based on the current level. - level: The level to initialise the status effect at. - - Returns: - Whether the status effect could be applied or not. - - Raises: - GameObjectAttributeError: The game object attribute cannot have a status - effect. - """ - # Check if the attribute can have a status effect - if not self.status_effect: - raise GameObjectAttributeError( - name=self.__class__.__name__, - error="have a status effect", - ) - - # Check if the attribute already has a status effect applied - if self.applied_status_effect: - return False - - # Apply the status effect to this attribute - self._applied_status_effect = StatusEffect( - increase(level), - duration(level), - self.value, - self.max_value, - ) - self._value += self._applied_status_effect.value - self._max_value += self._applied_status_effect.value - return True - - def on_update(self: GameObjectAttributeBase, delta_time: float) -> None: - """Process game object attribute update logic. - - Args: - delta_time: Time interval since the last time the function was called. - """ - # Update the status effect if one exists - if self.applied_status_effect: - self.applied_status_effect.time_counter += delta_time - if ( - self.applied_status_effect.time_counter - >= self.applied_status_effect.duration - ): - self.value = min(self.value, self.applied_status_effect.original_value) - self._max_value = self.applied_status_effect.original_max_value - self._applied_status_effect = None - - def __repr__(self: GameObjectAttributeBase) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return ( - f"<{self.__class__.__name__} (Value={self.value}) (Max" - f" value={self.max_value}) (Level={self.current_level}/{self.level_limit})>" - ) - - -class Armour(GameObjectAttributeBase): - """Allows a game object to have an armour attribute.""" - - # Class variables - component_type: ComponentType = ComponentType.ARMOUR - - -class Health(GameObjectAttributeBase): - """Allows a game object to have a health attribute.""" - - # Class variables - component_type: ComponentType = ComponentType.HEALTH - - -class ArmourRegenCooldown(GameObjectAttributeBase): - """Allows a game object to have an armour regen cooldown attribute.""" - - # Class variables - component_type: ComponentType = ComponentType.ARMOUR_REGEN_COOLDOWN - instant_effect: bool = False - maximum: bool = False - - -class FireRatePenalty(GameObjectAttributeBase): - """Allows a game object to have a fire rate penalty attribute.""" - - # Class variables - component_type: ComponentType = ComponentType.FIRE_RATE_PENALTY - instant_effect: bool = False - maximum: bool = False - - -class Money(GameObjectAttributeBase): - """Allows a game object to have a money attribute.""" - - # Class variables - component_type: ComponentType = ComponentType.MONEY - instant_effect: bool = False - maximum: bool = False - status_effect: bool = False - upgradable: bool = False - - -class MovementForce(GameObjectAttributeBase): - """Allows a game object to have a movement force attribute.""" - - # Class variables - component_type: ComponentType = ComponentType.MOVEMENT_FORCE - instant_effect: bool = False - maximum: bool = False - - -class ViewDistance(GameObjectAttributeBase): - """Allows a game object to have a view distance attribute.""" - - # Class variables - component_type: ComponentType = ComponentType.VIEW_DISTANCE - instant_effect: bool = False - maximum: bool = False - - -def deal_damage(game_object_id: int, system: ECS, damage: int) -> None: - """Deal damage to the game object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - damage: The damage that should be dealt to the game object. - """ - # Damage the armour and carry over the extra damage to the health - health, armour = cast( - Health, - system.get_component_for_game_object( - game_object_id, - ComponentType.HEALTH, - ), - ), cast( - Armour, - system.get_component_for_game_object( - game_object_id, - ComponentType.ARMOUR, - ), - ) - health.value -= max(damage - armour.value, 0) - armour.value -= damage diff --git a/src/hades/game_objects/base.py b/src/hades/game_objects/base.py deleted file mode 100644 index 849c5ed8..00000000 --- a/src/hades/game_objects/base.py +++ /dev/null @@ -1,262 +0,0 @@ -"""Stores the foundations for the entity component system and its functionality.""" -from __future__ import annotations - -# Builtin -import math -from enum import Enum, auto -from typing import TYPE_CHECKING, NamedTuple, TypedDict - -if TYPE_CHECKING: - from collections.abc import Callable, Mapping, Sequence - from collections.abc import Set as AbstractSet - - from hades.game_objects.system import ECS - -__all__ = ( - "AttackAlgorithms", - "GameObjectAttributeSectionType", - "ComponentData", - "ComponentType", - "GameObjectComponent", - "SteeringBehaviours", - "SteeringMovementState", - "Vec2d", -) - - -class AttackAlgorithms(Enum): - """Stores the different types of attack algorithms available.""" - - AREA_OF_EFFECT_ATTACK = auto() - MELEE_ATTACK = auto() - RANGED_ATTACK = auto() - - -class ComponentType(Enum): - """Stores the different types of components available.""" - - ARMOUR = auto() - ARMOUR_REGEN = auto() - ARMOUR_REGEN_COOLDOWN = auto() - ATTACKS = auto() - FIRE_RATE_PENALTY = auto() - FOOTPRINT = auto() - HEALTH = auto() - INSTANT_EFFECTS = auto() - INVENTORY = auto() - MONEY = auto() - MOVEMENTS = auto() - MOVEMENT_FORCE = auto() - STATUS_EFFECTS = auto() - VIEW_DISTANCE = auto() - - -class GameObjectAttributeSectionType(Enum): - """Stores the sections which group game object attributes together.""" - - ENDURANCE: AbstractSet[ComponentType] = { - ComponentType.HEALTH, - ComponentType.MOVEMENT_FORCE, - } - DEFENCE: AbstractSet[ComponentType] = { - ComponentType.ARMOUR, - ComponentType.ARMOUR_REGEN_COOLDOWN, - } - - -class SteeringBehaviours(Enum): - """Stores the different types of steering behaviours available.""" - - ARRIVE = auto() - EVADE = auto() - FLEE = auto() - FOLLOW_PATH = auto() - OBSTACLE_AVOIDANCE = auto() - PURSUIT = auto() - SEEK = auto() - WANDER = auto() - - -class SteeringMovementState(Enum): - """Stores the different states the steering movement component can be in.""" - - DEFAULT = auto() - FOOTPRINT = auto() - TARGET = auto() - - -class ComponentData(TypedDict, total=False): - """Holds the data needed to initialise the components. - - attributes: The data for the game object attributes. - enabled_attacks: The attacks which are enabled for the game object. - steering_behaviours: The steering behaviours to use. - instant_effects: The instant effects that this game object can apply. - inventory_size: The size of the game object's inventory. - status_effects: The status effects that this game object can apply. - """ - - attributes: Mapping[ComponentType, tuple[int, int]] - enabled_attacks: Sequence[AttackAlgorithms] - steering_behaviours: Mapping[SteeringMovementState, Sequence[SteeringBehaviours]] - instant_effects: tuple[int, Mapping[ComponentType, Callable[[int], float]]] - inventory_size: tuple[int, int] - status_effects: tuple[ - int, - Mapping[ComponentType, tuple[Callable[[int], float], Callable[[int], float]]], - ] - - -class GameObjectComponent: - """The base class for all game object components.""" - - __slots__ = ("game_object_id", "system") - - # Class variables - component_type: ComponentType - - def __init__( - self: GameObjectComponent, - game_object_id: int, - system: ECS, - _: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - """ - self.game_object_id: int = game_object_id - self.system: ECS = system - - def on_update(self: GameObjectComponent, delta_time: float) -> None: - """Process update logic. - - Args: - delta_time: Time interval since the last time the function was called. - """ - - -class Vec2d(NamedTuple): - """Represents a 2D vector. - - Attributes: - x: The x value of the vector. - y: The y value of the vector. - """ - - x: float - y: float - - def normalised(self: Vec2d) -> Vec2d: - """Normalise the vector. - - Returns: - The normalised vector. - """ - if magnitude := abs(self): - return Vec2d(self.x / magnitude, self.y / magnitude) - return Vec2d(0, 0) - - def rotated(self: Vec2d, angle: float) -> Vec2d: - """Rotate the vector by an angle. - - Args: - angle: The angle to rotate the vector by in radians. - - Returns: - The rotated vector. - """ - sine, cosine = math.sin(angle), math.cos(angle) - return Vec2d( - self.x * cosine - self.y * sine, - self.x * sine + self.y * cosine, - ) - - def get_angle_between(self: Vec2d, other: Vec2d) -> float: - """Get the angle between this vector and another vector. - - This will always be between 0 and 2Ï€. - - Args: - other: The vector to get the angle to. - - Returns: - The angle between this vector and the other vector. - """ - return math.atan2( - self.x * other.y - self.y * other.x, - self.x * other.x + self.y * other.y, - ) % (2 * math.pi) - - def get_distance_to(self: Vec2d, other: Vec2d) -> float: - """Get the distance to another vector. - - Args: - other: The vector to get the distance to. - - Returns: - The distance to the other vector. - """ - return abs(self - other) - - def __add__(self: Vec2d, other: Vec2d) -> Vec2d: - """Add another vector to this vector. - - Args: - other: The vector to add to this vector. - - Returns: - The result of the addition. - """ - return Vec2d(self.x + other.x, self.y + other.y) - - def __sub__(self: Vec2d, other: Vec2d) -> Vec2d: - """Subtract another vector from this vector. - - Args: - other: The vector to subtract from this vector. - - Returns: - The result of the subtraction. - """ - return Vec2d(self.x - other.x, self.y - other.y) - - def __mul__(self: Vec2d, other: float) -> Vec2d: - """Multiply the vector by a scalar. - - Args: - other: The scalar to multiply the vector by. - - Returns: - The result of the multiplication. - """ - return Vec2d(self.x * other, self.y * other) - - def __floordiv__(self: Vec2d, other: float) -> Vec2d: - """Divide the vector by a scalar. - - Args: - other: The scalar to divide the vector by. - - Returns: - The result of the division. - """ - return Vec2d(self.x // other, self.y // other) - - def __abs__(self: Vec2d) -> float: - """Return the absolute value of the vector. - - Returns: - The absolute value of the vector. - """ - return math.sqrt(self.x**2 + self.y**2) - - def __repr__(self: Vec2d) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return f"" diff --git a/src/hades/game_objects/components.py b/src/hades/game_objects/components.py deleted file mode 100644 index fd9915b5..00000000 --- a/src/hades/game_objects/components.py +++ /dev/null @@ -1,327 +0,0 @@ -"""Manages various components available to the game objects.""" -from __future__ import annotations - -# Builtin -from typing import TYPE_CHECKING, Generic, TypeVar, cast - -# Custom -from hades.constants import ARMOUR_REGEN_AMOUNT, FOOTPRINT_INTERVAL, FOOTPRINT_LIMIT -from hades.game_objects.attributes import Armour, ArmourRegenCooldown -from hades.game_objects.base import ComponentType, GameObjectComponent -from hades.game_objects.movements import MovementBase, SteeringMovement - -if TYPE_CHECKING: - from hades.game_objects.base import ComponentData, Vec2d - from hades.game_objects.movements import PhysicsObject - from hades.game_objects.system import ECS - -__all__ = ( - "ArmourRegen", - "Footprint", - "InstantEffects", - "Inventory", - "InventorySpaceError", - "StatusEffects", -) - -# Define a generic type for the inventory -T = TypeVar("T") - - -class InventorySpaceError(Exception): - """Raised when there is a space problem with the inventory.""" - - def __init__(self: InventorySpaceError, *, full: bool) -> None: - """Initialise the object. - - Args: - full: Whether the inventory is empty or full. - """ - super().__init__(f"The inventory is {'full' if full else 'empty'}.") - - -class ArmourRegen(GameObjectComponent): - """Allows a game object to regenerate armour. - - Attributes: - armour: The game object's armour component. - armour_regen_cooldown: The game object's armour regen cooldown component. - time_since_armour_regen: The time since the game object last regenerated armour. - """ - - __slots__ = ("armour", "armour_regen_cooldown", "time_since_armour_regen") - - # Class variables - component_type: ComponentType = ComponentType.ARMOUR_REGEN - - def __init__( - self: ArmourRegen, - game_object_id: int, - system: ECS, - component_data: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - component_data: The data for the components. - """ - super().__init__(game_object_id, system, component_data) - self.armour: Armour = cast( - Armour, - self.system.get_component_for_game_object( - self.game_object_id, - ComponentType.ARMOUR, - ), - ) - self.armour_regen_cooldown: ArmourRegenCooldown = cast( - ArmourRegenCooldown, - self.system.get_component_for_game_object( - self.game_object_id, - ComponentType.ARMOUR_REGEN_COOLDOWN, - ), - ) - self.time_since_armour_regen: float = 0 - - def on_update(self: ArmourRegen, delta_time: float) -> None: - """Process armour regeneration update logic. - - Args: - delta_time: Time interval since the last time the function was called. - """ - self.time_since_armour_regen += delta_time - if self.time_since_armour_regen >= self.armour_regen_cooldown.value: - self.armour.value += ARMOUR_REGEN_AMOUNT - self.time_since_armour_regen = 0 - - def __repr__(self: ArmourRegen) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return f"" - - -class Footprint(GameObjectComponent): - """Allows a game object to periodically leave footprints around the game map. - - Attributes: - footprints: The footprints created by the game object. - physics_object: The physics object for the game object. - time_since_last_footprint: The time since the game object last left a footprint. - """ - - __slots__ = ("footprints", "physics_object", "time_since_last_footprint") - - # Class variables - component_type: ComponentType = ComponentType.FOOTPRINT - - def __init__( - self: Footprint, - game_object_id: int, - system: ECS, - component_data: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - component_data: The data for the components. - """ - super().__init__(game_object_id, system, component_data) - self.footprints: list[Vec2d] = [] - self.physics_object: PhysicsObject = ( - self.system.get_physics_object_for_game_object(self.game_object_id) - ) - self.time_since_last_footprint: float = 0 - - def on_update(self: Footprint, delta_time: float) -> None: - """Process AI update logic. - - Args: - delta_time: Time interval since the last time the function was called. - """ - # Update the time since the last footprint then check if a new footprint should - # be created - self.time_since_last_footprint += delta_time - if self.time_since_last_footprint < FOOTPRINT_INTERVAL: - return - - # Reset the counter and create a new footprint making sure to only keep - # FOOTPRINT_LIMIT footprints - self.time_since_last_footprint = 0 - if len(self.footprints) >= FOOTPRINT_LIMIT: - self.footprints.pop(0) - self.footprints.append(self.physics_object.position) - - # Update the path list for all SteeringMovement components - for movement_component in self.system.get_components_for_component_type( - ComponentType.MOVEMENTS, - ): - if not cast(MovementBase, movement_component).is_player_controlled: - cast(SteeringMovement, movement_component).update_path_list( - self.footprints, - ) - - def __repr__(self: Footprint) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return ( - f"" - ) - - -class InstantEffects(GameObjectComponent): - """Allows a game object to provide instant effects. - - Attributes: - level_limit: The level limit of the instant effects. - instant_effects: The instant effects provided by the game object. - """ - - __slots__ = ("instant_effects", "level_limit") - - # Class variables - component_type: ComponentType = ComponentType.INSTANT_EFFECTS - - def __init__( - self: InstantEffects, - game_object_id: int, - system: ECS, - component_data: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - component_data: The data for the components. - """ - super().__init__(game_object_id, system, component_data) - self.level_limit, self.instant_effects = component_data["instant_effects"] - - def __repr__(self: InstantEffects) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return f"" - - -class Inventory(Generic[T], GameObjectComponent): - """Allows a game object to have a fixed size inventory. - - Attributes: - width: The width of the inventory. - height: The height of the inventory. - inventory: The game object's inventory. - """ - - __slots__ = ( - "width", - "height", - "inventory", - ) - - # Class variables - component_type: ComponentType = ComponentType.INVENTORY - - def __init__( - self: Inventory[T], - game_object_id: int, - system: ECS, - component_data: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - component_data: The data for the components. - """ - super().__init__(game_object_id, system, component_data) - self.width, self.height = component_data["inventory_size"] - self.inventory: list[T] = [] - - def add_item_to_inventory(self: Inventory[T], item: T) -> None: - """Add an item to the inventory. - - Args: - item: The item to add to the inventory. - - Raises: - InventorySpaceError: The inventory is full. - """ - if len(self.inventory) == self.width * self.height: - raise InventorySpaceError(full=True) - self.inventory.append(item) - - def remove_item_from_inventory(self: Inventory[T], index: int) -> T: - """Remove an item at a specific index. - - Args: - index: The index to remove an item at. - - Returns: - The item at position `index` in the inventory. - - Raises: - InventorySpaceError: The inventory is empty. - """ - if len(self.inventory) < index: - raise InventorySpaceError(full=False) - return self.inventory.pop(index) - - def __repr__(self: Inventory[T]) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return f"" - - -class StatusEffects(GameObjectComponent): - """Allows a game object to provide status effects. - - Attributes: - level_limit: The level limit of the status effects. - status_effects: The status effects provided by the game object. - """ - - __slots__ = ("level_limit", "status_effects") - - # Class variables - component_type: ComponentType = ComponentType.STATUS_EFFECTS - - def __init__( - self: StatusEffects, - game_object_id: int, - system: ECS, - component_data: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - component_data: The data for the components. - """ - super().__init__(game_object_id, system, component_data) - self.level_limit, self.status_effects = component_data["status_effects"] - - def __repr__(self: StatusEffects) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return f"" diff --git a/src/hades/game_objects/movements.py b/src/hades/game_objects/movements.py deleted file mode 100644 index f03dc852..00000000 --- a/src/hades/game_objects/movements.py +++ /dev/null @@ -1,544 +0,0 @@ -"""Manages the different movement algorithms available to the game objects.""" -from __future__ import annotations - -# Builtin -import math -import random -from abc import ABCMeta, abstractmethod -from dataclasses import dataclass -from typing import TYPE_CHECKING, cast - -# Custom -from hades.constants import ( - MAX_SEE_AHEAD, - MAX_VELOCITY, - OBSTACLE_AVOIDANCE_ANGLE, - PATH_POINT_RADIUS, - SLOWING_RADIUS, - SPRITE_SIZE, - TARGET_DISTANCE, - WANDER_CIRCLE_DISTANCE, - WANDER_CIRCLE_RADIUS, -) -from hades.game_objects.attributes import MovementForce -from hades.game_objects.base import ( - ComponentType, - GameObjectComponent, - SteeringBehaviours, - SteeringMovementState, - Vec2d, -) - -if TYPE_CHECKING: - from collections.abc import Mapping, Sequence - - from hades.game_objects.base import ComponentData - from hades.game_objects.system import ECS - -__all__ = ( - "KeyboardMovement", - "MovementBase", - "PhysicsObject", - "SteeringMovement", - "flee", - "seek", - "arrive", - "evade", - "follow_path", - "obstacle_avoidance", - "pursuit", - "wander", -) - - -@dataclass(slots=True) -class PhysicsObject: - """Stores various data about a game object for use in physics-related operations. - - position: The position of the game object. - velocity: The velocity of the game object. - rotation: The rotation of the game object. - """ - - position: Vec2d - velocity: Vec2d - rotation: float = 0 - - -def flee(current_position: Vec2d, target_position: Vec2d) -> Vec2d: - """Allow a game object to run away from another game object. - - Args: - current_position: The position of the game object. - target_position: The position of the target game object. - - Returns: - The new steering force from this behaviour. - """ - return (current_position - target_position).normalised() - - -def seek(current_position: Vec2d, target_position: Vec2d) -> Vec2d: - """Allow a game object to move towards another game object. - - Args: - current_position: The position of the game object. - target_position: The position of the target game object. - - Returns: - The new steering force from this behaviour. - """ - return (target_position - current_position).normalised() - - -def arrive(current_position: Vec2d, target_position: Vec2d) -> Vec2d: - """Allow a game object to move towards another game object and stand still. - - Args: - current_position: The position of the game object. - target_position: The position of the target game object. - - Returns: - The new steering force from this behaviour. - """ - # Calculate a vector to the target and its length - direction = target_position - current_position - - # Check if the game object is inside the slowing area - if abs(direction) < SLOWING_RADIUS: - return (direction * (abs(direction) / SLOWING_RADIUS)).normalised() - return direction.normalised() - - -def evade( - current_position: Vec2d, - target_position: Vec2d, - target_velocity: Vec2d, -) -> Vec2d: - """Allow a game object to flee from another game object's predicted position. - - Args: - current_position: The position of the game object. - target_position: The position of the target game object. - target_velocity: The velocity of the target game object. - - Returns: - The new steering force from this behaviour. - """ - # Calculate the future position of the target based on their distance and steer - # away from it. Higher distances will require more time to reach, so the future - # position will be further away - return flee( - current_position, - target_position - + target_velocity - * (target_position.get_distance_to(current_position) / MAX_VELOCITY), - ) - - -def follow_path(current_position: Vec2d, path_list: list[Vec2d]) -> Vec2d: - """Allow a game object to follow a pre-determined path. - - Args: - current_position: The position of the game object. - path_list: The list of points the game object should follow. - - Raises: - IndexError: The path list is empty. - - Returns: - The new steering force from this behaviour. - """ - if current_position.get_distance_to(path_list[0]) <= PATH_POINT_RADIUS: - path_list.append(path_list.pop(0)) - return seek(current_position, path_list[0]) - - -def obstacle_avoidance( - current_position: Vec2d, - current_velocity: Vec2d, - walls: set[tuple[int, int]], -) -> Vec2d: - """Allow a game object to avoid obstacles in its path. - - Returns: - The new steering force from this behaviour. - """ - - def _raycast(position: Vec2d, velocity: Vec2d, angle: float = 0) -> Vec2d: - """Cast a ray from the game object's position in the direction of its velocity. - - Args: - position: The position of the game object. - velocity: The velocity of the game object. - angle: The angle to rotate the velocity by in radians. - - Returns: - The point at which the ray collides with an obstacle. If this is -1, then - there is no collision. - """ - for point in ( - position + velocity.rotated(angle) * (step / 100) - for step in range(int(SPRITE_SIZE), int(MAX_SEE_AHEAD), int(SPRITE_SIZE)) - ): - if point // SPRITE_SIZE in walls: - return point - return Vec2d(-1, -1) - - # Check if the game object is going to collide with an obstacle - forward_ray = _raycast(current_position, current_velocity) - left_ray = _raycast( - current_position, - current_velocity, - OBSTACLE_AVOIDANCE_ANGLE, - ) - right_ray = _raycast( - current_position, - current_velocity, - -OBSTACLE_AVOIDANCE_ANGLE, - ) - - # Check if there are any obstacles ahead - if ( - forward_ray != Vec2d(-1, -1) - and left_ray != Vec2d(-1, -1) - and right_ray != Vec2d(-1, -1) - ): - # Turn around, there's a wall ahead - return flee(current_position, forward_ray) - if left_ray != Vec2d(-1, -1): - # Turn right, there's a wall left - return flee(current_position, left_ray) - if right_ray != Vec2d(-1, -1): - # Turn left, there's a wall right - return flee(current_position, right_ray) - - # No obstacles ahead, move forward - return Vec2d(0, 0) - - -def pursuit( - current_position: Vec2d, - target_position: Vec2d, - target_velocity: Vec2d, -) -> Vec2d: - """Allow a game object to seek towards another game object's predicted position. - - Args: - current_position: The position of the game object. - target_position: The position of the target game object. - target_velocity: The velocity of the target game object. - - Returns: - The new steering force from this behaviour. - """ - # Calculate the future position of the target based on their distance and steer - # towards it. Higher distances will require more time to reach, so the future - # position will be further away - return seek( - current_position, - target_position - + target_velocity - * (target_position.get_distance_to(current_position) / MAX_VELOCITY), - ) - - -def wander(current_velocity: Vec2d, displacement_angle: int) -> Vec2d: - """Allow a game object to move in a random direction for a short period of time. - - Args: - current_velocity: The velocity of the game object. - displacement_angle: The angle of the displacement force in degrees. - - Returns: - The new steering force from this behaviour. - """ - # Calculate the position of an invisible circle in front of the game object - circle_center = current_velocity.normalised() * WANDER_CIRCLE_DISTANCE - - # Add a displacement force to the centre of the circle to randomise the movement - return ( - circle_center - + (Vec2d(0, -1) * WANDER_CIRCLE_RADIUS).rotated( - math.radians(displacement_angle), - ) - ).normalised() - - -class MovementBase(GameObjectComponent, metaclass=ABCMeta): - """The base class for all movement algorithms. - - Attributes: - movement_force: The game object's movement force component. - """ - - __slots__ = ("movement_force",) - - # Class variables - component_type: ComponentType = ComponentType.MOVEMENTS - is_player_controlled: bool = False - - def __init__( - self: MovementBase, - game_object_id: int, - system: ECS, - component_data: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - component_data: The data for the components. - """ - super().__init__(game_object_id, system, component_data) - self.movement_force: MovementForce = cast( - MovementForce, - self.system.get_component_for_game_object( - self.game_object_id, - ComponentType.MOVEMENT_FORCE, - ), - ) - - @abstractmethod - def calculate_force(self: MovementBase) -> Vec2d: - """Calculate the new force to apply to the game object. - - Returns: - The new force to apply to the game object. - """ - - -class KeyboardMovement(MovementBase): - """Allows a game object's movement to be controlled by the keyboard. - - Attributes: - north_pressed: Whether the game object is moving north or not. - south_pressed: Whether the game object is moving south or not. - east_pressed: Whether the game object is moving east or not. - west_pressed: Whether the game object is moving west or not. - """ - - __slots__ = ( - "north_pressed", - "south_pressed", - "east_pressed", - "west_pressed", - ) - - # Class variables - is_player_controlled: bool = True - - def __init__( - self: KeyboardMovement, - game_object_id: int, - system: ECS, - _: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - """ - super().__init__(game_object_id, system, _) - self.north_pressed: bool = False - self.south_pressed: bool = False - self.east_pressed: bool = False - self.west_pressed: bool = False - - def calculate_force(self: KeyboardMovement) -> Vec2d: - """Calculate the new force to apply to the game object. - - Returns: - The new force to apply to the game object. - """ - return ( - Vec2d( - self.east_pressed - self.west_pressed, - self.north_pressed - self.south_pressed, - ) - * self.movement_force.value - ) - - def __repr__(self: KeyboardMovement) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return ( - f"" - ) - - -class SteeringMovement(MovementBase): - """Allows a game object's movement to be controlled by steering algorithms. - - Attributes: - target_id: The game object ID of the target. - walls: The list of wall positions in the game. - path_list: The list of points the game object should follow. - """ - - __slots__ = ( - "_behaviours", - "_movement_state", - "target_id", - "walls", - "path_list", - ) - - def __init__( - self: SteeringMovement, - game_object_id: int, - system: ECS, - component_data: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - component_data: The data for the components. - """ - super().__init__(game_object_id, system, component_data) - self._behaviours: Mapping[ - SteeringMovementState, - Sequence[SteeringBehaviours], - ] = component_data["steering_behaviours"] - self._movement_state: SteeringMovementState = SteeringMovementState.DEFAULT - self.target_id: int = -1 - self.walls: set[tuple[int, int]] = set() - self.path_list: list[Vec2d] = [] - - @property - def movement_state(self: SteeringMovement) -> SteeringMovementState: - """Get the current movement state of the game object. - - Returns: - The current movement state of the game object. - """ - return self._movement_state - - def calculate_force(self: SteeringMovement) -> Vec2d: - """Calculate the new force to apply to the game object. - - Returns: - The new force to apply to the game object. - """ - # Determine if the movement state should change or not - ( - current_physics, - target_physics, - ) = self.system.get_physics_object_for_game_object( - self.game_object_id, - ), self.system.get_physics_object_for_game_object( - self.target_id, - ) - if ( - current_physics.position.get_distance_to(target_physics.position) - <= TARGET_DISTANCE - ): - self._movement_state = SteeringMovementState.TARGET - elif self.path_list: - self._movement_state = SteeringMovementState.FOOTPRINT - else: - self._movement_state = SteeringMovementState.DEFAULT - - # Calculate the new force to apply to the game object - steering_force = Vec2d(0, 0) - for behaviour in self._behaviours.get(self._movement_state, []): - match behaviour: - case SteeringBehaviours.ARRIVE: - steering_force += arrive( - current_physics.position, - target_physics.position, - ) - case SteeringBehaviours.EVADE: - steering_force += evade( - current_physics.position, - target_physics.position, - target_physics.velocity, - ) - case SteeringBehaviours.FLEE: - steering_force += flee( - current_physics.position, - target_physics.position, - ) - case SteeringBehaviours.FOLLOW_PATH: - steering_force += follow_path( - current_physics.position, - self.path_list, - ) - case SteeringBehaviours.OBSTACLE_AVOIDANCE: - steering_force += obstacle_avoidance( - current_physics.position, - current_physics.velocity, - self.walls, - ) - case SteeringBehaviours.PURSUIT: - steering_force += pursuit( - current_physics.position, - target_physics.position, - target_physics.velocity, - ) - case SteeringBehaviours.SEEK: - steering_force += seek( - current_physics.position, - target_physics.position, - ) - case SteeringBehaviours.WANDER: - steering_force += wander( - current_physics.velocity, - random.randint(0, 360), - ) - case _: # pragma: no cover - # This should never happen as all behaviours are covered above - raise ValueError - return steering_force.normalised() * self.movement_force.value - - def update_path_list( - self: SteeringMovement, - footprints: list[Vec2d], - ) -> None: - """Update the path list for the game object to follow. - - Args: - footprints: The list of footprints to follow. - """ - # Get the closest footprint to the target and test if one exists - current_position = self.system.get_physics_object_for_game_object( - self.game_object_id, - ).position - closest_footprints = [ - footprint - for footprint in footprints - if current_position.get_distance_to(footprint) <= TARGET_DISTANCE - ] - if not closest_footprints: - self.path_list.clear() - return - - # Get the closest footprint to the target and start following the footprints - # from that footprint - target_footprint = min( - closest_footprints, - key=self.system.get_physics_object_for_game_object( - self.target_id, - ).position.get_distance_to, - ) - self.path_list = footprints[footprints.index(target_footprint) :] - - def __repr__(self: SteeringMovement) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return ( - f"" - ) diff --git a/src/hades/game_objects/system.py b/src/hades/game_objects/system.py deleted file mode 100644 index 2a62a401..00000000 --- a/src/hades/game_objects/system.py +++ /dev/null @@ -1,231 +0,0 @@ -"""Manages the entity component system and its processes.""" -from __future__ import annotations - -# Builtin -from typing import TYPE_CHECKING - -# Custom -from hades.game_objects.base import Vec2d -from hades.game_objects.movements import PhysicsObject - -if TYPE_CHECKING: - from hades.game_objects.base import ( - ComponentData, - ComponentType, - GameObjectComponent, - ) - -__all__ = ("ECS", "ECSError") - - -class ECSError(Exception): - """Raised when an error occurs with the ECS.""" - - def __init__( - self: ECSError, - *, - not_registered_type: str, - value: int | str | ComponentType, - error: str = "is not registered with the ECS", - ) -> None: - """Initialise the object. - - Args: - not_registered_type: The game object or component type that is not - registered. - value: The value that is not registered. - error: The problem raised by the ECS. - """ - super().__init__( - f"The {not_registered_type} `{value}` {error}.", - ) - - -class ECS: - """Stores and manages game objects registered with the entity component system.""" - - __slots__ = ( - "_next_game_object_id", - "_components", - "_physics_objects", - ) - - def __init__(self: ECS) -> None: - """Initialise the object.""" - self._next_game_object_id = 0 - self._components: dict[int, dict[ComponentType, GameObjectComponent]] = {} - self._physics_objects: dict[int, PhysicsObject] = {} - - def add_game_object( - self: ECS, - component_data: ComponentData, - *components: type[GameObjectComponent], - physics: bool = False, - ) -> int: - """Add a game object to the system with optional components. - - Args: - component_data: The data for the components. - *components: The optional list of components for the game object. - physics: Whether the game object should have a physics object or not. - - Returns: - The game object ID. - - Raises: - ECSError: The component type `type` is already registered with the ECS. - """ - # Create the game object and a physics object if required - self._components[self._next_game_object_id] = {} - if physics: - self._physics_objects[self._next_game_object_id] = PhysicsObject( - Vec2d(0, 0), - Vec2d(0, 0), - ) - - # Add the optional components to the system - for component in components: - if component.component_type in self._components[self._next_game_object_id]: - del self._components[self._next_game_object_id] - if physics: - del self._physics_objects[self._next_game_object_id] - raise ECSError( - not_registered_type="component type", - value=component.component_type, - error="is already registered with the ECS", - ) - - # Initialise the component and add it to the system - self._components[self._next_game_object_id][component.component_type] = ( - component(self._next_game_object_id, self, component_data) - ) - - # Increment _next_game_object_id and return the current game object ID - self._next_game_object_id += 1 - return self._next_game_object_id - 1 - - def remove_game_object(self: ECS, game_object_id: int) -> None: - """Remove a game object from the system. - - Args: - game_object_id: The game object ID. - - Raises: - ECSError: The game object ID `ID` is not registered with the ECS. - """ - # Check if the game object is registered or not - if game_object_id not in self._components: - raise ECSError( - not_registered_type="game object ID", - value=game_object_id, - ) - - # Delete the game object from the system - del self._components[game_object_id] - if game_object_id in self._physics_objects: - del self._physics_objects[game_object_id] - - def get_components_for_game_object( - self: ECS, - game_object_id: int, - ) -> dict[ComponentType, GameObjectComponent]: - """Get a game object's components. - - Args: - game_object_id: The game object ID. - - Returns: - The game object's components. - - Raises: - ECSError: The game object ID `ID` is not registered with the ECS. - """ - # Check if the game object ID is registered or not - if game_object_id not in self._components: - raise ECSError( - not_registered_type="game object ID", - value=game_object_id, - ) - - # Return the game object's components - return self._components[game_object_id] - - def get_component_for_game_object( - self: ECS, - game_object_id: int, - component_type: ComponentType, - ) -> GameObjectComponent: - """Get a component for a given game object ID. - - Args: - game_object_id: The game object ID. - component_type: The component type to get. - - Returns: - The component for the given game object ID. - - Raises: - ECSError: The game object ID `ID` is not registered with the ECS. - KeyError: The component type is not part of the game object. - """ - # Check if the game object ID is registered or not - if game_object_id not in self._components: - raise ECSError( - not_registered_type="game object ID", - value=game_object_id, - ) - - # Return the specified component - return self._components[game_object_id][component_type] - - def get_components_for_component_type( - self: ECS, - component_type: ComponentType, - ) -> list[GameObjectComponent]: - """Get a list of components for a given component type. - - Args: - component_type: The component type. - - Returns: - The list of components for the given component type. - """ - return [ - components[component_type] - for components in self._components.values() - if component_type in components - ] - - def get_physics_object_for_game_object( - self: ECS, - game_object_id: int, - ) -> PhysicsObject: - """Get a physics object for a given game object ID. - - Args: - game_object_id: The game object ID. - - Returns: - The physics object for the given game object ID. - - Raises: - ECSError: The game object ID `ID` does not have a physics object. - """ - # Check if the game object ID is registered or not - if game_object_id not in self._physics_objects: - raise ECSError( - not_registered_type="game object ID", - value=game_object_id, - error="does not have a physics object", - ) - - # Return the specified physics object - return self._physics_objects[game_object_id] - - def __repr__(self: ECS) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return f"" diff --git a/src/hades/physics.py b/src/hades/physics.py index 8b383378..6c321e71 100644 --- a/src/hades/physics.py +++ b/src/hades/physics.py @@ -1,4 +1,5 @@ """Manages the physics using an abstracted version of the Pymunk physics engine.""" + from __future__ import annotations # Builtin diff --git a/src/hades/sprite.py b/src/hades/sprite.py index d39b75a3..22b65d4d 100644 --- a/src/hades/sprite.py +++ b/src/hades/sprite.py @@ -1,22 +1,25 @@ """Manages the operations related to the sprite object.""" + from __future__ import annotations # Builtin -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING # Pip from arcade import Sprite # Custom -from hades.constants import SPRITE_SCALE -from hades.game_objects.base import ComponentType -from hades.game_objects.movements import MovementBase from hades.textures import grid_pos_to_pixel +from hades_extensions.game_objects import SPRITE_SCALE, Vec2d +from hades_extensions.game_objects.systems import ( + KeyboardMovementSystem, + SteeringMovementSystem, +) if TYPE_CHECKING: - from hades.game_objects.constructors import GameObjectTextures - from hades.game_objects.system import ECS + from hades.constructors import GameObjectTextures from hades.physics import PhysicsEngine + from hades_extensions.game_objects import Registry __all__ = ("HadesSprite",) @@ -26,23 +29,23 @@ class HadesSprite(Sprite): def __init__( self: HadesSprite, - game_object_id: int, - system: ECS, + game_object: tuple[int, bool], + registry: Registry, position: tuple[int, int], game_object_textures: GameObjectTextures, ) -> None: """Initialise the object. Args: - game_object_id: The game object's ID. - system: The entity component system which manages the game objects. + game_object: The game object's ID and whether it is AI controlled or not. + registry: The registry which manages the game objects. position: The position of the sprite object in the grid. game_object_textures: The collection of textures which relate to this game object. """ super().__init__(scale=SPRITE_SCALE) - self.game_object_id: int = game_object_id - self.system: ECS = system + self.game_object_id, ai_controlled = game_object + self.registry: Registry = registry self.position = grid_pos_to_pixel(*position) self.game_object_textures: GameObjectTextures = game_object_textures self.in_combat: bool = False @@ -50,6 +53,13 @@ def __init__( # Initialise the default sprite self.texture = self.game_object_textures.default_texture + # Get the correct movement system for the game object + self.target_movement_system: KeyboardMovementSystem | SteeringMovementSystem = ( + self.registry.get_system(SteeringMovementSystem) + if ai_controlled + else self.registry.get_system(KeyboardMovementSystem) + ) + @property def physics(self: HadesSprite) -> PhysicsEngine: """Get the game object's physics engine. @@ -59,28 +69,13 @@ def physics(self: HadesSprite) -> PhysicsEngine: """ return self.physics_engines[0] # type: ignore[misc,no-any-return] - def on_update(self: HadesSprite, delta_time: float = 1 / 60) -> None: - """Handle an on_update event for the game object. - - Args: - delta_time: The time interval since the last time the event was triggered. - """ - # Update the game object's components - for component in self.system.get_components_for_game_object( - self.game_object_id, - ).values(): - component.on_update(delta_time) - + def on_update(self: HadesSprite, _: float = 1 / 60) -> None: + """Handle an on_update event for the game object.""" # Calculate the game object's new movement force and apply it + force = self.target_movement_system.calculate_force(self.game_object_id) self.physics.apply_force( self, - cast( - MovementBase, - self.system.get_component_for_game_object( - self.game_object_id, - ComponentType.MOVEMENTS, - ), - ).calculate_force(), + (force.x, force.y), ) def pymunk_moved( @@ -93,14 +88,14 @@ def pymunk_moved( Args: physics_engine: The game object's physics engine. """ - physics_object, body = ( - self.system.get_physics_object_for_game_object(self.game_object_id), + kinematic_object, body = ( + self.registry.get_kinematic_object(self.game_object_id), physics_engine.get_physics_object(self).body, ) if body is None: return - physics_object.position = body.position - physics_object.velocity = body.velocity + kinematic_object.position = Vec2d(*body.position) + kinematic_object.velocity = Vec2d(*body.velocity) def __repr__(self: HadesSprite) -> str: """Return a human-readable representation of this object. diff --git a/src/hades/textures.py b/src/hades/textures.py index 983e1972..d6528d13 100644 --- a/src/hades/textures.py +++ b/src/hades/textures.py @@ -1,4 +1,5 @@ """Handles loading and storage of textures needed by the game.""" + from __future__ import annotations # Builtin @@ -9,7 +10,7 @@ from arcade import Texture, load_texture, load_texture_pair # Custom -from hades.constants import SPRITE_SIZE +from hades_extensions.game_objects import SPRITE_SIZE __all__ = ( "BiggerThanError", diff --git a/src/hades/views/__init__.py b/src/hades/views/__init__.py index 3de2054d..d6426c29 100644 --- a/src/hades/views/__init__.py +++ b/src/hades/views/__init__.py @@ -1,2 +1,3 @@ """Contains the functionality that allows the user to play the game.""" + from __future__ import annotations diff --git a/src/hades/views/game.py b/src/hades/views/game.py index 7da958b7..cc63363b 100644 --- a/src/hades/views/game.py +++ b/src/hades/views/game.py @@ -1,11 +1,12 @@ """Initialises and manages the main game.""" + from __future__ import annotations # Builtin import logging import math import random -from typing import NamedTuple, cast +from typing import TYPE_CHECKING, NamedTuple # Pip from arcade import ( @@ -19,32 +20,26 @@ key, schedule, ) -from hades_extensions import TileType, create_map # Custom from hades.constants import ( ENEMY_GENERATE_INTERVAL, ENEMY_GENERATION_DISTANCE, ENEMY_RETRY_COUNT, - SPRITE_SIZE, TOTAL_ENEMY_COUNT, GameObjectType, ) -from hades.game_objects.attacks import Attacks -from hades.game_objects.base import ComponentType -from hades.game_objects.constructors import ( - ENEMY, - FLOOR, - PLAYER, - POTION, - WALL, - GameObjectConstructor, -) -from hades.game_objects.movements import KeyboardMovement, SteeringMovement -from hades.game_objects.system import ECS +from hades.constructors import ENEMY, FLOOR, PLAYER, POTION, WALL from hades.physics import PhysicsEngine from hades.sprite import HadesSprite from hades.textures import grid_pos_to_pixel +from hades_extensions.game_objects import SPRITE_SIZE, Registry, Vec2d +from hades_extensions.game_objects.components import KeyboardMovement, SteeringMovement +from hades_extensions.game_objects.systems import AttackSystem +from hades_extensions.generation import TileType, create_map + +if TYPE_CHECKING: + from hades.constructors import GameObjectConstructor all__ = ("Game",) @@ -71,7 +66,7 @@ class Game(View): Attributes: level_constants: Holds the constants for the current level. - system: The entity component system which manages the game objects. + registry: The registry which manages the game objects. ids: The dictionary which stores the IDs and sprites for each game object type. tile_sprites: The sprite list for the tile game objects. entity_sprites: The sprite list for the entity game objects. @@ -101,14 +96,13 @@ def _initialise_game_object( The game object ID. """ # Initialise a sprite object - game_object_id = self.system.add_game_object( - constructor.component_data, - *constructor.components, - physics=constructor.physics, + game_object_id = self.registry.create_game_object( + constructor.components, + kinematic=constructor.kinematic, ) sprite_obj = HadesSprite( - game_object_id, - self.system, + (game_object_id, constructor.game_object_type is not GameObjectType.PLAYER), + self.registry, position, constructor.game_object_textures, ) @@ -125,6 +119,8 @@ def _initialise_game_object( constructor.game_object_type, blocking=constructor.blocking, ) + if constructor.blocking: + self.registry.add_wall(Vec2d(*position)) # Return the game object ID return game_object_id @@ -138,7 +134,7 @@ def __init__(self: Game, level: int) -> None: super().__init__() generation_result = create_map(level) self.level_constants: LevelConstants = LevelConstants(*generation_result[1]) - self.system: ECS = ECS() + self.registry: Registry = Registry() self.ids: dict[GameObjectType, list[HadesSprite]] = {} self.tile_sprites: SpriteList[HadesSprite] = SpriteList[HadesSprite]() self.entity_sprites: SpriteList[HadesSprite] = SpriteList[HadesSprite]() @@ -156,7 +152,8 @@ def __init__(self: Game, level: int) -> None: font_size=20, ) - # Initialise the game objects + # Initialise all the systems then the game objects + self.registry.add_systems() for count, tile in enumerate(generation_result[0]): # Skip all empty tiles if tile in {TileType.Empty, TileType.Obstacle}: @@ -231,6 +228,9 @@ def on_update(self: Game, delta_time: float) -> None: # Check if the game should end # if self.player.health.value <= 0 or not self.enemy_sprites: + # Update the systems + self.registry.update(delta_time) + # Update the entities self.entity_sprites.on_update(delta_time) @@ -248,12 +248,9 @@ def on_key_press(self: Game, symbol: int, modifiers: int) -> None: modifiers: Bitwise AND of all modifiers (shift, ctrl, num lock) pressed during this event. """ - player_movement = cast( + player_movement = self.registry.get_component( + self.ids[GameObjectType.PLAYER][0].game_object_id, KeyboardMovement, - self.system.get_component_for_game_object( - self.ids[GameObjectType.PLAYER][0].game_object_id, - ComponentType.MOVEMENTS, - ), ) logger.debug( "Received key press with key %r and modifiers %r", @@ -262,13 +259,13 @@ def on_key_press(self: Game, symbol: int, modifiers: int) -> None: ) match symbol: case key.W: - player_movement.north_pressed = True + player_movement.moving_north = True case key.S: - player_movement.south_pressed = True + player_movement.moving_south = True case key.A: - player_movement.west_pressed = True + player_movement.moving_west = True case key.D: - player_movement.east_pressed = True + player_movement.moving_east = True def on_key_release(self: Game, symbol: int, modifiers: int) -> None: """Process key release functionality. @@ -278,12 +275,9 @@ def on_key_release(self: Game, symbol: int, modifiers: int) -> None: modifiers: Bitwise AND of all modifiers (shift, ctrl, num lock) pressed during this event. """ - player_movement = cast( + player_movement = self.registry.get_component( + self.ids[GameObjectType.PLAYER][0].game_object_id, KeyboardMovement, - self.system.get_component_for_game_object( - self.ids[GameObjectType.PLAYER][0].game_object_id, - ComponentType.MOVEMENTS, - ), ) logger.debug( "Received key release with key %r and modifiers %r", @@ -292,13 +286,13 @@ def on_key_release(self: Game, symbol: int, modifiers: int) -> None: ) match symbol: case key.W: - player_movement.north_pressed = False + player_movement.moving_north = False case key.S: - player_movement.south_pressed = False + player_movement.moving_south = False case key.A: - player_movement.west_pressed = False + player_movement.moving_west = False case key.D: - player_movement.east_pressed = False + player_movement.moving_east = False def on_mouse_press(self: Game, x: int, y: int, button: int, modifiers: int) -> None: """Process mouse button functionality. @@ -318,13 +312,8 @@ def on_mouse_press(self: Game, x: int, y: int, button: int, modifiers: int) -> N modifiers, ) if button is MOUSE_BUTTON_LEFT: - cast( - Attacks, - self.system.get_component_for_game_object( - self.ids[GameObjectType.PLAYER][0].game_object_id, - ComponentType.ATTACKS, - ), - ).do_attack( + self.registry.get_system(AttackSystem).do_attack( + self.ids[GameObjectType.PLAYER][0].game_object_id, [ game_object.game_object_id for game_object in self.ids[GameObjectType.ENEMY] @@ -349,23 +338,13 @@ def generate_enemy(self: Game, _: float = 1 / 60) -> None: continue # Set the required data for the steering to correctly function - steering_movement = cast( + steering_movement = self.registry.get_component( + self._initialise_game_object(ENEMY, self.entity_sprites, position), SteeringMovement, - self.system.get_component_for_game_object( - self._initialise_game_object(ENEMY, self.entity_sprites, position), - ComponentType.MOVEMENTS, - ), ) steering_movement.target_id = self.ids[GameObjectType.PLAYER][ 0 ].game_object_id - steering_movement.walls = { - ( - int(wall_sprite.center_x / SPRITE_SIZE), - int(wall_sprite.center_y / SPRITE_SIZE), - ) - for wall_sprite in self.ids[GameObjectType.WALL] - } return def center_camera_on_player(self: Game) -> None: diff --git a/src/hades/views/inventory.py b/src/hades/views/inventory.py index f83298ea..72650c51 100644 --- a/src/hades/views/inventory.py +++ b/src/hades/views/inventory.py @@ -1,4 +1,5 @@ """Displays the player's inventory graphically.""" + from __future__ import annotations # Builtin diff --git a/src/hades/views/shop.py b/src/hades/views/shop.py index bc441c47..e69de29b 100644 --- a/src/hades/views/shop.py +++ b/src/hades/views/shop.py @@ -1,121 +0,0 @@ -"""Creates a shop for upgrades and special attributes/items.""" -from __future__ import annotations - -# Builtin -from typing import TYPE_CHECKING - -# Pip -import arcade -from arcade.gui import UIAnchorLayout, UIBoxLayout, UIFlatButton, UIManager - -if TYPE_CHECKING: - from arcade.gui.events import UIOnClickEvent - - from hades.game_objects.attributes import UpgradablePlayerSection - from hades.game_objects.players import Player - -__all__ = ( - "SectionUpgradeButton", - "Shop", -) - - -class SectionUpgradeButton(UIFlatButton): - """A button which will upgrade a player section if the player has enough money.""" - - section_ref: UpgradablePlayerSection | None = None - - def on_click(self: SectionUpgradeButton, _: UIOnClickEvent) -> None: - """Upgrade a player attribute section.""" - # Make sure variables needed are valid - assert self.section_ref is not None - - # Upgrade the section if possible - if self.section_ref.upgrade_section(): - self.text = ( - f"{self.section_ref.section_type.name} -" - f" {self.section_ref.next_level_cost}" - ) - - def __repr__(self: SectionUpgradeButton) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return ( - f"" - ) - - -def back_on_click(_: UIOnClickEvent) -> None: - """Return to the game when the button is clicked.""" - window = arcade.get_window() - window.show_view(window.views["Game"]) - - -class Shop(arcade.View): - """Display the shop UI so the player can upgrade their attributes. - - Attributes: - ui_manager: Manages all the different UI elements for this view. - """ - - def __init__(self: Shop, player: Player) -> None: - """Initialise the object. - - Args: - player: The player object used for accessing the inventory. - """ - super().__init__() - self.player: Player = player - self.ui_manager: UIManager = UIManager() - - # Create all the section upgrade buttons based on the amount of sections the - # player has - vertical_box: UIBoxLayout = UIBoxLayout(space_between=20) - for upgradable_player_section in self.player.upgrade_sections: - upgrade_section_button = SectionUpgradeButton( - text=( - f"{upgradable_player_section.section_type.name} -" - f" {upgradable_player_section.next_level_cost}" - ), - width=200, - ) - upgrade_section_button.section_ref = upgradable_player_section - vertical_box.add(upgrade_section_button) - - # Create the back button - back_button = UIFlatButton(text="Back", width=200) - back_button.on_click = back_on_click - vertical_box.add(back_button) - - # Add the vertical box layout to the UI - anchor_layout = UIAnchorLayout(anchor_x="center_x", anchor_y="center_y") - anchor_layout.add(vertical_box) - self.ui_manager.add(anchor_layout) - - def on_draw(self: Shop) -> None: - """Render the screen.""" - # Clear the screen - self.clear() - - # Draw the UI elements - self.ui_manager.draw() - - def on_show_view(self: Shop) -> None: - """Process show view functionality.""" - self.ui_manager.enable() - - def on_hide_view(self: Shop) -> None: - """Process hide view functionality.""" - self.ui_manager.disable() - - def __repr__(self: Shop) -> str: - """Return a human-readable representation of this object. - - Returns: - The human-readable representation of this object. - """ - return f"" diff --git a/src/hades/views/start_menu.py b/src/hades/views/start_menu.py index 34f35f20..19c67bd9 100644 --- a/src/hades/views/start_menu.py +++ b/src/hades/views/start_menu.py @@ -1,4 +1,5 @@ """Creates a start menu so the player can change their settings or game mode.""" + from __future__ import annotations # Builtin diff --git a/src/hades/window.py b/src/hades/window.py index f69430f3..bcdc87ad 100644 --- a/src/hades/window.py +++ b/src/hades/window.py @@ -1,4 +1,5 @@ """Acts as the entry point to the game by creating and initialising the window.""" + from __future__ import annotations # Builtin diff --git a/src/hades_extensions/.clang-format b/src/hades_extensions/.clang-format new file mode 100644 index 00000000..bfd118eb --- /dev/null +++ b/src/hades_extensions/.clang-format @@ -0,0 +1,2 @@ +BasedOnStyle: Google +ColumnLimit: 120 diff --git a/src/hades_extensions/.clang-tidy b/src/hades_extensions/.clang-tidy new file mode 100644 index 00000000..b8dca01f --- /dev/null +++ b/src/hades_extensions/.clang-tidy @@ -0,0 +1,34 @@ +Checks: " + bugprone-*, + -bugprone-easily-swappable-parameters, + + cert-*, + -cert-err58-cpp + + clang-analyzer-*, + + cppcoreguidelines-*, + + google-*, + + hicpp-*, + + misc-*, + + modernize-*, + + performance-*, + + portability-*, + + readability-*, + +" +WarningsAsErrors: "*" +CheckOptions: + - key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 1 + - key: readability-identifier-length.IgnoredParameterNames + value: "^[ijkxy_]$" + - key: readability-identifier-length.IgnoredLoopCounterNames + value: "^[ijkxy_]$" diff --git a/src/hades_extensions/CMakeLists.txt b/src/hades_extensions/CMakeLists.txt new file mode 100644 index 00000000..ca7bf731 --- /dev/null +++ b/src/hades_extensions/CMakeLists.txt @@ -0,0 +1,57 @@ +# Define the minimum CMake version +cmake_minimum_required(VERSION 3.25.2) + +# Define some environment variables +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(ENABLE_COVERAGE OFF CACHE BOOL "Enable coverage reporting for GCC/Clang") +set(DO_TESTS ON CACHE BOOL "Enable testing") + +# Define the module names and initialise the project +set(PY_MODULE hades_extensions) +set(CPP_LIB hades_extensions_lib) +set(TEST_MODULE hades_extensions_tests) +project(${PY_MODULE} LANGUAGES CXX) + +# Enable coverage if supported by the compiler +if (ENABLE_COVERAGE) + if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + message(STATUS "Coverage enabled (compiler is GCC/Clang)") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 --coverage") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") + else () + message(STATUS "Coverage disabled (compiler is not GCC/Clang)") + endif () +else () + message(STATUS "Coverage disabled (ENABLE_COVERAGE is not set)") +endif () + +# TODO: Clang-tidy just fails the build for some reason +## Enable clang-tidy if found +#find_program(CLANG_TIDY_EXE NAMES "clang-tidy") +#if (CLANG_TIDY_EXE) +# message(STATUS "Found clang-tidy: ${CLANG_TIDY_EXE}") +# set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") +#else () +# message(STATUS "No clang-tidy found") +#endif () + +## Enable cppcheck if found +#find_program(CPPCHECK_EXE NAMES "cppcheck") +#if (CPPCHECK_EXE) +# message(STATUS "Found cppcheck: ${CPPCHECK_EXE}") +# set(CMAKE_CXX_CPPCHECK "${CPPCHECK_EXE}") +# list(APPEND CMAKE_CXX_CPPCHECK "--enable=all" "--suppress=missingInclude" "--suppress=missingIncludeSystem") +#else () +# message(STATUS "No cppcheck found") +#endif () + +# Add the subdirectories for the different parts of the project +include(FetchContent) +add_subdirectory(src) +if (DO_TESTS) + add_subdirectory(tests) +endif () diff --git a/src/hades_extensions/CMakePresets.json b/src/hades_extensions/CMakePresets.json new file mode 100644 index 00000000..7af1eeb6 --- /dev/null +++ b/src/hades_extensions/CMakePresets.json @@ -0,0 +1,37 @@ +{ + "cmakeMinimumRequired": { + "major": 3, + "minor": 25, + "patch": 2 + }, + "configurePresets": [ + { + "binaryDir": "${sourceDir}/build-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + }, + "description": "Debug build", + "generator": "Ninja", + "name": "Debug" + }, + { + "binaryDir": "${sourceDir}/build-release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + }, + "description": "Release build", + "generator": "Ninja", + "name": "Release" + }, + { + "binaryDir": "${sourceDir}/build-coverage", + "cacheVariables": { + "ENABLE_COVERAGE": "ON" + }, + "description": "Coverage build", + "inherits": "Debug", + "name": "Coverage" + } + ], + "version": 3 +} diff --git a/src/hades_extensions/include/game_objects/registry.hpp b/src/hades_extensions/include/game_objects/registry.hpp new file mode 100644 index 00000000..59efc068 --- /dev/null +++ b/src/hades_extensions/include/game_objects/registry.hpp @@ -0,0 +1,267 @@ +// Ensure this file is only included once +#pragma once + +// Std headers +#include +#include +#include +#include +#include + +// Local headers +#include "steering.hpp" + +// ----- TYPEDEFS ------------------------------ +// Represents unique identifiers for game objects +using GameObjectID = int; +using ActionFunction = std::function; + +// ----- BASE TYPES ------------------------------ +// Add a forward declaration for the registry class +class Registry; + +/// The base class for all components. +struct ComponentBase { + /// The copy assignment operator. + /// + /// @param other - The other component to copy. + auto operator=(const ComponentBase &) -> ComponentBase & = default; + + /// The move assignment operator. + /// + /// @param other - The other component to move. + auto operator=(ComponentBase &&) -> ComponentBase & = default; + + /// The default constructor. + ComponentBase() = default; + + /// The virtual destructor. + virtual ~ComponentBase() = default; + + /// The copy constructor. + /// + /// @param other - The other component to copy. + ComponentBase(const ComponentBase &) = default; + + /// The move constructor. + /// + /// @param other - The other component to move. + ComponentBase(ComponentBase &&) = default; +}; + +/// The base class for all systems. +class SystemBase { + public: + /// The copy assignment operator. + /// + /// @param other - The other system to copy. + auto operator=(const SystemBase &) -> SystemBase & = default; + + /// The move assignment operator. + /// + /// @param other - The other system to move. + auto operator=(SystemBase &&) -> SystemBase & = default; + + /// Initialise the object. + /// + /// @param registry - The registry that manages the game objects, components, and systems. + explicit SystemBase(Registry *registry) : registry(registry) {} + + /// The virtual destructor. + virtual ~SystemBase() = default; + + /// The copy constructor. + /// + /// @param other - The other system to copy. + SystemBase(const SystemBase &) = default; + + /// The move constructor. + /// + /// @param other - The other system to move. + SystemBase(SystemBase &&) = default; + + /// Get the registry that manages the game objects, components, and systems. + /// + /// @return The registry that manages the game objects, components, and systems. + [[nodiscard]] inline auto get_registry() const -> Registry * { return registry; } + + /// Process update logic for a system. + /// + /// @param delta_time - The time interval since the last time the function was called. + virtual void update(double delta_time) const {}; + + private: + /// The registry that manages the game objects, components, and systems. + Registry *registry; +}; + +// ----- EXCEPTIONS ------------------------------ +/// Raised when an error occurs with the registry. +struct RegistryError : public std::runtime_error { + /// Initialise the object + /// + /// @param not_registered_type - The type of item that is not registered. + explicit RegistryError(const std::string &error = "is not registered with the registry") + : std::runtime_error("The templated type " + error + "."){}; + + /// Initialise the object. + /// + /// @tparam T - The type of item that is not registered. + /// @param not_registered_type - The type of item that is not registered. + /// @param value - The value that is not registered. + template + RegistryError(const std::string ¬_registered_type, const T &value, const std::string &extra = "") + : std::runtime_error("The " + not_registered_type + " `" + std::to_string(value) + + "` is not registered with the registry" + extra + "."){}; +}; + +// ----- CLASSES ------------------------------ +/// Manages game objects, components, and systems that are registered. +class Registry { + public: + /// Create a new game object. + /// + /// @param components - The components to add to the game object. + /// @param kinematic - Whether the game object should have a kinematic object or not. + /// @return The game object ID. + auto create_game_object(const std::vector> &&components, bool kinematic = false) + -> GameObjectID; + + /// Delete a game object. + /// + /// @param game_object_id - The game object ID. + /// @throws RegistryError - If the game object is not registered. + void delete_game_object(GameObjectID game_object_id); + + /// Checks if a game object has a given component or not. + /// + /// @param game_object_id - The game object ID. + /// @param component_type - The type of component to check for. + /// @return Whether the game object has the component or not. + [[nodiscard]] inline auto has_component(const GameObjectID game_object_id, + const std::type_index &component_type) const -> bool { + return game_objects_.contains(game_object_id) && game_objects_.at(game_object_id).contains(component_type); + } + + /// Get a component from the registry. + /// + /// @tparam T - The type of component to get. + /// @param game_object_id - The game object ID. + /// @throws RegistryError - If the game object is not registered or if the game object does not have the component. + /// @return The component from the registry. + template + inline auto get_component(const GameObjectID game_object_id) const -> std::shared_ptr { + return std::static_pointer_cast(get_component(game_object_id, typeid(T))); + } + + /// Get a component from the registry. + /// + /// @param game_object_id - The game object ID. + /// @param component_type - The type of component to get. + /// @throws RegistryError - If the game object is not registered or if the game object does not have the component. + /// @return The component from the registry. + [[nodiscard]] auto get_component(GameObjectID game_object_id, const std::type_index &component_type) const + -> std::shared_ptr; + + /// Find all the game objects that have the required components. + /// + /// @tparam Ts - The types of components to find. + /// @return A vector of tuples containing the game object ID and the required components. + template + auto find_components() const -> std::vector...>>> { + // Create a vector of tuples to store the components + std::vector...>>> components; + + // Iterate over all game objects + for (const auto &[game_object_id, game_object_components] : game_objects_) { + // Check if the game object has all the components using a fold expression + if (!(has_component(game_object_id, typeid(Ts)) && ...)) { + continue; + } + + // Game object has all the components, so cast them to T and add them to the vector + auto components_result{std::make_tuple(std::static_pointer_cast(game_object_components.at(typeid(Ts)))...)}; + components.emplace_back(game_object_id, components_result); + } + + // Return the components + return components; + } + + /// Add a system to the registry. + /// + /// @tparam T - The type of system to add. + /// @throws RegistryError - If the system is already registered. + template + void add_system() { + // Check if the system is already registered + const std::type_index system_type{typeid(T)}; + if (systems_.contains(system_type)) { + throw RegistryError("is already registered with the registry"); + } + + // Add the system to the registry + systems_[system_type] = std::make_shared(this); + } + + /// Get a system from the registry. + /// + /// @tparam T - The type of system to get. + /// @throws RegistryError - If the system is not registered. + /// @return The system from the registry. + template + auto get_system() const -> std::shared_ptr { + // Check if the system is registered + const std::type_index system_type{typeid(T)}; + auto system_result{systems_.find(system_type)}; + if (system_result == systems_.end()) { + throw RegistryError(); + } + + // Return the system + return std::static_pointer_cast(system_result->second); + } + + /// Update all the systems in the registry. + /// + /// @param delta_time - The time interval since the last time the function was called. + inline void update(const double delta_time) const { + for (const auto &[_, system] : systems_) { + system->update(delta_time); + } + } + + /// Get the kinematic object for a game object. + /// + /// @param game_object_id - The game object ID. + /// @throws RegistryError - If the game object is not registered or if the game object does not have a kinematic + /// object. + /// @return The kinematic object. + [[nodiscard]] auto get_kinematic_object(GameObjectID game_object_id) const -> std::shared_ptr; + + /// Add a wall to the registry. + /// + /// @param wall - The wall to add to the registry. + inline void add_wall(const Vec2d &wall) { walls_.emplace(wall); } + + /// Get the walls in the registry. + /// + /// @return The walls in the registry. + [[nodiscard]] inline auto get_walls() const -> const std::unordered_set & { return walls_; } + + private: + /// The next game object ID to use. + GameObjectID next_game_object_id_{0}; + + /// The game objects and their components registered with the registry. + std::unordered_map>> game_objects_; + + /// The systems registered with the registry. + std::unordered_map> systems_; + + /// The kinematic objects registered with the registry. + std::unordered_map> kinematic_objects_; + + /// The walls registered with the registry. + std::unordered_set walls_; +}; diff --git a/src/hades_extensions/include/game_objects/stats.hpp b/src/hades_extensions/include/game_objects/stats.hpp new file mode 100644 index 00000000..daed6e4b --- /dev/null +++ b/src/hades_extensions/include/game_objects/stats.hpp @@ -0,0 +1,105 @@ +// Ensure this file is only included once +#pragma once + +// Std headers +#include +#include + +// Local headers +#include "game_objects/registry.hpp" + +// ----- COMPONENTS ------------------------------ +/// Represents a component that has a variable value and maximum value. +class Stat : public ComponentBase { + public: + /// Initialise the object. + /// + /// @param value - The initial and maximum value of the stat. + /// @param maximum_level - The maximum level of the stat. + Stat(const double value, const int maximum_level) : value_(value), maximum_level(maximum_level), max_value_(value) {} + + /// Get the value of the stat. + /// + /// @return The value of the stat. + [[nodiscard]] inline auto get_value() const -> double { return value_; } + + /// Set the value of the stat. + /// + /// @param new_value - The new value of the stat. + inline void set_value(const double new_value) { value_ = std::max(std::min(new_value, max_value_), 0.0); } + + /// Get the maximum value of the stat. + /// + /// @return The maximum value of the stat. + [[nodiscard]] inline auto get_max_value() const -> double { return max_value_; } + + /// Add a value to the maximum value of the stat. + /// + /// @param value - The value to add to the maximum value of the stat. + inline void add_to_max_value(const double value) { max_value_ += value; } + + /// Get the current level of the stat. + /// + /// @return The current level of the stat. + [[nodiscard]] inline auto get_current_level() const -> int { return current_level; } + + /// Increment the current level of the stat. + inline void increment_current_level() { current_level++; } + + /// Get the maximum level of the stat. + /// + /// @return The maximum level of the stat. + [[nodiscard]] inline auto get_maximum_level() const -> int { return maximum_level; } + + private: + /// The current value of the variable. + double value_; + + /// The maximum value of the stat. + double max_value_; + + /// The current level of the stat. + int current_level{0}; + + /// The maximum level of the stat. + int maximum_level; +}; + +/// Allows a game object to have an armour stat. +struct Armour : public Stat { + /// Initialise the object. + /// + /// @param value - The initial and maximum value of the armour stat. + /// @param maximum_level - The maximum level of the armour stat. + Armour(const double value, const int maximum_level) : Stat(value, maximum_level) {} +}; + +/// Allows a game object to regenerate armour. +struct ArmourRegen : public Stat { + /// The time since the game object last regenerated armour. + double time_since_armour_regen{0}; + + /// Initialise the object. + /// + /// @param value - The initial and maximum value of the armour regen stat. + /// @param maximum_level - The maximum level of the armour regen stat. + ArmourRegen(const double value, const int maximum_level) : Stat(value, maximum_level) {} +}; + +/// Allows a game object to have a health stat. +struct Health : public Stat { + /// Initialise the object. + /// + /// @param value - The initial and maximum value of the health stat. + /// @param maximum_level - The maximum level of the health stat. + Health(const double value, const int maximum_level) : Stat(value, maximum_level) {} +}; + +/// Allows a game object to determine how fast it can move. +struct MovementForce : public Stat { + /// Initialise the object. + /// + /// @param value - The initial and maximum value of the movement force stat. + /// @param maximum_level - The maximum level of the movement force stat. + MovementForce(const double value, const int maximum_level) : Stat(value, maximum_level) {} +}; diff --git a/src/hades_extensions/include/game_objects/steering.hpp b/src/hades_extensions/include/game_objects/steering.hpp new file mode 100644 index 00000000..4af1228d --- /dev/null +++ b/src/hades_extensions/include/game_objects/steering.hpp @@ -0,0 +1,173 @@ +// Ensure this file is only included once +#pragma once + +// Std headers +#include +#include +#include +#include + +// Local headers +#include "hash_combine.hpp" + +// ----- CONSTANTS ------------------------------ +#define PI_RADIANS (std::numbers::pi / 180) +#define TWO_PI (2 * std::numbers::pi) +constexpr double SPRITE_SCALE{0.5}; +constexpr double SPRITE_SIZE{128 * SPRITE_SCALE}; + +// ----- STRUCTURES ------------------------------ +/// Represents a 2D vector. +struct Vec2d { + inline auto operator==(const Vec2d &vec) const -> bool { return x == vec.x && y == vec.y; } + + inline auto operator!=(const Vec2d &vec) const -> bool { return x != vec.x || y != vec.y; } + + inline auto operator+(const Vec2d &vec) const -> Vec2d { return {x + vec.x, y + vec.y}; } + + inline auto operator+=(const Vec2d &vec) -> Vec2d { + x += vec.x; + y += vec.y; + return *this; + } + + inline auto operator-(const Vec2d &vec) const -> Vec2d { return {x - vec.x, y - vec.y}; } + + inline auto operator*(const double val) const -> Vec2d { return {x * val, y * val}; } + + inline auto operator/(const double val) const -> Vec2d { return {std::floor(x / val), std::floor(y / val)}; } + + /// The x value of the vector. + double x; + + /// The y value of the vector. + double y; + + /// Get the magnitude of the vector. + /// + /// @return The magnitude of the vector. + [[nodiscard]] inline auto magnitude() const -> double { return std::hypot(x, y); } + + /// Normalise the vector + /// + /// @return The normalised vector. + [[nodiscard]] inline auto normalised() const -> Vec2d { + const double magnitude{this->magnitude()}; + return (magnitude == 0) ? Vec2d{0, 0} : Vec2d{x / magnitude, y / magnitude}; + } + + /// Rotate the vector by an angle. + /// + /// @param angle - The angle to rotate the vector by in radians. + /// + /// @return The rotated vector. + [[nodiscard]] inline auto rotated(const double angle) const -> Vec2d { + const double cos_angle{std::cos(angle)}; + const double sin_angle{std::sin(angle)}; + return {x * cos_angle - y * sin_angle, x * sin_angle + y * cos_angle}; + } + + /// Get the angle between this vector and another vector. + /// + /// @details This will always be between 0 and 2Ï€. + /// @param other - The other vector to get the angle between. + /// @return The angle between the two vectors. + [[nodiscard]] auto angle_between(const Vec2d &other) const -> double { + const double cross_product{x * other.y - y * other.x}; + const double dot_product{x * other.x + y * other.y}; + return std::fmod(std::atan2(cross_product, dot_product) + TWO_PI, TWO_PI); + } + + /// Get the distance to another vector. + /// + /// @param other - The vector to get the distance to. + /// @return The distance to the other vector. + [[nodiscard]] inline auto distance_to(const Vec2d &other) const -> double { + return std::hypot(x - other.x, y - other.y); + } +}; + +/// Stores various data about a game object for use in physics-related operations. +struct KinematicObject { + /// The position of the game object. + Vec2d position{0, 0}; + + /// The velocity of the game object. + Vec2d velocity{0, 0}; + + /// The rotation of the game object. + double rotation{0}; +}; + +// ----- HASHES ------------------------------ +template <> +struct std::hash { + auto operator()(const Vec2d &vec) const -> size_t { + size_t res{0}; + hash_combine(res, vec.x); + hash_combine(res, vec.y); + return res; + } +}; + +// ----- FUNCTIONS ------------------------------ +/// Allow a game object to move towards another game object and stand still. +/// +/// @param current_position - The position of the game object. +/// @param target_position - The position of the target game object. +/// @return The new steering force from this behaviour. +auto arrive(const Vec2d ¤t_position, const Vec2d &target_position) -> Vec2d; + +/// Allow a game object to flee from another game object's predicted position. +/// +/// @param current_position - The position of the game object. +/// @param target_position - The position of the target game object. +/// @param target_velocity - The velocity of the target game object. +/// @return The new steering force from this behaviour. +auto evade(const Vec2d ¤t_position, const Vec2d &target_position, const Vec2d &target_velocity) -> Vec2d; + +/// Allow a game object to run away from another game object. +/// +/// @param current_position - The position of the game object. +/// @param current_velocity - The velocity of the game object. +/// @return The new steering force from this behaviour. +auto flee(const Vec2d ¤t_position, const Vec2d &target_position) -> Vec2d; + +/// Allow a game object to follow a pre-determined path. +/// +/// @param current_position - The position of the game object. +/// @param path_list - The list of positions the game object should follow. +/// @throws std::length_error - The path list is empty. +/// @return The new steering force from this behaviour. +auto follow_path(const Vec2d ¤t_position, std::vector &path_list) -> Vec2d; + +/// Allow a game object to avoid obstacles in its path. +/// +/// @param current_position - The position of the game object. +/// @param current_velocity - The velocity of the game object. +/// @param walls - The set of walls in the game. +/// @return The new steering force from this behaviour. +auto obstacle_avoidance(const Vec2d ¤t_position, const Vec2d ¤t_velocity, + const std::unordered_set &walls) -> Vec2d; + +/// Allow a game object to seek towards another game object's predicted position. +/// +/// @param current_position - The position of the game object. +/// @param target_position - The position of the target game object. +/// @param target_velocity - The velocity of the target game object. +/// @return The new steering force from this behaviour. +auto pursue(const Vec2d ¤t_position, const Vec2d &target_position, const Vec2d &target_velocity) -> Vec2d; + +/// Allow a game object to move towards another game object. +/// +/// @param current_position - The position of the game object. +/// @param target_position - The position of the target game object. +/// @return The new steering force from this behaviour. +auto seek(const Vec2d ¤t_position, const Vec2d &target_position) -> Vec2d; + +/// Allow a game object to move in a random direction for a short period of time. +/// +/// @param current_velocity - The velocity of the game object. +/// @param displacement_angle - The angle of the displacement force in degrees. +/// @return The new steering force from this behaviour. +auto wander(const Vec2d ¤t_velocity, int displacement_angle) -> Vec2d; diff --git a/src/hades_extensions/include/game_objects/systems/armour_regen.hpp b/src/hades_extensions/include/game_objects/systems/armour_regen.hpp new file mode 100644 index 00000000..19430135 --- /dev/null +++ b/src/hades_extensions/include/game_objects/systems/armour_regen.hpp @@ -0,0 +1,19 @@ +// Ensure this file is only included once +#pragma once + +// Local headers +#include "game_objects/registry.hpp" + +// ----- SYSTEMS ------------------------------ +/// Provides facilities to manipulate armour regen components. +struct ArmourRegenSystem : public SystemBase { + /// Initialise the object. + /// + /// @param registry - The registry that manages the game objects, components, and systems. + explicit ArmourRegenSystem(Registry *registry) : SystemBase(registry) {} + + /// Process update logic for an armour regeneration component. + /// + /// @param delta_time - The time interval since the last time the function was called. + void update(double delta_time) const final; +}; diff --git a/src/hades_extensions/include/game_objects/systems/attacks.hpp b/src/hades_extensions/include/game_objects/systems/attacks.hpp new file mode 100644 index 00000000..8fdd8ec4 --- /dev/null +++ b/src/hades_extensions/include/game_objects/systems/attacks.hpp @@ -0,0 +1,86 @@ +// Ensure this file is only included once +#pragma once + +// Std headers +#include + +// Local headers +#include "game_objects/registry.hpp" + +// ----- ENUMS ------------------------------ +/// Stores the different types of attack algorithms available. +enum class AttackAlgorithm { + AreaOfEffect, + Melee, + Ranged, +}; + +// ----- COMPONENTS ------------------------------ +/// Allows a game object to attack other game objects. +struct Attacks : public ComponentBase { + /// The attack algorithms the game object can use. + std::vector attack_algorithms; + + /// The current state of the game object's attack. + int attack_state{0}; + + /// Initialise the object. + /// + /// @param attack_algorithms - The attack algorithms the game object can use. + explicit Attacks(const std::vector &attack_algorithms) : attack_algorithms(attack_algorithms) {} +}; + +// ----- SYSTEMS ------------------------------ +/// Provides facilities to manipulate attack components. +struct AttackSystem : public SystemBase { + /// Initialise the object. + /// + /// @param registry - The registry that manages the game objects, components, and systems. + explicit AttackSystem(Registry *registry) : SystemBase(registry) {} + + /// Perform the currently selected attack algorithm. + /// + /// @param game_object_id - The ID of the game object to perform the attack for. + /// @param targets - The targets to attack. + /// @throws RegistryError - If the game object does not exist or does not have an attack component. + /// @return The result of the attack. + [[nodiscard]] auto do_attack(int game_object_id, const std::vector &targets) const + -> std::optional>; + + /// Select the previous attack algorithm. + /// + /// @param game_object_id - The ID of the game object to select the previous attack for. + /// @throws RegistryError - If the game object does not exist or does not have an attack component. + inline void previous_attack(const GameObjectID game_object_id) const { + auto attacks{get_registry()->get_component(game_object_id)}; + if (attacks->attack_state > 0) { + attacks->attack_state--; + } + } + + /// Select the next attack algorithm. + /// + /// @param game_object_id - The ID of the game object to select the previous attack for. + /// @throws RegistryError - If the game object does not exist or does not have an attack component. + inline void next_attack(const GameObjectID game_object_id) const { + auto attacks{get_registry()->get_component(game_object_id)}; + if (!attacks->attack_algorithms.empty() && attacks->attack_state < attacks->attack_algorithms.size() - 1) { + attacks->attack_state++; + } + } +}; + +/// Provides facilities to damage game objects. +struct DamageSystem : public SystemBase { + /// Initialise the object. + /// + /// @param registry - The registry that manages the game objects, components, and systems. + explicit DamageSystem(Registry *registry) : SystemBase(registry) {} + + /// Deal damage to a game object. + /// + /// @param game_object_id - The game object ID to deal damage to. + /// @param damage - The amount of damage to deal to the game object. + /// @throws RegistryError - If the game object does not exist or does not have health and armour components. + void deal_damage(GameObjectID game_object_id, int damage) const; +}; diff --git a/src/hades_extensions/include/game_objects/systems/effects.hpp b/src/hades_extensions/include/game_objects/systems/effects.hpp new file mode 100644 index 00000000..5b1da587 --- /dev/null +++ b/src/hades_extensions/include/game_objects/systems/effects.hpp @@ -0,0 +1,134 @@ +// Ensure this file is only included once +#pragma once + +// Local headers +#include + +#include "game_objects/registry.hpp" + +// ----- ENUMS ------------------------------ +/// Stores the different types of status effects available. +enum class StatusEffectType { + TEMP, + TEMP2, +}; + +// ----- STRUCTURES ------------------------------ +/// Represents a status effect that can be applied to a game object. +struct StatusEffect { + /// The value that should be applied to the game object temporarily. + double value; + + /// The duration the status effect should be applied for. + double duration; + + /// The interval the status effect should be applied at. + double interval; + + /// The component the status effect should be applied to. + std::type_index target_component; + + /// Tracks the time the status effect has been applied for. + double time_counter{0}; + + /// Tracks the time left over from the last interval. + double leftover_time{0}; + + /// Initialise the object. + /// + /// @param value - The value that should be applied to the game object temporarily. + /// @param duration - The duration the status effect should be applied for. + /// @param interval - The interval the status effect should be applied at. + /// @param target_component - The component the status effect should be applied to. + StatusEffect(const double value, const double duration, const double interval, + const std::type_index &target_component) + : value(value), duration(duration), interval(interval), target_component(target_component) {} +}; + +// ----- COMPONENTS ------------------------------ +/// Represents the data required to apply a status effect. +struct StatusEffectData : public ComponentBase { + /// The type of status effect. + StatusEffectType status_effect_type; + + /// The increase function to apply. + ActionFunction increase; + + /// The duration function to apply. + ActionFunction duration; + + /// The interval function to apply. + ActionFunction interval; + + /// Initialise the object. + /// + /// @param status_effect_type - The type of status effect. + /// @param increase - The increase function to apply. + /// @param duration - The duration function to apply. + /// @param interval - The interval function to apply. + StatusEffectData(const StatusEffectType &status_effect_type, ActionFunction increase, ActionFunction duration, + ActionFunction interval) + : status_effect_type(status_effect_type), + increase(std::move(increase)), + duration(std::move(duration)), + interval(std::move(interval)) {} +}; + +/// Allows a game object to provide instant or status effects. +struct EffectApplier : public ComponentBase { + /// The instant effects the game object provides. + std::unordered_map instant_effects; + + /// The status effects the game object provides. + std::unordered_map status_effects; + + /// Initialise the object. + /// + /// @param instant_effects - The instant effects the game object provides. + /// @param status_effects - The status effects the game object provides. + EffectApplier(const std::unordered_map &instant_effects, + const std::unordered_map &status_effects) + : instant_effects(instant_effects), status_effects(status_effects) {} +}; + +/// Allows a game object to have status effects applied to it. +struct StatusEffects : public ComponentBase { + /// The status effects currently applied to the game object. + std::unordered_map applied_effects{}; +}; + +// ----- SYSTEMS ------------------------------ +/// Provides facilities to manipulate instant and status effects. +struct EffectSystem : public SystemBase { + /// Initialise the object. + /// + /// @param registry - The registry that manages the game objects, components, and systems. + explicit EffectSystem(Registry *registry) : SystemBase(registry) {} + + /// Process update logic for a status effect component. + /// + /// @param delta_time - The time interval since the last time the function was called. + void update(double delta_time) const final; + + /// Apply an instant effect to a game object. + /// + /// @param game_object_id - The ID of the game object to apply the effect to. + /// @param target_component - The component to apply the effect to. + /// @param increase_function - The increase function to apply. + /// @param level - The level of the effect to apply. + /// @throws RegistryError - If the game object does not exist or does not have the target component. + /// @return Whether the instant effect was applied or not. + auto apply_instant_effect(GameObjectID game_object_id, const std::type_index &target_component, + const ActionFunction &increase_function, int level) -> bool; + + /// Apply a status effect to a game object. + /// + /// @param game_object_id - The ID of the game object to apply the effect to. + /// @param target_component - The component to apply the effect to. + /// @param status_effect_data - The data required to apply the status effect. + /// @param level - The level of the effect to apply. + /// @throws RegistryError - If the game object does not exist or does not have the target component. + /// @return Whether the status effect was applied or not. + auto apply_status_effect(GameObjectID game_object_id, const std::type_index &target_component, + const StatusEffectData &status_effect_data, int level) -> bool; +}; diff --git a/src/hades_extensions/include/game_objects/systems/inventory.hpp b/src/hades_extensions/include/game_objects/systems/inventory.hpp new file mode 100644 index 00000000..f7a7838d --- /dev/null +++ b/src/hades_extensions/include/game_objects/systems/inventory.hpp @@ -0,0 +1,70 @@ +// Ensure this file is only included once +#pragma once + +// Local headers +#include "game_objects/registry.hpp" + +// ----- EXCEPTIONS ------------------------------ +/// Thrown when there is a space problem with the inventory. +struct InventorySpaceError : public std::runtime_error { + /// Initialise the object. + /// + /// @param message - The message to display. + explicit InventorySpaceError(const char *message) : std::runtime_error(message) {} + + /// Initialise the object. + /// + /// @param full - Whether the inventory is full or not. + explicit InventorySpaceError(const bool full) + : std::runtime_error(std::string("The inventory is ") + (full ? "full" : "empty") + ".") {} +}; + +// ----- COMPONENTS ------------------------------ +/// Allows a game object to have a fixed size inventory. +struct Inventory : public ComponentBase { + /// The width of the inventory. + int width; + + /// The height of the inventory. + int height; + + /// The game object's inventory. + std::vector items{}; + + /// Initialise the object. + /// + /// @param width - The width of the inventory. + /// @param height - The height of the inventory. + Inventory(const int width, const int height) : width(width), height(height) {} + + /// Get the capacity of the inventory. + /// + /// @return The capacity of the inventory. + [[nodiscard]] inline auto get_capacity() const -> int { return width * height; } +}; + +// ----- SYSTEMS -------------------------------- +/// Provides facilities to manipulate inventory components. +struct InventorySystem : public SystemBase { + /// Initialise the object. + /// + /// @param registry - The registry that manages the game objects, components, and systems. + explicit InventorySystem(Registry *registry) : SystemBase(registry) {} + + /// Add an item to the inventory of a game object. + /// + /// @param game_object_id - The ID of the game object to add the item to. + /// @param item - The item to add to the inventory. + /// @throws RegistryError - If the game object does not exist or does not have an inventory component. + /// @throws InventorySpaceError - If the inventory is full. + void add_item_to_inventory(GameObjectID game_object_id, GameObjectID item) const; + + /// Remove an item from the inventory of a game object. + /// + /// @param game_object_id - The ID of the game object to remove the item from. + /// @param index - The index of the item to remove from the inventory. + /// @throws RegistryError - If the game object does not exist or does not have an inventory component. + /// @throws InventorySpaceError - If the inventory is empty or the index is out of bounds. + /// @return The item that was removed from the inventory. + [[nodiscard]] auto remove_item_from_inventory(GameObjectID game_object_id, int index) const -> int; +}; diff --git a/src/hades_extensions/include/game_objects/systems/movements.hpp b/src/hades_extensions/include/game_objects/systems/movements.hpp new file mode 100644 index 00000000..d77a8c53 --- /dev/null +++ b/src/hades_extensions/include/game_objects/systems/movements.hpp @@ -0,0 +1,125 @@ +// Ensure this file is only included once +#pragma once + +// Std headers +#include + +// Local headers +#include "game_objects/registry.hpp" + +// ----- ENUMS ------------------------------ +/// Stores the different types of steering behaviours available. +enum class SteeringBehaviours { + Arrive, + Evade, + Flee, + FollowPath, + ObstacleAvoidance, + Pursue, + Seek, + Wander, +}; + +/// Stores the different states the steering movement component can be in. +enum class SteeringMovementState { + Default, + Footprint, + Target, +}; + +// ----- COMPONENTS ------------------------------ +/// Allows a game object to periodically leave footprints around the game map. +struct Footprints : public ComponentBase { + /// The footprints the game object has left. + std::deque footprints{}; + + /// The time since the game object last left a footprint. + double time_since_last_footprint{0}; +}; + +/// Allows a game object's movement to be controlled by the keyboard. +struct KeyboardMovement : public ComponentBase { + /// Whether the game object is moving north or not. + bool moving_north{false}; + + /// Whether the game object is moving east or not. + bool moving_east{false}; + + /// Whether the game object is moving south or not. + bool moving_south{false}; + + /// Whether the game object is moving west or not. + bool moving_west{false}; +}; + +/// Allows a game object's movement to be controlled by steering behaviours. +struct SteeringMovement : public ComponentBase { + /// The steering behaviours used by the game object. + std::unordered_map> behaviours; + + /// The current movement state of the game object. + SteeringMovementState movement_state{SteeringMovementState::Default}; + + /// The game object ID of the target. + int target_id{-1}; + + /// The list of positions the game object should follow. + std::vector path_list{}; + + /// Initialise the object. + /// + /// @param behaviours - The steering behaviours used by the game object. + explicit SteeringMovement( + const std::unordered_map> &behaviours) + : behaviours(behaviours) {} +}; + +// ----- SYSTEMS ------------------------------ +/// Provides facilities to manipulate footprint components. +struct FootprintSystem : public SystemBase { + /// Initialise the object. + /// + /// @param registry - The registry that manages the game objects, components, and systems. + explicit FootprintSystem(Registry *registry) : SystemBase(registry) {} + + /// Process update logic for a footprint component. + /// + /// @param delta_time - The time interval since the last time the function was called. + void update(double delta_time) const final; +}; + +/// Provides facilities to manipulate keyboard movement components. +struct KeyboardMovementSystem : public SystemBase { + /// Initialise the object. + /// + /// @param registry - The registry that manages the game objects, components, and systems. + explicit KeyboardMovementSystem(Registry *registry) : SystemBase(registry) {} + + /// Calculate the new keyboard force to apply to the game object. + /// + /// @param game_object_id - The ID of the game object to calculate the keyboard force for. + /// @throws RegistryError - If the game object does not exist or does not have a keyboard movement component. + /// @return The new force to apply to the game object. + [[nodiscard]] auto calculate_force(GameObjectID game_object_id) const -> Vec2d; +}; + +/// Provides facilities to manipulate steering movement components. +struct SteeringMovementSystem : public SystemBase { + /// Initialise the object. + /// + /// @param registry - The registry that manages the game objects, components, and systems. + explicit SteeringMovementSystem(Registry *registry) : SystemBase(registry) {} + + /// Calculate the new steering force to apply to the game object. + /// + /// @param game_object_id - The ID of the game object to calculate the steering force for. + /// @throws RegistryError - If the game object does not exist or does not have a steering movement component. + /// @return The new force to apply to the game object. + [[nodiscard]] auto calculate_force(GameObjectID game_object_id) const -> Vec2d; + + /// Update the path lists for the game objects to follow. + /// + /// @param target_game_object_id - The ID of the game object to follow. + /// @param footprints - The list of footprints to follow. + void update_path_list(GameObjectID target_game_object_id, const std::deque &footprints) const; +}; diff --git a/src/hades_extensions/include/game_objects/systems/upgrade.hpp b/src/hades_extensions/include/game_objects/systems/upgrade.hpp new file mode 100644 index 00000000..e9baf004 --- /dev/null +++ b/src/hades_extensions/include/game_objects/systems/upgrade.hpp @@ -0,0 +1,46 @@ +// Ensure this file is only included once +#pragma once + +// Local headers +#include "game_objects/registry.hpp" + +// ----- COMPONENTS ------------------------------ +/// Allows a game object to record the amount of money it has. +struct Money : public ComponentBase { + /// The amount of money the game object has. + int money; + + /// Initialise the object. + /// + /// @param money - The amount of money the game object has. + explicit Money(const int money) : money(money) {} +}; + +/// Allows a game object to be upgraded. +struct Upgrades : public ComponentBase { + /// The upgrades the game object has. + std::unordered_map upgrades; + + /// Initialise the object. + /// + /// @param upgrades - The upgrades the game object has. + explicit Upgrades(const std::unordered_map &upgrades) : upgrades(upgrades) {} +}; + +// ----- SYSTEMS -------------------------------- +/// Provides facilities to manipulate game object upgrades. +struct UpgradeSystem : public SystemBase { + /// Initialise the object. + /// + /// @param registry - The registry that manages the game objects, components, and systems. + explicit UpgradeSystem(Registry *registry) : SystemBase(registry) {} + + /// Upgrade a component to the next level if possible. + /// + /// @param game_object_id - The ID of the game object to upgrade the component for. + /// @param target_component - The type of component to upgrade. + /// @throws RegistryError if the game object does not exist or does not have the target component. + /// @return Whether the component upgrade was successful or not. + [[nodiscard]] auto upgrade_component(GameObjectID game_object_id, const std::type_index &target_component) const + -> bool; +}; diff --git a/src/hades_extensions/include/generation/astar.hpp b/src/hades_extensions/include/generation/astar.hpp new file mode 100644 index 00000000..fa3b5619 --- /dev/null +++ b/src/hades_extensions/include/generation/astar.hpp @@ -0,0 +1,17 @@ +// Ensure this file is only included once +#pragma once + +// Local headers +#include "primitives.hpp" + +// ----- FUNCTIONS ------------------------------ +/// Calculate the shortest path in a grid from one pair to another using the A* +/// algorithm. +/// +/// @details https://en.wikipedia.org/wiki/A%2A_search_algorithm +/// @param grid - The 2D grid which represents the dungeon. +/// @param start - The start position for the algorithm. +/// @param end - The end position for the algorithm. +/// @throws std::length_error - Grid size must be bigger than 0. +/// @return A vector of positions mapping out the shortest path from start to end. +auto calculate_astar_path(const Grid &grid, const Position &start, const Position &end) -> std::vector; diff --git a/src/hades_extensions/include/generation/bsp.hpp b/src/hades_extensions/include/generation/bsp.hpp new file mode 100644 index 00000000..ac2cafa4 --- /dev/null +++ b/src/hades_extensions/include/generation/bsp.hpp @@ -0,0 +1,44 @@ +// Ensure this file is only included once +#pragma once + +// Std headers +#include + +// Local headers +#include "primitives.hpp" + +// ----- STRUCTURES ------------------------------ +/// A binary spaced partition leaf used to generate the dungeon's rooms. +struct Leaf { + /// The rect object that represents this leaf. + std::unique_ptr container; + + /// The left container of this leaf. If this is null, we have reached the end of the branch. + std::unique_ptr left{}; + + /// The right container of this leaf. If this is null, we have reached the end of the branch. + std::unique_ptr right{}; + + /// The rect object for representing the room inside this leaf. + std::unique_ptr room{}; + + /// Initialise the object. + /// + /// @param container - The rect object that represents this leaf. + explicit Leaf(const Rect &container) : container(std::make_unique(container)) {} +}; + +// ----- FUNCTIONS ------------------------------- +/// Split a leaf either horizontally or vertically recursively. +/// +/// @param leaf - The leaf to split. +/// @param random_generator - The random generator to use. +void split(Leaf &leaf, std::mt19937 &random_generator); + +/// Create a random sized room inside a container. +/// +/// @param leaf - The leaf to create a room inside of. +/// @param grid - The 2D grid which represents the dungeon. +/// @param random_generator - The random generator to use. +/// @param rooms - The vector of rooms to add the new room to. +void create_room(Leaf &leaf, Grid &grid, std::mt19937 &random_generator, std::vector &rooms); diff --git a/src/hades_extensions/include/generation/map.hpp b/src/hades_extensions/include/generation/map.hpp new file mode 100644 index 00000000..5e4a30b1 --- /dev/null +++ b/src/hades_extensions/include/generation/map.hpp @@ -0,0 +1,91 @@ +// Ensure this file is only included once +#pragma once + +// Std headers +#include +#include +#include + +// Local headers +#include "bsp.hpp" + +// ----- STRUCTURES ------------------------------ +/// Represents an undirected weighted edge in a graph. +struct Edge { + // std::priority_queue uses a max heap, but we want a min heap, so the operator needs to be reversed + inline auto operator<(const Edge &edge) const -> bool { return cost > edge.cost; } + + inline auto operator==(const Edge &edge) const -> bool { + return cost == edge.cost && source == edge.source && destination == edge.destination; + } + + /// The cost of the edge. + int cost; + + /// The source rect. + Rect source; + + /// The destination rect. + Rect destination; +}; + +// ----- HASHES ------------------------------ +template <> +struct std::hash { + auto operator()(const Edge &edge) const -> size_t { + size_t res{0}; + hash_combine(res, edge.cost); + hash_combine(res, edge.source); + hash_combine(res, edge.destination); + return res; + } +}; + +// ----- FUNCTIONS ------------------------------ +/// Collect all positions in a given grid that match the target. +/// +/// @param grid - The 2D grid which represents the dungeon. +/// @param target - The TileType to test for. +/// @return A vector of positions which match the target. +auto collect_positions(const Grid &grid, TileType target) -> std::vector; + +/// Places a given tile in the 2D grid. +/// +/// @param grid - The 2D grid which represents the dungeon. +/// @param random_generator - The random generator used to pick the position. +/// @param target_tile - The tile to place in the 2D grid. +/// @param possible_tiles - The possible tiles that the tile can be placed into. +/// @throws std::length_error - Possible tiles size must be bigger than 0. +void place_tile(Grid &grid, std::mt19937 &random_generator, TileType target_tile, + std::vector &possible_tiles); + +/// Create a complete graph from a given list of rooms. +/// +/// @param rooms - The rooms to create connections between. +/// @throws std::length_error - Rooms size must be bigger than 0. +/// @return A adjacency list of all the rooms and their neighbours. +auto create_complete_graph(const std::vector &rooms) -> std::unordered_map>; + +/// Create a minimum spanning tree from a given complete graph. +/// +/// @details https://en.wikipedia.org/wiki/Prim%27s_algorithm +/// @param complete_graph - An adjacency list which represents a complete graph. This should not be empty. +/// @return A set of edges which form the connections between rects. +auto create_connections(const std::unordered_map> &complete_graph) -> std::unordered_set; + +/// Create the hallways by placing random obstacles and pathfinding around them. +/// +/// @param grid - The 2D grid which represents the dungeon. +/// @param random_generator - The random generator used to pick the obstacle positions. +/// @param connections - The connections to pathfind using the A* algorithm. +/// @param obstacle_count - The number of obstacles to place in the 2D grid. +void create_hallways(Grid &grid, std::mt19937 &random_generator, const std::unordered_set &connections, + int obstacle_count); + +/// Generate the game map for a given game level. +/// +/// @param level - The game level to generate a map for. +/// @param seed - The seed to initialise the random generator. If this is empty then one will be generated. +/// @return A tuple containing the generated map and the level constants. +auto create_map(int level, std::optional seed = std::nullopt) + -> std::pair, std::tuple>; diff --git a/src/hades_extensions/include/generation/primitives.hpp b/src/hades_extensions/include/generation/primitives.hpp new file mode 100644 index 00000000..934ba1a8 --- /dev/null +++ b/src/hades_extensions/include/generation/primitives.hpp @@ -0,0 +1,159 @@ +// Ensure this file is only included once +#pragma once + +// Std headers +#include +#include +#include +#include + +// Local headers +#include "hash_combine.hpp" + +// ----- ENUMS ------------------------------ +/// Stores the different types of tiles in the game map. +enum class TileType { + Empty, + Floor, + Wall, + Obstacle, + Player, + Potion, +}; + +// ----- STRUCTURES ------------------------------ +/// Represents a 2D position. +struct Position { + inline auto operator==(const Position &position) const -> bool { return x == position.x && y == position.y; } + + inline auto operator!=(const Position &position) const -> bool { return x != position.x || y != position.y; } + + inline auto operator+(const Position &position) const -> Position { return {x + position.x, y + position.y}; } + + inline auto operator-(const Position &position) const -> Position { + return {std::abs(x - position.x), std::abs(y - position.y)}; + } + + /// The x position of the position. + int x; + + /// The y position of the position. + int y; +}; + +/// Represents a 2D grid with a set width and height through a 1D vector. +struct Grid { + /// The width of the 2D grid. + int width; + + /// The height of the 2D grid. + int height; + + /// The vector which represents the 2D grid. + std::unique_ptr> grid; + + /// Initialise the object. + /// + /// @param width - The width of the 2D grid. + /// @param height - The height of the 2D grid. + Grid(const int width, const int height) + : width(width), height(height), grid(std::make_unique>(width * height, TileType::Empty)) {} + + /// Convert a 2D grid position to a 1D grid position. + /// + /// @param pos - The position to convert. + /// @throws std::out_of_range - Position must be within range. + /// @return The 1D grid position. + [[nodiscard]] inline auto convert_position(const Position &pos) const -> int { + if (pos.x < 0 || pos.x >= width || pos.y < 0 || pos.y >= height) { + throw std::out_of_range("Position must be within range"); + } + return width * pos.y + pos.x; + } + + /// Get a value in the 2D grid from a given position. + /// + /// @param pos - The position to get the value for. + /// @throws std::out_of_range - Position must be within range. + /// @return The value at the given position. + [[nodiscard]] inline auto get_value(const Position &pos) const -> TileType { return grid->at(convert_position(pos)); } + + /// Set a value in the 2D grid from a given position. + /// + /// @param pos - The position to set. + /// @throws std::out_of_range - Position must be within range. + inline void set_value(const Position &pos, const TileType target) const { grid->at(convert_position(pos)) = target; } +}; + +/// Represents a rectangle in 2D space. +struct Rect { + inline auto operator==(const Rect &rect) const -> bool { + return top_left == rect.top_left && bottom_right == rect.bottom_right; + } + + inline auto operator!=(const Rect &rect) const -> bool { + return top_left != rect.top_left || bottom_right != rect.bottom_right; + } + + /// The top left position of the rect. + Position top_left; + + /// The bottom right position of the rect. + Position bottom_right; + + /// The centre position of the rect. + Position centre; + + /// The width of the rect. + int width; + + /// The height of the rect. + int height; + + /// Initialise the object. + /// + /// @param top_left - The top left position of the rect. + /// @param bottom_right - The bottom right position of the rect. + Rect(const Position &top_left, const Position &bottom_right) + : top_left(top_left), + bottom_right(bottom_right), + centre(static_cast(std::round((top_left + bottom_right).x / 2.0)), // NOLINT + static_cast(std::round((top_left + bottom_right).y / 2.0))), // NOLINT + width((top_left - bottom_right).x), + height((top_left - bottom_right).y) {} + + /// Get the Chebyshev distance to another rect. + /// + /// @param other - The rect to find the distance to. + /// @return The Chebyshev distance between this rect and the given rect. + [[nodiscard]] inline auto get_distance_to(const Rect &other) const -> int { + return std::max(abs(centre.x - other.centre.x), abs(centre.y - other.centre.y)); + } + + /// Place the rect in the 2D grid. + /// + /// @details It is the responsibility of the caller to ensure that the rect fits in the grid. + /// @param grid - The 2D grid which represents the dungeon. + void place_rect(Grid &grid) const; +}; + +// ----- HASHES ------------------------------ +template <> +struct std::hash { + auto operator()(const Position &position) const -> std::size_t { + std::size_t res{0}; + hash_combine(res, position.x); + hash_combine(res, position.y); + return res; + } +}; + +template <> +struct std::hash { + auto operator()(const Rect &rect) const -> std::size_t { + std::size_t res{0}; + hash_combine(res, rect.top_left); + hash_combine(res, rect.bottom_right); + return res; + } +}; diff --git a/src/hades_extensions/include/hash_combine.hpp b/src/hades_extensions/include/hash_combine.hpp new file mode 100644 index 00000000..4cef6937 --- /dev/null +++ b/src/hades_extensions/include/hash_combine.hpp @@ -0,0 +1,13 @@ +// Ensure this file is only included once +#pragma once + +// ----- FUNCTIONS ------------------------------ +/// Allows multiple hashes to be combined for a struct +/// +/// @param seed - The seed for initialising the hasher. +/// @param value - The value to hash. +template +inline void hash_combine(size_t &seed, const T &value) { + std::hash hasher; + seed ^= hasher(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2); // NOLINT +} diff --git a/src/hades_extensions/include/macros.hpp b/src/hades_extensions/include/macros.hpp new file mode 100644 index 00000000..7c6d0bac --- /dev/null +++ b/src/hades_extensions/include/macros.hpp @@ -0,0 +1,19 @@ +// Ensure this file is only included once +#pragma once + +// External headers +#include "gtest/gtest.h" + +// ----- MACROS ------------------------------ +/// Assert that a statement throws an exception of a given type with a given message. +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define ASSERT_THROW_MESSAGE(statement, exception_type, expected_msg) \ + try { \ + static_cast(statement); \ + ADD_FAILURE() << "Expected exception of type " #exception_type; \ + } catch (const exception_type &e) { \ + ASSERT_STREQ(e.what(), expected_msg); \ + SUCCEED(); \ + } catch (...) { \ + ADD_FAILURE() << "Expected exception of type " #exception_type; \ + } diff --git a/src/hades_extensions/src/CMakeLists.txt b/src/hades_extensions/src/CMakeLists.txt new file mode 100644 index 00000000..844e44bf --- /dev/null +++ b/src/hades_extensions/src/CMakeLists.txt @@ -0,0 +1,34 @@ +# Fetch the pybind11 repository and initialise it +find_package(PythonInterp REQUIRED) +FetchContent_Declare( + pybind11_source + GIT_REPOSITORY https://github.com/pybind/pybind11.git + GIT_TAG v2.10.3 +) +FetchContent_GetProperties(pybind11_source) +FetchContent_Populate(pybind11_source) +add_subdirectory( + ${pybind11_source_SOURCE_DIR} + ${pybind11_source_BINARY_DIR} +) + +# Create the C++ library which will be used for the tests and the Python module +add_library(${CPP_LIB} STATIC + ${CMAKE_SOURCE_DIR}/src/game_objects/registry.cpp + ${CMAKE_SOURCE_DIR}/src/game_objects/steering.cpp + ${CMAKE_SOURCE_DIR}/src/game_objects/systems/armour_regen.cpp + ${CMAKE_SOURCE_DIR}/src/game_objects/systems/attacks.cpp + ${CMAKE_SOURCE_DIR}/src/game_objects/systems/effects.cpp + ${CMAKE_SOURCE_DIR}/src/game_objects/systems/inventory.cpp + ${CMAKE_SOURCE_DIR}/src/game_objects/systems/movements.cpp + ${CMAKE_SOURCE_DIR}/src/game_objects/systems/upgrade.cpp + ${CMAKE_SOURCE_DIR}/src/generation/astar.cpp + ${CMAKE_SOURCE_DIR}/src/generation/bsp.cpp + ${CMAKE_SOURCE_DIR}/src/generation/map.cpp + ${CMAKE_SOURCE_DIR}/src/generation/primitives.cpp +) +target_include_directories(${CPP_LIB} PUBLIC ${CMAKE_SOURCE_DIR}/include) + +# Create the Python module +pybind11_add_module(${PY_MODULE} ${CMAKE_SOURCE_DIR}/src/binding.cpp) +target_link_libraries(${PY_MODULE} PRIVATE ${CPP_LIB}) diff --git a/src/hades_extensions/src/binding.cpp b/src/hades_extensions/src/binding.cpp new file mode 100644 index 00000000..dfca307d --- /dev/null +++ b/src/hades_extensions/src/binding.cpp @@ -0,0 +1,727 @@ +// Std headers +#include + +// External headers +#include +#include +#include + +// Local headers +#include "game_objects/stats.hpp" +#include "game_objects/steering.hpp" +#include "game_objects/systems/armour_regen.hpp" +#include "game_objects/systems/attacks.hpp" +#include "game_objects/systems/effects.hpp" +#include "game_objects/systems/inventory.hpp" +#include "game_objects/systems/movements.hpp" +#include "game_objects/systems/upgrade.hpp" +#include "generation/map.hpp" + +// ----- STRUCTURES ------------------------------------------ +/// The hash function for a pybind11 handle. +struct py_handle_hash { + /// Calculate the hash of a pybind11 handle. + /// + /// @param handle The handle to calculate the hash of. + /// @return The hash of the handle. + auto operator()(const pybind11::handle &handle) const noexcept -> std::size_t { return pybind11::hash(handle); } + + /// Check if two pybind11 handles are equal. + /// + /// @param handle_one The first handle to compare. + /// @param handle_two The second handle to compare. + /// @return Whether the two handles are equal or not. + auto operator()(const pybind11::handle &handle_one, const pybind11::handle &handle_two) const noexcept -> bool { + return handle_one.is(handle_two); + } +}; + +// ----- FUNCTIONS ------------------------------------------- +/// Get the system from the registry. +/// +/// @tparam T The type of system to find. +/// @param registry The registry that manages the game objects, components, and systems. +/// @throws RegistryError If the system is not registered. +/// @return The system from the registry. +template +auto get_system_impl(const Registry ®istry) -> std::shared_ptr { + return registry.get_system(); +} + +/// Make the component types mapping. +/// +/// @return The component types mapping. +template +auto make_component_types() -> std::unordered_map { + return {{pybind11::type::of(), typeid(Ts)}...}; +} + +/// Make the system types mapping +/// +/// @return The system types mapping. +template +auto make_system_types() + -> std::unordered_map(const Registry &)>, + py_handle_hash> { + return {{pybind11::type::of(), get_system_impl}...}; +} + +/// Get the type index for a given component type. +/// +/// @param component_type The component type. +/// @throws std::runtime_error If the component type is invalid. +/// @return The type index for the component type. +inline auto get_type_index(const pybind11::handle &component_type) -> std::type_index { + static const auto &component_types = + make_component_types(); + auto iter = component_types.find(component_type); + if (iter == component_types.end()) { + throw std::runtime_error("Invalid component type provided."); + } + return iter->second; +} + +// ----- PYTHON MODULE CREATION ------------------------------ +PYBIND11_MODULE(hades_extensions, module) { // NOLINT + // Add the module docstring and the custom converters + module.doc() = "Manages the various C++ extension modules for the game."; + + // Create the generation module + pybind11::module generation = + module.def_submodule("generation", "Generates the dungeon and places game objects in it."); + generation.def("create_map", &create_map, pybind11::arg("level"), pybind11::arg("seed") = pybind11::none(), + ("Generate the game map for a given game level.\n\n" + "Args:\n" + " level: The game level to generate a map for.\n" + " seed: The seed to initialise the random generator.\n\n" + "Returns:\n" + " A tuple containing the generated map and the level constants.")); + pybind11::enum_(generation, "TileType") + .value("Empty", TileType::Empty) + .value("Floor", TileType::Floor) + .value("Wall", TileType::Wall) + .value("Obstacle", TileType::Obstacle) + .value("Player", TileType::Player) + .value("Potion", TileType::Potion); + + // Create the game objects, game_objects/systems, and game_objects/components modules + pybind11::module game_objects = module.def_submodule( + "game_objects", "Contains the registry and the various components and systems that can be used with it."); + const pybind11::module systems = + game_objects.def_submodule("systems", "Contains the systems which manage the game objects."); + const pybind11::module components = + game_objects.def_submodule("components", "Contains the components which can be added to game objects."); + + // Add the global constants and the base classes + game_objects.attr("SPRITE_SCALE") = SPRITE_SCALE; + game_objects.attr("SPRITE_SIZE") = SPRITE_SIZE; + game_objects.def( + "ActionFunction", + [](const ActionFunction &func) { return pybind11::cpp_function([func](int level) { return func(level); }); }, + "A function that can be applied to a component."); + const pybind11::class_> component_base( + game_objects, "ComponentBase", "The base class for all components."); + pybind11::class_>(game_objects, "SystemBase", + "The base class for all systems.") + .def(pybind11::init(), pybind11::arg("registry"), + ("Initialise the object.\n\n" + "Args:\n" + " registry: The registry that manages the game objects, components, and systems.")) + .def("get_registry", &SystemBase::get_registry, + ("Get the registry that manages the game objects, components, and systems.\n\n" + "Returns:\n" + " The registry.")) + .def("update", &SystemBase::update, pybind11::arg("delta_time"), + ("Process update logic for a system.\n\n" + "Args:\n" + " delta_time: The time interval since the last time the function was called.")); + + // Add the steering structures + pybind11::class_(game_objects, "Vec2d", "Represents a 2D vector.") + .def(pybind11::init(), pybind11::arg("x"), pybind11::arg("y"), + ("Initialise the object.\n\n" + "Args:\n" + " x: The x value of the vector.\n" + " y: The y value of the vector.")) + .def_readwrite("x", &Vec2d::x) + .def_readwrite("y", &Vec2d::y) + .def("__eq__", &Vec2d::operator==, + ("Check if this vector is equal to another vector.\n\n" + "Args:\n" + " other: The other vector to compare to.\n\n" + "Returns:\n" + " Whether the vectors are equal or not.")) + .def("__ne__", &Vec2d::operator!=, + ("Check if this vector is not equal to another vector.\n\n" + "Args:\n" + " other: The other vector to compare to.\n\n" + "Returns:\n" + " Whether the vectors are not equal or not.")) + .def("__add__", &Vec2d::operator+, + ("Add another vector to this vector.\n\n" + "Args:\n" + " other: The other vector to add.\n\n" + "Returns:\n" + " The sum of the two vectors.")) + .def("__iadd__", &Vec2d::operator+=, + ("Add another vector to this vector.\n\n" + "Args:\n" + " other: The other vector to add.\n\n" + "Returns:\n" + " This vector.")) + .def("__sub__", &Vec2d::operator-, + ("Subtract another vector from this vector.\n\n" + "Args:\n" + " other: The other vector to subtract.\n\n" + "Returns:\n" + " The difference of the two vectors.")) + .def("__mul__", &Vec2d::operator*, + ("Multiply this vector by a scalar.\n\n" + "Args:\n" + " scalar: The scalar to multiply by.\n\n" + "Returns:\n" + " The product of the vector and the scalar.")) + .def("__truediv__", &Vec2d::operator/, + ("Divide this vector by a scalar.\n\n" + "Args:\n" + " scalar: The scalar to divide by.\n\n" + "Returns:\n" + " The quotient of the vector and the scalar.")) + .def("magnitude", &Vec2d::magnitude, + ("Calculate the magnitude of the vector.\n\n" + "Returns:\n" + " The magnitude of the vector.")) + .def("normalised", &Vec2d::normalised, + ("Calculate the normalised vector.\n\n" + "Returns:\n" + " The normalised vector.")) + .def("rotated", &Vec2d::rotated, pybind11::arg("angle"), + ("Rotate the vector by an angle.\n\n" + "Args:\n" + " angle: The angle to rotate by.\n\n" + "Returns:\n" + " The rotated vector.")) + .def("angle_between", &Vec2d::angle_between, pybind11::arg("other"), + ("Calculate the angle between this vector and another vector.\n\n" + "Args:\n" + " other: The other vector to calculate the angle between.\n\n" + "Returns:\n" + " The angle between the two vectors.")) + .def("distance_to", &Vec2d::distance_to, pybind11::arg("other"), + ("Calculate the distance between this vector and another vector.\n\n" + "Args:\n" + " other: The other vector to calculate the distance between.\n\n" + "Returns:\n" + " The distance between the two vectors.")); + pybind11::class_>( + game_objects, "KinematicObject", "Stores various data about a game object for use in physics-related operations.") + .def_readwrite("position", &KinematicObject::position) + .def_readwrite("velocity", &KinematicObject::velocity) + .def_readwrite("rotation", &KinematicObject::rotation); + + // Add the registry class + register_exception(game_objects, "RegistryError"); + pybind11::class_ registry_class(game_objects, "Registry", + "Manages game objects, components, and systems that are registered."); + registry_class.def(pybind11::init<>(), "Initialise the object.") + .def("create_game_object", &Registry::create_game_object, pybind11::arg("components"), + pybind11::arg("kinematic") = false, + ("Create a new game object.\n\n" + "Args:\n" + " components: The components to add to the game object.\n" + " kinematic: Whether the game object should have a kinematic object or not.\n\n" + "Returns:\n" + " The game object ID.")) + .def("delete_game_object", &Registry::delete_game_object, pybind11::arg("game_object_id"), + ("Delete a game object.\n\n" + "Args:\n" + " game_object_id: The game object ID.\n\n" + "Raises:\n" + " RegistryError: If the game object is not registered.")) + .def( + "has_component", + [](const Registry ®istry, const GameObjectID game_object_id, const pybind11::handle &component_type) { + return registry.has_component(game_object_id, get_type_index(component_type)); + }, + pybind11::arg("game_object_id"), pybind11::arg("component_type"), + ("Checks if a game object has a given component or not.\n\n" + "Args:\n" + " game_object_id: The game object ID.\n" + " component_type: The type of component to check for.\n\n" + "Raises:\n" + " RuntimeError: If the component type is invalid.\n\n" + "Returns:\n" + " Whether the game object has the component or not.")) + .def( + "get_component", + [](const Registry ®istry, const GameObjectID game_object_id, const pybind11::handle &component_type) { + return registry.get_component(game_object_id, get_type_index(component_type)); + }, + pybind11::arg("game_object_id"), pybind11::arg("component_type"), + ("Get a component from the registry.\n\n" + "Args:\n" + " game_object_id: The game object ID.\n" + " component_type: The type of component to get.\n\n" + "Raises:\n" + " RegistryError: If the game object is not registered or if the game object does not have the " + "component." + " RuntimeError: If the component type is invalid.\n\n" + "Returns:\n" + " The component from the registry.")) + .def( + "add_systems", + [](Registry ®istry) { + registry.add_system(); + registry.add_system(); + registry.add_system(); + registry.add_system(); + registry.add_system(); + registry.add_system(); + registry.add_system(); + registry.add_system(); + registry.add_system(); + }, + ("Add all the systems into the registry.\n\n" + "Raises:\n" + " RegistryError: If one of the systems is already registered.")) + .def( + "get_system", + [](const Registry ®istry, const pybind11::object &system_type) { + // Get all the system types and check if the given system type exists + static const auto &system_types = + make_system_types(); + auto iter = system_types.find(system_type); + if (iter == system_types.end()) { + throw std::runtime_error("Invalid system type provided."); + } + + // Return the system from the registry + return iter->second(registry); + }, + pybind11::arg("system_type"), + ("Get a system from the registry.\n\n" + "Args:\n" + " system_type: The type of system to find.\n\n" + "Raises:\n" + " RegistryError: If the system type is not registered.\n" + " RuntimeError: If the system type is invalid..\n\n" + "Returns:\n" + " The system from the registry.")) + .def("update", &Registry::update, pybind11::arg("delta_time"), + ("Update all systems in the registry.\n\n" + "Args:\n" + " delta_time: The time interval since the last time the function was called.")) + .def("get_kinematic_object", &Registry::get_kinematic_object, pybind11::arg("game_object_id"), + ("Get the kinematic object for a game object.\n\n" + "Args:\n" + " game_object_id: The game object ID.\n\n" + "Raises:\n" + " RegistryError: If the game object is not registered or if the game object does not have a " + "kinematic object.\n\n" + "Returns:\n" + " The kinematic object.")) + .def("add_wall", &Registry::add_wall, pybind11::arg("wall"), + ("Add a wall to the registry.\n\n" + "Args:\n" + " wall: The wall to add to the registry.")) + .def("get_walls", &Registry::get_walls, + ("Get the walls from the registry.\n\n" + "Returns:\n" + " The walls in the registry.")); + + // Add the stat components + pybind11::class_>( + game_objects, "Stat", "Represents a component that has a variable value and maximum value.") + .def(pybind11::init(), pybind11::arg("value"), pybind11::arg("maximum_level"), + ("Initialise the object.\n\n" + "Args:\n" + " value: The initial and maximum value of the stat.\n" + " maximum_level: The maximum level of the stat.")) + .def("get_value", &Stat::get_value, + ("Get the value of the stat.\n\n" + "Returns:\n" + " The value of the stat.")) + .def("set_value", &Stat::set_value, + ("Set the value of the stat.\n\n" + "Args:\n" + " new_value: The new value of the stat.")) + .def("get_max_value", &Stat::get_max_value, + ("Get the maximum value of the stat.\n\n" + "Returns:\n" + " The maximum value of the stat.")) + .def("add_to_max_value", &Stat::add_to_max_value, + ("Add a value to the maximum value of the stat.\n\n" + "Args:\n" + " value: The value to add to the maximum value of the stat.")) + .def("get_current_level", &Stat::get_current_level, + ("Get the current level of the stat.\n\n" + "Returns:\n" + " The current level of the stat.")) + .def("increment_current_level", &Stat::increment_current_level, ("Increment the current level of the stat.")) + .def("get_maximum_level", &Stat::get_maximum_level, + ("Get the maximum level of the stat.\n\n" + "Returns:\n" + " The maximum level of the stat.")); + pybind11::class_>(components, "Armour", + "Allows a game object to have an armour stat.") + .def(pybind11::init(), pybind11::arg("value"), pybind11::arg("maximum_level"), + ("Initialise the object.\n\n" + "Args:\n" + " value: The initial and maximum value of the armour stat.\n" + " maximum_level: The maximum level of the armour stat.")); + pybind11::class_>(components, "ArmourRegen", + "Allows a game object to regenerate armour.") + .def(pybind11::init(), pybind11::arg("value"), pybind11::arg("maximum_level"), + ("Initialise the object.\n\n" + "Args:\n" + " value: The initial and maximum value of the armour regen stat.\n" + " maximum_level: The maximum level of the armour regen stat.")) + .def_readwrite("time_since_armour_regen", &ArmourRegen::time_since_armour_regen); + pybind11::class_>(components, "Health", + "Allows a game object to have a health stat.") + .def(pybind11::init(), pybind11::arg("value"), pybind11::arg("maximum_level"), + ("Initialise the object.\n\n" + "Args:\n" + " value: The initial and maximum value of the health stat.\n" + " maximum_level: The maximum level of the health stat.")); + pybind11::class_>( + components, "MovementForce", "Allows a game object to determine how fast it can move.") + .def(pybind11::init(), pybind11::arg("value"), pybind11::arg("maximum_level"), + ("Initialise the object.\n\n" + "Args:\n" + " value: The initial and maximum value of the movement force stat.\n" + " maximum_level: The maximum level of the movement force stat.")); + + // Add the armour regen system as well as relevant structures/components + pybind11::class_>( + systems, "ArmourRegenSystem", "Provides facilities to manipulate armour regen components.") + .def(pybind11::init(), pybind11::arg("registry"), + ("Initialise the object.\n\n" + "Args:\n" + " registry: The registry that manages the game objects, components, and systems.")) + .def("update", &ArmourRegenSystem::update, pybind11::arg("delta_time"), + ("Process update logic for an armour regeneration component.\n\n" + "Args:\n" + " delta_time: The time interval since the last time the function was called.")); + + // Add the attack system and the damage system as well as relevant structures/components + pybind11::enum_(game_objects, "AttackAlgorithm", + "Stores the different types of attack algorithms available.") + .value("AreaOfEffect", AttackAlgorithm::AreaOfEffect) + .value("Melee", AttackAlgorithm::Melee) + .value("Ranged", AttackAlgorithm::Ranged); + pybind11::class_>( + components, "Attacks", "Allows a game object to attack other game objects.") + .def(pybind11::init>(), pybind11::arg("attack_algorithms"), + ("Initialise the object.\n\n" + "Args:\n" + " attack_algorithms: The attack algorithms the game object can use.")) + .def_readwrite("attack_algorithms", &Attacks::attack_algorithms) + .def_readwrite("attack_state", &Attacks::attack_state); + pybind11::class_>( + systems, "AttackSystem", "Provides facilities to manipulate attack components.") + .def(pybind11::init(), pybind11::arg("registry"), + ("Initialise the object.\n\n" + "Args:\n" + " registry: The registry that manages the game objects, components, and systems.")) + .def("do_attack", &AttackSystem::do_attack, pybind11::arg("game_object_id"), pybind11::arg("targets"), + ("Perform the currently selected attack algorithm.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to perform the attack for.\n" + " targets: The targets to attack.\n\n" + "Raises:\n" + " RegistryError: If the game object does not exist or does not have an attack component.\n\n" + "Returns:\n" + " The result of the attack.")) + .def("previous_attack", &AttackSystem::previous_attack, pybind11::arg("game_object_id"), + ("Select the previous attack algorithm.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to select the previous attack algorithm for.\n\n" + "Raises:\n" + " RegistryError: If the game object does not exist or does not have an attack component.")) + .def("next_attack", &AttackSystem::next_attack, pybind11::arg("game_object_id"), + ("Select the next attack algorithm.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to select the next attack algorithm for.\n\n" + "Raises:\n" + " RegistryError: If the game object does not exist or does not have an attack component.")); + pybind11::class_>( + systems, "DamageSystem", "Provides facilities to damage game objects.") + .def(pybind11::init(), pybind11::arg("registry"), + ("Initialise the object.\n\n" + "Args:\n" + " registry: The registry that manages the game objects, components, and systems.")) + .def("deal_damage", &DamageSystem::deal_damage, + ("Deal damage to a game object.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to deal damage to.\n" + " damage: The amount of damage to deal.\n\n" + "Raises:\n" + " RegistryError: If the game object does not exist or does not have a health component.")); + + // Add the effect system as well as relevant structures/components + pybind11::enum_(game_objects, "StatusEffectType", + "Stores the different types of status effects available.") + .value("TEMP", StatusEffectType::TEMP) + .value("TEMP2", StatusEffectType::TEMP2); + pybind11::class_(components, "StatusEffect", + "Represents a status effect that can be applied to a game object.") + .def_readwrite("value", &StatusEffect::value) + .def_readwrite("duration", &StatusEffect::duration) + .def_readwrite("interval", &StatusEffect::interval) + .def_readwrite("target_component", &StatusEffect::target_component); + pybind11::class_>( + components, "StatusEffectData", "Represents the data required to apply a status effect.") + .def(pybind11::init(), + pybind11::arg("status_effect_type"), pybind11::arg("increase"), pybind11::arg("duration"), + pybind11::arg("interval"), + ("Initialise the object.\n\n" + "Args:\n" + " status_effect_type: The type of status effect.\n" + " increase: The increase function to apply.\n" + " duration: The duration function to apply.\n" + " interval: The interval function to apply.")) + .def_readwrite("status_effect_type", &StatusEffectData::status_effect_type) + .def_readwrite("increase", &StatusEffectData::increase) + .def_readwrite("duration", &StatusEffectData::duration) + .def_readwrite("interval", &StatusEffectData::interval); + pybind11::class_>( + components, "EffectApplier", "Allows a game object to provide instant or status effects.") + .def(pybind11::init([](const pybind11::dict &instant_effects, const pybind11::dict &status_effects) { + // Create two mappings to hold the instant and status effects + std::unordered_map target_instant_effects; + std::unordered_map target_status_effects; + + // Iterate through the instant effects and add them to the mapping + for (const auto &item : instant_effects) { + target_instant_effects.emplace(get_type_index(item.first), item.second.cast()); + } + + // Iterate through the status effects and add them to the mapping + for (const auto &item : status_effects) { + target_status_effects.emplace(get_type_index(item.first), item.second.cast()); + } + + // Initialise the object + return std::make_shared(target_instant_effects, target_status_effects); + }), + pybind11::arg("instant_effects"), pybind11::arg("status_effects"), + ("Initialise the object.\n\n" + "Args:\n" + " instant_effects: The instant effects the game object provides.\n" + " status_effects: The status effects the game object provides.")); + pybind11::class_>( + components, "StatusEffects", "Allows a game object to have status effects applied to it.") + .def(pybind11::init<>(), "Initialise the object."); + pybind11::class_>( + systems, "EffectSystem", "Provides facilities to manipulate instant and status effects.") + .def(pybind11::init(), pybind11::arg("registry"), + ("Initialise the object.\n\n" + "Args:\n" + " registry: The registry that manages the game objects, components, and systems.")) + .def("update", &EffectSystem::update, pybind11::arg("delta_time"), + ("Process update logic for a status effect component.\n\n" + "Args:\n" + " delta_time: The time interval since the last time the function was called.")) + .def("apply_instant_effect", &EffectSystem::apply_instant_effect, pybind11::arg("game_object_id"), + pybind11::arg("target_component"), pybind11::arg("increase"), pybind11::arg("level"), + ("Apply an instant effect to a game object.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to apply the effect to.\n" + " target_component: The component to apply the effect to.\n" + " increase: The increase function to apply.\n" + " level: The level of the effect to apply.\n\n" + "Raises:\n" + " RegistryError: If the game object does not exist or does not have the target component.\n\n" + "Returns:\n" + " Whether the instant effect was applied or not.")) + .def("apply_status_effect", &EffectSystem::apply_status_effect, pybind11::arg("game_object_id"), + pybind11::arg("target_component"), pybind11::arg("status_effect_data"), pybind11::arg("level"), + ("Apply a status effect to a game object.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to apply the effect to.\n" + " target_component: The component to apply the effect to.\n" + " status_effect_data: The data required to apply the status effect.\n" + " level: The level of the effect to apply.\n\n" + "Raises:\n" + " RegistryError: If the game object does not exist or does not have the target component.\n\n" + "Returns:\n" + " Whether the status effect was applied or not.")); + + // Add the inventory system as well as relevant structures/components + register_exception(game_objects, "InventorySpaceError"); + pybind11::class_>( + components, "Inventory", "Allows a game object to have a fixed size inventory.") + .def(pybind11::init(), pybind11::arg("width"), pybind11::arg("height"), + ("Initialise the object.\n\n" + "Args:\n" + " width: The width of the inventory.\n" + " height: The height of the inventory.")) + .def_readwrite("width", &Inventory::width) + .def_readwrite("height", &Inventory::height) + .def_readwrite("items", &Inventory::items) + .def("get_capacity", &Inventory::get_capacity, + ("Get the capacity of the inventory.\n\n" + "Returns:\n" + " The capacity of the inventory.")); + pybind11::class_>( + systems, "InventorySystem", "Provides facilities to manipulate inventory components.") + .def(pybind11::init(), pybind11::arg("registry"), + ("Initialise the object.\n\n" + "Args:\n" + " registry: The registry that manages the game objects, components, and systems.")) + .def("add_item_to_inventory", &InventorySystem::add_item_to_inventory, pybind11::arg("game_object_id"), + pybind11::arg("item"), + ("Add an item to the inventory of a game object.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to add the item to.\n" + " item: The item to add to the inventory.\n\n" + "Raises:\n" + " RegistryError: If the game object does not exist or does not have an inventory component.\n" + " InventorySpaceError: If the inventory is full.")) + .def("remove_item_from_inventory", &InventorySystem::remove_item_from_inventory, pybind11::arg("game_object_id"), + pybind11::arg("index"), + ("Remove an item from the inventory of a game object.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to remove the item from.\n" + " index: The index of the item to remove from the inventory.\n\n" + "Raises:\n" + " RegistryError: If the game object does not exist or does not have an inventory component.\n" + " InventorySpaceError: If the inventory is empty or if the index is out of bounds.\n\n" + "Returns:\n" + " The item removed from the inventory.")); + + // Add the footprint system, the keyboard movement system, and the steering movement system as well as relevant + // structures/components + pybind11::enum_(game_objects, "SteeringBehaviours", + "Stores the different types of steering behaviours available.") + .value("Arrive", SteeringBehaviours::Arrive) + .value("Evade", SteeringBehaviours::Evade) + .value("Flee", SteeringBehaviours::Flee) + .value("FollowPath", SteeringBehaviours::FollowPath) + .value("ObstacleAvoidance", SteeringBehaviours::ObstacleAvoidance) + .value("Pursue", SteeringBehaviours::Pursue) + .value("Seek", SteeringBehaviours::Seek) + .value("Wander", SteeringBehaviours::Wander); + pybind11::enum_(game_objects, "SteeringMovementState", + "Stores the different states the steering movement component can be in.") + .value("Default", SteeringMovementState::Default) + .value("Footprint", SteeringMovementState::Footprint) + .value("Target", SteeringMovementState::Target); + pybind11::class_>( + components, "Footprints", "Allows a game object to periodically leave footprints around the game map.") + .def(pybind11::init<>(), "Initialise the object.") + .def_readwrite("footprints", &Footprints::footprints) + .def_readwrite("time_since_last_footprint", &Footprints::time_since_last_footprint); + pybind11::class_>( + components, "KeyboardMovement", "Allows a game object's movement to be controlled by the keyboard.") + .def(pybind11::init<>(), "Initialise the object.") + .def_readwrite("moving_north", &KeyboardMovement::moving_north) + .def_readwrite("moving_east", &KeyboardMovement::moving_east) + .def_readwrite("moving_south", &KeyboardMovement::moving_south) + .def_readwrite("moving_west", &KeyboardMovement::moving_west); + pybind11::class_>( + components, "SteeringMovement", "Allows a game object's movement to be controlled by steering behaviours.") + .def(pybind11::init>>(), + pybind11::arg("behaviours"), + ("Initialise the object.\n\n" + "Args:\n" + " behaviours: The steering behaviours the game object can use.")) + .def_readwrite("behaviours", &SteeringMovement::behaviours) + .def_readwrite("movement_state", &SteeringMovement::movement_state) + .def_readwrite("target_id", &SteeringMovement::target_id) + .def_readwrite("path_list", &SteeringMovement::path_list); + pybind11::class_>( + systems, "FootprintSystem", "Provides facilities to manipulate footprint components.") + .def(pybind11::init(), pybind11::arg("registry"), + ("Initialise the object.\n\n" + "Args:\n" + " registry: The registry that manages the game objects, components, and systems.")) + .def("update", &FootprintSystem::update, pybind11::arg("delta_time"), + ("Process update logic for a footprint component.\n\n" + "Args:\n" + " delta_time: The time interval since the last time the function was called.")); + pybind11::class_>( + systems, "KeyboardMovementSystem", "Provides facilities to manipulate keyboard movement components.") + .def(pybind11::init(), pybind11::arg("registry"), + ("Initialise the object.\n\n" + "Args:\n" + " registry: The registry that manages the game objects, components, and systems.")) + .def("calculate_force", &KeyboardMovementSystem::calculate_force, + pybind11::arg("game_object_id"), + ("Calculate the new keyboard force to apply to the game object.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to calculate the keyboard force for.\n\n" + "Raises:\n" + " RegistryError: If the game object does not exist or does not have a keyboard movement " + "component.\n\n" + "Returns:\n" + " The new force to apply to the game object.")); + pybind11::class_>( + systems, "SteeringMovementSystem", "Provides facilities to manipulate steering movement components.") + .def(pybind11::init(), pybind11::arg("registry"), + ("Initialise the object.\n\n" + "Args:\n" + " registry: The registry that manages the game objects, components, and systems.")) + .def("calculate_force", &SteeringMovementSystem::calculate_force, + pybind11::arg("game_object_id"), + ("Calculate the new steering force to apply to the game object.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to calculate the steering force for.\n\n" + "Raises:\n" + " RegistryError: If the game object does not exist or does not have a steering movement " + "component.\n\n" + "Returns:\n" + " The new force to apply to the game object.")) + .def("update_path_list", &SteeringMovementSystem::update_path_list, pybind11::arg("game_object_id"), + pybind11::arg("path_list"), + ("Update the path lists for the game objects to follow.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to follow.\n" + " path_list: The list of footprints to follow.")); + + // Add the upgrade system as well as relevant structures/components + pybind11::class_>( + components, "Money", "Allows a game object to record the amount of money it has.") + .def(pybind11::init(), pybind11::arg("money"), + ("Initialise the object.\n\n" + "Args:\n" + " money: The amount of money the game object has.")) + .def_readwrite("money", &Money::money); + pybind11::class_>(components, "Upgrades", + "Allows a game object to be upgraded.") + .def(pybind11::init([](const pybind11::dict &upgrades) { + // Create a mapping to hold the upgrades + std::unordered_map target_upgrades; + + // Iterate through the upgrades and add them to the mapping + for (const auto &item : upgrades) { + target_upgrades.emplace(get_type_index(item.first), item.second.cast()); + } + + // Initialise the object + return std::make_shared(target_upgrades); + }), + pybind11::arg("upgrades"), + ("Initialise the object.\n\n" + "Args:\n" + " upgrades: The upgrades the game object has.")); + pybind11::class_>( + systems, "UpgradeSystem", "Provides facilities to manipulate game object upgrades.") + .def(pybind11::init(), pybind11::arg("registry"), + ("Initialise the object.\n\n" + "Args:\n" + " registry: The registry that manages the game objects, components, and systems.")) + .def("upgrade_component", &UpgradeSystem::upgrade_component, pybind11::arg("game_object_id"), + pybind11::arg("target_component"), + ("Upgrade a component to the next level if possible.\n\n" + "Args:\n" + " game_object_id: The ID of the game object to upgrade the component for.\n" + " target_component: The type of component to upgrade.\n\n" + "Raises:\n" + " RegistryError: If the game object does not exist or does not have the target component.\n\n" + "Returns:\n" + " Whether the component upgrade was successful or not.")); +} diff --git a/src/hades_extensions/src/game_objects/registry.cpp b/src/hades_extensions/src/game_objects/registry.cpp new file mode 100644 index 00000000..fc86a9d6 --- /dev/null +++ b/src/hades_extensions/src/game_objects/registry.cpp @@ -0,0 +1,65 @@ +// Related header +#include "game_objects/registry.hpp" + +// Std headers +#include + +// ----- FUNCTIONS ------------------------------ +auto Registry::create_game_object(const std::vector> &&components, const bool kinematic) + -> GameObjectID { + // Add the game object to the system + game_objects_[next_game_object_id_] = {}; + if (kinematic) { + kinematic_objects_[next_game_object_id_] = std::make_unique(); + } + + // Add the components to the game object + for (const auto &component : components) { + // Check if the component already exists in the registry + [[maybe_unused]] const auto &obj{*component}; + if (has_component(next_game_object_id_, typeid(obj))) { + continue; + } + + // Add the component to the registry + game_objects_[next_game_object_id_][typeid(obj)] = component; + } + + // Increment the game object ID and return the current game object ID + next_game_object_id_++; + return next_game_object_id_ - 1; +} + +void Registry::delete_game_object(const GameObjectID game_object_id) { + // Check if the game object is registered or not + if (!game_objects_.contains(game_object_id)) { + throw RegistryError("game object", game_object_id); + } + + // Delete the game object from the system + game_objects_.erase(game_object_id); + if (kinematic_objects_.contains(game_object_id)) { + kinematic_objects_.erase(game_object_id); + } +} + +auto Registry::get_component(const GameObjectID game_object_id, const std::type_index &component_type) const + -> std::shared_ptr { + // Check if the game object has the component or not + if (!has_component(game_object_id, component_type)) { + throw RegistryError("game object", game_object_id, " or does not have the required component"); + } + + // Return the specified component + return game_objects_.at(game_object_id).at(component_type); +} + +auto Registry::get_kinematic_object(const GameObjectID game_object_id) const -> std::shared_ptr { + // Check if the game object is registered or not + if (!kinematic_objects_.contains(game_object_id)) { + throw RegistryError("game object", game_object_id, " or is not kinematic"); + } + + // Return the kinematic object + return kinematic_objects_.at(game_object_id); +} diff --git a/src/hades_extensions/src/game_objects/steering.cpp b/src/hades_extensions/src/game_objects/steering.cpp new file mode 100644 index 00000000..c019fb4f --- /dev/null +++ b/src/hades_extensions/src/game_objects/steering.cpp @@ -0,0 +1,115 @@ +// Related header +#include "game_objects/steering.hpp" + +// Std headers +#include +#include +#include + +// ----- CONSTANTS ------------------------------ +constexpr double MAX_SEE_AHEAD{2 * SPRITE_SIZE}; +constexpr int MAX_VELOCITY{200}; +constexpr double OBSTACLE_AVOIDANCE_ANGLE{60 * PI_RADIANS}; +constexpr double PATH_POSITION_RADIUS{1 * SPRITE_SIZE}; +constexpr double SLOWING_RADIUS{3 * SPRITE_SIZE}; +constexpr int WANDER_CIRCLE_DISTANCE{50}; +constexpr int WANDER_CIRCLE_RADIUS{25}; + +// ----- FUNCTIONS ------------------------------ +auto arrive(const Vec2d ¤t_position, const Vec2d &target_position) -> Vec2d { + // Calculate a vector to the target and its length + const Vec2d direction{target_position - current_position}; + + // Check if the game object is inside the slowing area + if (direction.magnitude() < SLOWING_RADIUS) { + return (direction * (direction.magnitude() / SLOWING_RADIUS)).normalised(); + } + return direction.normalised(); +} + +auto evade(const Vec2d ¤t_position, const Vec2d &target_position, const Vec2d &target_velocity) -> Vec2d { + // Calculate the future position of the target based on their distance, and steer away from it. + // Higher distances will require more time to reach, so the future position will be further away + return flee(current_position, + target_position + target_velocity * (target_position.distance_to(current_position) / MAX_VELOCITY)); +} + +auto flee(const Vec2d ¤t_position, const Vec2d &target_position) -> Vec2d { + return (current_position - target_position).normalised(); +} + +auto follow_path(const Vec2d ¤t_position, std::vector &path_list) -> Vec2d { + // Check if the path list is empty + if (path_list.empty()) { + throw std::length_error("The path list is empty"); + } + + // Check if the game object has reached the current path position. If so, move it to the end of the vector + if (current_position.distance_to(path_list[0]) <= PATH_POSITION_RADIUS) { + path_list.push_back(path_list[0]); + path_list.erase(path_list.begin()); + } + return seek(current_position, path_list[0]); +} + +auto obstacle_avoidance(const Vec2d ¤t_position, const Vec2d ¤t_velocity, + const std::unordered_set &walls) -> Vec2d { + // Create the lambda function to cast a ray from the game object's position in the direction of its velocity at a + // given angle + auto raycast{[¤t_position, ¤t_velocity, &walls](double angle = 0) -> Vec2d { + // Pre-calculate some values used during the raycast + const auto rotated_velocity{current_velocity.rotated(angle)}; + const int step_count{static_cast(MAX_SEE_AHEAD / SPRITE_SIZE)}; + + // Perform the raycast + for (int step = 1; step <= step_count; step++) { + Vec2d position{current_position + rotated_velocity * ((step * SPRITE_SIZE) / 100.0)}; + if (walls.contains(position / SPRITE_SIZE)) { + return position; + } + } + return {-1, -1}; + }}; + + // Check if the game object is going to collide with an obstacle + const Vec2d forward_ray{raycast()}; + const Vec2d left_ray{raycast(OBSTACLE_AVOIDANCE_ANGLE)}; + const Vec2d right_ray{raycast(-OBSTACLE_AVOIDANCE_ANGLE)}; + + // Check if there are any obstacles ahead + if (forward_ray != Vec2d{-1, -1} && left_ray != Vec2d{-1, -1} && right_ray != Vec2d{-1, -1}) { + // Turn around, there's a wall ahead + return flee(current_position, forward_ray); + } + if (left_ray != Vec2d{-1, -1}) { + // Turn right, there's a wall left + return flee(current_position, left_ray); + } + if (right_ray != Vec2d{-1, -1}) { + // Turn left, there's a wall right + return flee(current_position, right_ray); + } + + // No obstacles ahead, move forward + return Vec2d{0, 0}; +} + +auto pursue(const Vec2d ¤t_position, const Vec2d &target_position, const Vec2d &target_velocity) -> Vec2d { + // Calculate the future position of the target based on their distance, and steer away from it. + // Higher distances will require more time to reach, so the future position will be further away + return seek(current_position, + target_position + target_velocity * (target_position.distance_to(current_position) / MAX_VELOCITY)); +} + +auto seek(const Vec2d ¤t_position, const Vec2d &target_position) -> Vec2d { + return (target_position - current_position).normalised(); +} + +auto wander(const Vec2d ¤t_velocity, const int displacement_angle) -> Vec2d { + // Calculate the position of an invisible circle in front of the game object + const Vec2d circle_center{current_velocity.normalised() * WANDER_CIRCLE_DISTANCE}; + + // Add a displacement force to the centre of the circle to randomise the movement + const Vec2d displacement{(Vec2d{0, -1} * WANDER_CIRCLE_RADIUS).rotated(displacement_angle * PI_RADIANS)}; + return (circle_center + displacement).normalised(); +} diff --git a/src/hades_extensions/src/game_objects/systems/armour_regen.cpp b/src/hades_extensions/src/game_objects/systems/armour_regen.cpp new file mode 100644 index 00000000..3c7767df --- /dev/null +++ b/src/hades_extensions/src/game_objects/systems/armour_regen.cpp @@ -0,0 +1,21 @@ +// Related header +#include "game_objects/systems/armour_regen.hpp" + +// Local headers +#include "game_objects/stats.hpp" + +// ----- CONSTANTS ------------------------------- +constexpr int ARMOUR_REGEN_AMOUNT{1}; + +// ----- FUNCTIONS ------------------------------ +void ArmourRegenSystem::update(const double delta_time) const { + // Update the time since the last armour regen then check if the armour should be regenerated + for (auto &[_, component_tuple] : get_registry()->find_components()) { + auto &[armour, armour_regen]{component_tuple}; + armour_regen->time_since_armour_regen += delta_time; + if (armour_regen->time_since_armour_regen >= armour_regen->get_value()) { + armour->set_value(armour->get_value() + ARMOUR_REGEN_AMOUNT); + armour_regen->time_since_armour_regen = 0; + } + } +} diff --git a/src/hades_extensions/src/game_objects/systems/attacks.cpp b/src/hades_extensions/src/game_objects/systems/attacks.cpp new file mode 100644 index 00000000..3922b970 --- /dev/null +++ b/src/hades_extensions/src/game_objects/systems/attacks.cpp @@ -0,0 +1,89 @@ +// Related header +#include "game_objects/systems/attacks.hpp" + +// Local headers +#include "game_objects/stats.hpp" + +// ----- CONSTANTS ------------------------------- +constexpr double ATTACK_RANGE{3 * SPRITE_SIZE}; +constexpr int BULLET_VELOCITY{300}; +constexpr int DAMAGE{10}; +constexpr double MELEE_ATTACK_OFFSET_LOWER{45 * PI_RADIANS}; +constexpr double MELEE_ATTACK_OFFSET_UPPER{(2 * (180 * PI_RADIANS)) - MELEE_ATTACK_OFFSET_LOWER}; + +// ----- FUNCTIONS ------------------------------ +/// Performs an area of effect attack around the game object. +/// +/// @param registry - The registry that manages the game objects, components, and systems. +/// @param current_position - The current position of the game object. +/// @param targets - The targets to attack. +void area_of_effect_attack(const Registry *registry, const Vec2d ¤t_position, const std::vector &targets) { + // Find all targets that are within range and attack them + for (auto target : targets) { + if (current_position.distance_to(registry->get_kinematic_object(target)->position) <= ATTACK_RANGE) { + registry->get_system()->deal_damage(target, DAMAGE); + } + } +} + +/// Performs a melee attack in the direction the game object is facing. +/// +/// @param registry - The registry that manages the game objects, components, and systems. +/// @param current_position - The current position of the game object. +/// @param current_rotation - The current rotation of the game object in radians. +/// @param targets - The targets to attack. +void melee_attack(const Registry *registry, const Vec2d ¤t_position, const double current_rotation, + const std::vector &targets) { + // Calculate a vector that is perpendicular to the current rotation of the game object + const Vec2d rotation{std::sin(current_rotation), std::cos(current_rotation)}; + + // Find all targets that can be attacked + for (const auto target : targets) { + // Calculate the angle between the current rotation of the game object and the direction the target is in + const Vec2d target_position{registry->get_kinematic_object(target)->position}; + const double theta{(target_position - current_position).angle_between(rotation)}; + + // Test if the target is within range and within the circle's sector + if (current_position.distance_to(target_position) <= ATTACK_RANGE && + (theta <= MELEE_ATTACK_OFFSET_LOWER || theta >= MELEE_ATTACK_OFFSET_UPPER)) { + registry->get_system()->deal_damage(target, DAMAGE); + } + } +} + +/// Performs a ranged attack in the direction the game object is facing. +/// +/// @param current_position - The current position of the game object. +/// @param current_rotation - The current rotation of the game object in radians. +/// @return The result of the attack. +auto ranged_attack(const Vec2d ¤t_position, const double current_rotation) -> std::tuple { + return {current_position, BULLET_VELOCITY * std::cos(current_rotation), BULLET_VELOCITY * std::sin(current_rotation)}; +} + +auto AttackSystem::do_attack(const GameObjectID game_object_id, const std::vector &targets) const + -> std::optional> { + // Perform the attack on the targets + auto attacks{get_registry()->get_component(game_object_id)}; + const auto kinematic_object{get_registry()->get_kinematic_object(game_object_id)}; + switch (attacks->attack_algorithms[attacks->attack_state]) { + case AttackAlgorithm::AreaOfEffect: + area_of_effect_attack(get_registry(), kinematic_object->position, targets); + break; + case AttackAlgorithm::Melee: + melee_attack(get_registry(), kinematic_object->position, kinematic_object->rotation * PI_RADIANS, targets); + break; + case AttackAlgorithm::Ranged: + return ranged_attack(kinematic_object->position, kinematic_object->rotation * PI_RADIANS); + } + + // Return an empty result as no ranged attack was performed + return std::nullopt; +} + +void DamageSystem::deal_damage(const GameObjectID game_object_id, const int damage) const { + // Damage the armour and carry over the extra damage to the health + auto health{get_registry()->get_component(game_object_id)}; + auto armour{get_registry()->get_component(game_object_id)}; + health->set_value(health->get_value() - std::max(damage - armour->get_value(), 0.0)); + armour->set_value(armour->get_value() - damage); +} diff --git a/src/hades_extensions/src/game_objects/systems/effects.cpp b/src/hades_extensions/src/game_objects/systems/effects.cpp new file mode 100644 index 00000000..61a3d60b --- /dev/null +++ b/src/hades_extensions/src/game_objects/systems/effects.cpp @@ -0,0 +1,66 @@ +// Related header +#include "game_objects/systems/effects.hpp" + +// Local headers +#include "game_objects/stats.hpp" + +// ----- FUNCTIONS ------------------------------ +void EffectSystem::update(const double delta_time) const { + for (auto &[game_object_id, component_tuple] : get_registry()->find_components()) { + // Create a vector to store the expired status effects + auto &applied_effects{std::get<0>(component_tuple)->applied_effects}; + std::vector expired_status_effects; + + // Update the status effects and keep track of the expired ones. + // Note that in reality, delta_time will be ~0.016 (60 FPS), so big jumps in time where multiple intervals are + // covered within a single update should never happen. + // But if they do, this will accumulate the leftover time and the status effect is applied in subsequent updates + for (auto &[status_effect_type, status_effect] : std::get<0>(component_tuple)->applied_effects) { + status_effect.time_counter += delta_time; + status_effect.leftover_time += delta_time; + if (status_effect.time_counter >= status_effect.duration) { + expired_status_effects.push_back(status_effect_type); + } else if (status_effect.leftover_time >= status_effect.interval) { + auto component = std::static_pointer_cast( + get_registry()->get_component(game_object_id, status_effect.target_component)); + component->set_value(component->get_value() + status_effect.value); + status_effect.leftover_time -= status_effect.interval; + } + } + + // Remove the expired status effects + for (auto &status_effect_type : expired_status_effects) { + applied_effects.erase(status_effect_type); + } + } +} + +auto EffectSystem::apply_instant_effect(const GameObjectID game_object_id, const std::type_index &target_component, + const ActionFunction &increase_function, const int level) -> bool { + // Check if the component is already at the maximum + auto component{std::static_pointer_cast(get_registry()->get_component(game_object_id, target_component))}; + if (component->get_value() == component->get_max_value()) { + return false; + } + + // Apply the instant effect + component->set_value(component->get_value() + increase_function(level)); + return true; +} + +auto EffectSystem::apply_status_effect(const GameObjectID game_object_id, const std::type_index &target_component, + const StatusEffectData &status_effect_data, const int level) -> bool { + // Check if the status effect has already been applied + auto status_effects{get_registry()->get_component(game_object_id)}; + if (status_effects->applied_effects.contains(status_effect_data.status_effect_type)) { + return false; + } + + // Apply the status effect + const StatusEffect status_effect{status_effect_data.increase(level), status_effect_data.duration(level), + status_effect_data.interval(level), target_component}; + status_effects->applied_effects.emplace(status_effect_data.status_effect_type, status_effect); + auto component{std::static_pointer_cast(get_registry()->get_component(game_object_id, target_component))}; + component->set_value(component->get_value() + status_effect.value); + return true; +} diff --git a/src/hades_extensions/src/game_objects/systems/inventory.cpp b/src/hades_extensions/src/game_objects/systems/inventory.cpp new file mode 100644 index 00000000..38f222d4 --- /dev/null +++ b/src/hades_extensions/src/game_objects/systems/inventory.cpp @@ -0,0 +1,24 @@ +// Related header +#include "game_objects/systems/inventory.hpp" + +// ----- FUNCTIONS ------------------------------ +void InventorySystem::add_item_to_inventory(const GameObjectID game_object_id, const GameObjectID item) const { + auto inventory{get_registry()->get_component(game_object_id)}; + if (inventory->items.size() == inventory->get_capacity()) { + throw InventorySpaceError(true); + } + inventory->items.push_back(item); +} + +auto InventorySystem::remove_item_from_inventory(const GameObjectID game_object_id, const int index) const -> int { + auto inventory{get_registry()->get_component(game_object_id)}; + if (inventory->items.empty()) { + throw InventorySpaceError(false); + } + if (index < 0 || index >= inventory->items.size()) { + throw InventorySpaceError("The index is out of range."); + } + const int item{inventory->items[index]}; + inventory->items.erase(inventory->items.begin() + index); + return item; +} diff --git a/src/hades_extensions/src/game_objects/systems/movements.cpp b/src/hades_extensions/src/game_objects/systems/movements.cpp new file mode 100644 index 00000000..cd4080bf --- /dev/null +++ b/src/hades_extensions/src/game_objects/systems/movements.cpp @@ -0,0 +1,129 @@ +// Related header +#include "game_objects/systems/movements.hpp" + +// Std headers +#include + +// Local headers +#include "game_objects/stats.hpp" + +// ----- CONSTANTS ------------------------------ +constexpr double FOOTPRINT_INTERVAL{0.5}; +constexpr int FOOTPRINT_LIMIT{10}; +constexpr double TARGET_DISTANCE{3 * SPRITE_SIZE}; +constexpr int MAX_DEGREE{360}; + +// ----- FUNCTIONS ------------------------------ +void FootprintSystem::update(const double delta_time) const { + // Update the time since the last footprint then check if a new footprint should be created + for (auto &[game_object_id, component_tuple] : get_registry()->find_components()) { + auto footprints{std::get<0>(component_tuple)}; + footprints->time_since_last_footprint += delta_time; + if (footprints->time_since_last_footprint < FOOTPRINT_INTERVAL) { + return; + } + + // Reset the counter and create a new footprint making sure to only keep FOOTPRINT_LIMIT footprints + const Vec2d current_position{get_registry()->get_kinematic_object(game_object_id)->position}; + footprints->time_since_last_footprint = 0; + if (footprints->footprints.size() >= FOOTPRINT_LIMIT) { + footprints->footprints.pop_front(); + } + footprints->footprints.push_back(current_position); + + // Update the path list for all SteeringMovement components + get_registry()->get_system()->update_path_list(game_object_id, footprints->footprints); + } +} + +auto KeyboardMovementSystem::calculate_force(GameObjectID game_object_id) const -> Vec2d { + auto keyboard_movement{get_registry()->get_component(game_object_id)}; + return Vec2d{static_cast(static_cast(keyboard_movement->moving_east) - + static_cast(keyboard_movement->moving_west)), + static_cast(static_cast(keyboard_movement->moving_north) - + static_cast(keyboard_movement->moving_south))} * + get_registry()->get_component(game_object_id)->get_value(); +} + +auto SteeringMovementSystem::calculate_force(GameObjectID game_object_id) const -> Vec2d { + // Determine if the movement state should change or not + auto steering_movement{get_registry()->get_component(game_object_id)}; + const auto kinematic_owner{get_registry()->get_kinematic_object(game_object_id)}; + const auto kinematic_target{get_registry()->get_kinematic_object(steering_movement->target_id)}; + if (kinematic_owner->position.distance_to(kinematic_target->position) <= TARGET_DISTANCE) { + steering_movement->movement_state = SteeringMovementState::Target; + } else if (!steering_movement->path_list.empty()) { + steering_movement->movement_state = SteeringMovementState::Footprint; + } else { + steering_movement->movement_state = SteeringMovementState::Default; + } + + // Calculate the new force to apply to the game object + Vec2d steering_force{0, 0}; + std::random_device random_device; + std::mt19937_64 number_generator{random_device()}; + for (const auto &behaviour : steering_movement->behaviours[steering_movement->movement_state]) { + switch (behaviour) { + case SteeringBehaviours::Arrive: + steering_force += arrive(kinematic_owner->position, kinematic_target->position); + break; + case SteeringBehaviours::Evade: + steering_force += evade(kinematic_owner->position, kinematic_target->position, kinematic_target->velocity); + break; + case SteeringBehaviours::Flee: + steering_force += flee(kinematic_owner->position, kinematic_target->position); + break; + case SteeringBehaviours::FollowPath: + steering_force += follow_path(kinematic_owner->position, steering_movement->path_list); + break; + case SteeringBehaviours::ObstacleAvoidance: + steering_force += + obstacle_avoidance(kinematic_owner->position, kinematic_owner->velocity, get_registry()->get_walls()); + break; + case SteeringBehaviours::Pursue: + steering_force += pursue(kinematic_owner->position, kinematic_target->position, kinematic_target->velocity); + break; + case SteeringBehaviours::Seek: + steering_force += seek(kinematic_owner->position, kinematic_target->position); + break; + case SteeringBehaviours::Wander: + steering_force += + wander(kinematic_owner->velocity, std::uniform_int_distribution<>{0, MAX_DEGREE}(number_generator)); + break; + } + } + + // Return the normalised steering force + return steering_force.normalised() * get_registry()->get_component(game_object_id)->get_value(); +} + +void SteeringMovementSystem::update_path_list(const GameObjectID target_game_object_id, + const std::deque &footprints) const { + // Update the path list for all SteeringMovement components that have the correct target ID + for (auto &[game_object_id, component_tuple] : get_registry()->find_components()) { + auto steering_movement{std::get<0>(component_tuple)}; + if (steering_movement->target_id != target_game_object_id) { + continue; + } + + // Get the current position of the target and clear the path list + const Vec2d current_position{get_registry()->get_kinematic_object(game_object_id)->position}; + steering_movement->path_list.clear(); + + // Get the closest footprint to the target that is still within range of the game object + auto closest_footprint{footprints.end()}; + double closest_distance{TARGET_DISTANCE}; + for (auto it = footprints.begin(); it != footprints.end(); ++it) { + const double distance{current_position.distance_to(*it)}; + if (distance < closest_distance) { + closest_footprint = it; + closest_distance = distance; + } + } + + // If a footprint was found, set the path list a slice of the footprints list from closest_footprint + if (closest_footprint != footprints.end()) { + steering_movement->path_list.assign(closest_footprint, footprints.end()); + } + } +} diff --git a/src/hades_extensions/src/game_objects/systems/upgrade.cpp b/src/hades_extensions/src/game_objects/systems/upgrade.cpp new file mode 100644 index 00000000..41d26017 --- /dev/null +++ b/src/hades_extensions/src/game_objects/systems/upgrade.cpp @@ -0,0 +1,26 @@ +// Related header +#include "game_objects/systems/upgrade.hpp" + +// Local headers +#include "game_objects/stats.hpp" + +// ----- FUNCTIONS ------------------------------ +auto UpgradeSystem::upgrade_component(const GameObjectID game_object_id, const std::type_index &target_component) const + -> bool { + // Get the component to upgrade as well as the upgrade function + auto component{std::static_pointer_cast(get_registry()->get_component(game_object_id, target_component))}; + auto upgrades_component{get_registry()->get_component(game_object_id)}; + + // Check if the component can be upgraded + if (upgrades_component == nullptr || !upgrades_component->upgrades.contains(target_component) || + component->get_current_level() >= component->get_maximum_level()) { + return false; + } + + // Upgrade the component + auto diff{upgrades_component->upgrades[target_component](component->get_current_level())}; + component->add_to_max_value(diff); + component->increment_current_level(); + component->set_value(component->get_value() + diff); + return true; +} diff --git a/src/hades_extensions/src/generation/astar.cpp b/src/hades_extensions/src/generation/astar.cpp new file mode 100644 index 00000000..78b24fec --- /dev/null +++ b/src/hades_extensions/src/generation/astar.cpp @@ -0,0 +1,87 @@ +// Related header +#include "generation/astar.hpp" + +// Std headers +#include +#include +#include + +// ----- STRUCTURES ------------------------------ +/// Represents a grid position and its distance from the start position. +struct Neighbour { + // std::priority_queue uses a max heap, but we want a min heap, so the operator needs to be reversed + inline auto operator<(const Neighbour &neighbour) const -> bool { return cost > neighbour.cost; } + + /// The cost to traverse to this neighbour. + int cost; + + /// The destination position in the grid. + Position destination; +}; + +// ----- CONSTANTS ------------------------------ +// Represents the north, south, east, west, north-east, north-west, south-east and south-west directions on a compass +constexpr std::array INTERCARDINAL_OFFSETS{Position{-1, -1}, Position{0, -1}, Position{1, -1}, + Position{-1, 0}, Position{1, 0}, Position{-1, 1}, + Position{0, 1}, Position{1, 1}}; + +// ----- FUNCTIONS ------------------------------ +auto calculate_astar_path(const Grid &grid, const Position &start, const Position &end) -> std::vector { + // Check if the grid size is not zero + if (grid.width == 0 || grid.height == 0) { + throw std::length_error("Grid size must be bigger than 0."); + } + + // Initialise the result vector, priority queue and neighbours map which will be used during the algorithm + std::vector result; + std::priority_queue queue; + std::unordered_map neighbours{{start, {0, start}}}; + queue.emplace(0, start); + + // Loop until we have explored every neighbour or until we've reached the end + while (!queue.empty()) { + Position current{queue.top().destination}; + queue.pop(); + + // Check if we've reached the end. If so, backtrack through the neighbours to get the resultant path + if (current == end) { + while (!(neighbours.at(current).destination == current)) { + result.push_back(current); + current = neighbours.at(current).destination; + } + + // Add the start position to the result and break out of the loop + result.push_back(start); + break; + } + + // Add all the neighbours to the heap with their cost being f = g + h: + // f - The total cost of traversing the neighbour + // g - The distance between the start pair and the neighbour pair + // h - The estimated distance from the neighbour pair to the end pair (this uses the Chebyshev distance) + for (const Position &offset : INTERCARDINAL_OFFSETS) { + // Get the neighbour position and check if it is within the grid + const Position neighbour{current + offset}; + if (neighbour.x < 0 || neighbour.y < 0 || neighbour.x >= grid.width || neighbour.y >= grid.height) { + continue; + } + + // Move around the neighbour if it is an obstacle as they have an infinite cost + if (grid.get_value(neighbour) == TileType::Obstacle) { + continue; + } + + // Check if we've found a more efficient path to the neighbour and if so, add all of its neighbours to the queue + const int distance{neighbours.at(current).cost + 1}; + if (!neighbours.contains(neighbour) || distance < neighbours.at(neighbour).cost) { + neighbours[neighbour] = {distance, current}; + + // Add the neighbour to the priority queue + queue.emplace(distance + std::max(abs(end.x - neighbour.x), abs(end.y - neighbour.y)), neighbour); + } + } + } + + // Return the most efficient path + return result; +} diff --git a/src/hades_extensions/src/generation/bsp.cpp b/src/hades_extensions/src/generation/bsp.cpp new file mode 100644 index 00000000..5b05b482 --- /dev/null +++ b/src/hades_extensions/src/generation/bsp.cpp @@ -0,0 +1,96 @@ +// Related header +#include "generation/bsp.hpp" + +// ----- CONSTANTS ------------------------------ +constexpr double CONTAINER_RATIO{1.25}; +constexpr int MIN_CONTAINER_SIZE{5}; +constexpr int MIN_ROOM_SIZE{4}; +constexpr double ROOM_RATIO{0.625}; + +// ----- FUNCTIONS ------------------------------ +void split(Leaf &leaf, std::mt19937 &random_generator) { // NOLINT(misc-no-recursion) + // Check if this leaf is already split or not + if (leaf.left && leaf.right) { + return; + } + + // To determine which direction to split it, we have three options: + // 1. Split vertically if the width is 25% larger than the height. + // 2. Split horizontally if the height is 25% larger than the width. + // 3. Split randomly if neither of the above is true. + bool split_vertical; // NOLINT(cppcoreguidelines-init-variables) + if (leaf.container->width >= CONTAINER_RATIO * leaf.container->height) { + split_vertical = true; + } else if (leaf.container->height >= CONTAINER_RATIO * leaf.container->width) { + split_vertical = false; + } else { + split_vertical = (std::uniform_int_distribution<>{0, 1}(random_generator) == 1); + } + + // Check if the container is too small to split + const int max_size{(split_vertical) ? leaf.container->width - MIN_CONTAINER_SIZE + : leaf.container->height - MIN_CONTAINER_SIZE}; + if (max_size <= MIN_CONTAINER_SIZE) { + return; + } + + // Determine the random split position to use ensuring that the containers are at least MIN_CONTAINER_SIZE wide + const int pos{std::uniform_int_distribution<>{MIN_CONTAINER_SIZE, max_size}(random_generator)}; + const int split_pos{(split_vertical) ? leaf.container->top_left.x + pos : leaf.container->top_left.y + pos}; + + // Generate the left and right leafs making sure that the containers do not include the split position + if (split_vertical) { + leaf.left = std::make_unique(Rect{{leaf.container->top_left.x, leaf.container->top_left.y}, + {split_pos - 1, leaf.container->bottom_right.y}}); + leaf.right = std::make_unique(Rect{{split_pos + 1, leaf.container->top_left.y}, + {leaf.container->bottom_right.x, leaf.container->bottom_right.y}}); + } else { + leaf.left = std::make_unique(Rect{{leaf.container->top_left.x, leaf.container->top_left.y}, + {leaf.container->bottom_right.x, split_pos - 1}}); + leaf.right = std::make_unique(Rect{{leaf.container->top_left.x, split_pos + 1}, + {leaf.container->bottom_right.x, leaf.container->bottom_right.y}}); + } + + // Split the left and right leafs until they are too small to split + split(*leaf.left, random_generator); + split(*leaf.right, random_generator); +} + +void create_room(Leaf &leaf, Grid &grid, std::mt19937 &random_generator, // NOLINT(misc-no-recursion) + std::vector &rooms) { + // Check if this leaf is already split or not, if so, create rooms for the left and right leafs + if (leaf.left && leaf.right) { + create_room(*leaf.left, grid, random_generator, rooms); + create_room(*leaf.right, grid, random_generator, rooms); + return; + } + + // Check if this leaf is too small to create a room. If the leaf has been split correctly, this should never happen + if (leaf.container->width < MIN_ROOM_SIZE || leaf.container->height < MIN_ROOM_SIZE) { + return; + } + + // Determine the width and height of the room making sure it is at least MIN_ROOM_SIZE wide + const int width{std::uniform_int_distribution<>{MIN_ROOM_SIZE, leaf.container->width}(random_generator)}; + const int height{std::uniform_int_distribution<>{MIN_ROOM_SIZE, leaf.container->height}(random_generator)}; + + // Determine the top left position of the new room based on the width and height + const int x_pos{std::uniform_int_distribution<>{leaf.container->top_left.x, + leaf.container->bottom_right.x - width}(random_generator)}; + const int y_pos{std::uniform_int_distribution<>{leaf.container->top_left.y, + leaf.container->bottom_right.y - height}(random_generator)}; + + // Create the room rect and check its width to height ratio so oddly shaped rooms can be avoided + const Rect rect{{x_pos, y_pos}, {x_pos + width - 1, y_pos + height - 1}}; + if ((static_cast(std::min(rect.width, rect.height)) / std::max(rect.width, rect.height)) < ROOM_RATIO) { + // Since MIN_ROOM_SIZE ensures the random generator will always raise an exception if a leaf is too small, a valid + // room will always be created, so we can just keep trying + create_room(leaf, grid, random_generator, rooms); + return; + } + + // Place the rect in the 2D grid then save it in the leaf and the rooms vector + rect.place_rect(grid); + leaf.room = std::make_unique(rect); + rooms.push_back(rect); +} diff --git a/src/hades_extensions/src/generation/map.cpp b/src/hades_extensions/src/generation/map.cpp new file mode 100644 index 00000000..d5fb3282 --- /dev/null +++ b/src/hades_extensions/src/generation/map.cpp @@ -0,0 +1,194 @@ +// Related header +#include "generation/map.hpp" + +// Std headers +#include +#include + +// Local headers +#include "generation/astar.hpp" + +// ----- STRUCTURES ------------------------------ +/// Stores a map generation constant which can be calculated. +/// +/// @param base_value - The base value for the exponential calculation. +/// @param increase - The percentage increase for the constant. +/// @param max_value - The max value for the exponential calculation. +struct MapGenerationConstant { + /// The base value for the exponential calculation. + double base_value; + + /// The percentage increase for the constant. + double increase; + + /// The max value for the exponential calculation. + double max_value; +}; + +// ----- CONSTANTS ------------------------------ +constexpr int HALLWAY_SIZE{5}; +constexpr int HALF_HALLWAY_SIZE{HALLWAY_SIZE / 2}; +const MapGenerationConstant WIDTH{30, 1.2, 150}; +const MapGenerationConstant HEIGHT{20, 1.2, 100}; +const MapGenerationConstant OBSTACLE_COUNT{20, 1.3, 200}; +const MapGenerationConstant ITEM_COUNT{5, 1.1, 30}; + +// ----- FUNCTIONS ------------------------------ +/// Generate a value based on the exponential equation. +/// +/// @param level - The game level to generate a value for. +/// @return The generated value. +[[nodiscard]] inline auto generate_value(const MapGenerationConstant &map_generation_constant, const int level) -> int { + return static_cast( + std::min(round(map_generation_constant.base_value * pow(map_generation_constant.increase, level)), + map_generation_constant.max_value)); +} + +auto collect_positions(const Grid &grid, const TileType target) -> std::vector { + // Get all positions in the grid that match the target + std::vector result; + for (int y = 0; y < grid.height; y++) { + for (int x = 0; x < grid.width; x++) { + if (grid.get_value({x, y}) == target) { + result.emplace_back(x, y); + } + } + } + return result; +} + +void place_tile(Grid &grid, std::mt19937 &random_generator, const TileType target_tile, + std::vector &possible_tiles) { + // Check if at least one tile exists + if (possible_tiles.empty()) { + throw std::length_error("Possible tiles size must be bigger than 0."); + } + + // Get a random tile and place the target tile there + const std::size_t tile_index{ + std::uniform_int_distribution{0, possible_tiles.size() - 1}(random_generator)}; + const Position possible_tile{possible_tiles[tile_index]}; + possible_tiles[tile_index] = possible_tiles.back(); + possible_tiles.pop_back(); + grid.set_value(possible_tile, target_tile); +} + +auto create_complete_graph(const std::vector &rooms) -> std::unordered_map> { + // Check if the rooms vector is empty + if (rooms.empty()) { + throw std::length_error("Rooms size must be bigger than 0."); + } + + // Create the complete graph of all rooms + std::unordered_map> complete_graph; + for (const Rect &room : rooms) { + std::vector temp; + std::copy_if(rooms.begin(), rooms.end(), std::back_inserter(temp), + [&room](const Rect &neighbour) { return neighbour != room; }); + complete_graph.insert({room, temp}); + } + return complete_graph; +} + +auto create_connections(const std::unordered_map> &complete_graph) -> std::unordered_set { + // Check if the complete_graph is empty + if (complete_graph.empty()) { + throw std::length_error("Complete graph size must be bigger than 0."); + } + + // Use Prim's algorithm to construct a minimum spanning tree from complete_graph + const Rect start{complete_graph.begin()->first}; + std::priority_queue unexplored; + std::unordered_set visited; + std::unordered_set mst; + unexplored.emplace(0, start, start); + while (mst.size() < complete_graph.size() && !unexplored.empty()) { + // Get the neighbour with the lowest cost + const Edge lowest{unexplored.top()}; + unexplored.pop(); + + // Check if the neighbour is already visited or not + if (visited.contains(lowest.destination)) { + continue; + } + + // Neighbour isn't visited so mark it as visited and add its neighbours to the heap + visited.emplace(lowest.destination); + for (const auto &neighbour : complete_graph.at(lowest.destination)) { + unexplored.emplace(lowest.destination.get_distance_to(neighbour), lowest.destination, neighbour); + } + + // Add a new edge towards the lowest cost neighbour onto the mst + if (lowest.source != lowest.destination) { + // Save the connection + mst.emplace(lowest); + } + } + + // Return the constructed minimum-spanning tree + return mst; +} + +void create_hallways(Grid &grid, std::mt19937 &random_generator, const std::unordered_set &connections, + const int obstacle_count) { + // Place random obstacles in the grid + std::vector obstacle_positions{collect_positions(grid, TileType::Empty)}; + for (int _ = 0; _ < obstacle_count; _++) { + place_tile(grid, random_generator, TileType::Obstacle, obstacle_positions); + } + + // Use the A* algorithm to connect each pair of rooms avoiding the obstacles + std::vector> path_positions(connections.size()); + std::transform(std::execution::par, connections.begin(), connections.end(), path_positions.begin(), + [&grid](Edge connection) { + return calculate_astar_path(grid, connection.source.centre, connection.destination.centre); + }); + + // Place a rect box around each path_position to create the hallways + for (const std::vector &path : path_positions) { + for (const Position &path_position : path) { + Rect{{path_position.x - HALF_HALLWAY_SIZE, path_position.y - HALF_HALLWAY_SIZE}, + {path_position.x + HALF_HALLWAY_SIZE, path_position.y + HALF_HALLWAY_SIZE}} + .place_rect(grid); + } + } +} + +auto create_map(const int level, std::optional seed) + -> std::pair, std::tuple> { + // Check that the level number is valid + if (level < 0) { + throw std::length_error("Level must be bigger than or equal to 0."); + } + + // Create the random generator generating a seed if one isn't provided + if (!seed.has_value()) { + std::random_device random_device; + std::mt19937_64 seed_generator{random_device()}; + seed = std::uniform_int_distribution{}(seed_generator); + } + std::mt19937 random_generator{seed.value()}; + + // Initialise a few variables needed for the map generation + const int grid_width{generate_value(WIDTH, level)}; + const int grid_height{generate_value(HEIGHT, level)}; + Grid grid{grid_width, grid_height}; + Leaf bsp{{{0, 0}, {grid_width - 1, grid_height - 1}}}; + + // Split the bsp, create the rooms, and create the hallways between the rooms + std::vector rooms; + split(bsp, random_generator); + create_room(bsp, grid, random_generator, rooms); + create_hallways(grid, random_generator, create_connections(create_complete_graph(rooms)), + generate_value(OBSTACLE_COUNT, level)); + + // Place the player tile as well as the items in the grid + std::vector possible_tiles{collect_positions(grid, TileType::Floor)}; + place_tile(grid, random_generator, TileType::Player, possible_tiles); + for (int _ = 0; _ < generate_value(ITEM_COUNT, level); _++) { + place_tile(grid, random_generator, TileType::Potion, possible_tiles); + } + + // Return the grid and a LevelConstants object + return std::make_pair(*grid.grid, std::make_tuple(level, grid_width, grid_height)); +} diff --git a/src/hades_extensions/src/generation/primitives.cpp b/src/hades_extensions/src/generation/primitives.cpp new file mode 100644 index 00000000..3b95c0b4 --- /dev/null +++ b/src/hades_extensions/src/generation/primitives.cpp @@ -0,0 +1,22 @@ +// Related header +#include "generation/primitives.hpp" + +// ----- FUNCTIONS ------------------------------ +void Rect::place_rect(Grid &grid) const { + // Place the walls + for (int y = std::max(top_left.y, 0); y < std::min(bottom_right.y + 1, grid.height); y++) { + for (int x = std::max(top_left.x, 0); x < std::min(bottom_right.x + 1, grid.width); x++) { + if (grid.get_value({x, y}) == TileType::Empty || grid.get_value({x, y}) == TileType::Obstacle) { + grid.set_value({x, y}, TileType::Wall); + } + } + } + + // Place the floors making sure the ranges are -1 in all directions since we don't want to overwrite the walls keeping + // the player in, but we still want to overwrite walls that block the path for hallways + for (int y = std::max(top_left.y + 1, 1); y < std::min(bottom_right.y, grid.height - 1); y++) { + for (int x = std::max(top_left.x + 1, 1); x < std::min(bottom_right.x, grid.width - 1); x++) { + grid.set_value({x, y}, TileType::Floor); + } + } +} diff --git a/src/hades_extensions/tests/CMakeLists.txt b/src/hades_extensions/tests/CMakeLists.txt new file mode 100644 index 00000000..3bb0eb91 --- /dev/null +++ b/src/hades_extensions/tests/CMakeLists.txt @@ -0,0 +1,31 @@ +# Fetch the GoogleTest repository and initialise it +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.13.0 +) +set(gtest_force_shared_crt ON CACHE BOOL "Always use msvcrt.dll" FORCE) +FetchContent_MakeAvailable(googletest) + +# Create an executable from our test code and link it against GTest and the C++ library then add it as a test +include(CTest) +add_executable(${TEST_MODULE} + ${CMAKE_SOURCE_DIR}/tests/game_objects/test_registry.cpp + ${CMAKE_SOURCE_DIR}/tests/game_objects/test_steering.cpp + ${CMAKE_SOURCE_DIR}/tests/game_objects/systems/test_armour_regen.cpp + ${CMAKE_SOURCE_DIR}/tests/game_objects/systems/test_attacks.cpp + ${CMAKE_SOURCE_DIR}/tests/game_objects/systems/test_effects.cpp + ${CMAKE_SOURCE_DIR}/tests/game_objects/systems/test_inventory.cpp + ${CMAKE_SOURCE_DIR}/tests/game_objects/systems/test_movements.cpp + ${CMAKE_SOURCE_DIR}/tests/game_objects/systems/test_upgrade.cpp + ${CMAKE_SOURCE_DIR}/tests/generation/test_astar.cpp + ${CMAKE_SOURCE_DIR}/tests/generation/test_bsp.cpp + ${CMAKE_SOURCE_DIR}/tests/generation/test_map.cpp + ${CMAKE_SOURCE_DIR}/tests/generation/test_primitives.cpp +) +target_link_libraries( + ${TEST_MODULE} + gtest_main + ${CPP_LIB} +) +add_test(NAME Tests COMMAND ${TEST_MODULE}) diff --git a/src/hades_extensions/tests/game_objects/systems/test_armour_regen.cpp b/src/hades_extensions/tests/game_objects/systems/test_armour_regen.cpp new file mode 100644 index 00000000..90d4231e --- /dev/null +++ b/src/hades_extensions/tests/game_objects/systems/test_armour_regen.cpp @@ -0,0 +1,65 @@ +// External includes +#include "gtest/gtest.h" + +// Local headers +#include "game_objects/stats.hpp" +#include "game_objects/systems/armour_regen.hpp" + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the ArmourRegenSystem tests. +class ArmourRegenSystemFixture : public testing::Test { + protected: + /// The registry that manages the game objects, components, and systems. + Registry registry{}; + + /// Set up the fixture for the tests. + void SetUp() override { + registry.create_game_object({std::make_shared(50, -1), std::make_shared(4, -1)}); + registry.add_system(); + } + + /// Get the armour regen system from the registry. + /// + /// @return The armour regen system. + auto get_armour_regen_system() -> std::shared_ptr { + return registry.get_system(); + } +}; + +// ----- TESTS ---------------------------------- +/// Test that the armour regen component is updated correctly when armour is full. +TEST_F(ArmourRegenSystemFixture, TestArmourRegenSystemUpdateFullArmour) { + get_armour_regen_system()->update(5); + ASSERT_EQ(registry.get_component(0)->get_value(), 50); + ASSERT_EQ(registry.get_component(0)->time_since_armour_regen, 0); +} + +/// Test that the armour regen component is updated with a small delta time. +TEST_F(ArmourRegenSystemFixture, TestArmourRegenSystemUpdateSmallDeltaTime) { + auto armour{registry.get_component(0)}; + armour->set_value(armour->get_value() - 10); + get_armour_regen_system()->update(2); + ASSERT_EQ(armour->get_value(), 40); + ASSERT_EQ(registry.get_component(0)->time_since_armour_regen, 2); +} + +/// Test that the armour regen component is updated with a large delta time. +TEST_F(ArmourRegenSystemFixture, TestArmourRegenSystemUpdateLargeDeltaTime) { + auto armour{registry.get_component(0)}; + armour->set_value(armour->get_value() - 10); + get_armour_regen_system()->update(6); + ASSERT_EQ(armour->get_value(), 41); + ASSERT_EQ(registry.get_component(0)->time_since_armour_regen, 0); +} + +/// Test that the armour regen component is updated multiple times correctly. +TEST_F(ArmourRegenSystemFixture, TestArmourRegenSystemUpdateMultipleUpdates) { + auto armour{registry.get_component(0)}; + armour->set_value(armour->get_value() - 10); + get_armour_regen_system()->update(1); + ASSERT_EQ(armour->get_value(), 40); + ASSERT_EQ(registry.get_component(0)->time_since_armour_regen, 1); + get_armour_regen_system()->update(2); + ASSERT_EQ(armour->get_value(), 40); + ASSERT_EQ(registry.get_component(0)->time_since_armour_regen, 3); +} diff --git a/src/hades_extensions/tests/game_objects/systems/test_attacks.cpp b/src/hades_extensions/tests/game_objects/systems/test_attacks.cpp new file mode 100644 index 00000000..69609811 --- /dev/null +++ b/src/hades_extensions/tests/game_objects/systems/test_attacks.cpp @@ -0,0 +1,222 @@ +// Local headers +#include "game_objects/stats.hpp" +#include "game_objects/systems/attacks.hpp" +#include "macros.hpp" + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the AttackSystem tests. +class AttackSystemFixture : public testing::Test { + protected: + /// The registry that manages the game objects, components, and systems. + Registry registry{}; + + /// A list of targets for use in testing. + std::vector targets{}; + + /// Set up the fixture for the tests. + void SetUp() override { + auto create_target{[&](Vec2d position) { + const int target{ + registry.create_game_object({std::make_shared(50, -1), std::make_shared(0, -1)}, true)}; + registry.get_kinematic_object(target)->position = position; + return target; + }}; + + // Create the targets and add the attacks system + targets = { + create_target({-20, -100}), create_target({20, 60}), create_target({-200, 100}), create_target({100, -100}), + create_target({-100, -99}), create_target({0, -200}), create_target({0, -192}), create_target({0, 0}), + }; + registry.add_system(); + registry.add_system(); + } + + /// Create an attacks component for a game object. + /// + /// @tparam T - The type of the component or system. + /// @param list - The initializer list to pass to the constructor. + void create_attack_component(const std::vector &&enabled_attacks) { + const int game_object_id{registry.create_game_object({std::make_shared(enabled_attacks)}, true)}; + registry.get_kinematic_object(game_object_id)->rotation = 180; + } + + /// Get the attacks system from the registry. + /// + /// @return The attacks system. + auto get_attacks_system() -> std::shared_ptr { return registry.get_system(); } +}; + +/// Implements the fixture for the DamageSystem tests. +class DamageSystemFixture : public testing::Test { + protected: + /// The registry that manages the game objects, components, and systems. + Registry registry{}; + + /// Set up the fixture for the tests. + void SetUp() override { registry.add_system(); } + + /// Create health and armour attributes for use in testing. + void create_health_and_armour_attributes() { + registry.create_game_object({std::make_shared(300, -1), std::make_shared(100, -1)}); + } + + /// Get the damage system from the registry. + /// + /// @return The damage system. + auto get_damage_system() -> std::shared_ptr { return registry.get_system(); } +}; + +// ----- TESTS ---------------------------------- +/// Test that performing an area of effect attack works correctly. +TEST_F(AttackSystemFixture, TestAttacksDoAreaOfEffectAttack) { + create_attack_component({AttackAlgorithm::AreaOfEffect}); + ASSERT_FALSE(get_attacks_system()->do_attack(8, targets).has_value()); + ASSERT_EQ(registry.get_component(targets[0])->get_value(), 40); + ASSERT_EQ(registry.get_component(targets[1])->get_value(), 40); + ASSERT_EQ(registry.get_component(targets[2])->get_value(), 50); + ASSERT_EQ(registry.get_component(targets[3])->get_value(), 40); + ASSERT_EQ(registry.get_component(targets[4])->get_value(), 40); + ASSERT_EQ(registry.get_component(targets[5])->get_value(), 50); + ASSERT_EQ(registry.get_component(targets[6])->get_value(), 40); + ASSERT_EQ(registry.get_component(targets[7])->get_value(), 40); +} + +/// Test that performing a melee attack works correctly. +TEST_F(AttackSystemFixture, TestAttacksDoMeleeAttack) { + create_attack_component({AttackAlgorithm::Melee}); + ASSERT_FALSE(get_attacks_system()->do_attack(8, targets).has_value()); + ASSERT_EQ(registry.get_component(targets[0])->get_value(), 40); + ASSERT_EQ(registry.get_component(targets[1])->get_value(), 50); + ASSERT_EQ(registry.get_component(targets[2])->get_value(), 50); + ASSERT_EQ(registry.get_component(targets[3])->get_value(), 40); + ASSERT_EQ(registry.get_component(targets[4])->get_value(), 50); + ASSERT_EQ(registry.get_component(targets[5])->get_value(), 50); + ASSERT_EQ(registry.get_component(targets[6])->get_value(), 40); + ASSERT_EQ(registry.get_component(targets[7])->get_value(), 40); +} + +/// Test that performing a ranged attack works correctly. +TEST_F(AttackSystemFixture, TestAttacksDoRangedAttack) { + // This is due to floating point precision + create_attack_component({AttackAlgorithm::Ranged}); + std::tuple attack_result{get_attacks_system()->do_attack(8, targets).value()}; + ASSERT_EQ(get<0>(attack_result), Vec2d(0, 0)); + ASSERT_EQ(get<1>(attack_result), -300); + ASSERT_NEAR(get<2>(attack_result), 0, 1e-13); +} + +/// Test that an exception is raised if an invalid game object ID is provided. +TEST_F(AttackSystemFixture, TestAttacksDoAttackInvalidGameObjectId) { + create_attack_component({AttackAlgorithm::Ranged}); + ASSERT_THROW_MESSAGE( + (get_attacks_system()->do_attack(-1, targets)), RegistryError, + "The game object `-1` is not registered with the registry or does not have the required component.") +} + +/// Test that switching between attacks once works correctly. +TEST_F(AttackSystemFixture, TestAttacksPreviousNextAttackSingle) { + create_attack_component({AttackAlgorithm::AreaOfEffect, AttackAlgorithm::Melee, AttackAlgorithm::Ranged}); + ASSERT_EQ(registry.get_component(8)->attack_state, 0); + get_attacks_system()->next_attack(8); + ASSERT_EQ(registry.get_component(8)->attack_state, 1); + get_attacks_system()->previous_attack(8); + ASSERT_EQ(registry.get_component(8)->attack_state, 0); +} + +/// Test that switching between attacks multiple times works correctly. +TEST_F(AttackSystemFixture, TestAttacksPreviousAttackMultiple) { + create_attack_component({AttackAlgorithm::AreaOfEffect, AttackAlgorithm::Melee, AttackAlgorithm::Ranged}); + ASSERT_EQ(registry.get_component(8)->attack_state, 0); + get_attacks_system()->next_attack(8); + ASSERT_EQ(registry.get_component(8)->attack_state, 1); + get_attacks_system()->next_attack(8); + ASSERT_EQ(registry.get_component(8)->attack_state, 2); + get_attacks_system()->next_attack(8); + ASSERT_EQ(registry.get_component(8)->attack_state, 2); + get_attacks_system()->previous_attack(8); + ASSERT_EQ(registry.get_component(8)->attack_state, 1); + get_attacks_system()->previous_attack(8); + ASSERT_EQ(registry.get_component(8)->attack_state, 0); + get_attacks_system()->previous_attack(8); + ASSERT_EQ(registry.get_component(8)->attack_state, 0); +} + +/// Test that changing the attack state works correctly when there are no attacks. +TEST_F(AttackSystemFixture, TestAttacksPreviousNextAttackEmptyAttacks) { + create_attack_component({}); + ASSERT_EQ(registry.get_component(8)->attack_state, 0); + get_attacks_system()->next_attack(8); + ASSERT_EQ(registry.get_component(8)->attack_state, 0); + get_attacks_system()->previous_attack(8); + ASSERT_EQ(registry.get_component(8)->attack_state, 0); +} + +/// Test that an exception is raised if an invalid game object ID is provided. +TEST_F(AttackSystemFixture, TestAttacksPreviousNextAttackInvalidGameObjectId) { + create_attack_component({}); + ASSERT_THROW_MESSAGE( + get_attacks_system()->next_attack(-1), RegistryError, + "The game object `-1` is not registered with the registry or does not have the required component.") + ASSERT_THROW_MESSAGE( + get_attacks_system()->previous_attack(-1), RegistryError, + "The game object `-1` is not registered with the registry or does not have the required component.") +} + +/// Test that damage is dealt when health and armour are lower than damage. +TEST_F(DamageSystemFixture, TestDamageSystemDealDamageLowHealthArmour) { + create_health_and_armour_attributes(); + get_damage_system()->deal_damage(0, 350); + ASSERT_EQ(registry.get_component(0)->get_value(), 50); + ASSERT_EQ(registry.get_component(0)->get_value(), 0); +} + +/// Test that no damage is dealt when armour is larger than damage. +TEST_F(DamageSystemFixture, TestDamageSystemDealDamageLargeArmour) { + create_health_and_armour_attributes(); + get_damage_system()->deal_damage(0, 50); + ASSERT_EQ(registry.get_component(0)->get_value(), 300); + ASSERT_EQ(registry.get_component(0)->get_value(), 50); +} + +/// Test that no damage is dealt when damage is zero. +TEST_F(DamageSystemFixture, TestDamageSystemDealDamageZeroDamage) { + create_health_and_armour_attributes(); + get_damage_system()->deal_damage(0, 0); + ASSERT_EQ(registry.get_component(0)->get_value(), 300); + ASSERT_EQ(registry.get_component(0)->get_value(), 100); +} + +/// Test that damage is dealt when armour is zero. +TEST_F(DamageSystemFixture, TestDamageSystemDealDamageZeroArmour) { + create_health_and_armour_attributes(); + auto armour{registry.get_component(0)}; + armour->set_value(0); + get_damage_system()->deal_damage(0, 100); + ASSERT_EQ(registry.get_component(0)->get_value(), 200); + ASSERT_EQ(armour->get_value(), 0); +} + +/// Test that damage is dealt when health is zero. +TEST_F(DamageSystemFixture, TestDamageSystemDealDamageZeroHealth) { + create_health_and_armour_attributes(); + auto health{registry.get_component(0)}; + health->set_value(0); + get_damage_system()->deal_damage(0, 50); + ASSERT_EQ(health->get_value(), 0); + ASSERT_EQ(registry.get_component(0)->get_value(), 50); +} + +/// Test that no damage is dealt when the attributes are not initialised. +TEST_F(DamageSystemFixture, TestDamageSystemDealDamageNonexistentAttributes) { + registry.create_game_object({}); + ASSERT_THROW_MESSAGE( + get_damage_system()->deal_damage(0, 100), RegistryError, + "The game object `0` is not registered with the registry or does not have the required component.") +} + +/// Test that an exception is raised if an invalid game object ID is provided. +TEST_F(DamageSystemFixture, TestDamageSystemDealDamageInvalidGameObjectId) { + ASSERT_THROW_MESSAGE( + get_damage_system()->deal_damage(-1, 100), RegistryError, + "The game object `-1` is not registered with the registry or does not have the required component.") +} diff --git a/src/hades_extensions/tests/game_objects/systems/test_effects.cpp b/src/hades_extensions/tests/game_objects/systems/test_effects.cpp new file mode 100644 index 00000000..23815c9a --- /dev/null +++ b/src/hades_extensions/tests/game_objects/systems/test_effects.cpp @@ -0,0 +1,192 @@ +// Local headers +#include "game_objects/stats.hpp" +#include "game_objects/systems/effects.hpp" +#include "macros.hpp" + +// ----- STRUCTURES ----------------------------- +/// Represents a test stat useful for testing. +struct TestStat : public Stat { + /// Initialise the object. + /// + /// @param value - The initial and maximum value of the test stat. + /// @param maximum_level - The maximum level of the test stat. + TestStat(const double value, const int maximum_level) : Stat(value, maximum_level) {} +}; + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the EffectSystem tests. +class EffectSystemFixture : public testing::Test { + protected: + /// The registry that manages the game objects, components, and systems. + Registry registry{}; + + /// The increase function for an effect. + ActionFunction increase_function{[](int level) { return 5 + std::pow(level, 2); }}; + + /// The duration function for an effect. + ActionFunction duration_function{[](int level) { return 10 * level; }}; + + /// The interval function for an effect. + ActionFunction interval_function{[](int level) { return std::pow(2, level); }}; + + /// The data for a status effect. + StatusEffectData status_effect_data{StatusEffectType::TEMP, increase_function, duration_function, interval_function}; + + /// Set up the fixture for the tests. + void SetUp() override { registry.add_system(); } + + /// Create a game object for the instant effect tests. + void create_instant_game_object() { registry.create_game_object({std::make_shared(100, -1)}); } + + /// Create a game object for the status effect tests. + void create_status_game_object() { + registry.create_game_object({std::make_shared(200, -1), std::make_shared()}); + } + + /// Get the effect system from the registry. + /// + /// @return The effect system. + auto get_effect_system() -> std::shared_ptr { return registry.get_system(); } +}; + +// ----- TESTS ---------------------------------- +/// Test that a status effect is updated correctly with a small delta time. +TEST_F(EffectSystemFixture, TestEffectSystemUpdateSmallDeltaTime) { + create_status_game_object(); + auto test_stat{registry.get_component(0)}; + test_stat->set_value(100); + get_effect_system()->apply_status_effect(0, typeid(TestStat), status_effect_data, 1); + ASSERT_EQ(test_stat->get_value(), 106); + get_effect_system()->update(5); + ASSERT_EQ(test_stat->get_value(), 112); + ASSERT_EQ(registry.get_component(0)->applied_effects.at(StatusEffectType::TEMP).time_counter, 5); +} + +/// Test that a status effect is updated correctly with a large delta time. +TEST_F(EffectSystemFixture, TestEffectSystemUpdateLargeDeltaTime) { + create_status_game_object(); + auto test_stat{registry.get_component(0)}; + test_stat->set_value(100); + get_effect_system()->apply_status_effect(0, typeid(TestStat), status_effect_data, 1); + ASSERT_EQ(test_stat->get_value(), 106); + get_effect_system()->update(20); + ASSERT_EQ(test_stat->get_value(), 106); + ASSERT_FALSE(registry.get_component(0)->applied_effects.contains(StatusEffectType::TEMP)); +} + +/// Test that a status effect is updated correctly after multiple updates. +TEST_F(EffectSystemFixture, TestEffectSystemUpdateMultipleDeltaTimes) { + create_status_game_object(); + auto test_stat{registry.get_component(0)}; + test_stat->set_value(100); + get_effect_system()->apply_status_effect(0, typeid(TestStat), status_effect_data, 1); + auto &applied_effects{registry.get_component(0)->applied_effects}; + get_effect_system()->update(1); + ASSERT_EQ(test_stat->get_value(), 106); + ASSERT_EQ(applied_effects.at(StatusEffectType::TEMP).time_counter, 1); + get_effect_system()->update(5); + ASSERT_EQ(test_stat->get_value(), 112); + ASSERT_EQ(applied_effects.at(StatusEffectType::TEMP).time_counter, 6); + get_effect_system()->update(1); + ASSERT_EQ(test_stat->get_value(), 118); + ASSERT_EQ(applied_effects.at(StatusEffectType::TEMP).time_counter, 7); + get_effect_system()->update(1); + ASSERT_EQ(test_stat->get_value(), 124); + ASSERT_EQ(applied_effects.at(StatusEffectType::TEMP).time_counter, 8); + get_effect_system()->update(2); + ASSERT_EQ(test_stat->get_value(), 124); + ASSERT_FALSE(applied_effects.contains(StatusEffectType::TEMP)); +} + +/// Test that a status effect is not updated if one does not exist. +TEST_F(EffectSystemFixture, TestEffectSystemUpdateNoStatusEffect) { + create_status_game_object(); + auto test_stat{registry.get_component(0)}; + test_stat->set_value(100); + get_effect_system()->update(5); + ASSERT_EQ(test_stat->get_value(), 100); +} + +/// Test that an instant effect is applied correctly if the value equals the maximum. +TEST_F(EffectSystemFixture, TestEffectSystemApplyInstantEffectValueEqualMax) { + create_instant_game_object(); + get_effect_system()->apply_instant_effect(0, typeid(TestStat), increase_function, 0); + ASSERT_EQ(registry.get_component(0)->get_value(), 100); +} + +/// Test that an instant effect is applied correctly if the value is lower than the maximum. +TEST_F(EffectSystemFixture, TestEffectSystemApplyInstantEffectValueLowerMax) { + create_instant_game_object(); + auto component{registry.get_component(0)}; + component->set_value(40); + get_effect_system()->apply_instant_effect(0, typeid(TestStat), increase_function, 0); + ASSERT_EQ(component->get_value(), 45); +} + +/// Test that an instant effect is not applied if the target component is not initialised. +TEST_F(EffectSystemFixture, TestEffectSystemApplyInstantEffectNonexistentTargetComponent) { + registry.create_game_object({}); + ASSERT_THROW_MESSAGE( + get_effect_system()->apply_instant_effect(0, typeid(TestStat), increase_function, 1), RegistryError, + "The game object `0` is not registered with the registry or does not have the required component.") +} + +/// Test that an exception is raised if an invalid game object ID is provided. +TEST_F(EffectSystemFixture, TestEffectSystemApplyInstantEffectInvalidGameObjectId){ASSERT_THROW_MESSAGE( + get_effect_system()->apply_instant_effect(-1, typeid(TestStat), increase_function, 1), RegistryError, + "The game object `-1` is not registered with the registry or does not have the required component.")} + +/// Test that a status effect is applied correctly if no status effect is currently applied. +TEST_F(EffectSystemFixture, TestEffectSystemApplyStatusEffectNoAppliedEffect) { + create_status_game_object(); + ASSERT_TRUE(get_effect_system()->apply_status_effect(0, typeid(TestStat), status_effect_data, 1)); + ASSERT_EQ(registry.get_component(0)->get_value(), 200); + auto applied_status_effect{registry.get_component(0)->applied_effects.at(StatusEffectType::TEMP)}; + ASSERT_EQ(applied_status_effect.value, 6); + ASSERT_EQ(applied_status_effect.duration, 10); + ASSERT_EQ(applied_status_effect.interval, 2); + ASSERT_EQ(applied_status_effect.time_counter, 0); +} + +/// Test that a status effect is applied correctly if the value is lower than the max. +TEST_F(EffectSystemFixture, TestEffectSystemApplyStatusEffectValueLowerMax) { + create_status_game_object(); + auto component{registry.get_component(0)}; + component->set_value(150); + ASSERT_TRUE(get_effect_system()->apply_status_effect(0, typeid(TestStat), status_effect_data, 1)); + ASSERT_EQ(component->get_value(), 156); +} + +/// Test that a status effect is not applied if a status effect is already applied. +TEST_F(EffectSystemFixture, TestEffectSystemApplyStatusEffectExistingStatusEffect) { + create_status_game_object(); + ASSERT_TRUE(get_effect_system()->apply_status_effect(0, typeid(TestStat), status_effect_data, 1)); + ASSERT_TRUE(registry.get_component(0)->applied_effects.contains(StatusEffectType::TEMP)); + ASSERT_FALSE(get_effect_system()->apply_status_effect(0, typeid(TestStat), status_effect_data, 1)); +} + +/// Test that multiple status effects are applied correctly. +TEST_F(EffectSystemFixture, TestEffectSystemApplyStatusEffectMultipleStatusEffects) { + create_status_game_object(); + ASSERT_TRUE(get_effect_system()->apply_status_effect(0, typeid(TestStat), status_effect_data, 1)); + const StatusEffectData status_effect_data_two{StatusEffectType::TEMP2, increase_function, duration_function, interval_function}; + ASSERT_TRUE(get_effect_system()->apply_status_effect(0, typeid(TestStat), status_effect_data_two, 1)); + auto status_effects{registry.get_component(0)->applied_effects}; + ASSERT_TRUE(status_effects.contains(StatusEffectType::TEMP)); + ASSERT_TRUE(status_effects.contains(StatusEffectType::TEMP2)); +} + +/// Test that a status effect is not applied if the target component is not initialised. +TEST_F(EffectSystemFixture, TestEffectSystemApplyStatusEffectNonexistentTargetComponent) { + registry.create_game_object({}); + ASSERT_THROW_MESSAGE( + get_effect_system()->apply_status_effect(0, typeid(TestStat), status_effect_data, 1), RegistryError, + "The game object `0` is not registered with the registry or does not have the required component.") +} + +/// Test that an exception is raised if an invalid game object ID is provided. +TEST_F(EffectSystemFixture, TestEffectSystemApplyStatusEffectInvalidGameObjectId) { + ASSERT_THROW_MESSAGE( + get_effect_system()->apply_status_effect(-1, typeid(TestStat), status_effect_data, 1), RegistryError, + "The game object `-1` is not registered with the registry or does not have the required component.") +} diff --git a/src/hades_extensions/tests/game_objects/systems/test_inventory.cpp b/src/hades_extensions/tests/game_objects/systems/test_inventory.cpp new file mode 100644 index 00000000..cbe0b584 --- /dev/null +++ b/src/hades_extensions/tests/game_objects/systems/test_inventory.cpp @@ -0,0 +1,75 @@ +// Local headers +#include "game_objects/systems/inventory.hpp" +#include "macros.hpp" + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the InventorySystem fixture. +class InventorySystemFixture : public testing::Test { + protected: + /// The registry that manages the game objects, components, and systems. + Registry registry{}; + + /// Set up the fixture for the tests. + void SetUp() override { + registry.create_game_object({std::make_shared(3, 6)}); + registry.add_system(); + } + + /// Get the inventory system from the registry. + /// + /// @return The inventory system. + auto get_inventory_system() -> std::shared_ptr { return registry.get_system(); } +}; + +// ----- TESTS ---------------------------------- +/// Test that InventorySpaceError is raised correctly when full. +TEST(Tests, TestThrowInventorySpaceErrorFull){ + ASSERT_THROW_MESSAGE(throw InventorySpaceError(true), InventorySpaceError, "The inventory is full.")} + +/// Test that InventorySpaceError is raised correctly when empty. +TEST(Tests, TestThrowInventorySpaceErrorEmpty){ + ASSERT_THROW_MESSAGE(throw InventorySpaceError(false), InventorySpaceError, "The inventory is empty.")} + +/// Test that a valid item is added to the inventory correctly. +TEST_F(InventorySystemFixture, TestInventorySystemAddItemToInventoryValid) { + get_inventory_system()->add_item_to_inventory(0, 50); + ASSERT_EQ(registry.get_component(0)->items, std::vector{50}); +} + +/// Test that a valid item is not added to a zero size inventory. +TEST_F(InventorySystemFixture, TestInventorySystemAddItemToInventoryZeroSize) { + registry.get_component(0)->width = 0; + ASSERT_THROW_MESSAGE(get_inventory_system()->add_item_to_inventory(0, 50), InventorySpaceError, + "The inventory is full.") +} + +/// Test that an exception is raised if an invalid game object ID is provided. +TEST_F(InventorySystemFixture, TestInventorySystemAddItemToInventoryInvalidGameObjectID){ASSERT_THROW_MESSAGE( + get_inventory_system()->add_item_to_inventory(-1, 50), RegistryError, + "The game object `-1` is not registered with the registry or does not have the required component.")} + +/// Test that a valid item is removed from the inventory correctly. +TEST_F(InventorySystemFixture, TestInventorySystemRemoveItemFromInventoryValid) { + const std::vector result{1, 4}; + get_inventory_system()->add_item_to_inventory(0, 1); + get_inventory_system()->add_item_to_inventory(0, 7); + get_inventory_system()->add_item_to_inventory(0, 4); + ASSERT_EQ(get_inventory_system()->remove_item_from_inventory(0, 1), 7); + ASSERT_EQ(registry.get_component(0)->items, result); +} + +/// Test that an exception is raised if a larger index is provided. +TEST_F(InventorySystemFixture, TestInventorySystemRemoveItemFromInventoryLargeIndex) { + get_inventory_system()->add_item_to_inventory(0, 5); + get_inventory_system()->add_item_to_inventory(0, 10); + get_inventory_system()->add_item_to_inventory(0, 50); + ASSERT_THROW_MESSAGE((get_inventory_system()->remove_item_from_inventory(0, 10)), InventorySpaceError, + "The index is out of range.") +} + +/// Test that an exception is raised if an invalid game object ID is provided. +TEST_F(InventorySystemFixture, TestInventorySystemRemoveItemFromInventoryInvalidGameObjectID) { + ASSERT_THROW_MESSAGE( + (get_inventory_system()->remove_item_from_inventory(-1, 0)), RegistryError, + "The game object `-1` is not registered with the registry or does not have the required component.") +} diff --git a/src/hades_extensions/tests/game_objects/systems/test_movements.cpp b/src/hades_extensions/tests/game_objects/systems/test_movements.cpp new file mode 100644 index 00000000..bbed460c --- /dev/null +++ b/src/hades_extensions/tests/game_objects/systems/test_movements.cpp @@ -0,0 +1,365 @@ +// Local headers +#include "game_objects/stats.hpp" +#include "game_objects/systems/movements.hpp" +#include "macros.hpp" + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the FootprintSystem tests. +class FootprintSystemFixture : public testing::Test { + protected: + /// The registry that manages the game objects, components, and systems. + Registry registry{}; + + /// Set up the fixture for the tests. + void SetUp() override { + registry.create_game_object({std::make_shared()}, true); + registry.add_system(); + registry.add_system(); + } + + /// Get the footprint system from the registry. + /// + /// @return The footprint system. + auto get_footprint_system() -> std::shared_ptr { return registry.get_system(); } +}; + +/// Implements the fixture for the KeyboardMovementSystem tests. +class KeyboardMovementFixture : public testing::Test { + protected: + /// The registry that manages the game objects, components, and systems. + Registry registry{}; + + /// Set up the fixture for the tests. + void SetUp() override { + registry.create_game_object({std::make_shared(100, -1), std::make_shared()}, true); + registry.add_system(); + } + + /// Get the keyboard movement system from the registry. + /// + /// @return The keyboard movement system. + auto get_keyboard_movement_system() -> std::shared_ptr { + return registry.get_system(); + } +}; + +/// Implements the fixture for the SteeringMovementSystem tests. +class SteeringMovementFixture : public testing::Test { + protected: + /// The registry that manages the game objects, components, and systems. + Registry registry{}; + + /// Set up the fixture for the tests. + void SetUp() override { + // Create the target game object + registry.create_game_object({std::make_shared()}, true); + + // Create the game object to follow the target and add the required systems + create_steering_movement_component({}); + registry.add_system(); + registry.add_system(); + } + + /// Get the steering movement system from the registry. + /// + /// @return The steering movement system. + auto get_steering_movement_system() -> std::shared_ptr { + return registry.get_system(); + } + + /// Create a steering movement component. + /// + /// @param steering_behaviours - The steering behaviours to initialise the component with. + /// @return The game object ID of the created game object. + auto create_steering_movement_component( + const std::unordered_map> &steering_behaviours) -> int { + const int game_object_id{registry.create_game_object( + {std::make_shared(100, -1), std::make_shared(steering_behaviours)}, true)}; + registry.get_component(game_object_id)->target_id = 0; + return game_object_id; + } +}; + +// ----- TESTS ---------------------------------- +/// Test that the footprint systems is updated with a small delta time. +TEST_F(FootprintSystemFixture, TestFootprintSystemUpdateSmallDeltaTime) { + get_footprint_system()->update(0.1); + auto footprints{registry.get_component(0)}; + ASSERT_EQ(footprints->footprints, std::deque{}); + ASSERT_EQ(footprints->time_since_last_footprint, 0.1); +} + +/// Test that the footprint system creates a footprint in an empty list. +TEST_F(FootprintSystemFixture, TestFootprintSystemUpdateLargeDeltaTimeEmptyList) { + get_footprint_system()->update(1); + auto footprints{registry.get_component(0)}; + ASSERT_EQ(footprints->footprints, std::deque{Vec2d(0, 0)}); + ASSERT_EQ(footprints->time_since_last_footprint, 0); +} + +/// Test that the footprint system creates a footprint in a non-empty list. +TEST_F(FootprintSystemFixture, TestFootprintSystemUpdateLargeDeltaTimeNonEmptyList) { + auto footprints{registry.get_component(0)}; + footprints->footprints = {{1, 1}, {2, 2}, {3, 3}}; + get_footprint_system()->update(0.5); + const std::deque expected_footprints{{1, 1}, {2, 2}, {3, 3}, {0, 0}}; + ASSERT_EQ(footprints->footprints, expected_footprints); +} + +/// Test that the footprint system creates a footprint and removes the oldest one. +TEST_F(FootprintSystemFixture, TestFootprintSystemUpdateLargeDeltaTimeFullList) { + auto footprints{registry.get_component(0)}; + footprints->footprints = {{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 6}, {7, 7}, {8, 8}, {9, 9}, {10, 10}}; + get_footprint_system()->update(0.5); + const std::deque expected_footprints{{2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 6}, + {7, 7}, {8, 8}, {9, 9}, {10, 10}, {0, 0}}; + ASSERT_EQ(footprints->footprints, expected_footprints); +} + +/// Test that the footprint system is updated correctly multiple times. +TEST_F(FootprintSystemFixture, TestFootprintSystemUpdateMultipleUpdates) { + auto footprints{registry.get_component(0)}; + get_footprint_system()->update(0.6); + ASSERT_EQ(footprints->footprints, std::deque{Vec2d(0, 0)}); + ASSERT_EQ(footprints->time_since_last_footprint, 0); + registry.get_kinematic_object(0)->position = {1, 1}; + get_footprint_system()->update(0.7); + const std::deque expected_footprints{{0, 0}, {1, 1}}; + ASSERT_EQ(footprints->footprints, expected_footprints); + ASSERT_EQ(footprints->time_since_last_footprint, 0); +} + +/// Test if the correct force is calculated if no keys are pressed. +TEST_F(KeyboardMovementFixture, TestKeyboardMovementSystemCalculateForceNone) { + ASSERT_EQ(get_keyboard_movement_system()->calculate_force(0), Vec2d(0, 0)); +} + +/// Test if the correct force is calculated for a move north. +TEST_F(KeyboardMovementFixture, TestKeyboardMovementSystemCalculateForceNorth) { + registry.get_component(0)->moving_north = true; + ASSERT_EQ(get_keyboard_movement_system()->calculate_force(0), Vec2d(0, 100)); +} + +/// Test if the correct force is calculated for a move south. +TEST_F(KeyboardMovementFixture, TestKeyboardMovementSystemCalculateForceSouth) { + registry.get_component(0)->moving_south = true; + ASSERT_EQ(get_keyboard_movement_system()->calculate_force(0), Vec2d(0, -100)); +} + +/// Test if the correct force is calculated for a move east. +TEST_F(KeyboardMovementFixture, TestKeyboardMovementSystemCalculateForceEast) { + registry.get_component(0)->moving_east = true; + ASSERT_EQ(get_keyboard_movement_system()->calculate_force(0), Vec2d(100, 0)); +} + +/// Test if the correct force is calculated for a move west. +TEST_F(KeyboardMovementFixture, TestKeyboardMovementSystemCalculateForceWest) { + registry.get_component(0)->moving_west = true; + ASSERT_EQ(get_keyboard_movement_system()->calculate_force(0), Vec2d(-100, 0)); +} + +/// Test if the correct force is calculated if east and west are pressed. +TEST_F(KeyboardMovementFixture, TestKeyboardMovementSystemCalculateForceEastWest) { + auto keyboard_movement{registry.get_component(0)}; + keyboard_movement->moving_east = true; + keyboard_movement->moving_west = true; + ASSERT_EQ(get_keyboard_movement_system()->calculate_force(0), Vec2d(0, 0)); +} + +/// Test if the correct force is calculated if north and south are pressed. +TEST_F(KeyboardMovementFixture, TestKeyboardMovementSystemCalculateForceNorthSouth) { + auto keyboard_movement{registry.get_component(0)}; + keyboard_movement->moving_east = true; + keyboard_movement->moving_west = true; + ASSERT_EQ(get_keyboard_movement_system()->calculate_force(0), Vec2d(0, 0)); +} + +/// Test if the correct force is calculated if north and west are pressed. +TEST_F(KeyboardMovementFixture, TestKeyboardMovementSystemCalculateForceNorthWest) { + auto keyboard_movement{registry.get_component(0)}; + keyboard_movement->moving_north = true; + keyboard_movement->moving_west = true; + ASSERT_EQ(get_keyboard_movement_system()->calculate_force(0), Vec2d(-100, 100)); +} + +/// Test if the correct force is calculated if south and east are pressed. +TEST_F(KeyboardMovementFixture, TestKeyboardMovementSystemCalculateForceSouthEast) { + auto keyboard_movement{registry.get_component(0)}; + keyboard_movement->moving_south = true; + keyboard_movement->moving_east = true; + ASSERT_EQ(get_keyboard_movement_system()->calculate_force(0), Vec2d(100, -100)); +} + +/// Test that an exception is raised if an invalid game object ID is provided. +TEST_F(KeyboardMovementFixture, TestKeyboardMovementSystemCalculateForceInvalidGameObjectId){ASSERT_THROW_MESSAGE( + (get_keyboard_movement_system()->calculate_force(-1)), RegistryError, + "The game object `-1` is not registered with the registry or does not have the required component.")} + +/// Test if the state is correctly changed to the default state. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceOutsideDistanceEmptyPathList) { + registry.get_kinematic_object(1)->position = {500, 500}; + static_cast(get_steering_movement_system()->calculate_force(1)); + ASSERT_EQ(registry.get_component(1)->movement_state, SteeringMovementState::Default); +} + +/// Test if the state is correctly changed to the footprint state. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceOutsideDistanceNonEmptyPathList) { + registry.get_kinematic_object(1)->position = {500, 500}; + auto steering_movement{registry.get_component(1)}; + steering_movement->path_list = {{300, 300}, {400, 400}}; + static_cast(get_steering_movement_system()->calculate_force(1)); + ASSERT_EQ(steering_movement->movement_state, SteeringMovementState::Footprint); +} + +/// Test if the state is correctly changed to the target stat. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceWithinDistance) { + static_cast(get_steering_movement_system()->calculate_force(1)); + ASSERT_EQ(registry.get_component(1)->movement_state, SteeringMovementState::Target); +} + +/// Test if a zero force is calculated if the state is missing. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceMissingState) { + ASSERT_EQ(get_steering_movement_system()->calculate_force(1), Vec2d(0, 0)); +} + +/// Test if the correct force is calculated for the arrive behaviour. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceArrive) { + create_steering_movement_component({{SteeringMovementState::Target, {SteeringBehaviours::Arrive}}}); + registry.get_kinematic_object(0)->position = {0, 0}; + registry.get_kinematic_object(2)->position = {100, 100}; + ASSERT_EQ(get_steering_movement_system()->calculate_force(2), Vec2d(-70.71067811865476, -70.71067811865476)); +} + +/// Test if the correct force is calculated for the evade behaviour. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceEvade) { + create_steering_movement_component({{SteeringMovementState::Target, {SteeringBehaviours::Evade}}}); + registry.get_kinematic_object(0)->position = {100, 100}; + registry.get_kinematic_object(0)->velocity = {-50, 0}; + ASSERT_EQ(get_steering_movement_system()->calculate_force(2), Vec2d(-54.28888213891886, -83.98045770360257)); +} + +/// Test if the correct force is calculated for the flee behaviour. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceFlee) { + create_steering_movement_component({{SteeringMovementState::Target, {SteeringBehaviours::Flee}}}); + registry.get_kinematic_object(0)->position = {50, 50}; + registry.get_kinematic_object(2)->position = {100, 100}; + ASSERT_EQ(get_steering_movement_system()->calculate_force(2), Vec2d(70.71067811865476, 70.71067811865476)); +} + +/// Test if the correct force is calculated for the follow path behaviour. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceFollowPath) { + create_steering_movement_component({{SteeringMovementState::Footprint, {SteeringBehaviours::FollowPath}}}); + registry.get_kinematic_object(2)->position = {200, 200}; + registry.get_component(2)->path_list = {{350, 350}, {500, 500}}; + ASSERT_EQ(get_steering_movement_system()->calculate_force(2), Vec2d(70.71067811865475, 70.71067811865475)); +} + +/// Test if the correct force is calculated for the obstacle avoidance behaviour. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceObstacleAvoidance) { + create_steering_movement_component({{SteeringMovementState::Target, {SteeringBehaviours::ObstacleAvoidance}}}); + registry.get_kinematic_object(2)->position = {100, 100}; + registry.get_kinematic_object(2)->velocity = {100, 100}; + registry.add_wall({1, 2}); + ASSERT_EQ(get_steering_movement_system()->calculate_force(2), Vec2d(25.881904510252056, -96.59258262890683)); +} + +/// Test if the correct force is calculated for the pursue behaviour. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForcePursue) { + create_steering_movement_component({{SteeringMovementState::Target, {SteeringBehaviours::Pursue}}}); + registry.get_kinematic_object(0)->position = {100, 100}; + registry.get_kinematic_object(0)->velocity = {-50, 0}; + ASSERT_EQ(get_steering_movement_system()->calculate_force(2), Vec2d(54.28888213891886, 83.98045770360257)); +} + +/// Test if the correct force is calculated for the seek behaviour. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceSeek) { + create_steering_movement_component({{SteeringMovementState::Target, {SteeringBehaviours::Seek}}}); + registry.get_kinematic_object(0)->position = {50, 50}; + registry.get_kinematic_object(2)->position = {100, 100}; + ASSERT_EQ(get_steering_movement_system()->calculate_force(2), Vec2d(-70.71067811865475, -70.71067811865475)); +} + +/// Test if the correct force is calculated for the wander behaviour. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceWander) { + create_steering_movement_component({{SteeringMovementState::Target, {SteeringBehaviours::Wander}}}); + registry.get_kinematic_object(2)->velocity = {100, -100}; + const Vec2d steering_force{get_steering_movement_system()->calculate_force(2)}; + ASSERT_EQ(round(steering_force.magnitude()), 100); + ASSERT_NE(steering_force, get_steering_movement_system()->calculate_force(2)); +} + +/// Test if the correct force is calculated when multiple behaviours are selected. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceMultipleBehaviours) { + create_steering_movement_component( + {{SteeringMovementState::Footprint, {SteeringBehaviours::FollowPath, SteeringBehaviours::Seek}}}); + registry.get_kinematic_object(2)->position = {300, 300}; + registry.get_component(2)->path_list = {{100, 200}, {-100, 0}}; + ASSERT_EQ(get_steering_movement_system()->calculate_force(2), Vec2d(-81.12421851755609, -58.47102846637651)); +} + +/// Test if the correct force is calculated when multiple states are initialised. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceMultipleStates) { + // Initialise the steering movement component with multiple states + create_steering_movement_component({{SteeringMovementState::Target, {SteeringBehaviours::Pursue}}, + {SteeringMovementState::Default, {SteeringBehaviours::Seek}}}); + + // Test the target state + registry.get_kinematic_object(0)->velocity = {-50, 100}; + registry.get_kinematic_object(2)->position = {100, 100}; + ASSERT_EQ(get_steering_movement_system()->calculate_force(2), Vec2d(-97.73793955511094, -21.14935392681019)); + + // Test the default state + registry.get_kinematic_object(2)->position = {300, 300}; + ASSERT_EQ(get_steering_movement_system()->calculate_force(2), Vec2d(-70.71067811865476, -70.71067811865476)); +} + +/// Test that an exception is raised if an invalid game object ID is provided. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemCalculateForceInvalidGameObjectId){ASSERT_THROW_MESSAGE( + (get_steering_movement_system()->calculate_force(-1)), RegistryError, + "The game object `-1` is not registered with the registry or does not have the required component.")} + +/// Test if the path list is updated if the position is within the view distance. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemUpdatePathListWithinDistance) { + get_steering_movement_system()->update_path_list(0, {{100, 100}, {300, 300}}); + const std::vector expected_path_list{{100, 100}, {300, 300}}; + ASSERT_EQ(registry.get_component(1)->path_list, expected_path_list); +} + +/// Test if the path list is updated if the position is outside the view distance. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemUpdatePathListOutsideDistance) { + get_steering_movement_system()->update_path_list(0, {{300, 300}, {500, 500}}); + ASSERT_EQ(registry.get_component(1)->path_list, std::vector{}); +} + +/// Test if the path list is updated if the position is equal to the view distance. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemUpdatePathListEqualDistance) { + get_steering_movement_system()->update_path_list(0, {{135.764501987, 135.764501987}}); + ASSERT_EQ(registry.get_component(1)->path_list, + std::vector{Vec2d(135.764501987, 135.764501987)}); +} + +/// Test if the path list is updated if multiple footprints are within view distance. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemUpdatePathListMultiplePoints) { + get_steering_movement_system()->update_path_list(0, {{100, 100}, {300, 300}, {50, 100}, {500, 500}}); + const std::vector expected_path_list{{50, 100}, {500, 500}}; + ASSERT_EQ(registry.get_component(1)->path_list, expected_path_list); +} + +/// Test if the path list is updated if the footprints list is empty. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemUpdatePathListEmptyList) { + get_steering_movement_system()->update_path_list(0, {}); + ASSERT_EQ(registry.get_component(1)->path_list, std::vector{}); +} + +/// Test if the path list is not updated if the target ID doesn't match. +TEST_F(SteeringMovementFixture, TestSteeringMovementUpdatePathListDifferentTargetId) { + registry.get_component(1)->target_id = -1; + get_steering_movement_system()->update_path_list(0, {{100, 100}}); + ASSERT_EQ(registry.get_component(1)->path_list, std::vector{}); +} + +/// Test if the path list is updated correctly if the Footprints component updates it. +TEST_F(SteeringMovementFixture, TestSteeringMovementSystemUpdatePathListFootprintUpdate) { + registry.get_system()->update(0.5); + ASSERT_EQ(registry.get_component(1)->path_list, std::vector{Vec2d(0, 0)}); +} diff --git a/src/hades_extensions/tests/game_objects/systems/test_upgrade.cpp b/src/hades_extensions/tests/game_objects/systems/test_upgrade.cpp new file mode 100644 index 00000000..0e39d278 --- /dev/null +++ b/src/hades_extensions/tests/game_objects/systems/test_upgrade.cpp @@ -0,0 +1,103 @@ +// Local headers +#include "game_objects/stats.hpp" +#include "game_objects/systems/upgrade.hpp" +#include "macros.hpp" + +// ----- STRUCTURES ----------------------------- +/// Represents a test stat useful for testing. +struct TestStat : public Stat { + /// Initialise the object. + /// + /// @param value - The initial and maximum value of the test stat. + /// @param maximum_level - The maximum level of the test stat. + TestStat(const double value, const int maximum_level) : Stat(value, maximum_level) {} +}; + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the UpgradeSystem tests. +class UpgradeSystemFixture : public testing::Test { + protected: + /// The registry that manages the game objects, components, and systems. + Registry registry{}; + + /// Set up the fixture for the tests. + void SetUp() override { registry.add_system(); } + + /// Create a game object with a given value and maximum level + /// + /// @param value - The value of the game object. + /// @param max_level - The maximum level of the game object. + void create_game_object(const int value, const int max_level) { + const std::unordered_map upgrades{ + {typeid(TestStat), [](int level) { return 150 * (level + 1); }}}; + registry.create_game_object({std::make_shared(value, max_level), std::make_shared(upgrades)}); + } + + /// Create a game object that is upgradable multiple times. + void create_upgradeable_game_object() { create_game_object(200, 3); } + + /// Get the upgrade system from the registry. + /// + /// @return The upgrade system. + auto get_upgrade_system() -> std::shared_ptr { return registry.get_system(); } +}; + +// ----- TESTS ---------------------------------- +/// Test that a test stat is upgraded correctly if the value equals the maximum. +TEST_F(UpgradeSystemFixture, TestUpgradeSystemUpgradeValueEqualMax) { + create_upgradeable_game_object(); + ASSERT_TRUE(get_upgrade_system()->upgrade_component(0, typeid(TestStat))); + ASSERT_EQ(registry.get_component(0)->get_value(), 350); + ASSERT_EQ(registry.get_component(0)->get_max_value(), 350); + ASSERT_EQ(registry.get_component(0)->get_current_level(), 1); +} + +/// Test that a test stat is upgraded correctly if the value is lower than the maximum. +TEST_F(UpgradeSystemFixture, TestUpgradeSystemUpgradeValueLowerMax) { + create_upgradeable_game_object(); + registry.get_component(0)->set_value(150); + ASSERT_TRUE(get_upgrade_system()->upgrade_component(0, typeid(TestStat))); + ASSERT_EQ(registry.get_component(0)->get_value(), 300); + ASSERT_EQ(registry.get_component(0)->get_max_value(), 350); + ASSERT_EQ(registry.get_component(0)->get_current_level(), 1); +} + +/// Test that a test stat that can be upgraded multiple times is upgraded correctly. +TEST_F(UpgradeSystemFixture, TestUpgradeSystemUpgradeMultipleTimes) { + create_upgradeable_game_object(); + ASSERT_TRUE(get_upgrade_system()->upgrade_component(0, typeid(TestStat))); + ASSERT_TRUE(get_upgrade_system()->upgrade_component(0, typeid(TestStat))); + ASSERT_TRUE(get_upgrade_system()->upgrade_component(0, typeid(TestStat))); + ASSERT_FALSE(get_upgrade_system()->upgrade_component(0, typeid(TestStat))); + ASSERT_EQ(registry.get_component(0)->get_value(), 1100); + ASSERT_EQ(registry.get_component(0)->get_max_value(), 1100); + ASSERT_EQ(registry.get_component(0)->get_current_level(), 3); +} + +/// Test that a test stat is upgraded only one time. +TEST_F(UpgradeSystemFixture, TestUpgradeSystemUpgradeOnce) { + create_game_object(150, 1); + ASSERT_TRUE(get_upgrade_system()->upgrade_component(0, typeid(TestStat))); + ASSERT_FALSE(get_upgrade_system()->upgrade_component(0, typeid(TestStat))); +} + +/// Test that a test stat is not upgraded if it is not allowed. +TEST_F(UpgradeSystemFixture, TestUpgradeSystemUpgradeNonUpgradeable) { + create_game_object(100, -1); + ASSERT_FALSE(get_upgrade_system()->upgrade_component(0, typeid(TestStat))); +} + +/// Test that a test stat is not upgraded if the target component is not initialised. +TEST_F(UpgradeSystemFixture, TestEffectSystemApplyStatusEffectNonexistentTargetComponent) { + registry.create_game_object({}); + ASSERT_THROW_MESSAGE( + get_upgrade_system()->upgrade_component(0, typeid(TestStat)), RegistryError, + "The game object `0` is not registered with the registry or does not have the required component.") +} + +/// Test that an exception is raised if an invalid game object ID is provided. +TEST_F(UpgradeSystemFixture, TestUpgradeSystemUpgradeInvalidGameObjectId) { + ASSERT_THROW_MESSAGE( + (get_upgrade_system()->upgrade_component(-1, typeid(TestStat))), RegistryError, + "The game object `-1` is not registered with the registry or does not have the required component.") +} diff --git a/src/hades_extensions/tests/game_objects/test_registry.cpp b/src/hades_extensions/tests/game_objects/test_registry.cpp new file mode 100644 index 00000000..b7c4a955 --- /dev/null +++ b/src/hades_extensions/tests/game_objects/test_registry.cpp @@ -0,0 +1,176 @@ +// Local headers +#include "game_objects/registry.hpp" +#include "macros.hpp" + +// ----- COMPONENTS ------------------------------ +/// Represents a game object component useful for testing. +struct TestGameObjectComponentOne : public ComponentBase {}; + +/// Represents a game object component with data useful for testing. +struct TestGameObjectComponentTwo : public ComponentBase { + /// A test list of integers. + std::vector test_list; + + /// Initialise the object. + /// + /// @param test_lst - The list to be used for testing. + explicit TestGameObjectComponentTwo(const std::vector &test_lst) : test_list(test_lst) {} +}; + +// ----- SYSTEMS -------------------------------- +/// Represents a test system useful for testing. +struct TestSystem : public SystemBase { + /// Whether the system has been called or not. + mutable bool called{false}; + + /// Initialise the system. + /// + /// @param registry - The registry that manages the game objects, components, and systems. + explicit TestSystem(Registry *registry) : SystemBase(registry) {} + + /// Update the system. + void update(const double /*delta_time*/) const final { called = true; } +}; + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the game_objects/registry.hpp tests. +class RegistryFixture : public testing::Test { + protected: + /// The registry that manages the game objects, components, and systems. + Registry registry{}; +}; + +// ----- TESTS ------------------------------ +/// Test that an exception is thrown if a component is not registered. +TEST_F(RegistryFixture, TestRegistryEmptyGameObject) { + ASSERT_EQ(registry.create_game_object({}), 0); + ASSERT_THROW_MESSAGE( + registry.get_component(0), RegistryError, + "The game object `0` is not registered with the registry or does not have the required component.") + ASSERT_THROW_MESSAGE( + (registry.get_component(0, typeid(TestGameObjectComponentTwo))), RegistryError, + "The game object `0` is not registered with the registry or does not have the required component.") + ASSERT_EQ(registry.find_components().size(), {}); + ASSERT_EQ(registry.find_components().size(), {}); + ASSERT_EQ(registry.get_walls().size(), 0); + ASSERT_THROW_MESSAGE((registry.get_kinematic_object(0)), RegistryError, + "The game object `0` is not registered with the registry or is not kinematic.") + registry.delete_game_object(0); + ASSERT_THROW_MESSAGE(registry.delete_game_object(0), RegistryError, + "The game object `0` is not registered with the registry.") +} + +/// Test that multiple components are added to the registry correctly. +TEST_F(RegistryFixture, TestRegistryGameObjectComponents) { + // Test that creating the game object works correctly + const std::vector test_list{10}; + registry.create_game_object( + {std::make_shared(), std::make_shared(test_list)}); + ASSERT_NE(registry.get_component(0), nullptr); + ASSERT_NE(registry.get_component(0, typeid(TestGameObjectComponentTwo)), nullptr); + ASSERT_EQ(registry.find_components().size(), 1); + ASSERT_EQ(registry.find_components().size(), 1); + auto multiple_result_one{registry.find_components().size()}; + ASSERT_EQ(multiple_result_one, 1); + + // Test that deleting the game object works correctly + registry.delete_game_object(0); + ASSERT_THROW_MESSAGE( + registry.get_component(0), RegistryError, + "The game object `0` is not registered with the registry or does not have the required component.") + ASSERT_THROW_MESSAGE( + (registry.get_component(0, typeid(TestGameObjectComponentTwo))), RegistryError, + "The game object `0` is not registered with the registry or does not have the required component.") + ASSERT_EQ(registry.find_components().size(), 0); + ASSERT_EQ(registry.find_components().size(), 0); + auto multiple_result_two{registry.find_components().size()}; + ASSERT_EQ(multiple_result_two, 0); +} + +/// Test that a kinematic game object is added to the registry correctly. +TEST_F(RegistryFixture, TestRegistryGameObjectKinematic) { + // Test that creating the kinematic game object works correctly + registry.create_game_object({}, true); + const std::shared_ptr kinematic_object{registry.get_kinematic_object(0)}; + ASSERT_EQ(kinematic_object->position, Vec2d(0, 0)); + ASSERT_EQ(kinematic_object->velocity, Vec2d(0, 0)); + ASSERT_EQ(kinematic_object->rotation, 0); + + // Test that deleting the kinematic game object works correctly + registry.delete_game_object(0); + ASSERT_THROW_MESSAGE((registry.get_kinematic_object(0)), RegistryError, + "The game object `0` is not registered with the registry or is not kinematic.") +} + +/// Test that multiple game objects are added to the registry correctly. +TEST_F(RegistryFixture, TestRegistryMultipleGameObjects) { + // Test that creating two game objects works correctly + std::vector test_list{10}; + ASSERT_EQ(registry.create_game_object({std::make_shared()}), 0); + ASSERT_EQ(registry.create_game_object({std::make_shared(), + std::make_shared(test_list)}), + 1); + ASSERT_EQ(registry.find_components().size(), 2); + ASSERT_EQ(registry.find_components().size(), 1); + auto multiple_result_one{registry.find_components().size()}; + ASSERT_EQ(multiple_result_one, 1); + + // Test that deleting the first game object works correctly + registry.delete_game_object(0); + ASSERT_EQ(registry.find_components().size(), 1); + ASSERT_EQ(registry.find_components().size(), 1); + auto multiple_result_two{registry.find_components().size()}; + ASSERT_EQ(multiple_result_two, 1); +} + +/// Test that a game object with duplicate components is added to the registry correctly. +TEST_F(RegistryFixture, TestRegistryGameObjectDuplicateComponents) { + // Test that creating a game object with two of the same components only adds the first one + std::vector test_list_one{10}; + std::vector test_list_two{20}; + registry.create_game_object({std::make_shared(test_list_one), + std::make_shared(test_list_two)}); + ASSERT_EQ(registry.get_component(0)->test_list[0], 10); +} + +/// Test that passing the same component to multiple game objects works correctly. +TEST_F(RegistryFixture, TestRegistryGameObjectSameComponent) { + std::vector test_list{10}; + const std::shared_ptr component_one{ + std::make_shared(test_list)}; + registry.create_game_object({component_one}); + registry.create_game_object({component_one}); + registry.get_component(0)->test_list[0] = 20; + ASSERT_EQ(registry.get_component(0)->test_list[0], 20); + ASSERT_EQ(registry.get_component(1)->test_list[0], 20); +} + +/// Test that an exception is thrown if a system is not registered. +TEST_F(RegistryFixture, + TestRegistryZeroSystems){ASSERT_THROW_MESSAGE(registry.get_system(), RegistryError, + "The templated type is not registered with the registry.")} + +/// Test that a system is updated correctly. +TEST_F(RegistryFixture, TestRegistrySystemUpdate) { + // Test that the system is added correctly + const std::vector test_list{10}; + registry.create_game_object({std::make_shared(test_list)}); + registry.add_system(); + ASSERT_THROW_MESSAGE(registry.add_system(), RegistryError, + "The templated type is already registered with the registry.") + auto system_result{registry.get_system()}; + ASSERT_NE(system_result, nullptr); + + // Test that the system is updated correctly + registry.update(0); + ASSERT_TRUE(system_result->called); +} + +/// Test that multiple walls are added to the system correctly. +TEST_F(RegistryFixture, TestRegistryWalls) { + ASSERT_EQ(registry.get_walls(), std::unordered_set()); + registry.add_wall({0, 0}); + registry.add_wall({1, 1}); + registry.add_wall({0, 0}); + ASSERT_EQ(registry.get_walls(), std::unordered_set({{0, 0}, {1, 1}})); +} diff --git a/src/hades_extensions/tests/game_objects/test_steering.cpp b/src/hades_extensions/tests/game_objects/test_steering.cpp new file mode 100644 index 00000000..2f028ac3 --- /dev/null +++ b/src/hades_extensions/tests/game_objects/test_steering.cpp @@ -0,0 +1,315 @@ +// Local headers +#include "game_objects/steering.hpp" +#include "macros.hpp" + +// ----- TESTS ------------------------------ +/// Test that adding two vectors produce the correct result. +TEST(Tests, TestVec2dAddition) { + ASSERT_EQ(Vec2d(0, 0) + Vec2d(1, 1), Vec2d(1, 1)); + ASSERT_EQ(Vec2d(-3, -2) + Vec2d(-1, -1), Vec2d(-4, -3)); + ASSERT_EQ(Vec2d(6, 3) + Vec2d(5, 5), Vec2d(11, 8)); + ASSERT_EQ(Vec2d(1, 1) + Vec2d(1, 1), Vec2d(2, 2)); + ASSERT_EQ(Vec2d(-5, 4) + Vec2d(7, -1), Vec2d(2, 3)); +} + +/// Test that compound adding two vectors produce the correct result. +TEST(Tests, TestVec2dCompoundAddition) { + ASSERT_EQ(Vec2d(0, 0) += Vec2d(1, 1), Vec2d(1, 1)); + ASSERT_EQ(Vec2d(-3, -2) += Vec2d(-1, -1), Vec2d(-4, -3)); + ASSERT_EQ(Vec2d(6, 3) += Vec2d(5, 5), Vec2d(11, 8)); + ASSERT_EQ(Vec2d(1, 1) += Vec2d(1, 1), Vec2d(2, 2)); + ASSERT_EQ(Vec2d(-5, 4) += Vec2d(7, -1), Vec2d(2, 3)); +} + +/// Test that subtracting two vectors produce the correct result. +TEST(Tests, TestVec2dSubtraction) { + ASSERT_EQ(Vec2d(0, 0) - Vec2d(1, 1), Vec2d(-1, -1)); + ASSERT_EQ(Vec2d(-3, -2) - Vec2d(-1, -1), Vec2d(-2, -1)); + ASSERT_EQ(Vec2d(6, 3) - Vec2d(5, 5), Vec2d(1, -2)); + ASSERT_EQ(Vec2d(1, 1) - Vec2d(1, 1), Vec2d(0, 0)); + ASSERT_EQ(Vec2d(-5, 4) - Vec2d(7, -1), Vec2d(-12, 5)); +} + +/// Test that multiplying a vector by a scalar produces the correct result. +TEST(Tests, TestVec2dMultiplication) { + ASSERT_EQ(Vec2d(0, 0) * 1, Vec2d(0, 0)); + ASSERT_EQ(Vec2d(-3, -2) * 2, Vec2d(-6, -4)); + ASSERT_EQ(Vec2d(6, 3) * 3, Vec2d(18, 9)); + ASSERT_EQ(Vec2d(1, 1) * 4, Vec2d(4, 4)); + ASSERT_EQ(Vec2d(-5, 4) * 5, Vec2d(-25, 20)); +} + +/// Test that dividing a vector by a scalar produces the correct result. +TEST(Tests, TestVec2dDivision) { + ASSERT_EQ(Vec2d(0, 0) / 1, Vec2d(0, 0)); + ASSERT_EQ(Vec2d(-3, -2) / 2, Vec2d(-2, -1)); + ASSERT_EQ(Vec2d(6, 3) / 3, Vec2d(2, 1)); + ASSERT_EQ(Vec2d(1, 1) / 4, Vec2d(0, 0)); + ASSERT_EQ(Vec2d(-5, 4) / 5, Vec2d(-1, 0)); +} + +/// Test that getting the magnitude of a vector produces the correct result. +TEST(Tests, TestVec2dMagnitude) { + ASSERT_EQ(Vec2d(0, 0).magnitude(), 0); + ASSERT_EQ(Vec2d(-3, -2).magnitude(), 3.605551275463989); + ASSERT_EQ(Vec2d(6, 3).magnitude(), 6.708203932499369); + ASSERT_EQ(Vec2d(1, 1).magnitude(), 1.4142135623730951); + ASSERT_EQ(Vec2d(-5, 4).magnitude(), 6.4031242374328485); +} + +/// Test that getting the normalised vector produces the correct result. +TEST(Tests, TestVec2dNormalised) { + ASSERT_EQ(Vec2d(0, 0).normalised(), Vec2d(0, 0)); + ASSERT_EQ(Vec2d(-3, -2).normalised(), Vec2d(-0.8320502943378437, -0.5547001962252291)); + ASSERT_EQ(Vec2d(6, 3).normalised(), Vec2d(0.8944271909999159, 0.4472135954999579)); + ASSERT_EQ(Vec2d(1, 1).normalised(), Vec2d(0.7071067811865475, 0.7071067811865475)); + ASSERT_EQ(Vec2d(-5, 4).normalised(), Vec2d(-0.7808688094430304, 0.6246950475544243)); +} + +/// Test that rotating a vector produces the correct result. +TEST(Tests, TestVec2dRotated) { + ASSERT_EQ(Vec2d(0, 0).rotated(360 * PI_RADIANS), Vec2d(0, 0)); + const Vec2d rotated_result_one{Vec2d{-3, -2}.rotated(270 * PI_RADIANS)}; + ASSERT_DOUBLE_EQ(rotated_result_one.x, -2); + ASSERT_DOUBLE_EQ(rotated_result_one.y, 3); + const Vec2d rotated_result_two{Vec2d{6, 3}.rotated(180 * PI_RADIANS)}; + ASSERT_DOUBLE_EQ(rotated_result_two.x, -6); + ASSERT_DOUBLE_EQ(rotated_result_two.y, -3); + const Vec2d rotated_result_three{Vec2d{1, 1}.rotated(90 * PI_RADIANS)}; + ASSERT_DOUBLE_EQ(rotated_result_three.x, -1); + ASSERT_DOUBLE_EQ(rotated_result_three.y, 1); + const Vec2d rotated_result_four{Vec2d{-5, 4}.rotated(0 * PI_RADIANS)}; + ASSERT_DOUBLE_EQ(rotated_result_four.x, -5); + ASSERT_DOUBLE_EQ(rotated_result_four.y, 4); +} + +/// Test that getting the angle of a vector produces the correct result. +TEST(Tests, TestVec2dAngleBetween) { + ASSERT_EQ(Vec2d(0, 0).angle_between({1, 1}), 0); + ASSERT_EQ(Vec2d(-3, -2).angle_between({-1, -1}), 0.19739555984988044); + ASSERT_EQ(Vec2d(6, 3).angle_between({5, 5}), 0.32175055439664213); + ASSERT_EQ(Vec2d(1, 1).angle_between({1, 1}), 0); + ASSERT_EQ(Vec2d(-5, 4).angle_between({7, -1}), 3.674436541209182); +} + +/// Test that getting the distance of two vectors produces the correct result. +TEST(Tests, TestVec2dDistanceTo) { + ASSERT_EQ(Vec2d(0, 0).distance_to({1, 1}), 1.4142135623730951); + ASSERT_EQ(Vec2d(-3, -2).distance_to({-1, -1}), 2.23606797749979); + ASSERT_EQ(Vec2d(6, 3).distance_to({5, 5}), 2.23606797749979); + ASSERT_EQ(Vec2d(1, 1).distance_to({1, 1}), 0); + ASSERT_EQ(Vec2d(-5, 4).distance_to({7, -1}), 13); +} + +/// Test if a position outside the radius produces the correct arrive force. +TEST(Tests, TestArriveOutsideSlowingRange) { + ASSERT_EQ(arrive({500, 500}, {0, 0}), Vec2d(-0.7071067811865475, -0.7071067811865475)); +} + +/// Test if a position on the radius produces the correct arrive force. +TEST(Tests, TestArriveOnSlowingRange) { + ASSERT_EQ(arrive({135, 135}, {0, 0}), Vec2d(-0.7071067811865475, -0.7071067811865475)); +} + +/// Test if a position inside the radius produces the correct arrive force. +TEST(Tests, TestArriveInsideSlowingRange) { + ASSERT_EQ(arrive({100, 100}, {0, 0}), Vec2d(-0.7071067811865476, -0.7071067811865476)); +} + +/// Test if a position near the target produces the correct arrive force. +TEST(Tests, TestArriveNearTarget) { + ASSERT_EQ(arrive({50, 50}, {0, 0}), Vec2d(-0.7071067811865476, -0.7071067811865476)); +} + +/// Test if a position on the target produces the correct arrive force. +TEST(Tests, TestArriveOnTarget) { ASSERT_EQ(arrive({0, 0}, {0, 0}), Vec2d(0, 0)); } + +/// Test if a non-moving target produces the correct evade force. +TEST(Tests, TestEvadeNonMovingTarget) { + ASSERT_EQ(evade({0, 0}, {100, 100}, {0, 0}), Vec2d(-0.7071067811865475, -0.7071067811865475)); +} + +/// Test if a moving target produces the correct evade force. +TEST(Tests, TestEvadeMovingTarget) { + ASSERT_EQ(evade({0, 0}, {100, 100}, {-50, 0}), Vec2d(-0.5428888213891885, -0.8398045770360255)); +} + +/// Test if having the same position produces the correct evade force. +TEST(Tests, TestEvadeSamePositions) { + ASSERT_EQ(evade({0, 0}, {0, 0}, {0, 0}), Vec2d(0, 0)); + ASSERT_EQ(evade({0, 0}, {0, 0}, {-50, 0}), Vec2d(0, 0)); +} + +/// Test if a higher current position produces the correct flee force. +TEST(Tests, TestFleeHigherCurrent) { + ASSERT_EQ(flee({100, 100}, {50, 50}), Vec2d(0.7071067811865475, 0.7071067811865475)); +} + +/// Test if a higher target position produces the correct flee force. +TEST(Tests, TestFleeHigherTarget) { + ASSERT_EQ(flee({50, 50}, {100, 100}), Vec2d(-0.7071067811865475, -0.7071067811865475)); +} + +/// Test if two equal positions produce the correct flee force. +TEST(Tests, TestFleeEqual) { ASSERT_EQ(flee({100, 100}, {100, 100}), Vec2d(0, 0)); } + +/// Test if a negative current position produces the correct flee force. +TEST(Tests, TestFleeNegativeCurrent) { + ASSERT_EQ(flee({-50, -50}, {100, 100}), Vec2d(-0.7071067811865475, -0.7071067811865475)); +} + +/// Test if a negative target position produces the correct flee force. +TEST(Tests, TestFleeNegativeTarget) { + ASSERT_EQ(flee({100, 100}, {-50, -50}), Vec2d(0.7071067811865475, 0.7071067811865475)); +} + +/// Test if two negative positions produce the correct flee force. +TEST(Tests, TestFleeNegativePositions) { ASSERT_EQ(flee({-50, -50}, {-50, -50}), Vec2d(0, 0)); } + +/// Test if a multiple position list produces the correct follow path force. +TEST(Tests, TestFollowPathSinglePosition) { + std::vector path_list{{250, 250}}; + ASSERT_EQ(follow_path({100, 100}, path_list), Vec2d(0.7071067811865475, 0.7071067811865475)); +} + +/// Test if reaching a position in a single position list produces the correct follow path force. +TEST(Tests, TestFollowPathSinglePositionReached) { + std::vector path_list{{100, 100}}; + ASSERT_EQ(follow_path({100, 100}, path_list), Vec2d(0, 0)); + ASSERT_EQ(path_list, std::vector{Vec2d(100, 100)}); +} + +/// Test if a multiple position list produces the correct follow path force. +TEST(Tests, TestFollowPathMultiplePositions) { + std::vector path_list{{350, 350}, {500, 500}}; + ASSERT_EQ(follow_path({200, 200}, path_list), Vec2d(0.7071067811865475, 0.7071067811865475)); +} + +/// Test if reaching a position in a multiple position list produces the correct follow path force. +TEST(Tests, TestFollowPathMultiplePositionsReached) { + std::vector path_list{{100, 100}, {250, 250}}; + ASSERT_EQ(follow_path({100, 100}, path_list), Vec2d(0.7071067811865475, 0.7071067811865475)); + ASSERT_EQ(path_list, std::vector({Vec2d(250, 250), Vec2d(100, 100)})); +} + +/// Test if an empty list raises the correct exception. +TEST(Tests, TestFollowPathEmptyList) { + std::vector path_list; + ASSERT_THROW_MESSAGE(follow_path({100, 100}, path_list), std::length_error, "The path list is empty") +} + +/// Test if no obstacles produce the correct obstacle avoidance force. +TEST(Tests, TestObstacleAvoidanceNoObstacles) { ASSERT_EQ(obstacle_avoidance({100, 100}, {0, 100}, {}), Vec2d(0, 0)); } + +/// Test if an out of range obstacle produces the correct obstacle avoidance force. +TEST(Tests, TestObstacleAvoidanceObstacleOutOfRange) { + ASSERT_EQ(obstacle_avoidance({100, 100}, {0, 100}, {{10, 10}}), Vec2d(0, 0)); +} + +/// Test if an angled velocity produces the correct obstacle avoidance force. +TEST(Tests, TestObstacleAvoidanceAngledVelocity) { + ASSERT_EQ(obstacle_avoidance({100, 100}, {100, 100}, {{1, 2}}), Vec2d(0.2588190451025206, -0.9659258262890683)); +} + +/// Test if a non-moving game object produces the correct obstacle avoidance force. +TEST(Tests, TestObstacleAvoidanceNonMoving) { + ASSERT_EQ(obstacle_avoidance({100, 100}, {0, 100}, {{1, 2}}), Vec2d(0, 0)); +} + +/// Test if a single forward obstacle produces the correct obstacle avoidance force. +TEST(Tests, TestObstacleAvoidanceSingleForward) { + ASSERT_EQ(obstacle_avoidance({100, 100}, {0, 100}, {{1, 2}}), Vec2d(0, 0)); +} + +/// Test if a single left obstacle produces the correct obstacle avoidance force. +TEST(Tests, TestObstacleAvoidanceSingleLeft) { + // This is due to floating point precision + const Vec2d single_left_result{obstacle_avoidance({100, 100}, {0, 100}, {{0, 2}})}; + ASSERT_EQ(single_left_result.x, 0.8660254037844387); + ASSERT_DOUBLE_EQ(single_left_result.y, -0.5); +} + +/// Test if a single right obstacle produces the correct obstacle avoidance force. +TEST(Tests, TestObstacleAvoidanceSingleRight) { + // This is due to floating point precision + const Vec2d single_right_result{obstacle_avoidance({100, 100}, {0, 100}, {{2, 2}})}; + ASSERT_EQ(single_right_result.x, -0.8660254037844386); + ASSERT_DOUBLE_EQ(single_right_result.y, -0.5); +} + +/// Test if a left and forward obstacle produces the correct obstacle avoidance force. +TEST(Tests, TestObstacleAvoidanceLeftForward) { + // This is due to floating point precision + const Vec2d left_forward_result{obstacle_avoidance({100, 100}, {0, 100}, {{0, 2}, {1, 2}})}; + ASSERT_EQ(left_forward_result.x, 0.8660254037844387); + ASSERT_DOUBLE_EQ(left_forward_result.y, -0.5); +} + +/// Test if a right and forward obstacle produces the correct obstacle avoidance force. +TEST(Tests, TestObstacleAvoidanceRightForward) { + // This is due to floating point precision + const Vec2d right_forward_result{obstacle_avoidance({100, 100}, {0, 100}, {{1, 2}, {2, 2}})}; + ASSERT_EQ(right_forward_result.x, -0.8660254037844386); + ASSERT_DOUBLE_EQ(right_forward_result.y, -0.5); +} + +/// Test if all three obstacles produce the correct obstacle avoidance force. +TEST(Tests, TestObstacleAvoidanceLeftRightForward) { + ASSERT_EQ(obstacle_avoidance({100, 100}, {0, 100}, {{0, 2}, {1, 2}, {2, 2}}), Vec2d(0, -1)); +} + +/// Test if a non-moving target produces the correct pursue force. +TEST(Tests, TestPursueNonMovingTarget) { + ASSERT_EQ(pursue({0, 0}, {100, 100}, {0, 0}), Vec2d(0.7071067811865475, 0.7071067811865475)); +} + +/// Test if a moving target produces the correct pursue force. +TEST(Tests, TestPursueMovingTarget) { + ASSERT_EQ(pursue({0, 0}, {100, 100}, {-50, 0}), Vec2d(0.5428888213891885, 0.8398045770360255)); +} + +/// Test if having the same position produces the correct pursue force. +TEST(Tests, TestPursueSamePositions) { + ASSERT_EQ(pursue({0, 0}, {0, 0}, {0, 0}), Vec2d(0, 0)); + ASSERT_EQ(pursue({0, 0}, {0, 0}, {-50, 0}), Vec2d(0, 0)); +} + +/// Test if a higher current position produces the correct seek force. +TEST(Tests, TestSeekHigherCurrent) { + ASSERT_EQ(seek({100, 100}, {50, 50}), Vec2d(-0.7071067811865475, -0.7071067811865475)); +} + +/// Test if a higher target position produces the correct seek force. +TEST(Tests, TestSeekHigherTarget) { + ASSERT_EQ(seek({50, 50}, {100, 100}), Vec2d(0.7071067811865475, 0.7071067811865475)); +} + +/// Test if two equal positions produce the correct seek force. +TEST(Tests, TestSeekEqual) { ASSERT_EQ(seek({100, 100}, {100, 100}), Vec2d(0, 0)); } + +/// Test if a negative current position produces the correct seek force. +TEST(Tests, TestSeekNegativeCurrent) { + ASSERT_EQ(seek({-50, -50}, {100, 100}), Vec2d(0.7071067811865475, 0.7071067811865475)); +} + +/// Test if a negative target position produces the correct seek force. +TEST(Tests, TestSeekNegativeTarget) { + ASSERT_EQ(seek({100, 100}, {-50, -50}), Vec2d(-0.7071067811865475, -0.7071067811865475)); +} + +/// Test if two negative positions produce the correct seek force. +TEST(Tests, TestSeekNegativePositions) { ASSERT_EQ(seek({-50, -50}, {-50, -50}), Vec2d(0, 0)); } + +/// Test if a non-moving game object produces the correct wander force. +TEST(Tests, TestWanderNonMoving) { + // This is due to floating point precision + const Vec2d non_moving_result{wander({0, 0}, 60)}; + ASSERT_EQ(non_moving_result.x, 0.8660254037844385); + ASSERT_DOUBLE_EQ(non_moving_result.y, -0.5); +} + +/// Test if a moving game object produces the correct wander force. +TEST(Tests, TestWanderMoving) { ASSERT_EQ(wander({100, -100}, 60), Vec2d(0.7659012135559103, -0.6429582654213131)); } + +/// Test if a zero angle produces the correct wander force. +TEST(Tests, TestWanderZeroAngle) { ASSERT_EQ(wander({0, 0}, 0), Vec2d(0, -1)); } diff --git a/src/hades_extensions/tests/generation/test_astar.cpp b/src/hades_extensions/tests/generation/test_astar.cpp new file mode 100644 index 00000000..e14452d9 --- /dev/null +++ b/src/hades_extensions/tests/generation/test_astar.cpp @@ -0,0 +1,65 @@ +// Local headers +#include "generation/astar.hpp" +#include "macros.hpp" + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the generation/astar.hpp tests. +class AstarFixture : public testing::Test { + protected: + /// A 2D grid for use in testing. + Grid grid{6, 9}; + + /// A position in the middle of the grid for use in testing. + Position position_one{3, 7}; + + /// An extra position in the middle of the grid for use in testing. + Position position_two{4, 1}; + + /// A position on the edge of the grid for use in testing. + Position position_three{4, 0}; + + /// Add obstacles to the grid for use in testing. + void add_obstacles() { + grid.set_value({1, 3}, TileType::Obstacle); + grid.set_value({2, 7}, TileType::Obstacle); + grid.set_value({3, 2}, TileType::Obstacle); + grid.set_value({3, 3}, TileType::Obstacle); + grid.set_value({3, 6}, TileType::Obstacle); + grid.set_value({4, 3}, TileType::Obstacle); + grid.set_value({4, 6}, TileType::Obstacle); + } +}; + +// ----- TESTS ------------------------------ +/// Test that A* works in a grid with no obstacles when started in the middle. +TEST_F(AstarFixture, TestCalculateAstarPathNoObstaclesMiddleStart) { + const std::vector no_obstacles_result{{4, 1}, {3, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 6}, {3, 7}}; + ASSERT_EQ(calculate_astar_path(grid, position_one, position_two), no_obstacles_result); +} + +/// Test that A* fails in a grid with no obstacles when ended on the edge. +TEST_F(AstarFixture, TestCalculateAstarPathNoObstaclesBoundaryEnd) { + const std::vector no_obstacles_result{{4, 0}, {3, 1}, {3, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 6}, {3, 7}}; + ASSERT_EQ(calculate_astar_path(grid, position_one, position_three), no_obstacles_result); +} + +/// Test that A* works in a grid with obstacles when started in the middle. +TEST_F(AstarFixture, TestCalculateAstarPathObstaclesMiddleStart) { + add_obstacles(); + const std::vector obstacles_result{{4, 1}, {4, 2}, {5, 3}, {4, 4}, {3, 5}, {2, 6}, {3, 7}}; + ASSERT_EQ(calculate_astar_path(grid, position_one, position_two), obstacles_result); +} + +/// Test that A* fails in a grid with obstacles when ended on the edge. +TEST_F(AstarFixture, TestCalculateAstarPathObstaclesBoundaryEnd) { + add_obstacles(); + const std::vector obstacles_result{{4, 0}, {3, 1}, {2, 2}, {2, 3}, {3, 4}, {2, 5}, {2, 6}, {3, 7}}; + ASSERT_EQ(calculate_astar_path(grid, position_one, position_three), obstacles_result); +} + +/// Test that A* fails in an empty grid. +TEST_F(AstarFixture, TestCalculateAstarPathEmptyGrid) { + const Grid empty_grid{0, 0}; + ASSERT_THROW_MESSAGE(calculate_astar_path(empty_grid, position_one, position_two), std::length_error, + "Grid size must be bigger than 0.") +} diff --git a/src/hades_extensions/tests/generation/test_bsp.cpp b/src/hades_extensions/tests/generation/test_bsp.cpp new file mode 100644 index 00000000..63c396f6 --- /dev/null +++ b/src/hades_extensions/tests/generation/test_bsp.cpp @@ -0,0 +1,170 @@ +// External headers +#include "gtest/gtest.h" + +// Local headers +#include "generation/bsp.hpp" + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the generation/bsp.hpp tests. +class BspFixture : public testing::Test { + protected: + /// The random number generator for use in testing. + std::mt19937 random_generator; + + /// A 2D grid for use in testing. + Grid grid{20, 20}; + + /// Set up the fixture for the tests. + void SetUp() override { random_generator.seed(0); } +}; + +// ----- TESTS ------------------------------ +/// Test that the split function correctly splits a leaf vertically. +TEST_F(BspFixture, TestBspSplitVertical) { + // Split a leaf vertically + Leaf leaf{{{0, 0}, {15, 10}}}; + split(leaf, random_generator); + + // Make sure the children are correct + const Rect left_container{{0, 0}, {7, 10}}; + const Rect right_container{{9, 0}, {15, 10}}; + ASSERT_EQ(*leaf.left->container, left_container); + ASSERT_EQ(*leaf.right->container, right_container); +} + +/// Test that the split function correctly splits a leaf horizontally. +TEST_F(BspFixture, TestBspSplitHorizontal) { + // Split a leaf horizontally + Leaf leaf{{{0, 0}, {10, 15}}}; + split(leaf, random_generator); + + // Make sure the children are correct + const Rect left_container{{0, 0}, {10, 7}}; + const Rect right_container{{0, 9}, {10, 15}}; + ASSERT_EQ(*leaf.left->container, left_container); + ASSERT_EQ(*leaf.right->container, right_container); +} + +/// Test that the split function correctly splits a leaf in a random direction. +TEST_F(BspFixture, TestBspSplitRandom) { + // Split a leaf randomly + Leaf leaf{{{0, 0}, {15, 15}}}; + split(leaf, random_generator); + + // Make sure the children are correct + const Rect left_container{{0, 0}, {7, 15}}; + const Rect right_container{{9, 0}, {15, 15}}; + ASSERT_EQ(*leaf.left->container, left_container); + ASSERT_EQ(*leaf.right->container, right_container); +} + +/// Test that the split function correctly splits a leaf multiple times. +TEST_F(BspFixture, TestBspSplitMultiple) { + // Split a leaf multiple times + Leaf leaf{{{0, 0}, {20, 20}}}; + split(leaf, random_generator); + + // Make sure the children are correct + const Rect left_container{{0, 0}, {10, 20}}; + const Rect left_left_container{{0, 0}, {10, 11}}; + const Rect left_right_container{{0, 13}, {10, 20}}; + const Rect right_container{{12, 0}, {20, 20}}; + const Rect right_left_container{{12, 0}, {20, 10}}; + const Rect right_right_container{{12, 12}, {20, 20}}; + ASSERT_EQ(*leaf.left->container, left_container); + ASSERT_EQ(*leaf.left->left->container, left_left_container); + ASSERT_EQ(*leaf.left->right->container, left_right_container); + ASSERT_EQ(*leaf.right->container, right_container); + ASSERT_EQ(*leaf.right->left->container, right_left_container); + ASSERT_EQ(*leaf.right->right->container, right_right_container); +} + +/// Test that the split function returns if the leaf is already split. +TEST_F(BspFixture, TestBspSplitExistingChildren) { + // Split a leaf that already has children + Leaf leaf{{{0, 0}, {100, 100}}}; + leaf.left = std::make_unique(Rect{{0, 0}, {0, 0}}); + leaf.right = std::make_unique(Rect{{0, 0}, {0, 0}}); + split(leaf, random_generator); + + // Make sure the children haven't changed + const Rect left_container{{0, 0}, {0, 0}}; + const Rect right_container{{0, 0}, {0, 0}}; + ASSERT_EQ(*leaf.left->container, left_container); + ASSERT_EQ(*leaf.right->container, right_container); +} + +/// Test that the split function overwrites the children if only one child exists. +TEST_F(BspFixture, TestBspSplitSingleChild) { + // Split a leaf that only has one child + Leaf leaf{{{0, 0}, {15, 15}}}; + leaf.left = std::make_unique(Rect{{0, 0}, {0, 0}}); + split(leaf, random_generator); + + // Make sure the children are correct + const Rect left_container{{0, 0}, {7, 15}}; + const Rect right_container{{9, 0}, {15, 15}}; + ASSERT_EQ(*leaf.left->container, left_container); + ASSERT_EQ(*leaf.right->container, right_container); +} + +/// Test that the split function returns if the leaf is too small to split. +TEST_F(BspFixture, TestBspSplitTooSmall) { + Leaf leaf{{{0, 0}, {0, 0}}}; + split(leaf, random_generator); + ASSERT_EQ(leaf.left, nullptr); + ASSERT_EQ(leaf.right, nullptr); +} + +/// Test that the create_room function creates a room in a leaf correctly. +TEST_F(BspFixture, TestBspCreateRoomSingleLeaf) { + // Create a room inside a leaf + std::vector rooms; + Leaf leaf{{{0, 0}, {15, 15}}}; + create_room(leaf, grid, random_generator, rooms); + + // Make sure the room is correct + const Rect room{{4, 4}, {13, 14}}; + ASSERT_EQ(*leaf.room, room); + ASSERT_EQ(rooms.size(), 1); +} + +/// Test that the create_room function creates rooms in the left and right children. +TEST_F(BspFixture, TestBspCreateRoomChildLeafs) { + // Create a room inside a leaf that has already been split + std::vector rooms; + Leaf leaf{{{0, 0}, {15, 15}}}; + leaf.left = std::make_unique(Rect{{0, 0}, {7, 15}}); + leaf.right = std::make_unique(Rect{{9, 0}, {15, 15}}); + create_room(leaf, grid, random_generator, rooms); + + // Make sure the room is correct + const Rect left_room{{2, 0}, {6, 6}}; + const Rect right_room{{9, 4}, {14, 10}}; + ASSERT_EQ(*leaf.left->room, left_room); + ASSERT_EQ(*leaf.right->room, right_room); + ASSERT_EQ(rooms.size(), 2); +} + +/// Test that the create_room function overwrites the room if it already exists. +TEST_F(BspFixture, TestBspCreateRoomExistingRoom) { + // Create a room inside a leaf that already has a room + std::vector rooms; + Leaf leaf{{{0, 0}, {15, 15}}}; + leaf.room = std::make_unique(Rect{{0, 0}, {0, 0}}); + create_room(leaf, grid, random_generator, rooms); + + // Make sure the room is correct + const Rect room{{4, 4}, {13, 14}}; + ASSERT_EQ(*leaf.room, room); + ASSERT_EQ(rooms.size(), 1); +} + +/// Test that the create_room function throws an exception if the leaf is too small. +TEST_F(BspFixture, TestBspCreateRoomTooSmallLeaf) { + std::vector rooms; + Leaf leaf{{{0, 0}, {0, 0}}}; + create_room(leaf, grid, random_generator, rooms); + ASSERT_EQ(leaf.room, nullptr); + ASSERT_TRUE(rooms.empty()); +} diff --git a/src/hades_extensions/tests/generation/test_map.cpp b/src/hades_extensions/tests/generation/test_map.cpp new file mode 100644 index 00000000..482b642e --- /dev/null +++ b/src/hades_extensions/tests/generation/test_map.cpp @@ -0,0 +1,196 @@ +// Local headers +#include "generation/map.hpp" +#include "macros.hpp" + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the generation/map.hpp tests. +class MapFixture : public testing::Test { + protected: + /// A random generator for use in testing. + std::mt19937 random_generator; + + /// An 2D grid for use in testing. + Grid grid{5, 5}; + + /// A large 2D grid for use in testing. + Grid large_grid{8, 8}; + + /// A rect that fits inside the grid for use in testing. + Rect rect_one{{0, 1}, {3, 4}}; + + /// A rect that fits inside the grid for use in testing. + Rect rect_two{{2, 1}, {4, 2}}; + + /// A large rect that doesn't fit inside the grid for use in testing. + Rect rect_three{{4, 4}, {6, 6}}; + + /// Set up the fixture for the tests. + void SetUp() override { random_generator.seed(0); } +}; + +// ----- TESTS ------------------------------ +/// Test that finding a tile that exists in the grid returns a vector of positions. +TEST_F(MapFixture, TestMapCollectPositionsExist) { + const std::vector tile_exists_result{{3, 0}, {0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 4}}; + for (const Position &position : tile_exists_result) { + grid.set_value(position, TileType::Floor); + } + ASSERT_EQ(collect_positions(grid, TileType::Floor), tile_exists_result); +} + +/// Test that finding a tile that doesn't exist in the grid returns an empty vector. +TEST_F(MapFixture, TestMapCollectPositionsNoExist) { ASSERT_TRUE(collect_positions(grid, TileType::Player).empty()); } + +/// Test that finding a tile in an empty grid returns an empty vector. +TEST_F(MapFixture, TestMapCollectPositionsEmptyGrid) { + const Grid empty_grid{0, 0}; + ASSERT_TRUE(collect_positions(empty_grid, TileType::Floor).empty()); +} + +/// Test that placing a tile in the grid with available positions works correctly. +TEST_F(MapFixture, TestMapPlaceTileGivenPositions) { + std::vector possible_tiles{{5, 6}, {4, 2}}; + place_tile(grid, random_generator, TileType::Player, possible_tiles); + ASSERT_EQ(std::count(grid.grid->begin(), grid.grid->end(), TileType::Player), 1); +} + +/// Test that placing a tile in the grid with no available positions throws an exception. +TEST_F(MapFixture, TestMapPlaceTileEmpty) { + std::vector possible_tiles; + ASSERT_THROW_MESSAGE(place_tile(grid, random_generator, TileType::Player, possible_tiles), std::length_error, + "Possible tiles size must be bigger than 0.") +} + +/// Test that creating a complete graph with a single room works correctly. +TEST_F(MapFixture, TestMapCreateCompleteGraphSingleRoom) { + const std::vector rooms{rect_one}; + const std::unordered_map> single_room_result{{rect_one, std::vector{}}}; + ASSERT_EQ(create_complete_graph(rooms), single_room_result); +} + +/// Test that creating a complete graph with multiple rooms works correctly. +TEST_F(MapFixture, TestMapCreateCompleteGraphMultipleRooms) { + const std::vector rooms{rect_one, rect_two, rect_three}; + const std::unordered_map> multiple_rooms_result{ + {rect_one, std::vector{rect_two, rect_three}}, + {rect_two, std::vector{rect_one, rect_three}}, + {rect_three, std::vector{rect_one, rect_two}}}; + ASSERT_EQ(create_complete_graph(rooms), multiple_rooms_result); +} + +/// Test that creating a complete graph with no rooms throws an exception. +TEST_F(MapFixture, TestMapCreateCompleteGraphNoRooms) { + const std::vector rooms; + ASSERT_THROW_MESSAGE(create_complete_graph(rooms), std::length_error, "Rooms size must be bigger than 0.") +} + +/// Test that creating a minimum spanning tree with a valid complete graph works correctly. +TEST_F(MapFixture, TestMapCreateConnectionsValidCompleteGraph) { + // Create the minimum-spanning tree and check its size + const std::unordered_map> complete_graph{{rect_one, std::vector{rect_two, rect_three}}, + {rect_two, std::vector{rect_one, rect_three}}, + {rect_three, std::vector{rect_one, rect_two}}}; + auto connections{create_connections(complete_graph)}; + const std::unordered_set all_rects{rect_one, rect_two, rect_three}; + ASSERT_EQ(connections.size(), 2); + + // Check that the minimum spanning tree has the correct total cost + int sum{0}; + for (const auto &edge : connections) { + sum += edge.cost; + } + ASSERT_EQ(sum, 4); + + // Check that every rect can be reached in the minimum spanning tree + for (const auto &rect : all_rects) { + ASSERT_TRUE(std::any_of(connections.begin(), connections.end(), + [&rect](const Edge &edge) { return edge.source == rect || edge.destination == rect; })); + } +} + +/// Test that creating a minimum spanning tree with an empty complete graph throws an exception. +TEST_F(MapFixture, TestMapCreateConnectionsEmptyCompleteGraph) { + const std::unordered_map> empty_complete_graph; + ASSERT_THROW_MESSAGE(create_connections(empty_complete_graph), std::length_error, + "Complete graph size must be bigger than 0.") +} + +/// Test that creating hallways with no obstacles works correctly. +TEST_F(MapFixture, TestMapCreateHallwaysNoObstacles) { + const std::unordered_set connections{{0, rect_one, rect_three}}; + create_hallways(large_grid, random_generator, connections, 0); + const std::vector no_obstacles_result{ + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, + TileType::Wall, TileType::Wall, TileType::Empty, TileType::Empty, TileType::Wall, TileType::Floor, + TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, TileType::Wall, TileType::Empty, + TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, + TileType::Wall, TileType::Wall, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, + TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, TileType::Wall, TileType::Wall, + TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, + TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Floor, TileType::Floor, + TileType::Floor, TileType::Wall, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Wall, + TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, + }; + ASSERT_EQ(*large_grid.grid, no_obstacles_result); +} + +/// Test that creating hallways with obstacles works correctly. +TEST_F(MapFixture, TestMapCreateHallwaysObstacles) { + const std::unordered_set connections{{0, rect_one, rect_three}}; + create_hallways(large_grid, random_generator, connections, 5); + const std::vector obstacles_result{ + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, + TileType::Wall, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Wall, TileType::Floor, + TileType::Floor, TileType::Floor, TileType::Wall, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, TileType::Wall, + TileType::Wall, TileType::Wall, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, + TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, TileType::Wall, TileType::Floor, + TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, + TileType::Wall, TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Floor, + TileType::Floor, TileType::Wall, TileType::Empty, TileType::Wall, TileType::Wall, TileType::Wall, + TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, + }; + ASSERT_EQ(*large_grid.grid, obstacles_result); +} + +/// Test that creating hallways with no connections doesn't do anything. +TEST_F(MapFixture, TestMapCreateHallwaysNoConnections) { + const std::unordered_set connections; + create_hallways(large_grid, random_generator, connections, 5); + const std::vector no_obstacles_result{ + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Obstacle, TileType::Obstacle, TileType::Obstacle, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Obstacle, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Obstacle, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + }; + ASSERT_EQ(*large_grid.grid, no_obstacles_result); +} + +/// Test that creating a map with a valid level and seed works correctly. +TEST_F(MapFixture, TestMapCreateMapValidLevelSeed) { + std::pair, std::tuple> create_map_valid{create_map(0, 5)}; + ASSERT_EQ(create_map_valid.second, std::make_tuple(0, 30, 20)); + ASSERT_EQ(std::count(create_map_valid.first.begin(), create_map_valid.first.end(), TileType::Player), 1); + ASSERT_EQ(std::count(create_map_valid.first.begin(), create_map_valid.first.end(), TileType::Potion), 5); +} + +/// Test that creating a map with a negative level throws an exception. +TEST_F(MapFixture, TestMapCreateMapNegativeLevel){ + ASSERT_THROW_MESSAGE(create_map(-1, 5), std::length_error, "Level must be bigger than or equal to 0.")} + +/// Test that creating a map without a seed works correctly. +TEST_F(MapFixture, TestMapCreateMapEmptySeed) { + const std::pair, std::tuple> create_map_empty_seed{create_map(0)}; + ASSERT_NE(create_map_empty_seed.first, create_map(0).first); +} diff --git a/src/hades_extensions/tests/generation/test_primitives.cpp b/src/hades_extensions/tests/generation/test_primitives.cpp new file mode 100644 index 00000000..dd6c87a5 --- /dev/null +++ b/src/hades_extensions/tests/generation/test_primitives.cpp @@ -0,0 +1,118 @@ +// Local headers +#include "generation/primitives.hpp" +#include "macros.hpp" + +// ----- FIXTURES ------------------------------ +/// Implements the fixture for the generation/primitives.hpp tests. +class PrimitivesFixture : public testing::Test { + protected: + /// A 2D grid for use in testing. + Grid grid{5, 5}; + + /// A rect inside the grid for use in testing. + Rect rect_one{{0, 0}, {2, 3}}; + + /// A rect inside the grid for use in testing. + Rect rect_two{{2, 2}, {4, 4}}; +}; + +// ----- TESTS ------------------------------ +/// Test that a position in the middle of the grid can be converted correctly. +TEST_F(PrimitivesFixture, TestGridConvertPositionMiddle) { ASSERT_EQ(grid.convert_position({1, 2}), 11); } + +/// Test that a position on the top of the grid can be converted correctly. +TEST_F(PrimitivesFixture, TestGridConvertPositionEdgeTop) { ASSERT_EQ(grid.convert_position({3, 0}), 3); } + +/// Test that a position on the bottom of the grid can be converted correctly. +TEST_F(PrimitivesFixture, TestGridConvertPositionEdgeBottom) { ASSERT_EQ(grid.convert_position({2, 4}), 22); } + +/// Test that a position on the left of the grid can be converted correctly. +TEST_F(PrimitivesFixture, TestGridConvertPositionEdgeLeft) { ASSERT_EQ(grid.convert_position({0, 3}), 15); } + +/// Test that a position on the right of the grid can be converted correctly. +TEST_F(PrimitivesFixture, TestGridConvertPositionEdgeRight) { ASSERT_EQ(grid.convert_position({1, 4}), 21); } + +/// Test that converting a position smaller than the array throws an exception. +TEST_F(PrimitivesFixture, TestGridConvertPositionSmaller){ + ASSERT_THROW_MESSAGE((grid.convert_position({-1, -1})), std::out_of_range, "Position must be within range")} + +/// Test that converting a position larger than the array throws an exception. +TEST_F(PrimitivesFixture, TestGridConvertPositionLarger){ + ASSERT_THROW_MESSAGE((grid.convert_position({10, 10})), std::out_of_range, "Position must be within range")} + +/// Test that a position in the middle of the grid can be retrieved correctly. +TEST_F(PrimitivesFixture, TestGridGetValueMiddle) { + (*grid.grid)[13] = TileType::Player; + ASSERT_EQ(grid.get_value({3, 2}), TileType::Player); +} + +/// Test that a position on the edge of the grid can be retrieved correctly. +TEST_F(PrimitivesFixture, TestGridGetValueEdge) { + (*grid.grid)[23] = TileType::Player; + ASSERT_EQ(grid.get_value({3, 4}), TileType::Player); +} + +/// Test that getting a position smaller than the array throws an exception. +TEST_F(PrimitivesFixture, TestGridGetValueSmaller){ + ASSERT_THROW_MESSAGE((grid.get_value({-1, -1})), std::out_of_range, "Position must be within range")} + +/// Test that getting a position larger than the array throws an exception. +TEST_F(PrimitivesFixture, TestGridGetValueLarger){ + ASSERT_THROW_MESSAGE((grid.get_value({10, 10})), std::out_of_range, "Position must be within range")} + +/// Test that a position in the middle can be set correctly. +TEST_F(PrimitivesFixture, TestGridSetValueMiddle) { + grid.set_value({1, 3}, TileType::Player); + ASSERT_EQ((*grid.grid)[16], TileType::Player); +} + +/// Test that a position on the edge can be set correctly. +TEST_F(PrimitivesFixture, TestGridSetValueEdge) { + grid.set_value({4, 4}, TileType::Player); + ASSERT_EQ((*grid.grid)[24], TileType::Player); +} + +/// Test that setting a position smaller than the array throws an exception. +TEST_F(PrimitivesFixture, + TestGridSetValueSmaller){ASSERT_THROW_MESSAGE((grid.set_value({-1, -1}, TileType::Player)), std::out_of_range, + "Position must be within range")} + +/// Test that setting a position larger than the array throws an exception. +TEST_F(PrimitivesFixture, + TestGridSetValueLarger){ASSERT_THROW_MESSAGE((grid.set_value({10, 10}, TileType::Player)), std::out_of_range, + "Position must be within range")} + +/// Test that finding the distance between two identical rects works correctly. +TEST_F(PrimitivesFixture, TestRectGetDistanceToIdentical) { + ASSERT_EQ(rect_one.get_distance_to(rect_one), 0); +} + +/// Test that finding the distance between two different rects works correctly. +TEST_F(PrimitivesFixture, TestRectGetDistanceToDifferent) { ASSERT_EQ(rect_one.get_distance_to(rect_two), 2); } + +/// Test that a rect can be placed correctly in a valid grid. +TEST_F(PrimitivesFixture, TestRectPlaceRectValidGrid) { + rect_one.place_rect(grid); + const std::vector target_result{ + TileType::Wall, TileType::Wall, TileType::Wall, TileType::Empty, TileType::Empty, + TileType::Wall, TileType::Floor, TileType::Wall, TileType::Empty, TileType::Empty, + TileType::Wall, TileType::Floor, TileType::Wall, TileType::Empty, TileType::Empty, + TileType::Wall, TileType::Wall, TileType::Wall, TileType::Empty, TileType::Empty, + TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, TileType::Empty, + }; + ASSERT_EQ(*grid.grid, target_result); +} + +/// Test that placing a rect that doesn't fit in the grid works correctly. +TEST_F(PrimitivesFixture, TestRectPlaceRectOutsideGrid) { + const Rect invalid_rect{{0, 0}, {10, 10}}; + invalid_rect.place_rect(grid); + const std::vector target_result{ + TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, + TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, + TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, + TileType::Wall, TileType::Floor, TileType::Floor, TileType::Floor, TileType::Wall, + TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, TileType::Wall, + }; + ASSERT_EQ(*grid.grid, target_result); +} diff --git a/tests/__init__.py b/tests/__init__.py index b99faf9c..7218b9ab 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,3 @@ """Runs automated tests on various parts of the game using pytest.""" + +from __future__ import annotations diff --git a/tests/game_objects/__init__.py b/tests/game_objects/__init__.py deleted file mode 100644 index 63e3df7e..00000000 --- a/tests/game_objects/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Runs automated tests on all files in game_objects/.""" diff --git a/tests/game_objects/test_attacks.py b/tests/game_objects/test_attacks.py deleted file mode 100644 index 683e4eb6..00000000 --- a/tests/game_objects/test_attacks.py +++ /dev/null @@ -1,378 +0,0 @@ -"""Tests all functions in game_objects/attacks.py.""" -from __future__ import annotations - -# Builtin -from typing import TYPE_CHECKING, cast - -# Pip -import pytest - -# Custom -from hades.game_objects.attacks import Attacks -from hades.game_objects.attributes import Armour, Health -from hades.game_objects.base import AttackAlgorithms, ComponentType, Vec2d -from hades.game_objects.system import ECS - -if TYPE_CHECKING: - from collections.abc import Callable - - from hades.game_objects.base import ComponentData - -__all__ = () - - -@pytest.fixture() -def ecs() -> ECS: - """Create an entity component system for use in testing. - - Returns: - The entity component system for use in testing. - """ - return ECS() - - -@pytest.fixture() -def attacks_factory(ecs: ECS) -> Callable[[list[AttackAlgorithms]], Attacks]: - """Create an attacks component factory for use in testing. - - Args: - ecs: The entity component system for use in testing. - - Returns: - The attacks component factory for use in testing. - """ - - def wrap(enabled_attacks: list[AttackAlgorithms]) -> Attacks: - game_object_id = ecs.add_game_object( - {"enabled_attacks": enabled_attacks}, - Attacks, - physics=True, - ) - ecs.get_physics_object_for_game_object(game_object_id).rotation = 180 - return cast( - Attacks, - ecs.get_component_for_game_object(game_object_id, ComponentType.ATTACKS), - ) - - return wrap - - -@pytest.fixture() -def targets(ecs: ECS) -> list[int]: - """Create a list of targets for use in testing. - - Args: - ecs: The entity component system for use in testing. - - Returns: - The list of targets for use in testing. - """ - - def create_target(position: Vec2d) -> int: - target = ecs.add_game_object(component_data, Health, Armour, physics=True) - ecs.get_physics_object_for_game_object(target).position = position - return target - - component_data: ComponentData = { - "attributes": { - ComponentType.HEALTH: (50, -1), - ComponentType.ARMOUR: (0, -1), - }, - } - return [ - create_target(Vec2d(-20, -100)), - create_target(Vec2d(20, 60)), - create_target(Vec2d(-200, 100)), - create_target(Vec2d(100, -100)), - create_target(Vec2d(-100, -99)), - create_target(Vec2d(0, -200)), - create_target(Vec2d(0, -192)), - create_target(Vec2d(0, 0)), - ] - - -def test_attacks_init( - attacks_factory: Callable[[list[AttackAlgorithms]], Attacks], -) -> None: - """Test that the attacks component is initialised correctly. - - Args: - attacks_factory: The attacks component factory for use in testing. - """ - assert ( - repr(attacks_factory([AttackAlgorithms.AREA_OF_EFFECT_ATTACK])) - == "" - ) - - -def test_attacks_do_attack_area_of_effect_attack( - attacks_factory: Callable[[list[AttackAlgorithms]], Attacks], - targets: list[int], -) -> None: - """Test that performing an area of effect attack works correctly. - - Args: - attacks_factory: The attacks component factory for use in testing. - targets: The list of targets for use in testing. - """ - attacks_obj = attacks_factory([AttackAlgorithms.AREA_OF_EFFECT_ATTACK]) - assert attacks_obj.do_attack(targets) == {} - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[0], - ComponentType.HEALTH, - ), - ).value - == 40 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[1], - ComponentType.HEALTH, - ), - ).value - == 40 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[2], - ComponentType.HEALTH, - ), - ).value - == 50 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[3], - ComponentType.HEALTH, - ), - ).value - == 40 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[4], - ComponentType.HEALTH, - ), - ).value - == 40 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[5], - ComponentType.HEALTH, - ), - ).value - == 50 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[6], - ComponentType.HEALTH, - ), - ).value - == 40 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[7], - ComponentType.HEALTH, - ), - ).value - == 40 - ) - - -def test_attacks_do_attack_melee_attack( - attacks_factory: Callable[[list[AttackAlgorithms]], Attacks], - targets: list[int], -) -> None: - """Test that performing a melee attack works correctly. - - Args: - attacks_factory: The attacks component factory for use in testing. - targets: The list of targets for use in testing. - """ - attacks_obj = attacks_factory([AttackAlgorithms.MELEE_ATTACK]) - assert attacks_obj.do_attack(targets) == {} - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[0], - ComponentType.HEALTH, - ), - ).value - == 40 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[1], - ComponentType.HEALTH, - ), - ).value - == 50 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[2], - ComponentType.HEALTH, - ), - ).value - == 50 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[3], - ComponentType.HEALTH, - ), - ).value - == 40 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[4], - ComponentType.HEALTH, - ), - ).value - == 50 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[5], - ComponentType.HEALTH, - ), - ).value - == 50 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[6], - ComponentType.HEALTH, - ), - ).value - == 40 - ) - assert ( - cast( - Health, - attacks_obj.system.get_component_for_game_object( - targets[7], - ComponentType.HEALTH, - ), - ).value - == 40 - ) - - -def test_attacks_do_attack_ranged_attack( - attacks_factory: Callable[[list[AttackAlgorithms]], Attacks], -) -> None: - """Test that performing a ranged attack works correctly. - - Args: - attacks_factory: The attacks component factory for use in testing. - """ - attacks_obj = attacks_factory([AttackAlgorithms.RANGED_ATTACK]) - assert attacks_obj.do_attack([]) == { - "ranged_attack": ( - Vec2d(0, 0), - -300.0, - pytest.approx(0), # This is due to floating point errors - ), - } - - -def test_attacks_previous_next_attack_single( - attacks_factory: Callable[[list[AttackAlgorithms]], Attacks], -) -> None: - """Test that switching between attacks once works correctly. - - Args: - attacks_factory: The attacks component factory for use in testing. - """ - attacks_obj = attacks_factory( - [ - AttackAlgorithms.AREA_OF_EFFECT_ATTACK, - AttackAlgorithms.MELEE_ATTACK, - AttackAlgorithms.RANGED_ATTACK, - ], - ) - attacks_obj.next_attack() - assert attacks_obj.attack_state == 1 - attacks_obj.previous_attack() - assert attacks_obj.attack_state == 0 - - -def test_attacks_previous_attack_multiple( - attacks_factory: Callable[[list[AttackAlgorithms]], Attacks], -) -> None: - """Test that switching between attacks multiple times works correctly. - - Args: - attacks_factory: The attacks component factory for use in testing. - """ - attacks_obj = attacks_factory( - [ - AttackAlgorithms.AREA_OF_EFFECT_ATTACK, - AttackAlgorithms.MELEE_ATTACK, - AttackAlgorithms.RANGED_ATTACK, - ], - ) - assert attacks_obj.attack_state == 0 - attacks_obj.next_attack() - assert attacks_obj.attack_state == 1 - attacks_obj.next_attack() - assert attacks_obj.attack_state == 2 - attacks_obj.next_attack() - assert attacks_obj.attack_state == 2 - attacks_obj.previous_attack() - assert attacks_obj.attack_state == 1 - attacks_obj.previous_attack() - assert attacks_obj.attack_state == 0 - attacks_obj.previous_attack() - assert attacks_obj.attack_state == 0 - - -def test_attacks_previous_next_attack_empty_attacks( - attacks_factory: Callable[[list[AttackAlgorithms]], Attacks], -) -> None: - """Test that changing the attack state works correctly when there are no attacks. - - Args: - attacks_factory: The attacks component factory for use in testing. - """ - attacks_obj = attacks_factory([]) - assert attacks_obj.attack_state == 0 - attacks_obj.next_attack() - assert attacks_obj.attack_state == -1 - attacks_obj.previous_attack() - assert attacks_obj.attack_state == 0 diff --git a/tests/game_objects/test_attributes.py b/tests/game_objects/test_attributes.py deleted file mode 100644 index 329c48bc..00000000 --- a/tests/game_objects/test_attributes.py +++ /dev/null @@ -1,618 +0,0 @@ -"""Tests all functions in game_objects/attributes.py.""" -from __future__ import annotations - -# Builtin -from typing import cast - -# Pip -import pytest - -# Custom -from hades.game_objects.attributes import ( - GameObjectAttributeBase, - GameObjectAttributeError, - deal_damage, -) -from hades.game_objects.base import ComponentType -from hades.game_objects.system import ECS - -__all__ = () - - -class FullGameObjectAttribute(GameObjectAttributeBase): - """Represents a full game object attribute useful for testing.""" - - # Class variables - component_type: ComponentType = ComponentType.HEALTH - - -class EmptyGameObjectAttribute(GameObjectAttributeBase): - """Represents an empty game object attribute useful for testing.""" - - # Class variables - component_type: ComponentType = ComponentType.ARMOUR - instant_effect: bool = False - maximum: bool = False - status_effect: bool = False - upgradable: bool = False - - -@pytest.fixture() -def ecs() -> ECS: - """Create an entity component system for use in testing. - - Returns: - The entity component system for use in testing. - """ - return ECS() - - -@pytest.fixture() -def full_game_object_attribute(ecs: ECS) -> FullGameObjectAttribute: - """Create a full game object attribute for use in testing. - - Args: - ecs: The entity component system for use in testing. - - Returns: - The full game object attribute for use in testing. - """ - ecs.add_game_object( - {"attributes": {ComponentType.HEALTH: (150, 3)}}, - FullGameObjectAttribute, - ) - return cast( - FullGameObjectAttribute, - ecs.get_component_for_game_object(0, ComponentType.HEALTH), - ) - - -@pytest.fixture() -def empty_game_object_attribute(ecs: ECS) -> EmptyGameObjectAttribute: - """Create an empty game object attribute for use in testing. - - Args: - ecs: The entity component system for use in testing. - - Returns: - The empty game object attribute for use in testing. - """ - ecs.add_game_object( - {"attributes": {ComponentType.ARMOUR: (100, 5)}}, - EmptyGameObjectAttribute, - ) - return cast( - EmptyGameObjectAttribute, - ecs.get_component_for_game_object(0, ComponentType.ARMOUR), - ) - - -@pytest.fixture() -def both_game_object_attributes( - ecs: ECS, -) -> tuple[FullGameObjectAttribute, EmptyGameObjectAttribute]: - """Create two game object attributes for use in testing. - - Args: - ecs: The entity component system for use in testing. - - Returns: - Both game object attributes for use in testing. - """ - ecs.add_game_object( - { - "attributes": { - ComponentType.HEALTH: (200, 4), - ComponentType.ARMOUR: (150, 6), - }, - }, - FullGameObjectAttribute, - EmptyGameObjectAttribute, - ) - return cast( - FullGameObjectAttribute, - ecs.get_component_for_game_object(0, ComponentType.HEALTH), - ), cast( - EmptyGameObjectAttribute, - ecs.get_component_for_game_object(0, ComponentType.ARMOUR), - ) - - -def test_raise_game_object_attribute_error() -> None: - """Test that GameObjectAttributeError is raised correctly.""" - with pytest.raises( - expected_exception=GameObjectAttributeError, - match="The game object attribute `test` cannot be upgraded.", - ): - raise GameObjectAttributeError(name="test", error="be upgraded") - - -def test_full_game_object_attribute_init( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a full game object attribute is initialised correctly. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - assert ( - repr(full_game_object_attribute) - == "" - ) - - -def test_empty_game_object_attribute_init( - empty_game_object_attribute: EmptyGameObjectAttribute, -) -> None: - """Test that an empty game object attribute is initialised correctly. - - Args: - empty_game_object_attribute: The empty game object attribute for use in testing. - """ - assert ( - repr(empty_game_object_attribute) - == "" - ) - - -def test_full_game_object_attribute_setter_lower( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a full game object attribute is set with a lower value correctly. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.value = 100 - assert full_game_object_attribute.value == 100 - - -def test_full_game_object_attribute_setter_higher( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a full game object attribute is set with a higher value correctly. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.value = 200 - assert full_game_object_attribute.value == 150 - - -def test_full_game_object_attribute_setter_isub( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that subtracting a value from the full game object attribute is correct. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.value -= 200 - assert full_game_object_attribute.value == 0 - - -def test_full_game_object_attribute_setter_iadd( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that adding a value to the full game object attribute is correct. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.value += 100 - assert full_game_object_attribute.value == 150 - - -def test_full_game_object_attribute_upgrade_value_equal_max( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a full game object attribute is upgraded correctly. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - assert full_game_object_attribute.upgrade(lambda level: 150 * (level + 1)) - assert full_game_object_attribute.value == 300 - assert full_game_object_attribute.max_value == 300 - assert full_game_object_attribute.current_level == 1 - - -def test_full_game_object_attribute_upgrade_value_lower_max( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a full game object attribute is upgraded if value is lower than max. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.value -= 50 - assert full_game_object_attribute.upgrade(lambda level: 150 + 2 ^ level) - assert full_game_object_attribute.value == 101 - assert full_game_object_attribute.max_value == 151 - assert full_game_object_attribute.current_level == 1 - - -def test_full_game_object_attribute_upgrade_max_limit( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a full game object attribute is not upgraded if level limit is reached. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.upgrade(lambda _: 0) - full_game_object_attribute.upgrade(lambda _: 0) - full_game_object_attribute.upgrade(lambda _: 0) - assert not full_game_object_attribute.upgrade(lambda _: 0) - - -def test_full_game_object_attribute_upgrade_invalid_increase( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a full game object attribute is not upgraded given an invalid increase. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - with pytest.raises(expected_exception=TypeError): - full_game_object_attribute.upgrade( - lambda _: "str", # type: ignore[arg-type,return-value] - ) - - -def test_full_game_object_attribute_apply_instant_effect_lower( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that an instant effect is applied if the value is lower than the max. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.value -= 50 - assert full_game_object_attribute.apply_instant_effect(lambda level: 10 * level, 2) - assert full_game_object_attribute.value == 120 - - -def test_full_game_object_attribute_apply_instant_effect_equal( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that an instant effect is not applied if the value is equal to the max. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - assert not full_game_object_attribute.apply_instant_effect(lambda _: 50, 3) - assert full_game_object_attribute.value == 150 - - -def test_full_game_object_attribute_apply_status_effect_no_applied_effect( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a status effect is applied if no status effect is currently applied. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - assert full_game_object_attribute.apply_status_effect( - lambda level: 150 + 3**level, - lambda level: 20 + 10 * level, - 2, - ) - assert ( - repr(full_game_object_attribute.applied_status_effect) - == "StatusEffect(value=159, duration=40, original_value=150," - " original_max_value=150, time_counter=0)" - ) - - -def test_full_game_object_attribute_apply_status_effect_value_lower_max( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a status effect is applied if the value is lower than the max. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.value -= 20 - assert full_game_object_attribute.apply_status_effect( - lambda level: 20 * level, - lambda level: 10 - 2**level, - 3, - ) - assert ( - repr(full_game_object_attribute.applied_status_effect) - == "StatusEffect(value=60, duration=2, original_value=130," - " original_max_value=150, time_counter=0)" - ) - - -def test_full_game_object_attribute_apply_status_effect_existing_status_effect( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a status effect is not applied if a status effect is already applied. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.apply_status_effect(lambda _: 50, lambda _: 20, 3) - assert not full_game_object_attribute.apply_status_effect( - lambda _: 60, - lambda _: 30, - 2, - ) - - -def test_full_game_object_attribute_on_update_no_deltatime( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a game object is updated when no time has passed. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.apply_status_effect( - lambda level: level * 2, - lambda level: level + 100, - 2, - ) - full_game_object_attribute.on_update(0) - assert full_game_object_attribute.applied_status_effect - assert full_game_object_attribute.applied_status_effect.time_counter == 0 - - -def test_full_game_object_attribute_on_update_larger_deltatime( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a status effect is removed if deltatime is larger than the duration. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.apply_status_effect( - lambda level: 2**level, - lambda _: 20, - 2, - ) - full_game_object_attribute.on_update(30) - assert full_game_object_attribute.value == 150 - assert full_game_object_attribute.max_value == 150 - assert not full_game_object_attribute.applied_status_effect - - -def test_full_game_object_attribute_on_update_multiple_deltatimes( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a status effect is removed after multiple updates. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - # Apply the status effect and check the attribute's state after one update - full_game_object_attribute.apply_status_effect( - lambda level: 50 + level / 2, - lambda level: 3**level + 50, - 3, - ) - full_game_object_attribute.on_update(40) - assert full_game_object_attribute.applied_status_effect - assert full_game_object_attribute.applied_status_effect.time_counter == 40 - assert full_game_object_attribute.value == 201.5 - assert full_game_object_attribute.max_value == 201.5 - - # Check the attribute's state after the final update - full_game_object_attribute.on_update(40) - assert full_game_object_attribute.value == 150 - assert full_game_object_attribute.max_value == 150 - assert not full_game_object_attribute.applied_status_effect - - -def test_full_game_object_attribute_on_update_less_than_original( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a status effect is removed when value is less than the original. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - full_game_object_attribute.apply_status_effect(lambda _: 100, lambda _: 20, 4) - full_game_object_attribute.value -= 200 - full_game_object_attribute.on_update(30) - assert full_game_object_attribute.value == 50 - assert full_game_object_attribute.max_value == 150 - assert not full_game_object_attribute.applied_status_effect - - -def test_full_game_object_attribute_on_update_no_status_effect( - full_game_object_attribute: FullGameObjectAttribute, -) -> None: - """Test that a game object is updated if a status effect does not exist. - - Args: - full_game_object_attribute: The full game object attribute for use in testing. - """ - assert not full_game_object_attribute.applied_status_effect - full_game_object_attribute.on_update(5) - assert not full_game_object_attribute.applied_status_effect - - -def test_empty_game_object_attribute_setter( - empty_game_object_attribute: EmptyGameObjectAttribute, -) -> None: - """Test that an empty game object attribute's value can be changed. - - Args: - empty_game_object_attribute: The empty game object attribute for use in testing. - """ - empty_game_object_attribute.value -= 10 - assert empty_game_object_attribute.value == 90 - - -def test_empty_game_object_attribute_upgrade( - empty_game_object_attribute: EmptyGameObjectAttribute, -) -> None: - """Test that an empty game object attribute raises an error when upgraded. - - Args: - empty_game_object_attribute: The empty game object attribute for use in testing. - """ - with pytest.raises( - expected_exception=GameObjectAttributeError, - match=( - "The game object attribute `EmptyGameObjectAttribute` cannot be upgraded." - ), - ): - empty_game_object_attribute.upgrade(lambda _: 0) - - -def test_empty_game_object_attribute_instant_effect( - empty_game_object_attribute: EmptyGameObjectAttribute, -) -> None: - """Test that an instant effect raises an error on an empty game object attribute. - - Args: - empty_game_object_attribute: The empty game object attribute for use in testing. - """ - with pytest.raises( - expected_exception=GameObjectAttributeError, - match=( - "The game object attribute `EmptyGameObjectAttribute` cannot have an" - " instant effect." - ), - ): - empty_game_object_attribute.apply_instant_effect(lambda _: 0, 5) - - -def test_empty_game_object_attribute_apply_status_effect( - empty_game_object_attribute: EmptyGameObjectAttribute, -) -> None: - """Test that a status effect raises an error on an empty game object attribute. - - Args: - empty_game_object_attribute: The empty game object attribute for use in testing. - """ - with pytest.raises( - expected_exception=GameObjectAttributeError, - match=( - "The game object attribute `EmptyGameObjectAttribute` cannot have a status" - " effect." - ), - ): - empty_game_object_attribute.apply_status_effect(lambda _: 0, lambda _: 0, 6) - - -def test_empty_game_object_attribute_on_update( - empty_game_object_attribute: EmptyGameObjectAttribute, -) -> None: - """Test that updating a game object fails on an empty game object attribute. - - Args: - empty_game_object_attribute: The empty game object attribute for use in testing. - """ - assert not empty_game_object_attribute.applied_status_effect - empty_game_object_attribute.on_update(1) - assert not empty_game_object_attribute.applied_status_effect - - -def test_deal_damage_low_health_armour( - ecs: ECS, - both_game_object_attributes: tuple[ - FullGameObjectAttribute, - EmptyGameObjectAttribute, - ], -) -> None: - """Test that damage is dealt when health and armour are lower than damage. - - Args: - ecs: The entity component system for use in testing. - both_game_object_attributes: Both game object attributes for use in testing. - """ - both_game_object_attributes[0].value = 150 - both_game_object_attributes[1].value = 100 - deal_damage(0, ecs, 200) - assert both_game_object_attributes[0].value == 50 - assert both_game_object_attributes[1].value == 0 - - -def test_deal_damage_large_armour( - ecs: ECS, - both_game_object_attributes: tuple[ - FullGameObjectAttribute, - EmptyGameObjectAttribute, - ], -) -> None: - """Test that no damage is dealt when armour is larger than damage. - - Args: - ecs: The entity component system for use in testing. - both_game_object_attributes: Both game object attributes for use in testing. - """ - deal_damage(0, ecs, 100) - assert both_game_object_attributes[0].value == 200 - assert both_game_object_attributes[1].value == 50 - - -def test_deal_damage_zero_damage( - ecs: ECS, - both_game_object_attributes: tuple[ - FullGameObjectAttribute, - EmptyGameObjectAttribute, - ], -) -> None: - """Test that no damage is dealt when damage is zero. - - Args: - ecs: The entity component system for use in testing. - both_game_object_attributes: Both game object attributes for use in testing. - """ - deal_damage(0, ecs, 0) - assert both_game_object_attributes[0].value == 200 - assert both_game_object_attributes[1].value == 150 - - -def test_deal_damage_zero_armour( - ecs: ECS, - both_game_object_attributes: tuple[ - FullGameObjectAttribute, - EmptyGameObjectAttribute, - ], -) -> None: - """Test that damage is dealt when armour is zero. - - Args: - ecs: The entity component system for use in testing. - both_game_object_attributes: Both game object attributes for use in testing. - """ - both_game_object_attributes[1].value = 0 - deal_damage(0, ecs, 100) - assert both_game_object_attributes[0].value == 100 - assert both_game_object_attributes[1].value == 0 - - -def test_deal_damage_zero_health( - ecs: ECS, - both_game_object_attributes: tuple[ - FullGameObjectAttribute, - EmptyGameObjectAttribute, - ], -) -> None: - """Test that damage is dealt when health is zero. - - Args: - ecs: The entity component system for use in testing. - both_game_object_attributes: Both game object attributes for use in testing. - """ - both_game_object_attributes[0].value = 0 - deal_damage(0, ecs, 200) - assert both_game_object_attributes[0].value == 0 - assert both_game_object_attributes[1].value == 0 - - -def test_deal_damage_nonexistent_attributes(ecs: ECS) -> None: - """Test that no damage is dealt when the attributes are not initialised. - - Args: - ecs: The entity component system for use in testing. - """ - ecs.add_game_object({}) - with pytest.raises(expected_exception=KeyError): - deal_damage(0, ecs, 100) diff --git a/tests/game_objects/test_base.py b/tests/game_objects/test_base.py deleted file mode 100644 index 717a8724..00000000 --- a/tests/game_objects/test_base.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Tests all functions in game_objects/base.py.""" -from __future__ import annotations - -# Builtin -import math - -# Pip -import pytest - -# Custom -from hades.game_objects.base import Vec2d - -__all__ = () - - -def test_vec2d_init() -> None: - """Test if the Vec2d class is initialised correctly.""" - assert repr(Vec2d(0, 0)) == "" - - -def test_vec2d_addition() -> None: - """Test that adding two vectors produce the correct result.""" - assert Vec2d(0, 0) + Vec2d(1, 1) == Vec2d(1, 1) - assert Vec2d(-3, -2) + Vec2d(-1, -1) == Vec2d(-4, -3) - assert Vec2d(6, 3) + Vec2d(5, 5) == Vec2d(11, 8) - assert Vec2d(1, 1) + Vec2d(1, 1) == Vec2d(2, 2) - assert Vec2d(-5, 4) + Vec2d(7, -1) == Vec2d(2, 3) - - -def test_vec2d_subtraction() -> None: - """Test that subtracting two vectors produce the correct result.""" - assert Vec2d(0, 0) - Vec2d(1, 1) == Vec2d(-1, -1) - assert Vec2d(-3, -2) - Vec2d(-1, -1) == Vec2d(-2, -1) - assert Vec2d(6, 3) - Vec2d(5, 5) == Vec2d(1, -2) - assert Vec2d(1, 1) - Vec2d(1, 1) == Vec2d(0, 0) - assert Vec2d(-5, 4) - Vec2d(7, -1) == Vec2d(-12, 5) - - -def test_vec2d_abs() -> None: - """Test that the absolute value of a vector is calculated correctly.""" - assert abs(Vec2d(0, 0)) == 0 - assert abs(Vec2d(-3, -2)) == 3.605551275463989 - assert abs(Vec2d(6, 3)) == 6.708203932499369 - assert abs(Vec2d(1, 1)) == 1.4142135623730951 - assert abs(Vec2d(-5, 4)) == 6.4031242374328485 - - -def test_vec2d_multiplication() -> None: - """Test that multiplying a vector by a scalar produces the correct result.""" - assert Vec2d(0, 0) * 1 == Vec2d(0, 0) - assert Vec2d(-3, -2) * 2 == Vec2d(-6, -4) - assert Vec2d(6, 3) * 3 == Vec2d(18, 9) - assert Vec2d(1, 1) * 4 == Vec2d(4, 4) - assert Vec2d(-5, 4) * 5 == Vec2d(-25, 20) - - -def test_vec2d_division() -> None: - """Test that dividing a vector by a scalar produces the correct result.""" - assert Vec2d(0, 0) // 1 == Vec2d(0, 0) - assert Vec2d(-3, -2) // 2 == Vec2d(-2, -1) - assert Vec2d(6, 3) // 3 == Vec2d(2, 1) - assert Vec2d(1, 1) // 4 == Vec2d(0, 0) - assert Vec2d(-5, 4) // 5 == Vec2d(-1, 0) - - -def test_vec2d_normalised() -> None: - """Test that normalising a vector produces the correct result.""" - assert Vec2d(0, 0).normalised() == Vec2d(0, 0) - assert Vec2d(-3, -2).normalised() == Vec2d(-0.8320502943378437, -0.5547001962252291) - assert Vec2d(6, 3).normalised() == Vec2d(0.8944271909999159, 0.4472135954999579) - assert Vec2d(1, 1).normalised() == Vec2d(0.7071067811865475, 0.7071067811865475) - assert Vec2d(-5, 4).normalised() == Vec2d(-0.7808688094430304, 0.6246950475544243) - - -def test_vec2d_rotated() -> None: - """Test that rotating a vector produces the correct result.""" - assert Vec2d(0, 0).rotated(math.radians(360)) == Vec2d(0, 0) - assert Vec2d(-3, -2).rotated(math.radians(270)) == pytest.approx(Vec2d(-2, 3)) - assert Vec2d(6, 3).rotated(math.radians(180)) == pytest.approx(Vec2d(-6, -3)) - assert Vec2d(1, 1).rotated(math.radians(90)) == pytest.approx(Vec2d(-1, 1)) - assert Vec2d(-5, 4).rotated(math.radians(0)) == Vec2d(-5, 4) - - -def test_vec2d_get_angle_between() -> None: - """Test that getting the angle between two vectors produces the correct result.""" - assert Vec2d(0, 0).get_angle_between(Vec2d(1, 1)) == 0 - assert Vec2d(-3, -2).get_angle_between(Vec2d(-1, -1)) == 0.19739555984988075 - assert Vec2d(6, 3).get_angle_between(Vec2d(5, 5)) == 0.3217505543966422 - assert Vec2d(1, 1).get_angle_between(Vec2d(1, 1)) == 0 - assert Vec2d(-5, 4).get_angle_between(Vec2d(7, -1)) == 3.674436541209182 - - -def test_vec2d_get_distance_to() -> None: - """Test that getting the distance of two vectors produces the correct result.""" - assert Vec2d(0, 0).get_distance_to(Vec2d(1, 1)) == 1.4142135623730951 - assert Vec2d(-3, -2).get_distance_to(Vec2d(-1, -1)) == 2.23606797749979 - assert Vec2d(6, 3).get_distance_to(Vec2d(5, 5)) == 2.23606797749979 - assert Vec2d(1, 1).get_distance_to(Vec2d(1, 1)) == 0 - assert Vec2d(-5, 4).get_distance_to(Vec2d(7, -1)) == 13 diff --git a/tests/game_objects/test_components.py b/tests/game_objects/test_components.py deleted file mode 100644 index 27ef7054..00000000 --- a/tests/game_objects/test_components.py +++ /dev/null @@ -1,390 +0,0 @@ -"""Tests all functions in game_objects/components.py.""" -from __future__ import annotations - -# Builtin -from typing import cast - -# Pip -import pytest - -# Custom -from hades.game_objects.attributes import Armour, ArmourRegenCooldown -from hades.game_objects.base import ComponentType, Vec2d -from hades.game_objects.components import ( - ArmourRegen, - Footprint, - InstantEffects, - Inventory, - InventorySpaceError, - StatusEffects, -) -from hades.game_objects.system import ECS - -__all__ = () - - -@pytest.fixture() -def ecs() -> ECS: - """Create an entity component system for use in testing. - - Returns: - The entity component system for use in testing. - """ - return ECS() - - -@pytest.fixture() -def armour_regen(ecs: ECS) -> ArmourRegen: - """Create an armour regen component for use in testing. - - Args: - ecs: The entity component system for use in testing. - - Returns: - The armour regen component for use in testing. - """ - ecs.add_game_object( - { - "attributes": { - ComponentType.ARMOUR: (50, 3), - ComponentType.ARMOUR_REGEN_COOLDOWN: (4, 5), - }, - }, - Armour, - ArmourRegenCooldown, - ArmourRegen, - ) - return cast( - ArmourRegen, - ecs.get_component_for_game_object(0, ComponentType.ARMOUR_REGEN), - ) - - -@pytest.fixture() -def footprint(ecs: ECS) -> Footprint: - """Create a footprint component for use in testing. - - Args: - ecs: The entity component system for use in testing. - - Returns: - The instant effects component for use in testing. - """ - ecs.add_game_object({}, Footprint, physics=True) - return cast( - Footprint, - ecs.get_component_for_game_object(0, ComponentType.FOOTPRINT), - ) - - -@pytest.fixture() -def instant_effects(ecs: ECS) -> InstantEffects: - """Create an instant effects component for use in testing. - - Args: - ecs: The entity component system for use in testing. - - Returns: - The instant effects component for use in testing. - """ - ecs.add_game_object( - {"instant_effects": (5, {ComponentType.HEALTH: lambda level: 2**level + 5})}, - InstantEffects, - ) - return cast( - InstantEffects, - ecs.get_component_for_game_object(0, ComponentType.INSTANT_EFFECTS), - ) - - -@pytest.fixture() -def inventory(ecs: ECS) -> Inventory[int]: - """Create an inventory component for use in testing. - - Args: - ecs: The entity component system for use in testing. - - Returns: - The inventory component for use in testing. - """ - ecs.add_game_object({"inventory_size": (3, 6)}, Inventory) - return cast( - Inventory[int], - ecs.get_component_for_game_object(0, ComponentType.INVENTORY), - ) - - -@pytest.fixture() -def status_effects(ecs: ECS) -> StatusEffects: - """Create a status effects component for use in testing. - - Args: - ecs: The entity component system for use in testing. - - Returns: - The status effects component for use in testing. - """ - ecs.add_game_object( - { - "status_effects": ( - 3, - { - ComponentType.ARMOUR: ( - lambda level: 3**level + 10, - lambda level: 3 * level + 5, - ), - }, - ), - }, - StatusEffects, - ) - return cast( - StatusEffects, - ecs.get_component_for_game_object(0, ComponentType.STATUS_EFFECTS), - ) - - -def test_raise_inventory_space_error_full() -> None: - """Test that InventorySpaceError is raised correctly when full.""" - with pytest.raises( - expected_exception=InventorySpaceError, - match="The inventory is full.", - ): - raise InventorySpaceError(full=True) - - -def test_raise_inventory_space_error_empty() -> None: - """Test that InventorySpaceError is raised correctly when empty.""" - with pytest.raises( - expected_exception=InventorySpaceError, - match="The inventory is empty.", - ): - raise InventorySpaceError(full=False) - - -def test_armour_regen_init(armour_regen: ArmourRegen) -> None: - """Test that the armour regen component is initialised correctly. - - Args: - armour_regen: The armour regen component for use in testing. - """ - assert repr(armour_regen) == "" - - -def test_armour_regen_on_update_small_deltatime(armour_regen: ArmourRegen) -> None: - """Test that the armour regen component is updated with a small deltatime. - - Args: - armour_regen: The armour regen component for use in testing. - """ - armour_regen.armour.value -= 10 - armour_regen.on_update(2) - assert armour_regen.armour.value == 40 - assert armour_regen.time_since_armour_regen == 2 - - -def test_armour_regen_on_update_large_deltatime(armour_regen: ArmourRegen) -> None: - """Test that the armour regen component is updated with a large deltatime. - - Args: - armour_regen: The armour regen component for use in testing. - """ - armour_regen.armour.value -= 10 - armour_regen.on_update(6) - assert armour_regen.armour.value == 41 - assert armour_regen.time_since_armour_regen == 0 - - -def test_armour_regen_on_update_multiple_updates(armour_regen: ArmourRegen) -> None: - """Test that the armour regen component is updated correctly multiple times. - - Args: - armour_regen: The armour regen component for use in testing. - """ - armour_regen.armour.value -= 10 - armour_regen.on_update(1) - assert armour_regen.armour.value == 40 - assert armour_regen.time_since_armour_regen == 1 - armour_regen.on_update(2) - assert armour_regen.armour.value == 40 - assert armour_regen.time_since_armour_regen == 3 - - -def test_armour_regen_on_update_full_armour(armour_regen: ArmourRegen) -> None: - """Test that the armour regen component is updated even when armour is already full. - - Args: - armour_regen: The armour regen component for use in testing. - """ - armour_regen.on_update(5) - assert armour_regen.armour.value == 50 - assert armour_regen.time_since_armour_regen == 0 - - -def test_footprint_init(footprint: Footprint) -> None: - """Test that the footprint component is initialised correctly.""" - assert ( - repr(footprint) - == "" - ) - - -def test_footprint_on_update_small_deltatime(footprint: Footprint) -> None: - """Test that the footprint component is updated with a small deltatime. - - Args: - footprint: The footprint component for use in testing. - """ - footprint.on_update(0.1) - assert footprint.footprints == [] - assert footprint.time_since_last_footprint == 0.1 - - -def test_footprint_on_update_large_deltatime_empty_list(footprint: Footprint) -> None: - """Test that the footprint component creates a footprint in an empty list. - - Args: - footprint: The footprint component for use in testing. - """ - footprint.on_update(1) - assert footprint.footprints == [Vec2d(0, 0)] - assert footprint.time_since_last_footprint == 0 - - -def test_footprint_on_update_large_deltatime_non_empty_list( - footprint: Footprint, -) -> None: - """Test that the footprint component creates a footprint in a non-empty list. - - Args: - footprint: The footprint component for use in testing. - """ - footprint.footprints = [Vec2d(1, 1), Vec2d(2, 2), Vec2d(3, 3)] - footprint.on_update(0.5) - assert footprint.footprints == [Vec2d(1, 1), Vec2d(2, 2), Vec2d(3, 3), Vec2d(0, 0)] - - -def test_footprint_on_update_large_deltatime_full_list(footprint: Footprint) -> None: - """Test that the footprint component creates a footprint and removes the oldest one. - - Args: - footprint: The footprint component for use in testing. - """ - footprint.footprints = [ - Vec2d(0, 0), - Vec2d(1, 1), - Vec2d(2, 2), - Vec2d(3, 3), - Vec2d(4, 4), - Vec2d(5, 5), - Vec2d(6, 6), - Vec2d(7, 7), - Vec2d(8, 8), - Vec2d(9, 9), - ] - footprint.system.get_physics_object_for_game_object(0).position = Vec2d(10, 10) - footprint.on_update(0.5) - assert footprint.footprints == [ - Vec2d(1, 1), - Vec2d(2, 2), - Vec2d(3, 3), - Vec2d(4, 4), - Vec2d(5, 5), - Vec2d(6, 6), - Vec2d(7, 7), - Vec2d(8, 8), - Vec2d(9, 9), - Vec2d(10, 10), - ] - - -def test_footprint_on_update_multiple_updates(footprint: Footprint) -> None: - """Test that the footprint component is updated correctly multiple times. - - Args: - footprint: The footprint component for use in testing. - """ - footprint.on_update(0.6) - assert footprint.footprints == [Vec2d(0, 0)] - assert footprint.time_since_last_footprint == 0 - footprint.system.get_physics_object_for_game_object(0).position = Vec2d(1, 1) - footprint.on_update(0.7) - assert footprint.footprints == [Vec2d(0, 0), Vec2d(1, 1)] - assert footprint.time_since_last_footprint == 0 - - -def test_instant_effects_init(instant_effects: InstantEffects) -> None: - """Test that the instant effects component is initialised correctly. - - Args: - instant_effects: The instant effects component for use in testing. - """ - assert repr(instant_effects) == "" - - -def test_inventory_init(inventory: Inventory[int]) -> None: - """Test that the inventory component is initialised correctly. - - Args: - inventory: The inventory component for use in testing. - """ - assert repr(inventory) == "" - assert not inventory.inventory - - -def test_inventory_add_item_to_inventory_valid(inventory: Inventory[int]) -> None: - """Test that a valid item is added to the inventory correctly. - - Args: - inventory: The inventory component for use in testing. - """ - inventory.add_item_to_inventory(50) - assert inventory.inventory == [50] - - -def test_inventory_add_item_to_inventory_zero_size(inventory: Inventory[int]) -> None: - """Test that a valid item is not added to a zero size inventory. - - Args: - inventory: The inventory component for use in testing. - """ - inventory.width = 0 - with pytest.raises(expected_exception=InventorySpaceError): - inventory.add_item_to_inventory(5) - - -def test_inventory_remove_item_from_inventory_valid(inventory: Inventory[int]) -> None: - """Test that a valid item is removed from the inventory correctly. - - Args: - inventory: The inventory component for use in testing. - """ - inventory.add_item_to_inventory(1) - inventory.add_item_to_inventory(7) - inventory.add_item_to_inventory(4) - assert inventory.remove_item_from_inventory(1) == 7 - assert inventory.inventory == [1, 4] - - -def test_inventory_remove_item_from_inventory_large_index( - inventory: Inventory[int], -) -> None: - """Test that an exception is raised if a larger index is provided. - - Args: - inventory: The inventory component for use in testing. - """ - inventory.add_item_to_inventory(5) - inventory.add_item_to_inventory(10) - inventory.add_item_to_inventory(50) - with pytest.raises(expected_exception=InventorySpaceError): - inventory.remove_item_from_inventory(10) - - -def test_status_effects_init(status_effects: StatusEffects) -> None: - """Test that the status effects component is initialised correctly. - - Args: - status_effects: The status effects component for use in testing. - """ - assert repr(status_effects) == "" diff --git a/tests/game_objects/test_movements.py b/tests/game_objects/test_movements.py deleted file mode 100644 index 6f773a1c..00000000 --- a/tests/game_objects/test_movements.py +++ /dev/null @@ -1,1016 +0,0 @@ -"""Tests all functions in game_objects/movements.py.""" -from __future__ import annotations - -# Builtin -from typing import TYPE_CHECKING, cast - -# Pip -import pytest - -# Custom -from hades.game_objects.attributes import MovementForce -from hades.game_objects.base import ( - ComponentType, - SteeringBehaviours, - SteeringMovementState, - Vec2d, -) -from hades.game_objects.components import Footprint -from hades.game_objects.movements import ( - KeyboardMovement, - SteeringMovement, - arrive, - evade, - flee, - follow_path, - obstacle_avoidance, - pursuit, - seek, - wander, -) -from hades.game_objects.system import ECS - -if TYPE_CHECKING: - from collections.abc import Callable - -__all__ = () - - -@pytest.fixture() -def ecs() -> ECS: - """Create an entity component system for use in testing. - - Returns: - The entity component system for use in testing. - """ - return ECS() - - -@pytest.fixture() -def keyboard_movement(ecs: ECS) -> KeyboardMovement: - """Create a keyboard movement component for use in testing. - - Args: - ecs: The entity component system for use in testing. - - Returns: - The keyboard movement component for use in testing. - """ - ecs.add_game_object( - {"attributes": {ComponentType.MOVEMENT_FORCE: (100, 5)}}, - Footprint, - MovementForce, - KeyboardMovement, - physics=True, - ) - return cast( - KeyboardMovement, - ecs.get_component_for_game_object(0, ComponentType.MOVEMENTS), - ) - - -@pytest.fixture() -def steering_movement_factory( - keyboard_movement: KeyboardMovement, -) -> Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, -]: - """Create a steering movement component factory for use in testing. - - Args: - keyboard_movement: The keyboard movement component to use in testing. - - Returns: - The steering movement component factory for use in testing. - """ - - def wrap( - steering_behaviours: dict[SteeringMovementState, list[SteeringBehaviours]], - ) -> SteeringMovement: - game_object_id = keyboard_movement.system.add_game_object( - { - "attributes": {ComponentType.MOVEMENT_FORCE: (100, 5)}, - "steering_behaviours": steering_behaviours, - }, - MovementForce, - SteeringMovement, - physics=True, - ) - steering_movement = cast( - SteeringMovement, - keyboard_movement.system.get_component_for_game_object( - game_object_id, - ComponentType.MOVEMENTS, - ), - ) - steering_movement.target_id = 0 - return steering_movement - - return wrap - - -@pytest.fixture() -def steering_movement( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> SteeringMovement: - """Create a steering movement component for use in testing. - - Args: - steering_movement_factory: The steering movement component factory to use in - testing. - - Returns: - The steering movement component for use in testing. - """ - return steering_movement_factory({}) - - -def test_arrive_outside_slowing_radius() -> None: - """Test if a position outside the radius produces the correct arrive force.""" - assert arrive(Vec2d(500, 500), Vec2d(0, 0)) == Vec2d( - -0.7071067811865475, - -0.7071067811865475, - ) - - -def test_arrive_on_slowing_range() -> None: - """Test if a position on the radius produces the correct arrive force.""" - assert arrive(Vec2d(135, 135), Vec2d(0, 0)) == Vec2d( - -0.7071067811865475, - -0.7071067811865475, - ) - - -def test_arrive_inside_slowing_range() -> None: - """Test if a position inside the radius produces the correct arrive force.""" - assert arrive(Vec2d(100, 100), Vec2d(0, 0)) == Vec2d( - -0.7071067811865476, - -0.7071067811865476, - ) - - -def test_arrive_near_target() -> None: - """Test if a position near the target produces the correct arrive force.""" - assert arrive(Vec2d(50, 50), Vec2d(0, 0)) == Vec2d( - -0.7071067811865476, - -0.7071067811865476, - ) - - -def test_arrive_on_target() -> None: - """Test if a position on the target produces the correct arrive force.""" - assert arrive(Vec2d(0, 0), Vec2d(0, 0)) == Vec2d(0, 0) - - -def test_evade_non_moving_target() -> None: - """Test if a non-moving target produces the correct evade force.""" - assert evade(Vec2d(0, 0), Vec2d(100, 100), Vec2d(0, 0)) == Vec2d( - -0.7071067811865475, - -0.7071067811865475, - ) - - -def test_evade_moving_target() -> None: - """Test if a moving target produces the correct evade force.""" - assert evade(Vec2d(0, 0), Vec2d(100, 100), Vec2d(-50, 0)) == Vec2d( - -0.5428888213891885, - -0.8398045770360255, - ) - - -def test_evade_same_positions() -> None: - """Test if having the same position produces the correct evade force.""" - assert evade(Vec2d(0, 0), Vec2d(0, 0), Vec2d(0, 0)) == Vec2d(0, 0) - assert evade(Vec2d(0, 0), Vec2d(0, 0), Vec2d(-50, 0)) == Vec2d(0, 0) - - -def test_flee_higher_current() -> None: - """Test if a higher current position produces the correct flee force.""" - assert flee(Vec2d(100, 100), Vec2d(50, 50)) == Vec2d( - 0.7071067811865475, - 0.7071067811865475, - ) - - -def test_flee_higher_target() -> None: - """Test if a higher target position produces the correct flee force.""" - assert flee(Vec2d(50, 50), Vec2d(100, 100)) == Vec2d( - -0.7071067811865475, - -0.7071067811865475, - ) - - -def test_flee_equal() -> None: - """Test if two equal positions produce the correct flee force.""" - assert flee(Vec2d(100, 100), Vec2d(100, 100)) == Vec2d(0, 0) - - -def test_flee_negative_current() -> None: - """Test if a negative current position produces the correct flee force.""" - assert flee(Vec2d(-50, -50), Vec2d(100, 100)) == Vec2d( - -0.7071067811865475, - -0.7071067811865475, - ) - - -def test_flee_negative_target() -> None: - """Test if a negative target position produces the correct flee force.""" - assert flee(Vec2d(100, 100), Vec2d(-50, -50)) == Vec2d( - 0.7071067811865475, - 0.7071067811865475, - ) - - -def test_flee_negative_positions() -> None: - """Test if two negative positions produce the correct flee force.""" - assert flee(Vec2d(-50, -50), Vec2d(-50, -50)) == Vec2d(0, 0) - - -def test_follow_path_single_point() -> None: - """Test if a single point produces the correct follow path force.""" - assert follow_path(Vec2d(100, 100), [Vec2d(250, 250)]) == Vec2d( - 0.7071067811865475, - 0.7071067811865475, - ) - - -def test_follow_path_single_point_reached() -> None: - """Test if reaching a single-point list produces the correct follow path force.""" - path_list = [Vec2d(100, 100)] - assert follow_path(Vec2d(100, 100), path_list) == Vec2d(0, 0) - assert path_list == [Vec2d(100, 100)] - - -def test_follow_path_multiple_points() -> None: - """Test if multiple points produces the correct follow path force.""" - assert follow_path(Vec2d(200, 200), [Vec2d(350, 350), Vec2d(500, 500)]) == Vec2d( - 0.7071067811865475, - 0.7071067811865475, - ) - - -def test_follow_path_multiple_points_reached() -> None: - """Test if reaching a multiple point list produces the correct follow path force.""" - path_list = [Vec2d(100, 100), Vec2d(250, 250)] - assert follow_path(Vec2d(100, 100), path_list) == Vec2d( - 0.7071067811865475, - 0.7071067811865475, - ) - assert path_list == [Vec2d(250, 250), Vec2d(100, 100)] - - -def test_follow_path_empty_list() -> None: - """Test if an empty list raises the correct exception.""" - with pytest.raises(expected_exception=IndexError): - follow_path(Vec2d(100, 100), []) - - -def test_obstacle_avoidance_no_obstacles() -> None: - """Test if no obstacles produce the correct avoidance force.""" - assert obstacle_avoidance(Vec2d(100, 100), Vec2d(0, 100), set()) == Vec2d(0, 0) - - -def test_obstacle_avoidance_obstacle_out_of_range() -> None: - """Test if an out of range obstacle produces the correct avoidance force.""" - assert obstacle_avoidance(Vec2d(100, 100), Vec2d(0, 100), {(10, 10)}) == Vec2d(0, 0) - - -def test_obstacle_avoidance_angled_velocity() -> None: - """Test if an angled velocity produces the correct avoidance force.""" - assert obstacle_avoidance(Vec2d(100, 100), Vec2d(100, 100), {(1, 2)}) == Vec2d( - 0.2588190451025206, - -0.9659258262890683, - ) - - -def test_obstacle_avoidance_non_moving() -> None: - """Test if a non-moving game object produces the correct avoidance force.""" - assert obstacle_avoidance(Vec2d(100, 100), Vec2d(0, 100), {(1, 2)}) == Vec2d(0, 0) - - -def test_obstacle_avoidance_single_forward() -> None: - """Test if a single forward obstacle produces the correct avoidance force.""" - assert obstacle_avoidance(Vec2d(100, 100), Vec2d(0, 100), {(1, 2)}) == Vec2d(0, 0) - - -def test_obstacle_avoidance_single_left() -> None: - """Test if a single left obstacle produces the correct avoidance force.""" - assert obstacle_avoidance( - Vec2d(100, 100), - Vec2d(0, 100), - {(0, 2)}, - ) == pytest.approx(Vec2d(0.8660254037844387, -0.5)) - - -def test_obstacle_avoidance_single_right() -> None: - """Test if a single right obstacle produces the correct avoidance force.""" - assert obstacle_avoidance( - Vec2d(100, 100), - Vec2d(0, 100), - {(2, 2)}, - ) == pytest.approx(Vec2d(-0.8660254037844386, -0.5)) - - -def test_obstacle_avoidance_left_forward() -> None: - """Test if a left and forward obstacle produces the correct avoidance force.""" - assert obstacle_avoidance( - Vec2d(100, 100), - Vec2d(0, 100), - {(0, 2), (1, 2)}, - ) == pytest.approx(Vec2d(0.8660254037844387, -0.5)) - - -def test_obstacle_avoidance_right_forward() -> None: - """Test if a right and forward obstacle produces the correct avoidance force.""" - assert obstacle_avoidance( - Vec2d(100, 100), - Vec2d(0, 100), - {(1, 2), (2, 2)}, - ) == pytest.approx(Vec2d(-0.8660254037844386, -0.5)) - - -def test_obstacle_avoidance_left_right_forward() -> None: - """Test if all three obstacles produce the correct avoidance force.""" - assert obstacle_avoidance( - Vec2d(100, 100), - Vec2d(0, 100), - {(0, 2), (1, 2), (2, 2)}, - ) == Vec2d(0, -1) - - -def test_pursuit_non_moving_target() -> None: - """Test if a non-moving target produces the correct pursuit force.""" - assert pursuit(Vec2d(0, 0), Vec2d(100, 100), Vec2d(0, 0)) == Vec2d( - 0.7071067811865475, - 0.7071067811865475, - ) - - -def test_pursuit_moving_target() -> None: - """Test if a moving target produces the correct pursuit force.""" - assert pursuit(Vec2d(0, 0), Vec2d(100, 100), Vec2d(-50, 0)) == Vec2d( - 0.5428888213891885, - 0.8398045770360255, - ) - - -def test_pursuit_same_positions() -> None: - """Test if having the same position produces the correct pursuit force.""" - assert pursuit(Vec2d(0, 0), Vec2d(0, 0), Vec2d(0, 0)) == Vec2d(0, 0) - assert pursuit(Vec2d(0, 0), Vec2d(0, 0), Vec2d(-50, 0)) == Vec2d(0, 0) - - -def test_seek_higher_current() -> None: - """Test if a higher current position produces the correct seek force.""" - assert seek(Vec2d(100, 100), Vec2d(50, 50)) == Vec2d( - -0.7071067811865475, - -0.7071067811865475, - ) - - -def test_seek_higher_target() -> None: - """Test if a higher target position produces the correct seek force.""" - assert seek(Vec2d(50, 50), Vec2d(100, 100)) == Vec2d( - 0.7071067811865475, - 0.7071067811865475, - ) - - -def test_seek_equal() -> None: - """Test if two equal positions produce the correct seek force.""" - assert seek(Vec2d(100, 100), Vec2d(100, 100)) == Vec2d(0, 0) - - -def test_seek_negative_current() -> None: - """Test if a negative current position produces the correct seek force.""" - assert seek(Vec2d(-50, -50), Vec2d(100, 100)) == Vec2d( - 0.7071067811865475, - 0.7071067811865475, - ) - - -def test_seek_negative_target() -> None: - """Test if a negative target position produces the correct seek force.""" - assert seek(Vec2d(100, 100), Vec2d(-50, -50)) == Vec2d( - -0.7071067811865475, - -0.7071067811865475, - ) - - -def test_seek_negative_positions() -> None: - """Test if two negative positions produce the correct seek force.""" - assert seek(Vec2d(-50, -50), Vec2d(-50, -50)) == Vec2d(0, 0) - - -def test_wander_non_moving() -> None: - """Test if a non-moving game object produces the correct wander force.""" - assert wander(Vec2d(0, 0), 60) == pytest.approx(Vec2d(0.8660254037844385, -0.5)) - - -def test_wander_moving() -> None: - """Test if a moving game object produces the correct wander force.""" - assert wander(Vec2d(100, -100), 60) == Vec2d( - 0.7659012135559103, - -0.6429582654213131, - ) - - -def test_keyboard_movement_init(keyboard_movement: KeyboardMovement) -> None: - """Test if the keyboard movement component is initialised correctly. - - Args: - keyboard_movement: The keyboard movement component for use in testing. - """ - assert ( - repr(keyboard_movement) - == "" - ) - - -def test_keyboard_movement_calculate_force_none( - keyboard_movement: KeyboardMovement, -) -> None: - """Test if the correct force is calculated if no keys are pressed. - - Args: - keyboard_movement: The keyboard movement component for use in testing. - """ - assert keyboard_movement.calculate_force() == (0, 0) - - -def test_keyboard_movement_calculate_force_north( - keyboard_movement: KeyboardMovement, -) -> None: - """Test if the correct force is calculated for a move north. - - Args: - keyboard_movement: The keyboard movement component for use in testing. - """ - keyboard_movement.north_pressed = True - assert keyboard_movement.calculate_force() == (0, 100) - - -def test_keyboard_movement_calculate_force_south( - keyboard_movement: KeyboardMovement, -) -> None: - """Test if the correct force is calculated for a move south. - - Args: - keyboard_movement: The keyboard movement component for use in testing. - """ - keyboard_movement.south_pressed = True - assert keyboard_movement.calculate_force() == (0, -100) - - -def test_keyboard_movement_calculate_force_east( - keyboard_movement: KeyboardMovement, -) -> None: - """Test if the correct force is calculated for a move east. - - Args: - keyboard_movement: The keyboard movement component for use in testing. - """ - keyboard_movement.east_pressed = True - assert keyboard_movement.calculate_force() == (100, 0) - - -def test_keyboard_movement_calculate_force_west( - keyboard_movement: KeyboardMovement, -) -> None: - """Test if the correct force is calculated for a move west. - - Args: - keyboard_movement: The keyboard movement component for use in testing. - """ - keyboard_movement.west_pressed = True - assert keyboard_movement.calculate_force() == (-100, 0) - - -def test_keyboard_movement_calculate_force_east_west( - keyboard_movement: KeyboardMovement, -) -> None: - """Test if the correct force is calculated if east and west are pressed. - - Args: - keyboard_movement: The keyboard movement component for use in testing. - """ - keyboard_movement.east_pressed = True - keyboard_movement.west_pressed = True - assert keyboard_movement.calculate_force() == (0, 0) - - -def test_keyboard_movement_calculate_force_north_south( - keyboard_movement: KeyboardMovement, -) -> None: - """Test if the correct force is calculated if north and south are pressed. - - Args: - keyboard_movement: The keyboard movement component for use in testing. - """ - keyboard_movement.east_pressed = True - keyboard_movement.west_pressed = True - assert keyboard_movement.calculate_force() == (0, 0) - - -def test_keyboard_movement_calculate_force_north_west( - keyboard_movement: KeyboardMovement, -) -> None: - """Test if the correct force is calculated if north and west are pressed. - - Args: - keyboard_movement: The keyboard movement component for use in testing. - """ - keyboard_movement.north_pressed = True - keyboard_movement.west_pressed = True - assert keyboard_movement.calculate_force() == (-100, 100) - - -def test_keyboard_movement_calculate_force_south_east( - keyboard_movement: KeyboardMovement, -) -> None: - """Test if the correct force is calculated if south and east are pressed. - - Args: - keyboard_movement: The keyboard movement component for use in testing. - """ - keyboard_movement.south_pressed = True - keyboard_movement.east_pressed = True - assert keyboard_movement.calculate_force() == (100, -100) - - -def test_steering_movement_init(steering_movement: SteeringMovement) -> None: - """Test if the steering movement component is initialised correctly. - - Args: - steering_movement: The steering movement component for use in testing. - """ - assert ( - repr(steering_movement) - == "" - ) - - -def test_steering_movement_calculate_force_within_target_distance_empty_path_list( - steering_movement: SteeringMovement, -) -> None: - """Test if the state is correctly changed to the target state. - - Args: - steering_movement: The steering movement component for use in testing. - """ - steering_movement.system.get_physics_object_for_game_object(1).position = Vec2d( - 100, - 100, - ) - steering_movement.calculate_force() - assert steering_movement.movement_state == SteeringMovementState.TARGET - - -def test_steering_movement_calculate_force_within_target_distance_non_empty_path_list( - steering_movement: SteeringMovement, -) -> None: - """Test if the state is correctly changed to the target state. - - Args: - steering_movement: The steering movement component for use in testing. - """ - steering_movement.system.get_physics_object_for_game_object(1).position = Vec2d( - 100, - 100, - ) - steering_movement.path_list = [Vec2d(300, 300), Vec2d(400, 400)] - steering_movement.calculate_force() - assert steering_movement.movement_state == SteeringMovementState.TARGET - - -def test_steering_movement_calculate_force_outside_target_distance_empty_path_list( - steering_movement: SteeringMovement, -) -> None: - """Test if the state is correctly changed to the default state. - - Args: - steering_movement: The steering movement component for use in testing. - """ - steering_movement.system.get_physics_object_for_game_object(1).position = Vec2d( - 500, - 500, - ) - steering_movement.calculate_force() - assert steering_movement.movement_state == SteeringMovementState.DEFAULT - - -def test_steering_movement_calculate_force_outside_target_distance_non_empty_path_list( - steering_movement: SteeringMovement, -) -> None: - """Test if the state is correctly changed to the footprint state. - - Args: - steering_movement: The steering movement component for use in testing. - """ - steering_movement.system.get_physics_object_for_game_object(1).position = Vec2d( - 500, - 500, - ) - steering_movement.path_list = [Vec2d(300, 300), Vec2d(400, 400)] - steering_movement.calculate_force() - assert steering_movement.movement_state == SteeringMovementState.FOOTPRINT - - -def test_steering_movement_calculate_force_missing_state( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> None: - """Test if a zero force is calculated if the state is missing. - - Args: - steering_movement_factory: The steering movement component factory for use in - testing. - """ - assert steering_movement_factory({}).calculate_force() == Vec2d(0, 0) - - -def test_steering_movement_calculate_force_arrive( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> None: - """Test if the correct force is calculated for the arrive behaviour. - - Args: - steering_movement_factory: The steering movement component factory for use in - testing. - """ - steering_movement = steering_movement_factory( - {SteeringMovementState.TARGET: [SteeringBehaviours.ARRIVE]}, - ) - steering_movement.system.get_physics_object_for_game_object(0).position = Vec2d( - 0, - 0, - ) - steering_movement.system.get_physics_object_for_game_object(1).position = Vec2d( - 100, - 100, - ) - assert steering_movement.calculate_force() == Vec2d( - -70.71067811865476, - -70.71067811865476, - ) - - -def test_steering_movement_calculate_force_evade( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> None: - """Test if the correct force is calculated for the evade behaviour. - - Args: - steering_movement_factory: The steering movement component factory for use in - testing. - """ - steering_movement = steering_movement_factory( - {SteeringMovementState.TARGET: [SteeringBehaviours.EVADE]}, - ) - physics_object = steering_movement.system.get_physics_object_for_game_object(0) - physics_object.position = Vec2d(100, 100) - physics_object.velocity = Vec2d(-50, 0) - assert steering_movement.calculate_force() == Vec2d( - -54.28888213891886, - -83.98045770360257, - ) - - -def test_steering_movement_calculate_force_flee( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> None: - """Test if the correct force is calculated for the flee behaviour. - - Args: - steering_movement_factory: The steering movement component factory for use in - testing. - """ - steering_movement = steering_movement_factory( - {SteeringMovementState.TARGET: [SteeringBehaviours.FLEE]}, - ) - steering_movement.system.get_physics_object_for_game_object(0).position = Vec2d( - 50, - 50, - ) - steering_movement.system.get_physics_object_for_game_object(1).position = Vec2d( - 100, - 100, - ) - assert steering_movement.calculate_force() == Vec2d( - 70.71067811865475, - 70.71067811865475, - ) - - -def test_steering_movement_calculate_force_follow_path( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> None: - """Test if the correct force is calculated for the follow path behaviour. - - Args: - steering_movement_factory: The steering movement component factory for use in - testing. - """ - steering_movement = steering_movement_factory( - {SteeringMovementState.FOOTPRINT: [SteeringBehaviours.FOLLOW_PATH]}, - ) - steering_movement.system.get_physics_object_for_game_object(1).position = Vec2d( - 200, - 200, - ) - steering_movement.path_list = [Vec2d(350, 350), Vec2d(500, 500)] - assert steering_movement.calculate_force() == Vec2d( - 70.71067811865475, - 70.71067811865475, - ) - - -def test_steering_movement_calculate_force_obstacle_avoidance( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> None: - """Test if the correct force is calculated for the obstacle avoidance behaviour. - - Args: - steering_movement_factory: The steering movement component factory for use in - testing. - """ - steering_movement = steering_movement_factory( - {SteeringMovementState.TARGET: [SteeringBehaviours.OBSTACLE_AVOIDANCE]}, - ) - physics_object = steering_movement.system.get_physics_object_for_game_object(1) - physics_object.position = Vec2d(100, 100) - physics_object.velocity = Vec2d(100, 100) - steering_movement.walls = {(1, 2)} - assert steering_movement.calculate_force() == Vec2d( - 25.881904510252056, - -96.59258262890683, - ) - - -def test_steering_movement_calculate_force_pursuit( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> None: - """Test if the correct force is calculated for the pursuit behaviour. - - Args: - steering_movement_factory: The steering movement component factory for use in - testing. - """ - steering_movement = steering_movement_factory( - {SteeringMovementState.TARGET: [SteeringBehaviours.PURSUIT]}, - ) - physics_object = steering_movement.system.get_physics_object_for_game_object(0) - physics_object.position = Vec2d(100, 100) - physics_object.velocity = Vec2d(-50, 0) - assert steering_movement.calculate_force() == Vec2d( - 54.28888213891886, - 83.98045770360257, - ) - - -def test_steering_movement_calculate_force_seek( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> None: - """Test if the correct force is calculated for the seek behaviour. - - Args: - steering_movement_factory: The steering movement component factory for use in - testing. - """ - steering_movement = steering_movement_factory( - {SteeringMovementState.TARGET: [SteeringBehaviours.SEEK]}, - ) - steering_movement.system.get_physics_object_for_game_object(0).position = Vec2d( - 50, - 50, - ) - steering_movement.system.get_physics_object_for_game_object(1).position = Vec2d( - 100, - 100, - ) - assert steering_movement.calculate_force() == Vec2d( - -70.71067811865475, - -70.71067811865475, - ) - - -def test_steering_movement_calculate_force_wander( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> None: - """Test if the correct force is calculated for the wander behaviour. - - Args: - steering_movement_factory: The steering movement component factory for use in - testing. - """ - steering_movement = steering_movement_factory( - {SteeringMovementState.TARGET: [SteeringBehaviours.WANDER]}, - ) - steering_movement.system.get_physics_object_for_game_object(1).velocity = Vec2d( - 100, - -100, - ) - steering_force = steering_movement.calculate_force() - assert steering_force != steering_movement.calculate_force() - assert round(abs(steering_force)) == 100 - - -def test_steering_movement_calculate_force_multiple_behaviours( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> None: - """Test if the correct force is calculated when multiple behaviours are selected. - - Args: - steering_movement_factory: The steering movement component factory for use in - testing. - """ - steering_movement = steering_movement_factory( - { - SteeringMovementState.FOOTPRINT: [ - SteeringBehaviours.FOLLOW_PATH, - SteeringBehaviours.SEEK, - ], - }, - ) - steering_movement.system.get_physics_object_for_game_object(1).position = Vec2d( - 300, - 300, - ) - steering_movement.path_list = [Vec2d(100, 200), Vec2d(-100, 0)] - assert steering_movement.calculate_force() == Vec2d( - -81.12421851755609, - -58.47102846637651, - ) - - -def test_steering_movement_calculate_force_multiple_states( - steering_movement_factory: Callable[ - [dict[SteeringMovementState, list[SteeringBehaviours]]], - SteeringMovement, - ], -) -> None: - """Test if the correct force is calculated when multiple states are initialised. - - Args: - steering_movement_factory: The steering movement component factory for use in - testing. - """ - # Initialise the steering movement component with multiple states - steering_movement = steering_movement_factory( - { - SteeringMovementState.TARGET: [SteeringBehaviours.PURSUIT], - SteeringMovementState.DEFAULT: [SteeringBehaviours.SEEK], - }, - ) - - # Test the target state - steering_movement.system.get_physics_object_for_game_object(0).velocity = Vec2d( - -50, - 100, - ) - steering_movement.system.get_physics_object_for_game_object(1).position = Vec2d( - 100, - 100, - ) - assert steering_movement.calculate_force() == Vec2d( - -97.73793955511094, - -21.14935392681019, - ) - - # Test the default state - steering_movement.system.get_physics_object_for_game_object(1).position = Vec2d( - 300, - 300, - ) - assert steering_movement.calculate_force() == Vec2d( - -70.71067811865476, - -70.71067811865476, - ) - - -def test_steering_movement_update_path_list_within_distance( - steering_movement: SteeringMovement, -) -> None: - """Test if the path list is updated if the position is within the view distance. - - Args: - steering_movement: The steering movement component for use in testing. - """ - steering_movement.update_path_list([Vec2d(300, 300), Vec2d(100, 100)]) - assert steering_movement.path_list == [(100, 100)] - - -def test_steering_movement_update_path_list_outside_distance( - steering_movement: SteeringMovement, -) -> None: - """Test if the path list is updated if the position is outside the view distance. - - Args: - steering_movement: The steering movement component for use in testing. - """ - steering_movement.update_path_list([Vec2d(300, 300), Vec2d(500, 500)]) - assert steering_movement.path_list == [] - - -def test_steering_movement_update_path_list_equal_distance( - steering_movement: SteeringMovement, -) -> None: - """Test if the path list is updated if the position is equal to the view distance. - - Args: - steering_movement: The steering movement component for use in testing. - """ - steering_movement.update_path_list( - [Vec2d(300, 300), Vec2d(135.764501987, 135.764501987)], - ) - assert steering_movement.path_list == [(135.764501987, 135.764501987)] - - -def test_steering_movement_update_path_list_slice( - steering_movement: SteeringMovement, -) -> None: - """Test if the path list is updated with the array slice. - - Args: - steering_movement: The steering movement component for use in testing. - """ - steering_movement.update_path_list([Vec2d(100, 100), Vec2d(300, 300)]) - assert steering_movement.path_list == [(100, 100), (300, 300)] - - -def test_steering_movement_update_path_list_empty_list( - steering_movement: SteeringMovement, -) -> None: - """Test if the path list is updated if the footprints list is empty. - - Args: - steering_movement: The steering movement component for use in testing. - """ - steering_movement.update_path_list([]) - assert steering_movement.path_list == [] - - -def test_steering_movement_update_path_list_multiple_points( - steering_movement: SteeringMovement, -) -> None: - """Test if the path list is updated if multiple footprints are within view distance. - - Args: - steering_movement: The steering movement component for use in testing. - """ - steering_movement.update_path_list( - [Vec2d(100, 100), Vec2d(300, 300), Vec2d(50, 100), Vec2d(500, 500)], - ) - assert steering_movement.path_list == [(50, 100), (500, 500)] - - -def test_steering_movement_update_path_list_footprint_on_update( - steering_movement: SteeringMovement, -) -> None: - """Test if the path list is updated correctly if the Footprint component updates it. - - Args: - steering_movement: The steering movement component for use in testing. - """ - cast( - Footprint, - steering_movement.system.get_component_for_game_object( - 0, - ComponentType.FOOTPRINT, - ), - ).on_update(0.5) - assert steering_movement.path_list == [(0, 0)] diff --git a/tests/game_objects/test_system.py b/tests/game_objects/test_system.py deleted file mode 100644 index 5303ab75..00000000 --- a/tests/game_objects/test_system.py +++ /dev/null @@ -1,326 +0,0 @@ -"""Tests all functions in game_objects/system.py.""" -from __future__ import annotations - -# Builtin -from typing import TYPE_CHECKING, cast - -# Pip -import pytest - -# Custom -from hades.game_objects.base import ComponentType, GameObjectComponent -from hades.game_objects.system import ECS, ECSError - -if TYPE_CHECKING: - from hades.game_objects.base import ComponentData - -__all__ = () - - -class GameObjectComponentOne(GameObjectComponent): - """Represents a game object component useful for testing.""" - - # Class variables - component_type: ComponentType = ComponentType.HEALTH - - -class GameObjectComponentTwo(GameObjectComponent): - """Represents a game object component useful for testing.""" - - # Class variables - component_type: ComponentType = ComponentType.ARMOUR - - -class GameObjectComponentData(GameObjectComponent): - """Represents a game object component that has data useful for testing.""" - - __slots__ = ("test_data",) - - # Class variables - component_type: ComponentType = ComponentType.MONEY - - def __init__( - self: GameObjectComponentData, - game_object_id: int, - system: ECS, - component_data: ComponentData, - ) -> None: - """Initialise the object. - - Args: - game_object_id: The game object ID. - system: The entity component system which manages the game objects. - component_data: The data for the components. - """ - super().__init__(game_object_id, system, component_data) - self.test_data: int = component_data["test"] # type: ignore[typeddict-item] - - -class GameObjectComponentInvalid: - """Represents an invalid game object component useful for testing.""" - - -@pytest.fixture() -def ecs() -> ECS: - """Create an entity component system for use in testing. - - Returns: - The entity component system for use in testing. - """ - return ECS() - - -def test_raise_not_registered_error() -> None: - """Test that ECSError is raised correctly.""" - with pytest.raises( - expected_exception=ECSError, - match="The test `10` is not registered with the ECS.", - ): - raise ECSError(not_registered_type="test", value=10) - - -def test_raise_not_registered_error_custom_error() -> None: - """Test that ECSError is raised correctly with a custom error message.""" - with pytest.raises( - expected_exception=ECSError, - match="The test `temp` error.", - ): - raise ECSError( - not_registered_type="test", - value="temp", - error="error", - ) - - -def test_ecs_init(ecs: ECS) -> None: - """Test that the entity component system is initialised correctly. - - Args: - ecs: The entity component system for use in testing. - """ - assert repr(ecs) == "" - - -def test_ecs_game_object_with_zero_components(ecs: ECS) -> None: - """Test the ECS with a game object that has no components. - - Args: - ecs: The entity component system for use in testing. - """ - # Test that adding the game object works correctly - assert ecs.add_game_object({}) == 0 - assert ecs.get_components_for_game_object(0) == {} - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` does not have a physics object.", - ): - ecs.get_physics_object_for_game_object(0) - - # Test that removing the game object works correctly - ecs.remove_game_object(0) - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` is not registered with the ECS.", - ): - ecs.remove_game_object(0) - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` is not registered with the ECS.", - ): - ecs.get_components_for_game_object(0) - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` does not have a physics object.", - ): - ecs.get_physics_object_for_game_object(0) - - -def test_ecs_game_object_with_multiple_components(ecs: ECS) -> None: - """Test the ECS with a game object that has multiple components. - - Args: - ecs: The entity component system for use in testing. - """ - # Test that adding the game object works correctly - ecs.add_game_object({}, GameObjectComponentOne, GameObjectComponentTwo) - assert list(ecs.get_components_for_game_object(0).keys()) == [ - ComponentType.HEALTH, - ComponentType.ARMOUR, - ] - assert ecs.get_component_for_game_object(0, ComponentType.HEALTH) - assert ecs.get_component_for_game_object(0, ComponentType.ARMOUR) - assert len(ecs.get_components_for_component_type(ComponentType.HEALTH)) == 1 - assert len(ecs.get_components_for_component_type(ComponentType.ARMOUR)) == 1 - with pytest.raises(expected_exception=KeyError): - ecs.get_component_for_game_object(0, ComponentType.MONEY) - - # Test that removing the game object works correctly - ecs.remove_game_object(0) - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` is not registered with the ECS.", - ): - ecs.get_components_for_game_object(0) - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` is not registered with the ECS.", - ): - ecs.get_component_for_game_object(0, ComponentType.HEALTH) - assert ecs.get_components_for_component_type(ComponentType.HEALTH) == [] - - -def test_ecs_game_object_with_steering(ecs: ECS) -> None: - """Test the ECS with a game object that has steering. - - Args: - ecs: The entity component system for use in testing. - """ - # Test that adding the game object with steering works correctly - ecs.add_game_object({}, physics=True) - physics_object = ecs.get_physics_object_for_game_object(0) - assert physics_object.position == (0, 0) - assert physics_object.velocity == (0, 0) - - # Test that removing the game object works correctly - ecs.remove_game_object(0) - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` does not have a physics object.", - ): - ecs.get_physics_object_for_game_object(0) - - -def test_ecs_multiple_game_objects(ecs: ECS) -> None: - """Test the ECS with multiple game objects. - - Args: - ecs: The entity component system for use in testing. - """ - # Test that adding two game object works correctly - assert ecs.add_game_object({}, GameObjectComponentOne) == 0 - assert ecs.add_game_object({}, GameObjectComponentOne, GameObjectComponentTwo) == 1 - assert ecs.get_component_for_game_object(0, ComponentType.HEALTH) - assert ecs.get_component_for_game_object(1, ComponentType.HEALTH) - assert ecs.get_component_for_game_object(1, ComponentType.ARMOUR) - assert list(ecs.get_components_for_game_object(0).keys()) == [ComponentType.HEALTH] - assert list(ecs.get_components_for_game_object(1).keys()) == [ - ComponentType.HEALTH, - ComponentType.ARMOUR, - ] - - # Test that removing the first game object works correctly - ecs.remove_game_object(0) - assert ecs.get_component_for_game_object(1, ComponentType.HEALTH) - assert ecs.get_component_for_game_object(1, ComponentType.ARMOUR) - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` is not registered with the ECS.", - ): - ecs.get_components_for_game_object(0) - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` is not registered with the ECS.", - ): - ecs.get_component_for_game_object(0, ComponentType.HEALTH) - - -def test_ecs_component_data(ecs: ECS) -> None: - """Test the ECS with a component that has data. - - Args: - ecs: The entity component system for use in testing. - """ - assert ( - ecs.add_game_object( - {"test": 10}, # type: ignore[typeddict-unknown-key] - GameObjectComponentData, - ) - == 0 - ) - assert ( - cast( - GameObjectComponentData, - ecs.get_component_for_game_object(0, ComponentType.MONEY), - ).test_data - == 10 - ) - assert list(ecs.get_components_for_game_object(0).keys()) == [ComponentType.MONEY] - - -def test_ecs_nonexistent_component_data(ecs: ECS) -> None: - """Test the ECS with a component that has data which is not provided. - - Args: - ecs: The entity component system for use in testing. - """ - with pytest.raises(expected_exception=KeyError): - assert ecs.add_game_object({}, GameObjectComponentData) == 0 - - -def test_ecs_duplicate_components(ecs: ECS) -> None: - """Test the ECS with duplicate components for the same game object. - - Args: - ecs: The entity component system for use in testing. - """ - # Test that adding a game object with two of the same components raises an error - with pytest.raises( - expected_exception=ECSError, - match=( - "The component type `ComponentType.HEALTH` is already registered with the" - " ECS." - ), - ): - ecs.add_game_object({}, GameObjectComponentOne, GameObjectComponentOne) - - # Test that the game object does not exist - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` is not registered with the ECS.", - ): - ecs.get_component_for_game_object(0, ComponentType.HEALTH) - - -def test_ecs_duplicate_components_steering(ecs: ECS) -> None: - """Test the ECS with duplicate components for the same game object with steering. - - Args: - ecs: The entity component system for use in testing. - """ - # Test that adding a game object with steering that has two of the same components - # raises an error - with pytest.raises( - expected_exception=ECSError, - match=( - "The component type `ComponentType.HEALTH` is already registered with the" - " ECS." - ), - ): - ecs.add_game_object( - {}, - GameObjectComponentOne, - GameObjectComponentOne, - physics=True, - ) - - # Test that the game object does not exist - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` is not registered with the ECS.", - ): - ecs.get_component_for_game_object(0, ComponentType.HEALTH) - with pytest.raises( - expected_exception=ECSError, - match="The game object ID `0` does not have a physics object.", - ): - ecs.get_physics_object_for_game_object(0) - - -def test_ecs_invalid_component(ecs: ECS) -> None: - """Test the ECS with a game object that has an invalid component. - - Args: - ecs: The entity component system for use in testing. - """ - with pytest.raises(expected_exception=AttributeError): - ecs.add_game_object({}, GameObjectComponentInvalid) # type: ignore[arg-type] diff --git a/tests/test_textures.py b/tests/test_textures.py index a65f51b5..474a87c2 100644 --- a/tests/test_textures.py +++ b/tests/test_textures.py @@ -1,4 +1,5 @@ -"""Tests all functions in textures.py.""" +"""Tests all classes and functions in textures.py.""" + from __future__ import annotations # Pip