First cut of pulling out the relevant parts of my original game to make a little framework.
This commit is contained in:
commit
6a0c9e8d46
177 changed files with 18197 additions and 0 deletions
208
tests/ai.cpp
Normal file
208
tests/ai.cpp
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "dbc.hpp"
|
||||
#include "ai/ai.hpp"
|
||||
#include "ai/ai_debug.hpp"
|
||||
#include <iostream>
|
||||
|
||||
using namespace dbc;
|
||||
using namespace nlohmann;
|
||||
|
||||
TEST_CASE("state and actions work", "[ai]") {
|
||||
enum StateNames {
|
||||
ENEMY_IN_RANGE,
|
||||
ENEMY_DEAD
|
||||
};
|
||||
|
||||
ai::State goal;
|
||||
ai::State start;
|
||||
std::vector<ai::Action> actions;
|
||||
|
||||
// start off enemy not dead and not in range
|
||||
start[ENEMY_DEAD] = false;
|
||||
start[ENEMY_IN_RANGE] = false;
|
||||
|
||||
// end goal is enemy is dead
|
||||
goal[ENEMY_DEAD] = true;
|
||||
|
||||
ai::Action move_closer("move_closer", 10);
|
||||
move_closer.needs(ENEMY_IN_RANGE, false);
|
||||
move_closer.effect(ENEMY_IN_RANGE, true);
|
||||
|
||||
REQUIRE(move_closer.can_effect(start));
|
||||
auto after_move_state = move_closer.apply_effect(start);
|
||||
REQUIRE(start[ENEMY_IN_RANGE] == false);
|
||||
REQUIRE(after_move_state[ENEMY_IN_RANGE] == true);
|
||||
REQUIRE(after_move_state[ENEMY_DEAD] == false);
|
||||
// start is clean but after move is dirty
|
||||
REQUIRE(move_closer.can_effect(start));
|
||||
REQUIRE(!move_closer.can_effect(after_move_state));
|
||||
REQUIRE(ai::distance_to_goal(start, after_move_state) == 1);
|
||||
|
||||
ai::Action kill_it("kill_it", 10);
|
||||
kill_it.needs(ENEMY_IN_RANGE, true);
|
||||
kill_it.needs(ENEMY_DEAD, false);
|
||||
kill_it.effect(ENEMY_DEAD, true);
|
||||
|
||||
REQUIRE(!kill_it.can_effect(start));
|
||||
REQUIRE(kill_it.can_effect(after_move_state));
|
||||
|
||||
auto after_kill_state = kill_it.apply_effect(after_move_state);
|
||||
REQUIRE(!kill_it.can_effect(after_kill_state));
|
||||
REQUIRE(ai::distance_to_goal(after_move_state, after_kill_state) == 1);
|
||||
|
||||
kill_it.ignore(ENEMY_IN_RANGE);
|
||||
REQUIRE(kill_it.can_effect(after_move_state));
|
||||
|
||||
actions.push_back(kill_it);
|
||||
actions.push_back(move_closer);
|
||||
|
||||
REQUIRE(start != goal);
|
||||
}
|
||||
|
||||
TEST_CASE("basic feature tests", "[ai]") {
|
||||
enum StateNames {
|
||||
ENEMY_IN_RANGE,
|
||||
ENEMY_DEAD
|
||||
};
|
||||
|
||||
ai::State goal;
|
||||
ai::State start;
|
||||
std::vector<ai::Action> actions;
|
||||
|
||||
// start off enemy not dead and not in range
|
||||
start[ENEMY_DEAD] = false;
|
||||
start[ENEMY_IN_RANGE] = false;
|
||||
|
||||
// end goal is enemy is dead
|
||||
goal[ENEMY_DEAD] = true;
|
||||
|
||||
ai::Action move_closer("move_closer", 10);
|
||||
move_closer.needs(ENEMY_IN_RANGE, false);
|
||||
move_closer.effect(ENEMY_IN_RANGE, true);
|
||||
|
||||
ai::Action kill_it("kill_it", 10);
|
||||
kill_it.needs(ENEMY_IN_RANGE, true);
|
||||
// this is duplicated on purpose to confirm that setting
|
||||
// a positive then a negative properly cancels out
|
||||
kill_it.needs(ENEMY_DEAD, true);
|
||||
kill_it.needs(ENEMY_DEAD, false);
|
||||
|
||||
// same thing with effects
|
||||
kill_it.effect(ENEMY_DEAD, false);
|
||||
kill_it.effect(ENEMY_DEAD, true);
|
||||
|
||||
// order seems to matter which is wrong
|
||||
actions.push_back(kill_it);
|
||||
actions.push_back(move_closer);
|
||||
|
||||
auto result = ai::plan_actions(actions, start, goal);
|
||||
REQUIRE(result.complete);
|
||||
|
||||
auto state = start;
|
||||
|
||||
for(auto& action : result.script) {
|
||||
state = action.apply_effect(state);
|
||||
}
|
||||
|
||||
REQUIRE(state[ENEMY_DEAD]);
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("ai as a module like sound/sprites", "[ai]") {
|
||||
ai::reset();
|
||||
ai::init("tests/ai_fixture.json");
|
||||
|
||||
auto start = ai::load_state("test_start");
|
||||
auto goal = ai::load_state("test_goal");
|
||||
|
||||
auto a_plan = ai::plan("test1", start, goal);
|
||||
REQUIRE(a_plan.complete);
|
||||
|
||||
auto state = start;
|
||||
for(auto& action : a_plan.script) {
|
||||
fmt::println("ACTION: {}", action.name);
|
||||
state = action.apply_effect(state);
|
||||
}
|
||||
|
||||
REQUIRE(ai::test(state, "target_dead"));
|
||||
}
|
||||
|
||||
TEST_CASE("ai autowalker ai test", "[ai]") {
|
||||
ai::reset();
|
||||
ai::init("ai");
|
||||
auto start = ai::load_state("Host::initial_state");
|
||||
auto goal = ai::load_state("Host::final_state");
|
||||
int enemy_count = 5;
|
||||
|
||||
ai::set(start, "no_more_enemies", enemy_count == 0);
|
||||
|
||||
// find an enemy and kill them
|
||||
auto a_plan = ai::plan("Host::actions", start, goal);
|
||||
REQUIRE(!a_plan.complete);
|
||||
|
||||
auto result = ai::dump_script("\n\nWALKER KILL STUFF", start, a_plan.script);
|
||||
REQUIRE(ai::test(result, "enemy_found"));
|
||||
REQUIRE(!ai::test(result, "no_more_enemies"));
|
||||
|
||||
// health is low, go heal
|
||||
ai::set(result, "health_good", false);
|
||||
ai::set(result, "in_combat", false);
|
||||
ai::set(result, "enemy_found", false);
|
||||
ai::set(result, "have_healing", true);
|
||||
ai::set(result, "have_item", true);
|
||||
REQUIRE(!ai::test(result, "health_good"));
|
||||
|
||||
auto health_plan = ai::plan("Host::actions", result, goal);
|
||||
result = ai::dump_script("\n\nWALKER NEED HEALTH", result, health_plan.script);
|
||||
REQUIRE(!health_plan.complete);
|
||||
REQUIRE(ai::test(result, "health_good"));
|
||||
|
||||
// health is good, enemies dead, go get stuff
|
||||
ai::set(result, "no_more_enemies", true);
|
||||
REQUIRE(ai::test(result, "no_more_enemies"));
|
||||
|
||||
auto new_plan = ai::plan("Host::actions", result, goal);
|
||||
result = ai::dump_script("\n\nWALKER COLLECT ITEMS", result, new_plan.script);
|
||||
REQUIRE(ai::test(result, "no_more_items"));
|
||||
REQUIRE(ai::test(result, "no_more_enemies"));
|
||||
}
|
||||
|
||||
TEST_CASE("Confirm EntityAI behaves as expected", "[ai]") {
|
||||
ai::reset();
|
||||
ai::init("ai");
|
||||
auto ai_start = ai::load_state("Enemy::initial_state");
|
||||
auto ai_goal = ai::load_state("Enemy::final_state");
|
||||
|
||||
ai::EntityAI enemy("Enemy::actions", ai_start, ai_goal);
|
||||
|
||||
enemy.set_state("detect_enemy", true);
|
||||
enemy.update();
|
||||
REQUIRE(enemy.wants_to("find_enemy"));
|
||||
|
||||
enemy.set_state("enemy_found", true);
|
||||
enemy.set_state("in_combat", true);
|
||||
enemy.update();
|
||||
REQUIRE(enemy.wants_to("kill_enemy"));
|
||||
|
||||
enemy.set_state("have_item", true);
|
||||
enemy.set_state("have_healing", true);
|
||||
enemy.set_state("in_combat", false);
|
||||
enemy.set_state("health_good", false);
|
||||
enemy.update();
|
||||
REQUIRE(enemy.wants_to("use_healing"));
|
||||
|
||||
enemy.set_state("have_healing", false);
|
||||
enemy.set_state("tough_personality", true);
|
||||
enemy.set_state("in_combat", true);
|
||||
enemy.set_state("health_good", true);
|
||||
enemy.update();
|
||||
REQUIRE(enemy.wants_to("kill_enemy"));
|
||||
|
||||
fmt::println("\n\n\n\n=============================\n\n\n\n");
|
||||
enemy.set_state("have_healing", false);
|
||||
enemy.set_state("tough_personality", false);
|
||||
enemy.set_state("in_combat", true);
|
||||
enemy.set_state("health_good", false);
|
||||
enemy.update();
|
||||
REQUIRE(enemy.wants_to("run_away"));
|
||||
}
|
||||
85
tests/ai_fixture.json
Normal file
85
tests/ai_fixture.json
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
{
|
||||
"profile": {
|
||||
"target_acquired": 0,
|
||||
"target_lost": 1,
|
||||
"target_in_warhead_range": 2,
|
||||
"target_dead": 3
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"name": "searchSpiral",
|
||||
"cost": 10,
|
||||
"needs": {
|
||||
"target_acquired": false,
|
||||
"target_lost": true
|
||||
},
|
||||
"effects": {
|
||||
"target_acquired": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "searchSerpentine",
|
||||
"cost": 5,
|
||||
"needs": {
|
||||
"target_acquired": false,
|
||||
"target_lost": false
|
||||
},
|
||||
"effects": {
|
||||
"target_acquired": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "searchSpiral",
|
||||
"cost": 5,
|
||||
"needs": {
|
||||
"target_acquired": false,
|
||||
"target_lost": true
|
||||
},
|
||||
"effects": {
|
||||
"target_acquired": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "interceptTarget",
|
||||
"cost": 5,
|
||||
"needs": {
|
||||
"target_acquired": true,
|
||||
"target_dead": false
|
||||
},
|
||||
"effects": {
|
||||
"target_in_warhead_range": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detonateNearTarget",
|
||||
"cost": 5,
|
||||
"needs": {
|
||||
"target_in_warhead_range": true,
|
||||
"target_acquired": true,
|
||||
"target_dead": false
|
||||
},
|
||||
"effects": {
|
||||
"target_dead": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"states": {
|
||||
"test_start": {
|
||||
"target_acquired": false,
|
||||
"target_lost": true,
|
||||
"target_in_warhead_range": false,
|
||||
"target_dead": false
|
||||
},
|
||||
"test_goal": {
|
||||
"target_dead": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test1": [
|
||||
"searchSpiral",
|
||||
"searchSerpentine",
|
||||
"searchSpiral",
|
||||
"interceptTarget",
|
||||
"detonateNearTarget"]
|
||||
}
|
||||
}
|
||||
169
tests/animation.cpp
Normal file
169
tests/animation.cpp
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "graphics/textures.hpp"
|
||||
#include "algos/dinkyecs.hpp"
|
||||
#include "game/config.hpp"
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include "algos/rand.hpp"
|
||||
#include "graphics/animation.hpp"
|
||||
#include "game/sound.hpp"
|
||||
#include "game/components.hpp"
|
||||
|
||||
using namespace components;
|
||||
using namespace textures;
|
||||
using namespace std::chrono_literals;
|
||||
using namespace animation;
|
||||
|
||||
Animation load_animation(const string& name) {
|
||||
auto anim = animation::load("assets/animation.json", "rat_king_boss");
|
||||
anim.set_form("attack");
|
||||
|
||||
anim.transform.looped = false;
|
||||
|
||||
for(size_t i = 0; i < anim.sequence.durations.size(); i++) {
|
||||
anim.sequence.durations[i] = Random::uniform(1, 5);
|
||||
}
|
||||
|
||||
return anim;
|
||||
}
|
||||
|
||||
void FAKE_RENDER() {
|
||||
std::this_thread::sleep_for(Random::milliseconds(5, 32));
|
||||
}
|
||||
|
||||
void PLAY_TEST(Animation &anim) {
|
||||
REQUIRE(anim.transform.looped == false);
|
||||
anim.play();
|
||||
|
||||
while(anim.playing) {
|
||||
anim.update();
|
||||
FAKE_RENDER();
|
||||
}
|
||||
|
||||
REQUIRE(anim.playing == false);
|
||||
}
|
||||
|
||||
// /*
|
||||
// * Animation is a Sheet + Sequence + Transform.
|
||||
// *
|
||||
// * A Sheet is just a grid of images with a predefined size for each cell. Arbitrary sized cells not supported.
|
||||
// *
|
||||
// * A Sequence is a list of Sheet cells _in any order_. See https://github.com/yottahmd/ganim8-lib. Sequences have a timing element for the cells, possibly a list of durations or a single duration.
|
||||
// *
|
||||
// * A Transform is combinations of scale and/or position easing/motion, shader effects, and things like if it's looped or toggled.
|
||||
// *
|
||||
// * I like the ganim8 onLoop concept, just a callback that says what to do when the animation has looped.
|
||||
// */
|
||||
// TEST_CASE("new animation system", "[animation-new]") {
|
||||
// textures::init();
|
||||
// sound::init();
|
||||
// sound::mute(true);
|
||||
//
|
||||
// auto anim = load_animation("rat_king_boss");
|
||||
// PLAY_TEST(anim);
|
||||
//
|
||||
// // test that toggled works
|
||||
// anim.transform.toggled = true;
|
||||
// PLAY_TEST(anim);
|
||||
// REQUIRE(anim.sequence.current == anim.sequence.frames.size() - 1);
|
||||
// anim.transform.toggled = false;
|
||||
//
|
||||
// bool onLoop_ran = false;
|
||||
// anim.onLoop = [&](auto& seq, auto& tr) -> bool {
|
||||
// seq.current = 0;
|
||||
// onLoop_ran = true;
|
||||
// return tr.looped;
|
||||
// };
|
||||
//
|
||||
// PLAY_TEST(anim);
|
||||
// REQUIRE(onLoop_ran == true);
|
||||
//
|
||||
// // only runs twice
|
||||
// anim.onLoop = [](auto& seq, auto& tr) -> bool {
|
||||
// if(seq.loop_count == 2) {
|
||||
// seq.current = 0;
|
||||
// return false;
|
||||
// } else {
|
||||
// seq.current = seq.current % seq.frame_count;
|
||||
// return true;
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// PLAY_TEST(anim);
|
||||
// REQUIRE(anim.sequence.loop_count == 2);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// TEST_CASE("confirm frame sequencing works", "[animation-new]") {
|
||||
// textures::init();
|
||||
// sound::init();
|
||||
// sound::mute(true);
|
||||
//
|
||||
// auto anim = load_animation("rat_king_boss");
|
||||
//
|
||||
// auto boss = textures::get_sprite("rat_king_boss");
|
||||
// sf::IntRect init_rect{{0,0}, {anim.sheet.frame_width, anim.sheet.frame_height}};
|
||||
//
|
||||
// anim.play();
|
||||
// bool loop_ran = false;
|
||||
//
|
||||
// // this will check that it moved to the next frame
|
||||
// anim.onLoop = [&](auto& seq, auto& tr) -> bool {
|
||||
// seq.current = 0;
|
||||
// loop_ran = true;
|
||||
// return false;
|
||||
// };
|
||||
//
|
||||
// anim.onFrame = [&](){
|
||||
// anim.apply(*boss.sprite);
|
||||
// };
|
||||
//
|
||||
// while(anim.playing) {
|
||||
// anim.update();
|
||||
// FAKE_RENDER();
|
||||
// }
|
||||
//
|
||||
// REQUIRE(loop_ran == true);
|
||||
// REQUIRE(anim.playing == false);
|
||||
// }
|
||||
//
|
||||
// TEST_CASE("confirm transition changes work", "[animation-new]") {
|
||||
// textures::init();
|
||||
// sound::init();
|
||||
// sound::mute(true);
|
||||
//
|
||||
// auto sprite = *textures::get_sprite("rat_king_boss").sprite;
|
||||
// sf::Vector2f pos{100,100};
|
||||
// sprite.setPosition(pos);
|
||||
// auto scale = sprite.getScale();
|
||||
// auto anim = load_animation("rat_king_boss");
|
||||
//
|
||||
// // also testing that onFrame being null means it's not run
|
||||
// REQUIRE(anim.onFrame == nullptr);
|
||||
//
|
||||
// anim.play();
|
||||
// REQUIRE(anim.playing == true);
|
||||
//
|
||||
// while(anim.playing) {
|
||||
// anim.update();
|
||||
// anim.motion(sprite, pos, scale);
|
||||
// FAKE_RENDER();
|
||||
// }
|
||||
//
|
||||
// REQUIRE(anim.playing == false);
|
||||
// REQUIRE(pos == sf::Vector2f{100, 100});
|
||||
// REQUIRE(scale != sf::Vector2f{0,0});
|
||||
// }
|
||||
|
||||
TEST_CASE("playing with delta time", "[animation-new]") {
|
||||
animation::Timer timer;
|
||||
timer.start();
|
||||
|
||||
for(int i = 0; i < 20; i++) {
|
||||
FAKE_RENDER();
|
||||
auto [tick_count, alpha] = timer.commit();
|
||||
// fmt::println("tick: {}, alpha: {}", tick_count, alpha);
|
||||
}
|
||||
}
|
||||
9
tests/base.cpp
Normal file
9
tests/base.cpp
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
|
||||
using namespace fmt;
|
||||
|
||||
TEST_CASE("base test", "[base]") {
|
||||
REQUIRE(1 == 1);
|
||||
}
|
||||
92
tests/battle.cpp
Normal file
92
tests/battle.cpp
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include "combat/battle.hpp"
|
||||
#include "algos/simplefsm.hpp"
|
||||
#include "algos/dinkyecs.hpp"
|
||||
#include "gui/backend.hpp"
|
||||
#include "game/level.hpp"
|
||||
#include "game/components.hpp"
|
||||
#include "ai/ai.hpp"
|
||||
#include "graphics/palette.hpp"
|
||||
|
||||
using namespace combat;
|
||||
using namespace components;
|
||||
|
||||
TEST_CASE("battle operations fantasy", "[combat-battle]") {
|
||||
ai::reset();
|
||||
ai::init("ai");
|
||||
|
||||
auto ai_start = ai::load_state("Enemy::initial_state");
|
||||
auto ai_goal = ai::load_state("Enemy::final_state");
|
||||
auto host_start = ai::load_state("Host::initial_state");
|
||||
auto host_goal = ai::load_state("Host::final_state");
|
||||
BattleEngine battle;
|
||||
|
||||
DinkyECS::Entity host = 0;
|
||||
ai::EntityAI host_ai("Host::actions", host_start, host_goal);
|
||||
components::Combat host_combat{
|
||||
.hp=100, .max_hp=100, .ap_delta=6, .max_ap=12, .damage=20};
|
||||
battle.add_enemy({host, &host_ai, &host_combat, true});
|
||||
|
||||
DinkyECS::Entity axe_ranger = 1;
|
||||
ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal);
|
||||
components::Combat axe_combat{
|
||||
.hp=20, .max_hp=20, .ap_delta=8, .max_ap=12, .damage=20};
|
||||
battle.add_enemy({axe_ranger, &axe_ai, &axe_combat});
|
||||
|
||||
DinkyECS::Entity rat = 2;
|
||||
ai::EntityAI rat_ai("Enemy::actions", ai_start, ai_goal);
|
||||
components::Combat rat_combat{
|
||||
.hp=10, .max_hp=10, .ap_delta=2, .max_ap=10, .damage=10};
|
||||
battle.add_enemy({rat, &rat_ai, &rat_combat});
|
||||
|
||||
battle.set_all("enemy_found", true);
|
||||
battle.set_all("in_combat", true);
|
||||
battle.set_all("tough_personality", true);
|
||||
battle.set_all("health_good", true);
|
||||
|
||||
battle.set(rat, "tough_personality", false);
|
||||
battle.set(host, "have_healing", false);
|
||||
battle.set(host, "tough_personality", false);
|
||||
|
||||
while(host_combat.hp > 0) {
|
||||
battle.set(host, "health_good", host_combat.hp > 20);
|
||||
|
||||
battle.player_request("use_healing");
|
||||
battle.player_request("kill_enemy");
|
||||
|
||||
battle.ap_refresh();
|
||||
battle.plan();
|
||||
|
||||
while(auto act = battle.next()) {
|
||||
auto& [enemy, wants_to, cost, enemy_state] = *act;
|
||||
|
||||
// fmt::println(">>>>> entity: {} wants to {} cost={}; has {} HP; {} ap",
|
||||
// enemy.entity, wants_to,
|
||||
// cost, enemy.combat->hp,
|
||||
// enemy.combat->ap);
|
||||
|
||||
switch(enemy_state) {
|
||||
case BattleHostState::agree:
|
||||
// fmt::println("HOST and PLAYER requests match {}, doing it.", wants_to);
|
||||
break;
|
||||
case BattleHostState::disagree:
|
||||
// fmt::println("REBELIOUS ACT: {}", wants_to);
|
||||
battle.clear_requests();
|
||||
REQUIRE(battle.$player_requests.size() == 0);
|
||||
break;
|
||||
case BattleHostState::not_host:
|
||||
if(wants_to == "kill_enemy") {
|
||||
enemy.combat->attack(host_combat);
|
||||
}
|
||||
break;
|
||||
case BattleHostState::out_of_ap:
|
||||
// fmt::println("ENEMY OUT OF AP");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE(!battle.next());
|
||||
}
|
||||
}
|
||||
6
tests/camera.cpp
Normal file
6
tests/camera.cpp
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
|
||||
TEST_CASE("view based camera system", "[camera]") {
|
||||
REQUIRE(1 == 1);
|
||||
}
|
||||
50
tests/components.cpp
Normal file
50
tests/components.cpp
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "game/components.hpp"
|
||||
#include "algos/dinkyecs.hpp"
|
||||
#include "game/config.hpp"
|
||||
#include <iostream>
|
||||
|
||||
using namespace components;
|
||||
using namespace DinkyECS;
|
||||
|
||||
TEST_CASE("confirm component loading works", "[components]") {
|
||||
std::vector<std::string> test_list{
|
||||
"assets/enemies.json", "assets/items.json", "assets/devices.json"};
|
||||
|
||||
components::init();
|
||||
DinkyECS::World world;
|
||||
|
||||
for(auto test_data : test_list) {
|
||||
auto config = settings::get(test_data);
|
||||
auto data_list = config.json();
|
||||
|
||||
for(auto& [key, data] : data_list.items()) {
|
||||
auto& components = data["components"];
|
||||
fmt::println("TEST COMPONENT: {} from file {}", key, test_data);
|
||||
auto ent = world.entity();
|
||||
components::configure_entity(world, ent, components);
|
||||
auto tile = components::get<Tile>(components[0]);
|
||||
REQUIRE(tile.display != L' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TEST_CASE("make sure json_mods works", "[components]") {
|
||||
// auto config = settings::get("bosses");
|
||||
// // this confirms that loading something with an optional
|
||||
// // field works with the json conversions in json_mods.hpp
|
||||
// for(auto& comp_data : config["RAT_KING"]["components"]) {
|
||||
// if(comp_data["_type"] == "AnimatedScene") {
|
||||
// auto comp = components::convert<components::AnimatedScene>(comp_data);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // this then confirms everything else about the json conversion
|
||||
// components::init();
|
||||
//
|
||||
// DinkyECS::World world;
|
||||
// auto rat_king = world.entity();
|
||||
//
|
||||
// components::configure_entity(world, rat_king, config["RAT_KING"]["components"]);
|
||||
// auto boss = world.get<AnimatedScene>(rat_king);
|
||||
// }
|
||||
28
tests/config.cpp
Normal file
28
tests/config.cpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "game/config.hpp"
|
||||
#include <iostream>
|
||||
|
||||
TEST_CASE("confirm basic config loader ops", "[config]") {
|
||||
settings::Config::set_base_dir("./");
|
||||
auto config = settings::get("devices");
|
||||
auto data_list = config.json();
|
||||
auto the_keys = config.keys();
|
||||
|
||||
REQUIRE(the_keys.size() > 0);
|
||||
|
||||
for(auto& [key, data] : data_list.items()) {
|
||||
auto wide1 = config.wstring(key, "name");
|
||||
auto& comps = data["components"];
|
||||
|
||||
for(auto& comp_data : comps) {
|
||||
REQUIRE(comp_data.contains("_type"));
|
||||
}
|
||||
}
|
||||
|
||||
auto indexed = settings::get("tests/config_test.json");
|
||||
auto& test_0 = indexed[0];
|
||||
REQUIRE(test_0["test"] == 0);
|
||||
|
||||
auto& test_1 = indexed[1];
|
||||
REQUIRE(test_1["test"] == 1);
|
||||
}
|
||||
4
tests/config_test.json
Normal file
4
tests/config_test.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[
|
||||
{"test": 0},
|
||||
{"test": 1}
|
||||
]
|
||||
137
tests/cyclic_rituals.json
Normal file
137
tests/cyclic_rituals.json
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"profile": {
|
||||
"has_spikes": 0,
|
||||
"has_magick": 1,
|
||||
"shiny_bauble": 2,
|
||||
"cursed_item": 3,
|
||||
"$does_physical": 4,
|
||||
"$does_magick": 5,
|
||||
"$does_damage": 6,
|
||||
"$user_cursed": 7,
|
||||
"$does_healing": 8,
|
||||
"$damage_boost": 9,
|
||||
"$large_boost": 10,
|
||||
"$is_complete": 11
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"name": "pierce_type",
|
||||
"cost": 100,
|
||||
"needs": {
|
||||
"has_spikes": true,
|
||||
"$is_complete": false
|
||||
},
|
||||
"effects": {
|
||||
"$does_physical": true,
|
||||
"$does_damage": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "magick_type",
|
||||
"cost": 100,
|
||||
"needs": {
|
||||
"$is_complete": false,
|
||||
"has_magick": true
|
||||
},
|
||||
"effects": {
|
||||
"$does_magick": true,
|
||||
"$does_damage": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "combined",
|
||||
"cost": 0,
|
||||
"needs": {
|
||||
"$does_damage": true
|
||||
},
|
||||
"effects": {
|
||||
"$is_complete": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "boost_magick",
|
||||
"cost": 0,
|
||||
"needs": {
|
||||
"shiny_bauble": true,
|
||||
"$does_magick": true,
|
||||
"$does_damage": true,
|
||||
"$is_complete": false,
|
||||
"$user_cursed": false
|
||||
},
|
||||
"effects": {
|
||||
"$damage_boost": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "boost_damage_large",
|
||||
"cost": 0,
|
||||
"needs": {
|
||||
"cursed_item": true,
|
||||
"$is_complete": false,
|
||||
"$does_damage": true
|
||||
},
|
||||
"effects": {
|
||||
"$large_boost": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "curses_user",
|
||||
"cost": 0,
|
||||
"needs": {
|
||||
"cursed_item": true
|
||||
},
|
||||
"effects": {
|
||||
"$user_cursed": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "heals_user",
|
||||
"cost": 0,
|
||||
"needs": {
|
||||
"cursed_item": true,
|
||||
"$does_damage": false
|
||||
},
|
||||
"effects": {
|
||||
"$does_healing": true,
|
||||
"$is_complete": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"states": {
|
||||
"initial": {
|
||||
"shiny_bauble": false,
|
||||
"cursed_item": false,
|
||||
"has_spikes": false,
|
||||
"has_magick": false,
|
||||
"$user_cursed": false,
|
||||
"$does_damage": false,
|
||||
"$is_complete": false,
|
||||
"$does_healing": false,
|
||||
"$does_magick": false,
|
||||
"$does_physical": false,
|
||||
"$large_boost": false,
|
||||
"$damage_boost": false
|
||||
},
|
||||
"final": {
|
||||
"$user_cursed": true,
|
||||
"$does_damage": true,
|
||||
"$is_complete": true,
|
||||
"$does_healing": true,
|
||||
"$does_magick": true,
|
||||
"$does_physical": true,
|
||||
"$large_boost": true,
|
||||
"$damage_boost": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"actions": [
|
||||
"boost_magick",
|
||||
"pierce_type",
|
||||
"magick_type",
|
||||
"heals_user",
|
||||
"curses_user",
|
||||
"boost_damage_large",
|
||||
"combined"
|
||||
]
|
||||
}
|
||||
}
|
||||
18
tests/dbc.cpp
Normal file
18
tests/dbc.cpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "dbc.hpp"
|
||||
|
||||
using namespace dbc;
|
||||
|
||||
TEST_CASE("basic feature tests", "[dbc]") {
|
||||
log("Logging a message.");
|
||||
|
||||
pre("confirm positive cases work", 1 == 1);
|
||||
|
||||
pre("confirm positive lambda", [&]{ return 1 == 1;});
|
||||
|
||||
post("confirm positive post", 1 == 1);
|
||||
|
||||
post("confirm postitive post with lamdba", [&]{ return 1 == 1;});
|
||||
|
||||
check(1 == 1, "one equals 1");
|
||||
}
|
||||
212
tests/dinkyecs.cpp
Normal file
212
tests/dinkyecs.cpp
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "algos/dinkyecs.hpp"
|
||||
#include <iostream>
|
||||
#include <fmt/core.h>
|
||||
|
||||
using namespace fmt;
|
||||
using DinkyECS::Entity;
|
||||
using std::string;
|
||||
|
||||
struct Point {
|
||||
size_t x;
|
||||
size_t y;
|
||||
};
|
||||
|
||||
struct Player {
|
||||
string name;
|
||||
Entity eid;
|
||||
};
|
||||
|
||||
struct Position {
|
||||
Point location;
|
||||
};
|
||||
|
||||
struct Motion {
|
||||
int dx;
|
||||
int dy;
|
||||
bool random=false;
|
||||
};
|
||||
|
||||
struct Velocity {
|
||||
double x, y;
|
||||
};
|
||||
|
||||
struct Gravity {
|
||||
double level;
|
||||
};
|
||||
|
||||
struct DaGUI {
|
||||
int event;
|
||||
};
|
||||
|
||||
/*
|
||||
* Using a function catches instances where I'm not copying
|
||||
* the data into the world.
|
||||
*/
|
||||
void configure(DinkyECS::World &world, Entity &test) {
|
||||
println("---Configuring the base system.");
|
||||
Entity test2 = world.entity();
|
||||
|
||||
world.set<Position>(test, {10,20});
|
||||
world.set<Velocity>(test, {1,2});
|
||||
|
||||
world.set<Position>(test2, {1,1});
|
||||
world.set<Velocity>(test2, {9,19});
|
||||
|
||||
println("---- Setting up the player as a fact in the system.");
|
||||
|
||||
auto player_eid = world.entity();
|
||||
Player player_info{"Zed", player_eid};
|
||||
// just set some player info as a fact with the entity id
|
||||
world.set_the<Player>(player_info);
|
||||
|
||||
world.set<Velocity>(player_eid, {0,0});
|
||||
world.set<Position>(player_eid, {0,0});
|
||||
|
||||
auto enemy = world.entity();
|
||||
world.set<Velocity>(enemy, {0,0});
|
||||
world.set<Position>(enemy, {0,0});
|
||||
|
||||
println("--- Creating facts (singletons)");
|
||||
world.set_the<Gravity>({0.9});
|
||||
}
|
||||
|
||||
TEST_CASE("confirm ECS system works", "[ecs]") {
|
||||
DinkyECS::World world;
|
||||
Entity test = world.entity();
|
||||
|
||||
configure(world, test);
|
||||
|
||||
Position &pos = world.get<Position>(test);
|
||||
REQUIRE(pos.location.x == 10);
|
||||
REQUIRE(pos.location.y == 20);
|
||||
|
||||
Velocity &vel = world.get<Velocity>(test);
|
||||
REQUIRE(vel.x == 1);
|
||||
REQUIRE(vel.y == 2);
|
||||
|
||||
world.query<Position>([](const auto &ent, auto &pos) {
|
||||
REQUIRE(ent > 0);
|
||||
REQUIRE(pos.location.x >= 0);
|
||||
REQUIRE(pos.location.y >= 0);
|
||||
});
|
||||
|
||||
world.query<Velocity>([](const auto &ent, auto &vel) {
|
||||
REQUIRE(ent > 0);
|
||||
REQUIRE(vel.x >= 0);
|
||||
REQUIRE(vel.y >= 0);
|
||||
});
|
||||
|
||||
println("--- Manually get the velocity in position system:");
|
||||
world.query<Position>([&](const auto &ent, auto &pos) {
|
||||
Velocity &vel = world.get<Velocity>(ent);
|
||||
|
||||
REQUIRE(ent > 0);
|
||||
REQUIRE(pos.location.x >= 0);
|
||||
REQUIRE(pos.location.y >= 0);
|
||||
REQUIRE(ent > 0);
|
||||
REQUIRE(vel.x >= 0);
|
||||
REQUIRE(vel.y >= 0);
|
||||
});
|
||||
|
||||
println("--- Query only entities with Position and Velocity:");
|
||||
world.query<Position, Velocity>([&](const auto &ent, auto &pos, auto &vel) {
|
||||
Gravity &grav = world.get_the<Gravity>();
|
||||
REQUIRE(grav.level <= 1.0f);
|
||||
REQUIRE(grav.level > 0.5f);
|
||||
REQUIRE(ent > 0);
|
||||
REQUIRE(pos.location.x >= 0);
|
||||
REQUIRE(pos.location.y >= 0);
|
||||
REQUIRE(ent > 0);
|
||||
REQUIRE(vel.x >= 0);
|
||||
REQUIRE(vel.y >= 0);
|
||||
});
|
||||
|
||||
// now remove Velocity
|
||||
REQUIRE(world.has<Velocity>(test));
|
||||
world.remove<Velocity>(test);
|
||||
REQUIRE_THROWS(world.get<Velocity>(test));
|
||||
REQUIRE(!world.has<Velocity>(test));
|
||||
|
||||
println("--- After remove test, should only result in test2:");
|
||||
world.query<Position, Velocity>([&](const auto &ent, auto &pos, auto &vel) {
|
||||
auto &in_position = world.get<Position>(ent);
|
||||
auto &in_velocity = world.get<Velocity>(ent);
|
||||
REQUIRE(pos.location.x >= 0);
|
||||
REQUIRE(pos.location.y >= 0);
|
||||
REQUIRE(in_position.location.x == pos.location.x);
|
||||
REQUIRE(in_position.location.y == pos.location.y);
|
||||
REQUIRE(in_velocity.x == vel.x);
|
||||
REQUIRE(in_velocity.y == vel.y);
|
||||
});
|
||||
}
|
||||
|
||||
enum GUIEvent {
|
||||
HIT, MISS
|
||||
};
|
||||
|
||||
TEST_CASE("confirm that the event system works", "[ecs]") {
|
||||
DinkyECS::World world;
|
||||
DinkyECS::Entity player = world.entity();
|
||||
|
||||
// this confirms we can send these in a for-loop and get them out
|
||||
int i = 0;
|
||||
for(; i < 10; i++) {
|
||||
world.send<GUIEvent>(GUIEvent::HIT, player, string{"hello"});
|
||||
}
|
||||
|
||||
// just count down and should get the same number
|
||||
while(world.has_event<GUIEvent>()) {
|
||||
auto [event, entity, data] = world.recv<GUIEvent>();
|
||||
REQUIRE(event == GUIEvent::HIT);
|
||||
REQUIRE(entity == player);
|
||||
auto &str_data = std::any_cast<string&>(data);
|
||||
REQUIRE(string{"hello"} == str_data);
|
||||
i--;
|
||||
}
|
||||
|
||||
REQUIRE(i == 0);
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("confirm copying and constants", "[ecs-constants]") {
|
||||
DinkyECS::World world1;
|
||||
|
||||
Player player_info{"Zed", world1.entity()};
|
||||
world1.set_the<Player>(player_info);
|
||||
|
||||
world1.set<Position>(player_info.eid, {10,10});
|
||||
world1.make_constant(player_info.eid);
|
||||
|
||||
DinkyECS::World world2;
|
||||
world1.clone_into(world2);
|
||||
|
||||
auto &test1 = world1.get<Position>(player_info.eid);
|
||||
auto &test2 = world2.get<Position>(player_info.eid);
|
||||
|
||||
REQUIRE(test2.location.x == test1.location.x);
|
||||
REQUIRE(test2.location.y == test1.location.y);
|
||||
|
||||
// check for accidental reference
|
||||
test1.location.x = 100;
|
||||
REQUIRE(test2.location.x != test1.location.x);
|
||||
|
||||
// test the facts copy over
|
||||
auto &player2 = world2.get_the<Player>();
|
||||
REQUIRE(player2.eid == player_info.eid);
|
||||
}
|
||||
|
||||
TEST_CASE("can destroy all entity", "[ecs-destroy]") {
|
||||
DinkyECS::World world;
|
||||
auto entity = world.entity();
|
||||
|
||||
world.set<Velocity>(entity, {10,10});
|
||||
world.set<Gravity>(entity, {1});
|
||||
world.set<Motion>(entity, {0,0});
|
||||
|
||||
world.destroy(entity);
|
||||
|
||||
REQUIRE(!world.has<Velocity>(entity));
|
||||
REQUIRE(!world.has<Gravity>(entity));
|
||||
REQUIRE(!world.has<Motion>(entity));
|
||||
}
|
||||
57
tests/event_router.cpp
Normal file
57
tests/event_router.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "gui/event_router.hpp"
|
||||
|
||||
using namespace fmt;
|
||||
using namespace gui;
|
||||
using enum gui::routing::Event;
|
||||
using enum gui::routing::State;
|
||||
|
||||
using EventScript = std::vector<routing::Event>;
|
||||
|
||||
void run_script(routing::Router& router, routing::State expected, EventScript script) {
|
||||
for(auto ev : script) {
|
||||
router.event(ev);
|
||||
}
|
||||
|
||||
REQUIRE(router.in_state(expected));
|
||||
}
|
||||
|
||||
TEST_CASE("basic router operations test", "[event_router]") {
|
||||
routing::Router router;
|
||||
|
||||
// start goes to idle
|
||||
run_script(router, IDLE, {
|
||||
STARTED
|
||||
});
|
||||
|
||||
// simulate drag and drop
|
||||
run_script(router, IDLE, {
|
||||
MOUSE_DOWN,
|
||||
MOUSE_MOVE,
|
||||
MOUSE_UP,
|
||||
KEY_PRESS
|
||||
});
|
||||
|
||||
// moving the mouse outside dnd
|
||||
run_script(router, IDLE, {
|
||||
MOUSE_MOVE,
|
||||
KEY_PRESS,
|
||||
MOUSE_MOVE
|
||||
});
|
||||
|
||||
// regular mouse click
|
||||
run_script(router, IDLE, {
|
||||
MOUSE_DOWN,
|
||||
MOUSE_UP
|
||||
});
|
||||
|
||||
// possible bad key press in a move?
|
||||
run_script(router, IDLE, {
|
||||
MOUSE_DOWN,
|
||||
MOUSE_MOVE,
|
||||
KEY_PRESS,
|
||||
MOUSE_UP,
|
||||
});
|
||||
}
|
||||
67
tests/fsm.cpp
Normal file
67
tests/fsm.cpp
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "algos/simplefsm.hpp"
|
||||
|
||||
using namespace fmt;
|
||||
using std::string;
|
||||
|
||||
enum class MyState {
|
||||
START, RUNNING, END
|
||||
};
|
||||
|
||||
enum class MyEvent {
|
||||
STARTED, PUSH, QUIT
|
||||
};
|
||||
|
||||
class MyFSM : public DeadSimpleFSM<MyState, MyEvent> {
|
||||
public:
|
||||
void event(MyEvent ev, string data="") {
|
||||
switch($state) {
|
||||
FSM_STATE(MyState, START, ev);
|
||||
FSM_STATE(MyState, RUNNING, ev, data);
|
||||
FSM_STATE(MyState, END, ev);
|
||||
}
|
||||
}
|
||||
|
||||
void START(MyEvent ev) {
|
||||
println("<<< START {}", (int)ev);
|
||||
state(MyState::RUNNING);
|
||||
}
|
||||
|
||||
void RUNNING(MyEvent ev, string &data) {
|
||||
if(ev == MyEvent::QUIT) {
|
||||
println("<<< QUITTING {}", data);
|
||||
state(MyState::END);
|
||||
} else {
|
||||
println("<<< RUN: {}", data);
|
||||
state(MyState::RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
void END(MyEvent ev) {
|
||||
println("<<< STOP {}", (int)ev);
|
||||
state(MyState::END);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CASE("confirm fsm works with optional data", "[utils]") {
|
||||
MyFSM fsm;
|
||||
|
||||
REQUIRE(fsm.in_state(MyState::START));
|
||||
|
||||
fsm.event(MyEvent::STARTED);
|
||||
REQUIRE(fsm.in_state(MyState::RUNNING));
|
||||
|
||||
fsm.event(MyEvent::PUSH);
|
||||
REQUIRE(fsm.in_state(MyState::RUNNING));
|
||||
|
||||
fsm.event(MyEvent::PUSH);
|
||||
REQUIRE(fsm.in_state(MyState::RUNNING));
|
||||
|
||||
fsm.event(MyEvent::PUSH);
|
||||
REQUIRE(fsm.in_state(MyState::RUNNING));
|
||||
|
||||
fsm.event(MyEvent::QUIT, "DONE!");
|
||||
REQUIRE(fsm.in_state(MyState::END));
|
||||
}
|
||||
52
tests/inventory.cpp
Normal file
52
tests/inventory.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "game/inventory.hpp"
|
||||
|
||||
using namespace fmt;
|
||||
|
||||
TEST_CASE("base test", "[inventory]") {
|
||||
return;
|
||||
inventory::Model inv;
|
||||
DinkyECS::Entity test_ent = 1;
|
||||
|
||||
bool good = inv.add("hand_l", test_ent);
|
||||
inv.invariant();
|
||||
REQUIRE(good);
|
||||
|
||||
auto& slot = inv.get(test_ent);
|
||||
REQUIRE(slot == "hand_l");
|
||||
|
||||
// confirm that we get false when trying to do it again
|
||||
// BUG: this dies
|
||||
good = inv.add("hand_l", test_ent);
|
||||
REQUIRE(!good);
|
||||
|
||||
auto ent = inv.get(slot);
|
||||
REQUIRE(ent == test_ent);
|
||||
|
||||
REQUIRE(inv.has(ent));
|
||||
REQUIRE(inv.has(slot));
|
||||
|
||||
// test base remove
|
||||
inv.remove(ent);
|
||||
REQUIRE(!inv.has(slot));
|
||||
REQUIRE(!inv.has(ent));
|
||||
}
|
||||
|
||||
TEST_CASE("test swapping items", "[inventory]") {
|
||||
inventory::Model inv;
|
||||
DinkyECS::Entity hand_l_ent = 10;
|
||||
DinkyECS::Entity hand_r_ent = 20;
|
||||
|
||||
inv.add("hand_l", hand_l_ent);
|
||||
inv.add("hand_r", hand_r_ent);
|
||||
REQUIRE(inv.count() == 2);
|
||||
|
||||
inv.swap(hand_l_ent, hand_r_ent);
|
||||
|
||||
REQUIRE(inv.get("hand_l") == hand_r_ent);
|
||||
REQUIRE(inv.get("hand_r") == hand_l_ent);
|
||||
|
||||
REQUIRE(inv.count() == 2);
|
||||
}
|
||||
42
tests/lighting.cpp
Normal file
42
tests/lighting.cpp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include "game/map.hpp"
|
||||
#include "game/level.hpp"
|
||||
#include "graphics/lights.hpp"
|
||||
#include "algos/point.hpp"
|
||||
|
||||
using namespace lighting;
|
||||
|
||||
TEST_CASE("lighting a map works", "[lighting]") {
|
||||
GameDB::init();
|
||||
auto& level = GameDB::current_level();
|
||||
auto& map = *level.map;
|
||||
|
||||
Point light1, light2;
|
||||
|
||||
REQUIRE(map.place_entity(0, light1));
|
||||
REQUIRE(map.place_entity(0, light1));
|
||||
|
||||
LightSource source1{6, 1.0};
|
||||
LightSource source2{4,3};
|
||||
|
||||
LightRender lr(map.walls());
|
||||
|
||||
lr.reset_light();
|
||||
|
||||
lr.set_light_target(light1);
|
||||
lr.set_light_target(light2);
|
||||
|
||||
lr.path_light(map.walls());
|
||||
|
||||
lr.render_light(source1, light1);
|
||||
lr.render_light(source2, light2);
|
||||
|
||||
lr.clear_light_target(light1);
|
||||
lr.clear_light_target(light2);
|
||||
|
||||
Matrix &lighting = lr.lighting();
|
||||
(void)lighting;
|
||||
}
|
||||
21
tests/loot.cpp
Normal file
21
tests/loot.cpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "game/components.hpp"
|
||||
#include "algos/dinkyecs.hpp"
|
||||
|
||||
using namespace fmt;
|
||||
using namespace components;
|
||||
|
||||
TEST_CASE("test the loot ui", "[loot]") {
|
||||
auto items = settings::get("assets/items.json");
|
||||
DinkyECS::World world;
|
||||
auto torch = world.entity();
|
||||
auto& data = items["TORCH_BAD"];
|
||||
|
||||
components::init();
|
||||
components::configure_entity(world, torch, data["components"]);
|
||||
|
||||
auto& torch_sprite = world.get<Sprite>(torch);
|
||||
REQUIRE(torch_sprite.name == "torch_horizontal_floor");
|
||||
}
|
||||
85
tests/map.cpp
Normal file
85
tests/map.cpp
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include "game/map.hpp"
|
||||
#include "game/level.hpp"
|
||||
#include "game/systems.hpp"
|
||||
#include <cmath>
|
||||
#include "graphics/textures.hpp"
|
||||
#include "algos/rand.hpp"
|
||||
|
||||
using namespace fmt;
|
||||
using namespace nlohmann;
|
||||
using std::string;
|
||||
|
||||
json load_test_data(const string &fname) {
|
||||
std::ifstream infile(fname);
|
||||
return json::parse(infile);
|
||||
}
|
||||
|
||||
TEST_CASE("camera control", "[map]") {
|
||||
GameDB::init();
|
||||
|
||||
auto& level = GameDB::current_level();
|
||||
auto& map = *level.map;
|
||||
|
||||
Point center = map.center_camera({10,10}, 5, 5);
|
||||
|
||||
// map.dump(center.x, center.y);
|
||||
REQUIRE(center.x == 8);
|
||||
REQUIRE(center.y == 8);
|
||||
|
||||
Point translation = map.map_to_camera({10,10}, center);
|
||||
|
||||
REQUIRE(translation.x == 2);
|
||||
REQUIRE(translation.y == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("map placement test", "[map-fail]") {
|
||||
GameDB::init();
|
||||
|
||||
for(int i = 0; i < 10; i++) {
|
||||
auto& level = GameDB::create_level();
|
||||
|
||||
for(size_t rnum = 0; rnum < level.map->room_count(); rnum++) {
|
||||
Point pos;
|
||||
|
||||
REQUIRE(level.map->place_entity(rnum, pos));
|
||||
|
||||
REQUIRE(!level.map->iswall(pos.x, pos.y));
|
||||
REQUIRE(level.map->inmap(pos.x, pos.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("map image test", "[map]") {
|
||||
GameDB::init();
|
||||
|
||||
auto& level = GameDB::current_level();
|
||||
Matrix map_tiles = matrix::make(7,7);
|
||||
EntityGrid entity_map;
|
||||
|
||||
auto render = std::make_shared<sf::RenderTexture>();
|
||||
sf::Sprite sprite{render->getTexture()};
|
||||
auto player = level.world->get_the<components::Player>();
|
||||
auto& player_pos = level.world->get<components::Position>(player.entity);
|
||||
auto player_display = level.world->get<components::Tile>(player.entity).display;
|
||||
|
||||
for(matrix::each_row it{level.map->walls()}; it.next();) {
|
||||
player_pos.location.x = it.x;
|
||||
player_pos.location.y = it.y;
|
||||
System::draw_map(map_tiles, entity_map);
|
||||
System::render_map(map_tiles, entity_map, *render, 2, player_display);
|
||||
|
||||
// randomly test about 80% of them
|
||||
if(Random::uniform(0, 100) < 20) break;
|
||||
|
||||
#ifdef TEST_RENDER
|
||||
// confirm we get two different maps
|
||||
auto out_img = render->getTexture().copyToImage();
|
||||
bool worked = out_img.saveToFile(fmt::format("tmp/map_render{}{}.png", it.x, it.y));
|
||||
REQUIRE(worked);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
277
tests/matrix.cpp
Normal file
277
tests/matrix.cpp
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "game/config.hpp"
|
||||
#include "algos/matrix.hpp"
|
||||
#include "algos/rand.hpp"
|
||||
#include "game/level.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include "game/map.hpp"
|
||||
#include <memory>
|
||||
#include "graphics/textures.hpp"
|
||||
|
||||
using namespace nlohmann;
|
||||
using namespace fmt;
|
||||
using std::string, std::shared_ptr;
|
||||
using matrix::Matrix;
|
||||
|
||||
std::shared_ptr<Map> make_map() {
|
||||
GameDB::init();
|
||||
return GameDB::current_level().map;
|
||||
}
|
||||
|
||||
// BUG: create a test that randomizes a map then does matrix ops on it
|
||||
|
||||
inline void random_matrix(Matrix &out) {
|
||||
for(size_t y = 0; y < out.size(); y++) {
|
||||
for(size_t x = 0; x < out[0].size(); x++) {
|
||||
out[y][x] = Random::uniform<int>(-10,10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("thrash matrix iterators", "[matrix]") {
|
||||
for(int count = 0; count < 5; count++) {
|
||||
size_t width = Random::uniform<size_t>(1, 100);
|
||||
size_t height = Random::uniform<size_t>(1, 100);
|
||||
|
||||
Matrix test(height, matrix::Row(width));
|
||||
random_matrix(test);
|
||||
|
||||
// first make a randomized matrix
|
||||
matrix::each_cell cells{test};
|
||||
cells.next(); // kick off the other iterator
|
||||
|
||||
for(matrix::each_row it{test};
|
||||
it.next(); cells.next())
|
||||
{
|
||||
REQUIRE(test[cells.y][cells.x] == test[it.y][it.x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("thrash box distance iterators", "[matrix]") {
|
||||
size_t width = Random::uniform<size_t>(10, 21);
|
||||
size_t height = Random::uniform<size_t>(10, 25);
|
||||
|
||||
Matrix result(height, matrix::Row(width));
|
||||
matrix::assign(result, 0);
|
||||
|
||||
size_t size = Random::uniform<int>(4, 10);
|
||||
|
||||
Point target{width/2, height/2};
|
||||
matrix::box box{result, target.x, target.y, size};
|
||||
while(box.next()) {
|
||||
result[box.y][box.x] = box.distance();
|
||||
}
|
||||
|
||||
// matrix::dump(format("MAP {}x{} @ {},{}; BOX {}x{}; size: {}",
|
||||
// matrix::width(result), matrix::height(result),
|
||||
// target.x, target.y, box.right - box.left, box.bottom - box.top, size),
|
||||
// result, target.x, target.y);
|
||||
}
|
||||
|
||||
TEST_CASE("thrash box iterators", "[matrix]") {
|
||||
for(int count = 0; count < 5; count++) {
|
||||
size_t width = Random::uniform<size_t>(1, 25);
|
||||
size_t height = Random::uniform<size_t>(1, 33);
|
||||
|
||||
Matrix test(height, matrix::Row(width));
|
||||
random_matrix(test);
|
||||
|
||||
// this will be greater than the random_matrix cells
|
||||
int test_i = Random::uniform<size_t>(20,30);
|
||||
|
||||
// go through every cell
|
||||
for(matrix::each_cell target{test}; target.next();) {
|
||||
PointList result;
|
||||
// make a random size box
|
||||
size_t size = Random::uniform<int>(1, 33);
|
||||
matrix::box box{test, target.x, target.y, size};
|
||||
|
||||
while(box.next()) {
|
||||
test[box.y][box.x] = test_i;
|
||||
result.push_back({box.x, box.y});
|
||||
}
|
||||
|
||||
for(auto point : result) {
|
||||
REQUIRE(test[point.y][point.x] == test_i);
|
||||
test[point.y][point.x] = 10; // kind of reset it for another try
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("thrash compass iterators", "[matrix]") {
|
||||
for(int count = 0; count < 5; count++) {
|
||||
size_t width = Random::uniform<size_t>(1, 25);
|
||||
size_t height = Random::uniform<size_t>(1, 33);
|
||||
|
||||
Matrix test(height, matrix::Row(width));
|
||||
random_matrix(test);
|
||||
|
||||
// this will be greater than the random_matrix cells
|
||||
int test_i = Random::uniform<size_t>(20,30);
|
||||
|
||||
// go through every cell
|
||||
for(matrix::each_cell target{test}; target.next();) {
|
||||
PointList result;
|
||||
// make a random size box
|
||||
matrix::compass compass{test, target.x, target.y};
|
||||
|
||||
while(compass.next()) {
|
||||
test[compass.y][compass.x] = test_i;
|
||||
result.push_back({compass.x, compass.y});
|
||||
}
|
||||
|
||||
for(auto point : result) {
|
||||
REQUIRE(test[point.y][point.x] == test_i);
|
||||
test[point.y][point.x] = 10; // kind of reset it for another try
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("prototype line algorithm", "[matrix]") {
|
||||
size_t width = Random::uniform<size_t>(10, 12);
|
||||
size_t height = Random::uniform<size_t>(10, 15);
|
||||
Map map(width,height);
|
||||
// create a target for the paths
|
||||
Point start{.x=map.width() / 2, .y=map.height()/2};
|
||||
|
||||
for(matrix::box box{map.walls(), start.x, start.y, 3};
|
||||
box.next();)
|
||||
{
|
||||
Matrix result = map.walls();
|
||||
result[start.y][start.x] = 1;
|
||||
Point end{.x=box.x, .y=box.y};
|
||||
|
||||
for(matrix::line it{start, end}; it.next();)
|
||||
{
|
||||
REQUIRE(map.inmap(it.x, it.y));
|
||||
result[it.y][it.x] = 15;
|
||||
}
|
||||
|
||||
result[start.y][start.x] = 15;
|
||||
|
||||
// matrix::dump("RESULT AFTER LINE", result, end.x, end.y);
|
||||
|
||||
bool f_found = false;
|
||||
for(matrix::each_cell it{result}; it.next();) {
|
||||
if(result[it.y][it.x] == 15) {
|
||||
f_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE(f_found);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("prototype circle algorithm", "[matrix]") {
|
||||
for(int count = 0; count < 5; count++) {
|
||||
size_t width = Random::uniform<size_t>(10, 13);
|
||||
size_t height = Random::uniform<size_t>(10, 15);
|
||||
int pos_mod = Random::uniform<int>(-3,3);
|
||||
Map map(width,height);
|
||||
|
||||
// create a target for the paths
|
||||
Point start{.x=map.width() / 2 + pos_mod, .y=map.height()/2 + pos_mod};
|
||||
|
||||
for(float radius = 1.0f; radius < 4.0f; radius += 0.1f) {
|
||||
// use an empty map
|
||||
Matrix result = map.walls();
|
||||
|
||||
for(matrix::circle it{result, start, radius}; it.next();) {
|
||||
for(int x = it.left; x < it.right; x++) {
|
||||
// println("top={}, bottom={}, center.y={}, dy={}, left={}, right={}, x={}, y={}", it.top, it.bottom, it.center.y, it.dy, it.left, it.right, x, it.y);
|
||||
// println("RESULT {},{}", matrix::width(result), matrix::height(result));
|
||||
REQUIRE(it.y >= 0);
|
||||
REQUIRE(x >= 0);
|
||||
REQUIRE(it.y < int(matrix::height(result)));
|
||||
REQUIRE(x < int(matrix::width(result)));
|
||||
result[it.y][x] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// matrix::dump(format("RESULT AFTER CIRCLE radius {}", radius), result, start.x, start.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("viewport iterator", "[matrix]") {
|
||||
components::init();
|
||||
textures::init();
|
||||
GameDB::init();
|
||||
size_t width = Random::uniform<size_t>(20, 22);
|
||||
size_t height = Random::uniform<size_t>(21, 25);
|
||||
shared_ptr<Map> map = make_map();
|
||||
|
||||
size_t view_width = width/2;
|
||||
size_t view_height = height/2;
|
||||
Point player;
|
||||
REQUIRE(map->place_entity(1, player));
|
||||
Point start = map->center_camera(player, view_width, view_height);
|
||||
|
||||
size_t end_x = std::min(view_width, map->width() - start.x);
|
||||
size_t end_y = std::min(view_height, map->height() - start.y);
|
||||
|
||||
matrix::viewport it{map->walls(), start, int(view_width), int(view_height)};
|
||||
|
||||
for(size_t y = 0; y < end_y; ++y) {
|
||||
for(size_t x = 0; x < end_x && it.next(); ++x) {
|
||||
// still working on this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("random rectangle", "[matrix]") {
|
||||
components::init();
|
||||
for(int i = 0; i < 5; i++) {
|
||||
shared_ptr<Map> map = make_map();
|
||||
map->invert_space();
|
||||
auto wall_copy = map->walls();
|
||||
|
||||
for(size_t rnum = 0; rnum < map->room_count(); rnum++) {
|
||||
Room &room = map->room(rnum);
|
||||
Point pos;
|
||||
|
||||
for(matrix::rando_rect it{map->walls(), room.x, room.y, room.width, room.height}; it.next();)
|
||||
{
|
||||
REQUIRE(size_t(it.x) >= room.x);
|
||||
REQUIRE(size_t(it.y) >= room.y);
|
||||
REQUIRE(size_t(it.x) <= room.x + room.width);
|
||||
REQUIRE(size_t(it.y) <= room.y + room.height);
|
||||
|
||||
wall_copy[it.y][it.x] = wall_copy[it.y][it.x] + 5;
|
||||
}
|
||||
}
|
||||
// matrix::dump("WALLS FILLED", wall_copy);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("standard rectangle", "[matrix]") {
|
||||
components::init();
|
||||
for(int i = 0; i < 5; i++) {
|
||||
shared_ptr<Map> map = make_map();
|
||||
auto wall_copy = map->walls();
|
||||
|
||||
for(size_t rnum = 0; rnum < map->room_count(); rnum++) {
|
||||
Room &room = map->room(rnum);
|
||||
Point pos;
|
||||
|
||||
for(matrix::rectangle it{map->walls(), room.x, room.y, room.width, room.height}; it.next();)
|
||||
{
|
||||
REQUIRE(size_t(it.x) >= room.x);
|
||||
REQUIRE(size_t(it.y) >= room.y);
|
||||
REQUIRE(size_t(it.x) <= room.x + room.width);
|
||||
REQUIRE(size_t(it.y) <= room.y + room.height);
|
||||
|
||||
wall_copy[it.y][it.x] = wall_copy[it.y][it.x] + 5;
|
||||
}
|
||||
}
|
||||
|
||||
// matrix::dump("WALLS FILLED", wall_copy);
|
||||
}
|
||||
}
|
||||
182
tests/mazes.cpp
Normal file
182
tests/mazes.cpp
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "algos/matrix.hpp"
|
||||
#include "algos/rand.hpp"
|
||||
#include "constants.hpp"
|
||||
#include "algos/maze.hpp"
|
||||
#include "algos/stats.hpp"
|
||||
|
||||
#define DUMP 0
|
||||
|
||||
using std::string;
|
||||
using matrix::Matrix;
|
||||
|
||||
TEST_CASE("hunt-and-kill", "[mazes]") {
|
||||
Map map(21, 21);
|
||||
maze::Builder maze(map);
|
||||
|
||||
maze.hunt_and_kill();
|
||||
REQUIRE(maze.repair() == true);
|
||||
|
||||
if(DUMP) maze.dump("BASIC MAZE");
|
||||
|
||||
maze.randomize_rooms(ROOM_SIZE);
|
||||
maze.hunt_and_kill();
|
||||
maze.place_doors();
|
||||
REQUIRE(maze.repair() == true);
|
||||
|
||||
if(DUMP) maze.dump("ROOM MAZE");
|
||||
|
||||
REQUIRE(map.$dead_ends.size() > 0);
|
||||
REQUIRE(map.$rooms.size() > 0);
|
||||
}
|
||||
|
||||
TEST_CASE("hunt-and-kill box", "[mazes]") {
|
||||
for(int i = 25; i < 65; i += 2) {
|
||||
Map map(i, i);
|
||||
maze::Builder maze(map);
|
||||
|
||||
maze.hunt_and_kill();
|
||||
maze.clear();
|
||||
maze.inner_box(6, 4);
|
||||
maze.randomize_rooms(ROOM_SIZE);
|
||||
|
||||
maze.hunt_and_kill();
|
||||
maze.open_box(6);
|
||||
maze.place_doors();
|
||||
auto valid = maze.repair();
|
||||
|
||||
if(i == 41 && DUMP) {
|
||||
maze.dump(valid ? "INNER BOX" : "FAILED BOX");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("hunt-and-kill ring", "[mazes]") {
|
||||
Map map(21, 21);
|
||||
maze::Builder maze(map);
|
||||
|
||||
maze.inner_donut(5.5, 3.5);
|
||||
maze.hunt_and_kill();
|
||||
REQUIRE(maze.repair() == true);
|
||||
|
||||
if(DUMP) maze.dump("INNER RING");
|
||||
|
||||
REQUIRE(maze.$rooms.size() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("hunt-and-kill fissure", "[mazes]") {
|
||||
Map map(21, 21);
|
||||
maze::Builder maze(map);
|
||||
|
||||
maze.divide({3,3}, {19,18});
|
||||
maze.hunt_and_kill();
|
||||
REQUIRE(maze.repair() == true);
|
||||
|
||||
if(DUMP) maze.dump("FISSURE MAZE");
|
||||
|
||||
REQUIRE(maze.$rooms.size() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("hunt-and-kill no-dead-ends", "[mazes]") {
|
||||
Map map(21, 21);
|
||||
maze::Builder maze(map);
|
||||
|
||||
maze.hunt_and_kill();
|
||||
maze.remove_dead_ends();
|
||||
REQUIRE(maze.repair() == true);
|
||||
|
||||
if(DUMP) maze.dump("NO DEAD ENDS");
|
||||
}
|
||||
|
||||
TEST_CASE("hunt-and-kill too much", "[mazes]") {
|
||||
for(int i = 25; i < 65; i += 2) {
|
||||
Map map(i, i);
|
||||
maze::Builder maze(map);
|
||||
|
||||
maze.hunt_and_kill();
|
||||
maze.randomize_rooms(ROOM_SIZE);
|
||||
maze.clear();
|
||||
maze.inner_donut(9, 4);
|
||||
maze.divide({3,3}, {15,16});
|
||||
maze.hunt_and_kill();
|
||||
maze.place_doors();
|
||||
auto valid = maze.repair();
|
||||
|
||||
if(i == 41 && DUMP && valid) {
|
||||
maze.dump("COMBINED");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("hunt-and-kill validator", "[mazes]") {
|
||||
bool valid = true;
|
||||
Stats mofm;
|
||||
|
||||
for(int i = 0; i < 10; i++) {
|
||||
Stats door_prob;
|
||||
|
||||
do {
|
||||
Map map(33, 33);
|
||||
maze::Builder maze(map);
|
||||
|
||||
maze.hunt_and_kill();
|
||||
maze.clear();
|
||||
maze.inner_box(6, 4);
|
||||
maze.randomize_rooms(ROOM_SIZE);
|
||||
maze.hunt_and_kill();
|
||||
maze.open_box(6);
|
||||
maze.place_doors();
|
||||
valid = maze.repair();
|
||||
|
||||
if(i == 9 && DUMP) {
|
||||
maze.dump(valid ? "VALIDATED" : "FAILED!");
|
||||
}
|
||||
door_prob.sample(valid);
|
||||
} while(!valid);
|
||||
|
||||
if(DUMP) door_prob.dump();
|
||||
mofm.sample(door_prob.mean());
|
||||
}
|
||||
|
||||
if(DUMP) {
|
||||
fmt::println("FINAL m-of-m");
|
||||
mofm.dump();
|
||||
}
|
||||
|
||||
REQUIRE(mofm.mean() > 0.20);
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("hunt-and-kill scripting", "[mazes]") {
|
||||
using namespace nlohmann::literals;
|
||||
|
||||
// go up by 2 to keep odd
|
||||
for(int i = 0; i < 20; i+=2) {
|
||||
auto script = R"(
|
||||
[
|
||||
{"action": "hunt_and_kill"},
|
||||
{"action": "clear"},
|
||||
{"action": "inner_box", "data": [6, 4]},
|
||||
{"action": "randomize_rooms", "data": [4]},
|
||||
{"action": "divide", "data": [3, 3, 10, 10]},
|
||||
{"action": "inner_donut", "data": [5.5,4.5]},
|
||||
{"action": "hunt_and_kill"},
|
||||
{"action": "open_box", "data": [6]},
|
||||
{"action": "remove_dead_ends"},
|
||||
{"action": "place_doors"}
|
||||
]
|
||||
)"_json;
|
||||
|
||||
Map map(23+i, 23+i);
|
||||
auto [maze, valid] = maze::script(map, script);
|
||||
|
||||
if(valid) {
|
||||
REQUIRE(maze.validate() == true);
|
||||
REQUIRE(map.INVARIANT() == true);
|
||||
}
|
||||
|
||||
if(DUMP) maze.dump(valid ? "SCRIPTED" : "SCRIPTED FAIL!");
|
||||
}
|
||||
}
|
||||
27
tests/meson.build
Normal file
27
tests/meson.build
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
tests = files(
|
||||
'ai.cpp',
|
||||
'animation.cpp',
|
||||
'base.cpp',
|
||||
'battle.cpp',
|
||||
'camera.cpp',
|
||||
'components.cpp',
|
||||
'config.cpp',
|
||||
'dbc.cpp',
|
||||
'dinkyecs.cpp',
|
||||
'event_router.cpp',
|
||||
'fsm.cpp',
|
||||
'inventory.cpp',
|
||||
'lighting.cpp',
|
||||
'loot.cpp',
|
||||
'map.cpp',
|
||||
'matrix.cpp',
|
||||
'mazes.cpp',
|
||||
'palette.cpp',
|
||||
'pathing.cpp',
|
||||
'shaders.cpp',
|
||||
'sound.cpp',
|
||||
'spatialmap.cpp',
|
||||
'stats.cpp',
|
||||
'systems.cpp',
|
||||
'textures.cpp',
|
||||
)
|
||||
24
tests/palette.cpp
Normal file
24
tests/palette.cpp
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "graphics/palette.hpp"
|
||||
|
||||
using namespace fmt;
|
||||
|
||||
TEST_CASE("color palette test", "[color-palette]") {
|
||||
palette::init();
|
||||
REQUIRE(palette::initialized() == true);
|
||||
// confirm it's idempotent
|
||||
palette::init();
|
||||
|
||||
sf::Color expect{10, 10, 10, 255};
|
||||
|
||||
auto gui_text = palette::get("gui/theme:dark_dark");
|
||||
REQUIRE(gui_text == expect);
|
||||
|
||||
gui_text = palette::get("gui/theme", "mid");
|
||||
REQUIRE(gui_text != expect);
|
||||
|
||||
expect = {100, 100, 100, 255};
|
||||
REQUIRE(gui_text == expect);
|
||||
}
|
||||
53
tests/pathing.cpp
Normal file
53
tests/pathing.cpp
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include "algos/pathing.hpp"
|
||||
#include "algos/matrix.hpp"
|
||||
#include "ai/ai.hpp"
|
||||
#include "game/level.hpp"
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include "algos/rand.hpp"
|
||||
#include "game/systems.hpp"
|
||||
#include "constants.hpp"
|
||||
|
||||
using namespace fmt;
|
||||
using namespace nlohmann;
|
||||
using std::string;
|
||||
using namespace components;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
json load_test_pathing(const string &fname) {
|
||||
std::ifstream infile(fname);
|
||||
return json::parse(infile);
|
||||
}
|
||||
|
||||
TEST_CASE("multiple targets can path", "[pathing]") {
|
||||
GameDB::init();
|
||||
auto level = GameDB::create_level();
|
||||
auto walls_copy = level.map->$walls;
|
||||
Pathing paths{matrix::width(walls_copy), matrix::height(walls_copy)};
|
||||
|
||||
System::multi_path<Combat>(level, paths, walls_copy);
|
||||
|
||||
bool diag = Random::uniform<int>(0, 1);
|
||||
auto pos = GameDB::player_position().location;
|
||||
auto found = paths.find_path(pos, PATHING_TOWARD, diag);
|
||||
|
||||
while(found == PathingResult::CONTINUE) {
|
||||
// fmt::println("\033[2J\033[1;1H");
|
||||
// matrix::dump(diag ? "diag" : "simple", paths.$paths, pos.x, pos.y);
|
||||
// std::this_thread::sleep_for(200ms);
|
||||
found = paths.find_path(pos, PATHING_TOWARD, diag);
|
||||
}
|
||||
|
||||
// fmt::println("\033[2J\033[1;1H");
|
||||
// matrix::dump(diag ? "diag" : "simple", paths.$paths, pos.x, pos.y);
|
||||
|
||||
if(found == PathingResult::FOUND) {
|
||||
fmt::println("FOUND!");
|
||||
} else if(found == PathingResult::FAIL && !diag) {
|
||||
REQUIRE(found != PathingResult::FAIL);
|
||||
}
|
||||
}
|
||||
65
tests/save.cpp
Normal file
65
tests/save.cpp
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "algos/dinkyecs.hpp"
|
||||
#include "game/components.hpp"
|
||||
#include "save.hpp"
|
||||
#include <optional>
|
||||
#include <iostream>
|
||||
#include "game/map.hpp"
|
||||
#include "game/worldbuilder.hpp"
|
||||
#include "tser.hpp"
|
||||
|
||||
using namespace fmt;
|
||||
using std::string;
|
||||
using namespace components;
|
||||
|
||||
TEST_CASE("basic save a world", "[save]") {
|
||||
/*
|
||||
DinkyECS::World world;
|
||||
Map map(20, 20);
|
||||
WorldBuilder builder(map);
|
||||
builder.generate_map();
|
||||
|
||||
// configure a player as a fact of the world
|
||||
Player player{world.entity()};
|
||||
world.set_the<Player>(player);
|
||||
|
||||
world.set<Position>(player.entity, {10,10});
|
||||
world.set<Motion>(player.entity, {0, 0});
|
||||
world.set<Combat>(player.entity, {100, 10});
|
||||
world.set<Tile>(player.entity, {"@"});
|
||||
world.set<Inventory>(player.entity, {102});
|
||||
|
||||
save::to_file("./savetest.world", world, map);
|
||||
|
||||
DinkyECS::World in_world;
|
||||
Map in_map(0, 0); // this will be changed on load
|
||||
save::from_file("./savetest.world", in_world, in_map);
|
||||
|
||||
Position &position1 = world.get<Position>(player.entity);
|
||||
Position &position2 = in_world.get<Position>(player.entity);
|
||||
REQUIRE(position1.location.x == position2.location.x);
|
||||
REQUIRE(position1.location.y == position2.location.y);
|
||||
|
||||
Combat &combat1 = world.get<Combat>(player.entity);
|
||||
Combat &combat2 = in_world.get<Combat>(player.entity);
|
||||
REQUIRE(combat1.hp == combat2.hp);
|
||||
|
||||
Motion &motion1 = world.get<Motion>(player.entity);
|
||||
Motion &motion2 = in_world.get<Motion>(player.entity);
|
||||
REQUIRE(motion1.dx == motion2.dx);
|
||||
REQUIRE(motion1.dy == motion2.dy);
|
||||
|
||||
Tile &tile1 = world.get<Tile>(player.entity);
|
||||
Tile &tile2 = in_world.get<Tile>(player.entity);
|
||||
REQUIRE(tile1.chr == tile2.chr);
|
||||
|
||||
REQUIRE(map.width() == in_map.width());
|
||||
REQUIRE(map.height() == in_map.height());
|
||||
REQUIRE(map.$walls == in_map.$walls);
|
||||
|
||||
Inventory &inv = world.get<Inventory>(player.entity);
|
||||
REQUIRE(inv.gold == 102);
|
||||
*/
|
||||
}
|
||||
26
tests/shaders.cpp
Normal file
26
tests/shaders.cpp
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "graphics/shaders.hpp"
|
||||
|
||||
using namespace fmt;
|
||||
|
||||
TEST_CASE("shader loading/init works", "[shaders]") {
|
||||
shaders::init();
|
||||
int version = shaders::version();
|
||||
|
||||
std::shared_ptr<sf::Shader> ui_shader = shaders::get("ui_shader");
|
||||
auto other_test = shaders::get("ui_shader");
|
||||
|
||||
REQUIRE(ui_shader != nullptr);
|
||||
REQUIRE(ui_shader == other_test);
|
||||
REQUIRE(shaders::updated(version) == false);
|
||||
|
||||
int new_version = shaders::reload();
|
||||
REQUIRE(version != shaders::version());
|
||||
REQUIRE(version != new_version);
|
||||
REQUIRE(shaders::version() == new_version);
|
||||
REQUIRE(shaders::updated(version) == true);
|
||||
version = new_version;
|
||||
|
||||
}
|
||||
14
tests/sound.cpp
Normal file
14
tests/sound.cpp
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "game/sound.hpp"
|
||||
|
||||
using namespace fmt;
|
||||
|
||||
TEST_CASE("test sound manager", "[sound]") {
|
||||
sound::init();
|
||||
|
||||
sound::play("blank");
|
||||
|
||||
sound::play_at("blank", 0.1, 0.1, 0.1);
|
||||
}
|
||||
252
tests/spatialmap.cpp
Normal file
252
tests/spatialmap.cpp
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "algos/spatialmap.hpp"
|
||||
#include "algos/dinkyecs.hpp"
|
||||
#include "algos/rand.hpp"
|
||||
#include <limits>
|
||||
#include <fmt/core.h>
|
||||
|
||||
using DinkyECS::Entity;
|
||||
using namespace fmt;
|
||||
|
||||
|
||||
TEST_CASE("SpatialMap::insert", "[spatialmap]") {
|
||||
DinkyECS::World world;
|
||||
SpatialMap map;
|
||||
|
||||
auto player = world.entity();
|
||||
auto item = world.entity();
|
||||
auto potion = world.entity();
|
||||
auto enemy = world.entity();
|
||||
Point at{10,10};
|
||||
Point enemy_at{11,11};
|
||||
|
||||
map.insert(at, item, false);
|
||||
map.insert(at, potion, false);
|
||||
REQUIRE(!map.occupied(at));
|
||||
|
||||
map.insert(at, player, true);
|
||||
REQUIRE(map.occupied_by(at) == player);
|
||||
|
||||
REQUIRE_THROWS(map.insert(at, enemy, true));
|
||||
|
||||
map.insert(enemy_at, enemy, true);
|
||||
REQUIRE(map.occupied_by(enemy_at) == enemy);
|
||||
REQUIRE(map.occupied(enemy_at));
|
||||
}
|
||||
|
||||
TEST_CASE("SpatialMap::remove", "[spatialmap]") {
|
||||
DinkyECS::World world;
|
||||
SpatialMap map;
|
||||
|
||||
auto player = world.entity();
|
||||
auto item = world.entity();
|
||||
Point at{120, 120};
|
||||
|
||||
// confirm that things can be in any order
|
||||
map.insert(at, player, true);
|
||||
map.insert(at, item, false);
|
||||
REQUIRE(map.occupied(at));
|
||||
REQUIRE(map.occupied_by(at) == player);
|
||||
|
||||
auto data = map.remove(at, player);
|
||||
REQUIRE(!map.occupied(at));
|
||||
REQUIRE(data.entity == player);
|
||||
REQUIRE(data.collision == true);
|
||||
|
||||
REQUIRE_THROWS(map.remove(at, player));
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("SpatialMap::move", "[spatialmap]") {
|
||||
DinkyECS::World world;
|
||||
SpatialMap map;
|
||||
|
||||
auto player = world.entity();
|
||||
auto item = world.entity();
|
||||
Point at{10, 320};
|
||||
map.insert(at, player, true);
|
||||
map.insert(at, item, false);
|
||||
REQUIRE(map.occupied(at));
|
||||
|
||||
auto enemy = world.entity();
|
||||
auto potion = world.entity();
|
||||
Point enemy_at{11, 320};
|
||||
map.insert(enemy_at, enemy, true);
|
||||
map.insert(enemy_at, potion, false);
|
||||
REQUIRE(map.occupied(enemy_at));
|
||||
REQUIRE(map.occupied_by(enemy_at) == enemy);
|
||||
|
||||
Point target{at.x + 1, at.y};
|
||||
|
||||
// try bad move with a slot that's empty
|
||||
REQUIRE_THROWS(map.move({0,0}, target, player));
|
||||
|
||||
// try move into an occupied spot also fails
|
||||
REQUIRE_THROWS(map.move(at, target, player));
|
||||
|
||||
// now move to a new spot, need to add them back
|
||||
map.insert(at, player, true);
|
||||
target.x++; // just move farther
|
||||
map.move(at, target, player);
|
||||
|
||||
REQUIRE(map.occupied(target));
|
||||
REQUIRE(map.occupied_by(target) == player);
|
||||
auto data = map.remove(target, player);
|
||||
REQUIRE(data.entity == player);
|
||||
REQUIRE(data.collision == true);
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("SpatialMap::occupied/something_there", "[spatialmap]") {
|
||||
DinkyECS::World world;
|
||||
SpatialMap map;
|
||||
|
||||
auto player = world.entity();
|
||||
auto item = world.entity();
|
||||
|
||||
Point at{1000, 20};
|
||||
// first test empty locations
|
||||
REQUIRE(!map.something_there(at));
|
||||
REQUIRE(!map.occupied(at));
|
||||
|
||||
// then when there's something without collision
|
||||
map.insert(at, item, false);
|
||||
REQUIRE(map.something_there(at));
|
||||
REQUIRE(!map.occupied(at));
|
||||
|
||||
// finally with collision and an item there
|
||||
map.insert(at, player, true);
|
||||
REQUIRE(map.something_there(at));
|
||||
REQUIRE(map.occupied(at));
|
||||
REQUIRE(map.occupied_by(at) == player);
|
||||
|
||||
// then remove the item and still have collision
|
||||
|
||||
map.remove(at, item);
|
||||
REQUIRE(map.something_there(at));
|
||||
REQUIRE(map.occupied(at));
|
||||
REQUIRE(map.occupied_by(at) == player);
|
||||
|
||||
// remove player and back to no collision
|
||||
map.remove(at, player);
|
||||
REQUIRE(!map.something_there(at));
|
||||
REQUIRE(!map.occupied(at));
|
||||
|
||||
// last thing, put just the player in at a new spot
|
||||
Point target{at.x+1, at.y+10};
|
||||
map.insert(target, player, true);
|
||||
REQUIRE(map.something_there(target));
|
||||
REQUIRE(map.occupied(target));
|
||||
REQUIRE(map.occupied_by(target) == player);
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("SpatialMap::get", "[spatialmap]") {
|
||||
DinkyECS::World world;
|
||||
SpatialMap map;
|
||||
|
||||
auto player = world.entity();
|
||||
auto item = world.entity();
|
||||
Point at{101, 31};
|
||||
|
||||
// finally with collision and an item there
|
||||
map.insert(at, player, true);
|
||||
REQUIRE(map.occupied(at));
|
||||
REQUIRE(map.occupied_by(at) == player);
|
||||
|
||||
auto entity = map.get(at);
|
||||
REQUIRE(player == entity);
|
||||
|
||||
// This probably doesn't work so need to
|
||||
// rethink how get works.
|
||||
map.insert(at, item, false);
|
||||
entity = map.get(at);
|
||||
REQUIRE(entity == item);
|
||||
}
|
||||
|
||||
TEST_CASE("SpatialMap::find", "[spatialmap-find]") {
|
||||
DinkyECS::World world;
|
||||
SpatialMap map;
|
||||
Point at{101, 31};
|
||||
DinkyECS::Entity should_collide = DinkyECS::NONE;
|
||||
|
||||
for(int i = 0; i < 10; i++) {
|
||||
auto ent = world.entity();
|
||||
map.insert(at, ent, i == 8);
|
||||
|
||||
if(i == 8) {
|
||||
should_collide = ent;
|
||||
}
|
||||
}
|
||||
|
||||
auto collision = map.find(at, [&](auto data) -> bool {
|
||||
return data.collision;
|
||||
});
|
||||
|
||||
REQUIRE(collision == should_collide);
|
||||
|
||||
auto no_collide = map.find(at, [&](auto data) -> bool {
|
||||
return !data.collision;
|
||||
});
|
||||
|
||||
REQUIRE(no_collide != should_collide);
|
||||
}
|
||||
|
||||
TEST_CASE("SpatialMap::neighbors", "[spatialmap-neighbors]") {
|
||||
DinkyECS::World world;
|
||||
SpatialMap map;
|
||||
|
||||
auto player = world.entity();
|
||||
auto enemy1 = world.entity();
|
||||
auto enemy2 = world.entity();
|
||||
//auto item1 = world.entity();
|
||||
//auto item2 = world.entity();
|
||||
Point at{101, 31};
|
||||
|
||||
map.insert(at, player, true);
|
||||
map.insert({at.x+1, at.y}, enemy1, true);
|
||||
map.insert({at.x-1, at.y+1}, enemy2, true);
|
||||
|
||||
auto result = map.neighbors(at, true);
|
||||
REQUIRE(result.found);
|
||||
REQUIRE(result.nearby.size() == 2);
|
||||
|
||||
bool maybe = result.nearby[0] == enemy1 || result.nearby[1] == enemy1;
|
||||
REQUIRE(maybe);
|
||||
|
||||
maybe = result.nearby[0] == enemy2 || result.nearby[1] == enemy2;
|
||||
REQUIRE(maybe);
|
||||
|
||||
result = map.neighbors(at, false);
|
||||
REQUIRE(result.found);
|
||||
REQUIRE(result.nearby.size() == 1);
|
||||
REQUIRE(result.nearby[0] == enemy1);
|
||||
}
|
||||
|
||||
TEST_CASE("SpatialMap::distance_sorted", "[spatialmap]") {
|
||||
DinkyECS::World world;
|
||||
SpatialMap map;
|
||||
|
||||
auto player = world.entity();
|
||||
auto enemy1 = world.entity();
|
||||
auto item = world.entity();
|
||||
|
||||
map.insert({1,1}, player, true);
|
||||
map.insert({4,4}, enemy1, true);
|
||||
map.insert({3, 3}, item, false);
|
||||
|
||||
SortedEntities result;
|
||||
map.distance_sorted(result, {1, 1}, 100);
|
||||
REQUIRE(result.size() == 3);
|
||||
REQUIRE(result[0].entity == enemy1);
|
||||
REQUIRE(result[1].entity == item);
|
||||
REQUIRE(result[2].entity == player);
|
||||
|
||||
int prev_dist = std::numeric_limits<int>::max();
|
||||
for(auto rec : result) {
|
||||
REQUIRE(rec.dist_square < prev_dist);
|
||||
prev_dist = rec.dist_square;
|
||||
}
|
||||
}
|
||||
27
tests/stats.cpp
Normal file
27
tests/stats.cpp
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "algos/stats.hpp"
|
||||
#include "algos/rand.hpp"
|
||||
#include <cmath>
|
||||
#include <fmt/core.h>
|
||||
|
||||
TEST_CASE("basic stats tests", "[stats]") {
|
||||
Stats stat1;
|
||||
stat1.sample(1.0);
|
||||
|
||||
for(int i = 0; i < 20; i++) {
|
||||
double x = Random::normal(20.0,5.0);
|
||||
stat1.sample(x);
|
||||
REQUIRE(!std::isnan(stat1.stddev()));
|
||||
REQUIRE(stat1.mean() < stat1.mean() + stat1.stddev() * 4.0);
|
||||
}
|
||||
|
||||
stat1.dump();
|
||||
|
||||
stat1.reset();
|
||||
REQUIRE(stat1.n == 0.0);
|
||||
|
||||
auto timer = stat1.time_start();
|
||||
for(int i = 0; i < 20; i++) {
|
||||
stat1.sample_time(timer);
|
||||
}
|
||||
}
|
||||
36
tests/systems.cpp
Normal file
36
tests/systems.cpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include "game/systems.hpp"
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
|
||||
|
||||
TEST_CASE("figure out best rotation direction", "[systems-rotate]") {
|
||||
Matrix map = matrix::make(3, 3);
|
||||
|
||||
Point player_at{1, 1};
|
||||
map[player_at.y][player_at.x] = 2;
|
||||
|
||||
|
||||
for(matrix::box target{map, player_at.x, player_at.y, 1}; target.next();) {
|
||||
for(matrix::box aiming_at{map, player_at.x, player_at.y, 1}; aiming_at.next();) {
|
||||
map[aiming_at.y][aiming_at.x] = 10;
|
||||
|
||||
float target_dx = float(player_at.x) - float(target.x);
|
||||
float target_dy = float(player_at.y) - float(target.y);
|
||||
float aiming_dx = float(player_at.x) - float(aiming_at.x);
|
||||
float aiming_dy = float(player_at.y) - float(aiming_at.y);
|
||||
|
||||
float target_angle = atan2(-target_dy, target_dx) * (180.0 / std::numbers::pi);
|
||||
float aiming_angle = atan2(-aiming_dy, aiming_dx) * (180.0 / std::numbers::pi);
|
||||
|
||||
float diff = target_angle - aiming_angle;
|
||||
double normalized = fmod(diff + 360.0, 360.0);
|
||||
|
||||
REQUIRE(normalized >= 0);
|
||||
REQUIRE(normalized <= 360);
|
||||
|
||||
map[aiming_at.y][aiming_at.x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
tests/textures.cpp
Normal file
43
tests/textures.cpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <string>
|
||||
#include "graphics/textures.hpp"
|
||||
#include "constants.hpp"
|
||||
#include "game/components.hpp"
|
||||
|
||||
using namespace fmt;
|
||||
|
||||
TEST_CASE("test texture management", "[textures]") {
|
||||
components::init();
|
||||
textures::init();
|
||||
|
||||
auto spider = textures::get_sprite("rat_with_sword");
|
||||
REQUIRE(spider.sprite != nullptr);
|
||||
REQUIRE(spider.texture != nullptr);
|
||||
REQUIRE(spider.frame_size.x == TEXTURE_WIDTH);
|
||||
REQUIRE(spider.frame_size.y == TEXTURE_HEIGHT);
|
||||
|
||||
auto image = textures::load_image("assets/sprites/rat_with_sword.png");
|
||||
|
||||
size_t floor_tile = textures::get_id("floor_tile");
|
||||
size_t gray_stone = textures::get_id("door_plain");
|
||||
|
||||
auto floor_ptr = textures::get_surface(floor_tile);
|
||||
REQUIRE(floor_ptr != nullptr);
|
||||
|
||||
auto gray_stone_ptr = textures::get_surface(gray_stone);
|
||||
REQUIRE(gray_stone_ptr != nullptr);
|
||||
|
||||
auto& light = textures::get_ambient_light();
|
||||
REQUIRE(light.size() > 0);
|
||||
REQUIRE(light[floor_tile] == 0);
|
||||
REQUIRE(light[gray_stone] > 0);
|
||||
|
||||
auto& tiles = textures::get_map_tile_set();
|
||||
REQUIRE(tiles.size() > 0);
|
||||
REQUIRE(tiles[floor_tile] > 0);
|
||||
REQUIRE(tiles[gray_stone] > 0);
|
||||
|
||||
auto ceiling = textures::get_ceiling(floor_tile);
|
||||
REQUIRE(ceiling != nullptr);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue