Started to convert from catch2 to fuc2.

This commit is contained in:
Zed A. Shaw 2026-06-16 04:38:38 -04:00
parent 4deda37665
commit b8b42d5681
10 changed files with 615 additions and 493 deletions

View file

@ -1,4 +1,3 @@
#include <catch2/catch_test_macros.hpp>
#include "graphics/textures.hpp" #include "graphics/textures.hpp"
#include "algos/dinkyecs.hpp" #include "algos/dinkyecs.hpp"
#include "game/config.hpp" #include "game/config.hpp"
@ -10,160 +9,176 @@
#include "graphics/animation.hpp" #include "graphics/animation.hpp"
#include "game/sound.hpp" #include "game/sound.hpp"
#include "game/components.hpp" #include "game/components.hpp"
#include <fuc2/testing.hpp>
using namespace components; using namespace components;
using namespace textures; using namespace textures;
using namespace std::chrono_literals; using namespace std::chrono_literals;
using namespace animation; using namespace animation;
using namespace fuc2;
Animation load_animation(const string& name) { namespace animation_tests {
auto anim = animation::load("assets/animation.json", "rat_king_boss"); Animation load_animation(const string& name) {
anim.set_form("attack"); auto anim = animation::load("assets/animation.json", "rat_king_boss");
anim.set_form("attack");
anim.transform.looped = false; anim.transform.looped = false;
for(size_t i = 0; i < anim.sequence.durations.size(); i++) { for(size_t i = 0; i < anim.sequence.durations.size(); i++) {
anim.sequence.durations[i] = Random::uniform(1, 5); 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); return 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); void FAKE_RENDER() {
REQUIRE(anim.playing == false); std::this_thread::sleep_for(Random::milliseconds(5, 32));
}
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); void PLAY_TEST(Animation &anim) {
REQUIRE(pos == sf::Vector2f{100, 100}); CHECK(anim.transform.looped == false);
REQUIRE(scale != sf::Vector2f{0,0}); anim.play();
}
TEST_CASE("playing with delta time", "[animation-new]") { while(anim.playing) {
animation::Timer timer; anim.update();
timer.start(); FAKE_RENDER();
}
for(int i = 0; i < 20; i++) { CHECK(anim.playing == false);
FAKE_RENDER();
auto [tick_count, alpha] = timer.commit();
// fmt::println("tick: {}, alpha: {}", tick_count, alpha);
} }
/*
* 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.
*/
void test_new_animation_system() {
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);
CHECK(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);
CHECK(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);
CHECK(anim.sequence.loop_count == 2);
}
void test_confirm_frame_sequencing_works() {
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();
}
CHECK(loop_ran == true);
CHECK(anim.playing == false);
}
void test_confirm_transition_changes_work() {
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
CHECK(anim.onFrame == nullptr);
anim.play();
CHECK(anim.playing == true);
while(anim.playing) {
anim.update();
anim.motion(sprite, pos, scale);
FAKE_RENDER();
}
CHECK(anim.playing == false);
CHECK(pos == sf::Vector2f{100, 100});
CHECK(scale != sf::Vector2f{0,0});
}
void test_playing_with_delta_time() {
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);
}
}
fuc2::Set TESTS{
.name="ai functionality",
.tests={
TEST(test_new_animation_system),
TEST(test_confirm_frame_sequencing_works),
TEST(test_confirm_transition_changes_work),
TEST(test_playing_with_delta_time),
}
};
} }

View file

@ -1,9 +1,18 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h> #include <fmt/core.h>
#include <string> #include <string>
#include <fuc2/testing.hpp>
using namespace fmt; using namespace fuc2;
TEST_CASE("base test", "[base]") { namespace base_tests {
REQUIRE(1 == 1); void test_base_test() {
CHECK(1 == 1);
}
fuc2::Set TESTS{
.name="base template",
.tests={
TEST(test_base_test),
}
};
} }

View file

@ -1,4 +1,3 @@
#include <catch2/catch_test_macros.hpp>
#include <iostream> #include <iostream>
#include <set> #include <set>
#include "game/rituals.hpp" #include "game/rituals.hpp"
@ -11,134 +10,146 @@
#include "game/components.hpp" #include "game/components.hpp"
#include "ai/ai.hpp" #include "ai/ai.hpp"
#include "graphics/palette.hpp" #include "graphics/palette.hpp"
#include <fuc2/testing.hpp>
using namespace combat; using namespace combat;
using namespace boss; using namespace boss;
using namespace components; using namespace components;
using namespace fuc2;
TEST_CASE("battle operations fantasy", "[fail]") { namespace battle_tests {
ai::reset(); void test_battle_operations_fantasy() {
ai::init("ai"); ai::reset();
ai::init("ai");
auto ai_start = ai::load_state("Enemy::initial_state"); auto ai_start = ai::load_state("Enemy::initial_state");
auto ai_goal = ai::load_state("Enemy::final_state"); auto ai_goal = ai::load_state("Enemy::final_state");
auto host_start = ai::load_state("Host::initial_state"); auto host_start = ai::load_state("Host::initial_state");
auto host_goal = ai::load_state("Host::final_state"); auto host_goal = ai::load_state("Host::final_state");
BattleEngine battle; BattleEngine battle;
DinkyECS::Entity host = 0; DinkyECS::Entity host = 0;
ai::EntityAI host_ai("Host::actions", host_start, host_goal); ai::EntityAI host_ai("Host::actions", host_start, host_goal);
components::Combat host_combat{ components::Combat host_combat{
.hp=100, .max_hp=100, .ap_delta=6, .max_ap=12, .damage=20}; .hp=100, .max_hp=100, .ap_delta=6, .max_ap=12, .damage=20};
battle.add_enemy({host, &host_ai, &host_combat, true}); battle.add_enemy({host, &host_ai, &host_combat, true});
DinkyECS::Entity axe_ranger = 1; DinkyECS::Entity axe_ranger = 1;
ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal); ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal);
components::Combat axe_combat{ components::Combat axe_combat{
.hp=20, .max_hp=20, .ap_delta=8, .max_ap=12, .damage=20}; .hp=20, .max_hp=20, .ap_delta=8, .max_ap=12, .damage=20};
battle.add_enemy({axe_ranger, &axe_ai, &axe_combat}); battle.add_enemy({axe_ranger, &axe_ai, &axe_combat});
DinkyECS::Entity rat = 2; DinkyECS::Entity rat = 2;
ai::EntityAI rat_ai("Enemy::actions", ai_start, ai_goal); ai::EntityAI rat_ai("Enemy::actions", ai_start, ai_goal);
components::Combat rat_combat{ components::Combat rat_combat{
.hp=10, .max_hp=10, .ap_delta=2, .max_ap=10, .damage=10}; .hp=10, .max_hp=10, .ap_delta=2, .max_ap=10, .damage=10};
battle.add_enemy({rat, &rat_ai, &rat_combat}); battle.add_enemy({rat, &rat_ai, &rat_combat});
battle.set_all("enemy_found", true); battle.set_all("enemy_found", true);
battle.set_all("in_combat", true); battle.set_all("in_combat", true);
battle.set_all("tough_personality", true); battle.set_all("tough_personality", true);
battle.set_all("health_good", true); battle.set_all("health_good", true);
battle.set(rat, "tough_personality", false); battle.set(rat, "tough_personality", false);
battle.set(host, "have_healing", false); battle.set(host, "have_healing", false);
battle.set(host, "tough_personality", false); battle.set(host, "tough_personality", false);
int host_combat_loop = 0; int host_combat_loop = 0;
for(host_combat_loop = 0; host_combat_loop < 1000; host_combat_loop++) { for(host_combat_loop = 0; host_combat_loop < 1000; host_combat_loop++) {
if(host_combat.is_dead()) break; if(host_combat.is_dead()) break;
battle.set(host, "health_good", host_combat.hp > 20); battle.set(host, "health_good", host_combat.hp > 20);
battle.player_request("use_healing"); battle.player_request("use_healing");
battle.player_request("kill_enemy"); battle.player_request("kill_enemy");
battle.ap_refresh(); battle.ap_refresh();
battle.plan(); battle.plan();
int battle_count = 0; int battle_count = 0;
for(int battle_count = 0; battle_count < 1000; battle_count++) { for(int battle_count = 0; battle_count < 1000; battle_count++) {
auto act = battle.next(); auto act = battle.next();
if(!act) break; if(!act) break;
auto& [enemy, wants_to, cost, enemy_state] = *act; auto& [enemy, wants_to, cost, enemy_state] = *act;
// fmt::println(">>>>> entity: {} wants to {} cost={}; has {} HP; {} ap", // fmt::println(">>>>> entity: {} wants to {} cost={}; has {} HP; {} ap",
// enemy.entity, wants_to, // enemy.entity, wants_to,
// cost, enemy.combat->hp, // cost, enemy.combat->hp,
// enemy.combat->ap); // enemy.combat->ap);
switch(enemy_state) { switch(enemy_state) {
case BattleHostState::agree: case BattleHostState::agree:
// fmt::println("HOST and PLAYER requests match {}, doing it.", wants_to); // fmt::println("HOST and PLAYER requests match {}, doing it.", wants_to);
break; break;
case BattleHostState::disagree: case BattleHostState::disagree:
// fmt::println("REBELIOUS ACT: {}", wants_to); // fmt::println("REBELIOUS ACT: {}", wants_to);
battle.clear_requests(); battle.clear_requests();
REQUIRE(battle.$player_requests.size() == 0); CHECK(battle.$player_requests.size() == 0);
break; break;
case BattleHostState::not_host: case BattleHostState::not_host:
if(wants_to == "kill_enemy") { if(wants_to == "kill_enemy") {
enemy.combat->attack(host_combat); enemy.combat->attack(host_combat);
} }
break; break;
case BattleHostState::out_of_ap: case BattleHostState::out_of_ap:
// fmt::println("ENEMY OUT OF AP"); // fmt::println("ENEMY OUT OF AP");
break; break;
}
} }
CHECK(!battle.next());
dbc::check(battle_count < 1000, "infinite battle loop");
} }
REQUIRE(!battle.next()); dbc::check(host_combat_loop < 1000, "infinite host combat loop, host won't die!");
dbc::check(battle_count < 1000, "infinite battle loop");
} }
dbc::check(host_combat_loop < 1000, "infinite host combat loop, host won't die!"); void test_boss_systems_work() {
} components::init();
gui::Backend backend;
guecs::init(&backend);
TEST_CASE("boss/systems.cpp works", "[combat-battle]") { ai::reset();
components::init(); ai::init("ai");
gui::Backend backend; GameDB::init();
guecs::init(&backend); cinematic::init();
auto host = GameDB::the_player();
ai::reset(); auto fight = System::create_bossfight();
ai::init("ai"); auto battle = System::create_battle(fight->$world, fight->$boss_id);
GameDB::init(); auto& host_combat = fight->$world->get<Combat>(host);
cinematic::init();
auto host = GameDB::the_player();
auto fight = System::create_bossfight(); System::initialize_actor_ai(*fight->$world, fight->$boss_id);
auto battle = System::create_battle(fight->$world, fight->$boss_id);
auto& host_combat = fight->$world->get<Combat>(host);
System::initialize_actor_ai(*fight->$world, fight->$boss_id); // NEED UPDATE STATE
battle.set_all("enemy_found", true);
battle.set_all("in_combat", true);
battle.set_all("tough_personality", true);
battle.set_all("health_good", true);
// NEED UPDATE STATE battle.ap_refresh();
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.ap_refresh(); battle.player_request("kill_enemy");
int pending_ap = battle.player_pending_ap();
battle.player_request("kill_enemy"); CHECK(pending_ap < host_combat.ap);
int pending_ap = battle.player_pending_ap();
REQUIRE(pending_ap < host_combat.ap); System::plan_battle(battle, fight->$world, fight->$boss_id);
System::plan_battle(battle, fight->$world, fight->$boss_id); while(auto action = battle.next()) {
dbc::log("ACTION!");
System::combat(*action, fight->$world, fight->$boss_id, 0);
}
while(auto action = battle.next()) { CHECK(host_combat.ap == pending_ap);
dbc::log("ACTION!");
System::combat(*action, fight->$world, fight->$boss_id, 0);
} }
REQUIRE(host_combat.ap == pending_ap); fuc2::Set TESTS{
.name="battle tests",
.tests={
TEST(test_battle_operations_fantasy),
TEST(test_boss_systems_work),
}
};
} }

View file

@ -1,6 +1,17 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h> #include <fmt/core.h>
#include <fuc2/testing.hpp>
TEST_CASE("view based camera system", "[camera]") { using namespace fuc2;
REQUIRE(1 == 1);
namespace camera_tests {
void test_view_based_camera_system() {
CHECK(1 == 1);
}
fuc2::Set TESTS{
.name="camera tests (EMPTY!)",
.tests={
TEST(test_view_based_camera_system),
}
};
} }

View file

@ -1,4 +1,4 @@
#include <catch2/catch_test_macros.hpp> #include <fuc2/testing.hpp>
#include "game/components.hpp" #include "game/components.hpp"
#include "algos/dinkyecs.hpp" #include "algos/dinkyecs.hpp"
#include "game/config.hpp" #include "game/config.hpp"
@ -7,44 +7,56 @@
using namespace components; using namespace components;
using namespace DinkyECS; using namespace DinkyECS;
TEST_CASE("confirm component loading works", "[components]") { using namespace fuc2;
std::vector<std::string> test_list{
"assets/enemies.json", "assets/items.json", "assets/devices.json"};
components::init(); namespace component_tests {
DinkyECS::World world; void test_confirm_component_loading_works() {
std::vector<std::string> test_list{
"assets/enemies.json", "assets/items.json", "assets/devices.json"};
for(auto test_data : test_list) { components::init();
auto config = settings::get(test_data); DinkyECS::World world;
auto data_list = config.json();
for(auto& [key, data] : data_list.items()) { for(auto test_data : test_list) {
auto& components = data["components"]; auto config = settings::get(test_data);
fmt::println("TEST COMPONENT: {} from file {}", key, test_data); auto data_list = config.json();
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]") { for(auto& [key, data] : data_list.items()) {
auto config = settings::get("bosses"); auto& components = data["components"];
// this confirms that loading something with an optional fmt::println("TEST COMPONENT: {} from file {}", key, test_data);
// field works with the json conversions in json_mods.hpp auto ent = world.entity();
for(auto& comp_data : config["RAT_KING"]["components"]) { components::configure_entity(world, ent, components);
if(comp_data["_type"] == "AnimatedScene") { auto tile = components::get<Tile>(components[0]);
auto comp = components::convert<components::AnimatedScene>(comp_data); CHECK(tile.display != L' ');
}
} }
} }
// this then confirms everything else about the json conversion void test_make_sure_json_mods_works() {
components::init(); 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);
}
}
DinkyECS::World world; // this then confirms everything else about the json conversion
auto rat_king = world.entity(); components::init();
components::configure_entity(world, rat_king, config["RAT_KING"]["components"]); DinkyECS::World world;
auto boss = world.get<AnimatedScene>(rat_king); auto rat_king = world.entity();
components::configure_entity(world, rat_king, config["RAT_KING"]["components"]);
auto boss = world.get<AnimatedScene>(rat_king);
}
fuc2::Set TESTS{
.name="components",
.tests={
TEST(test_make_sure_json_mods_works),
TEST(test_confirm_component_loading_works),
}
};
} }

View file

@ -1,28 +1,40 @@
#include <catch2/catch_test_macros.hpp> #include <fuc2/testing.hpp>
#include "game/config.hpp" #include "game/config.hpp"
#include <iostream> #include <iostream>
TEST_CASE("confirm basic config loader ops", "[config]") { using namespace fuc2;
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); namespace config_tests {
void test_confirm_basic_config_loader_ops() {
settings::Config::set_base_dir("./");
auto config = settings::get("devices");
auto data_list = config.json();
auto the_keys = config.keys();
for(auto& [key, data] : data_list.items()) { CHECK(the_keys.size() > 0);
auto wide1 = config.wstring(key, "name");
auto& comps = data["components"];
for(auto& comp_data : comps) { for(auto& [key, data] : data_list.items()) {
REQUIRE(comp_data.contains("_type")); auto wide1 = config.wstring(key, "name");
auto& comps = data["components"];
for(auto& comp_data : comps) {
CHECK(comp_data.contains("_type"));
}
} }
auto indexed = settings::get("tests/config_test.json");
auto& test_0 = indexed[0];
CHECK(test_0["test"] == 0);
auto& test_1 = indexed[1];
CHECK(test_1["test"] == 1);
} }
auto indexed = settings::get("tests/config_test.json"); fuc2::Set TESTS{
auto& test_0 = indexed[0]; .name="config",
REQUIRE(test_0["test"] == 0); .tests={
TEST(test_confirm_basic_config_loader_ops),
}
};
auto& test_1 = indexed[1];
REQUIRE(test_1["test"] == 1);
} }

View file

@ -1,18 +1,28 @@
#include <catch2/catch_test_macros.hpp> #include <fuc2/testing.hpp>
#include "dbc.hpp" #include "dbc.hpp"
using namespace dbc; using namespace dbc;
TEST_CASE("basic feature tests", "[dbc]") { namespace dbc_tests {
log("Logging a message."); void test_basic_dbc_features() {
log("Logging a message.");
pre("confirm positive cases work", 1 == 1); pre("confirm positive cases work", 1 == 1);
pre("confirm positive lambda", [&]{ return 1 == 1;}); pre("confirm positive lambda", [&]{ return 1 == 1;});
post("confirm positive post", 1 == 1); post("confirm positive post", 1 == 1);
post("confirm postitive post with lamdba", [&]{ return 1 == 1;}); post("confirm postitive post with lamdba", [&]{ return 1 == 1;});
check(1 == 1, "one equals 1");
}
fuc2::Set TESTS{
.name="dbc",
.tests={
TEST(test_basic_dbc_features),
}
};
check(1 == 1, "one equals 1");
} }

View file

@ -1,4 +1,4 @@
#include <catch2/catch_test_macros.hpp> #include <fuc2/testing.hpp>
#include "algos/dinkyecs.hpp" #include "algos/dinkyecs.hpp"
#include <iostream> #include <iostream>
#include <fmt/core.h> #include <fmt/core.h>
@ -6,207 +6,224 @@
using namespace fmt; using namespace fmt;
using DinkyECS::Entity; using DinkyECS::Entity;
using std::string; using std::string;
using namespace fuc2;
struct Point { namespace dinkyecs_tests {
size_t x; struct Point {
size_t y; size_t x;
}; size_t y;
};
struct Player { struct Player {
string name; string name;
Entity eid; Entity eid;
}; };
struct Position { struct Position {
Point location; Point location;
}; };
struct Motion { struct Motion {
int dx; int dx;
int dy; int dy;
bool random=false; bool random=false;
}; };
struct Velocity { struct Velocity {
double x, y; double x, y;
}; };
struct Gravity { struct Gravity {
double level; double level;
}; };
struct DaGUI { struct DaGUI {
int event; int event;
}; };
/* /*
* Using a function catches instances where I'm not copying * Using a function catches instances where I'm not copying
* the data into the world. * the data into the world.
*/ */
void configure(DinkyECS::World &world, Entity &test) { void configure(DinkyECS::World &world, Entity &test) {
println("---Configuring the base system."); println("---Configuring the base system.");
Entity test2 = world.entity(); Entity test2 = world.entity();
world.set<Position>(test, {10,20}); world.set<Position>(test, {10,20});
world.set<Velocity>(test, {1,2}); world.set<Velocity>(test, {1,2});
world.set<Position>(test2, {1,1}); world.set<Position>(test2, {1,1});
world.set<Velocity>(test2, {9,19}); world.set<Velocity>(test2, {9,19});
println("---- Setting up the player as a fact in the system."); println("---- Setting up the player as a fact in the system.");
auto player_eid = world.entity(); auto player_eid = world.entity();
Player player_info{"Zed", player_eid}; Player player_info{"Zed", player_eid};
// just set some player info as a fact with the entity id // just set some player info as a fact with the entity id
world.set_the<Player>(player_info); world.set_the<Player>(player_info);
world.set<Velocity>(player_eid, {0,0}); world.set<Velocity>(player_eid, {0,0});
world.set<Position>(player_eid, {0,0}); world.set<Position>(player_eid, {0,0});
auto enemy = world.entity(); auto enemy = world.entity();
world.set<Velocity>(enemy, {0,0}); world.set<Velocity>(enemy, {0,0});
world.set<Position>(enemy, {0,0}); world.set<Position>(enemy, {0,0});
println("--- Creating facts (singletons)"); println("--- Creating facts (singletons)");
world.set_the<Gravity>({0.9}); 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 void test_confirm_ECS_system_works() {
while(world.has_event<GUIEvent>()) { DinkyECS::World world;
auto [event, entity, data] = world.recv<GUIEvent>(); Entity test = world.entity();
REQUIRE(event == GUIEvent::HIT);
REQUIRE(entity == player); configure(world, test);
auto &str_data = std::any_cast<string&>(data);
REQUIRE(string{"hello"} == str_data); Position &pos = world.get<Position>(test);
i--; CHECK(pos.location.x == 10);
CHECK(pos.location.y == 20);
Velocity &vel = world.get<Velocity>(test);
CHECK(vel.x == 1);
CHECK(vel.y == 2);
world.query<Position>([](const auto &ent, auto &pos) {
CHECK(ent > 0);
CHECK(pos.location.x >= 0);
CHECK(pos.location.y >= 0);
});
world.query<Velocity>([](const auto &ent, auto &vel) {
CHECK(ent > 0);
CHECK(vel.x >= 0);
CHECK(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);
CHECK(ent > 0);
CHECK(pos.location.x >= 0);
CHECK(pos.location.y >= 0);
CHECK(ent > 0);
CHECK(vel.x >= 0);
CHECK(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>();
CHECK(grav.level <= 1.0f);
CHECK(grav.level > 0.5f);
CHECK(ent > 0);
CHECK(pos.location.x >= 0);
CHECK(pos.location.y >= 0);
CHECK(ent > 0);
CHECK(vel.x >= 0);
CHECK(vel.y >= 0);
});
// now remove Velocity
CHECK(world.has<Velocity>(test));
world.remove<Velocity>(test);
BLOWS_UP([&]() {
world.get<Velocity>(test);
});
CHECK(!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);
CHECK(pos.location.x >= 0);
CHECK(pos.location.y >= 0);
CHECK(in_position.location.x == pos.location.x);
CHECK(in_position.location.y == pos.location.y);
CHECK(in_velocity.x == vel.x);
CHECK(in_velocity.y == vel.y);
});
} }
REQUIRE(i == 0); enum GUIEvent {
} HIT, MISS
};
TEST_CASE("confirm copying and constants", "[ecs-constants]") { void test_confirm_that_the_event_system_works() {
DinkyECS::World world1; DinkyECS::World world;
DinkyECS::Entity player = world.entity();
Player player_info{"Zed", world1.entity()};
world1.set_the<Player>(player_info); // this confirms we can send these in a for-loop and get them out
int i = 0;
world1.set<Position>(player_info.eid, {10,10}); for(; i < 10; i++) {
world1.make_constant(player_info.eid); world.send<GUIEvent>(GUIEvent::HIT, player, string{"hello"});
}
DinkyECS::World world2;
world1.clone_into(world2); // just count down and should get the same number
while(world.has_event<GUIEvent>()) {
auto &test1 = world1.get<Position>(player_info.eid); auto [event, entity, data] = world.recv<GUIEvent>();
auto &test2 = world2.get<Position>(player_info.eid); CHECK(event == GUIEvent::HIT);
CHECK(entity == player);
REQUIRE(test2.location.x == test1.location.x); auto &str_data = std::any_cast<string&>(data);
REQUIRE(test2.location.y == test1.location.y); CHECK(string{"hello"} == str_data);
i--;
// check for accidental reference }
test1.location.x = 100;
REQUIRE(test2.location.x != test1.location.x); CHECK(i == 0);
}
// test the facts copy over
auto &player2 = world2.get_the<Player>();
REQUIRE(player2.eid == player_info.eid); void test_confirm_copying_and_constants() {
} DinkyECS::World world1;
TEST_CASE("can destroy all entity", "[ecs-destroy]") { Player player_info{"Zed", world1.entity()};
DinkyECS::World world; world1.set_the<Player>(player_info);
auto entity = world.entity();
world1.set<Position>(player_info.eid, {10,10});
world.set<Velocity>(entity, {10,10}); world1.make_constant(player_info.eid);
world.set<Gravity>(entity, {1});
world.set<Motion>(entity, {0,0}); DinkyECS::World world2;
world1.clone_into(world2);
world.destroy(entity);
auto &test1 = world1.get<Position>(player_info.eid);
REQUIRE(!world.has<Velocity>(entity)); auto &test2 = world2.get<Position>(player_info.eid);
REQUIRE(!world.has<Gravity>(entity));
REQUIRE(!world.has<Motion>(entity)); CHECK(test2.location.x == test1.location.x);
CHECK(test2.location.y == test1.location.y);
// check for accidental reference
test1.location.x = 100;
CHECK(test2.location.x != test1.location.x);
// test the facts copy over
auto &player2 = world2.get_the<Player>();
CHECK(player2.eid == player_info.eid);
}
void test_can_destroy_all_entity() {
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);
CHECK(!world.has<Velocity>(entity));
CHECK(!world.has<Gravity>(entity));
CHECK(!world.has<Motion>(entity));
}
fuc2::Set TESTS{
.name="dinkyecs",
.tests={
TEST(test_confirm_ECS_system_works),
TEST(test_confirm_that_the_event_system_works),
TEST(test_confirm_copying_and_constants),
TEST(test_can_destroy_all_entity),
}
};
} }

25
tests/main.cpp Normal file
View file

@ -0,0 +1,25 @@
#include <fuc2/run.hpp>
using namespace fuc2;
TEST_SET(ai_tests);
TEST_SET(animation_tests);
TEST_SET(base_tests);
TEST_SET(battle_tests);
TEST_SET(camera_tests);
TEST_SET(component_tests);
TEST_SET(config_tests);
TEST_SET(dbc_tests);
TEST_SET(dinkyecs_tests);
int main(int argc, char* argv[]) {
run(ai_tests::TESTS);
run(animation_tests::TESTS);
run(base_tests::TESTS);
run(battle_tests::TESTS);
run(camera_tests::TESTS);
run(component_tests::TESTS);
run(config_tests::TESTS);
run(dbc_tests::TESTS);
run(dinkyecs_tests::TESTS);
}

View file

@ -1,12 +1,4 @@
tests = files( tests = files(
'animation.cpp',
'base.cpp',
'battle.cpp',
'camera.cpp',
'components.cpp',
'config.cpp',
'dbc.cpp',
'dinkyecs.cpp',
'event_router.cpp', 'event_router.cpp',
'fsm.cpp', 'fsm.cpp',
'inventory.cpp', 'inventory.cpp',
@ -27,6 +19,14 @@ tests = files(
) )
fuc2_tests = files( fuc2_tests = files(
'dinkyecs.cpp',
'dbc.cpp',
'ai.cpp', 'ai.cpp',
'animation.cpp',
'base.cpp',
'battle.cpp',
'camera.cpp',
'components.cpp',
'config.cpp',
'main.cpp' 'main.cpp'
) )