Initial include of fuc2 to eventually replace catch2.

This commit is contained in:
Zed A. Shaw 2026-06-16 03:13:11 -04:00
parent a58439a289
commit 4deda37665
5 changed files with 228 additions and 187 deletions

View file

@ -60,6 +60,7 @@ elif build_machine.system() == 'darwin'
endif endif
catch2 = subproject('catch2').get_variable('catch2_with_main_dep') catch2 = subproject('catch2').get_variable('catch2_with_main_dep')
fuc2 = subproject('fuc2').get_variable('fuc2_dep')
fmt = subproject('fmt').get_variable('fmt_dep') fmt = subproject('fmt').get_variable('fmt_dep')
json = subproject('nlohmann_json').get_variable('nlohmann_json_dep') json = subproject('nlohmann_json').get_variable('nlohmann_json_dep')
freetype2 = subproject('freetype2').get_variable('freetype_dep') freetype2 = subproject('freetype2').get_variable('freetype_dep')
@ -114,6 +115,13 @@ executable('runtests', sources + tests,
override_options: exe_defaults, override_options: exe_defaults,
dependencies: dependencies + [catch2]) dependencies: dependencies + [catch2])
executable('fuc2it', sources + fuc2_tests,
cpp_args: cpp_args,
link_args: link_args,
include_directories: inc_dirs,
override_options: exe_defaults,
dependencies: dependencies + [fuc2])
executable('zedcaster', executable('zedcaster',
[ 'src/main.cpp' ], [ 'src/main.cpp' ],
cpp_args: cpp_args, cpp_args: cpp_args,

View file

@ -38,15 +38,17 @@ union ColorConv {
* and I guess the compiler can handle it better than shifting * and I guess the compiler can handle it better than shifting
* bits around. * bits around.
*/ */
inline RGBA lighting_calc(RGBA pixel, float dist, int level) { inline float intensity_calc(float dist, int level) {
return (float(level) * PERCENT) / (dist + 1) * LIGHT_MULTIPLIER;
}
inline RGBA lighting_calc(RGBA pixel, float intensity) {
ColorConv conv{.as_int=pixel}; ColorConv conv{.as_int=pixel};
if(conv.as_color.b < GLOW_LIMIT if(conv.as_color.b < GLOW_LIMIT
&& conv.as_color.r < GLOW_LIMIT && conv.as_color.r < GLOW_LIMIT
&& conv.as_color.g < GLOW_LIMIT) && conv.as_color.g < GLOW_LIMIT)
{ {
float intensity = (float(level) * PERCENT) / (dist + 1) * LIGHT_MULTIPLIER;
conv.as_color.r *= intensity; conv.as_color.r *= intensity;
conv.as_color.g *= intensity; conv.as_color.g *= intensity;
conv.as_color.b *= intensity; conv.as_color.b *= intensity;
@ -55,6 +57,7 @@ inline RGBA lighting_calc(RGBA pixel, float dist, int level) {
return conv.as_int; return conv.as_int;
} }
Raycaster::Raycaster(int x, int y, int width, int height) : Raycaster::Raycaster(int x, int y, int width, int height) :
$view_texture(sf::Vector2u{(unsigned int)width, (unsigned int)height}), $view_texture(sf::Vector2u{(unsigned int)width, (unsigned int)height}),
$view_sprite($view_texture), $view_sprite($view_texture),
@ -336,12 +339,14 @@ void Raycaster::cast_rays() {
// Starting texture coordinate // Starting texture coordinate
double tex_pos = (draw_start - $pitch - $height / 2 + line_height / 2) * step; double tex_pos = (draw_start - $pitch - $height / 2 + line_height / 2) * step;
int light_level = lights[map_y][map_x];
float intensity = intensity_calc(perp_wall_dist, light_level);
for(int y = draw_start; y < draw_end; y++) { for(int y = draw_start; y < draw_end; y++) {
int tex_y = (int)tex_pos & (texture_height - 1); int tex_y = (int)tex_pos & (texture_height - 1);
tex_pos += step; tex_pos += step;
RGBA pixel = texture[texture_height * tex_y + tex_x]; RGBA pixel = texture[texture_height * tex_y + tex_x];
int light_level = lights[map_y][map_x]; $pixels[pixcoord(x, y)] = lighting_calc(pixel, intensity);
$pixels[pixcoord(x, y)] = lighting_calc(pixel, perp_wall_dist, light_level);
} }
// SET THE ZBUFFER FOR THE SPRITE CASTING // SET THE ZBUFFER FOR THE SPRITE CASTING
@ -422,14 +427,15 @@ void Raycaster::draw_ceiling_floor() {
} }
// NOTE: use map_x/y to get the floor, ceiling texture. // NOTE: use map_x/y to get the floor, ceiling texture.
float intensity = intensity_calc(row_distance, light_level);
// FLOOR // FLOOR
color = floor_texture[texture_width * ty + tx]; color = floor_texture[texture_width * ty + tx];
$pixels[pixcoord(x, y)] = lighting_calc(color, row_distance, light_level); $pixels[pixcoord(x, y)] = lighting_calc(color, intensity);
// CEILING // CEILING
color = ceiling_texture[texture_width * ty + tx]; color = ceiling_texture[texture_width * ty + tx];
$pixels[pixcoord(x, $height - y - 1)] = lighting_calc(color, row_distance, light_level); $pixels[pixcoord(x, $height - y - 1)] = lighting_calc(color, intensity);
} }
} }
} }

View file

@ -1,14 +1,16 @@
#include <catch2/catch_test_macros.hpp>
#include "dbc.hpp" #include "dbc.hpp"
#include "ai/ai.hpp" #include "ai/ai.hpp"
#include "ai/ai_debug.hpp" #include "ai/ai_debug.hpp"
#include <iostream> #include <iostream>
#include "game/rituals.hpp" #include "game/rituals.hpp"
#include <fuc2/testing.hpp>
using namespace dbc; using namespace dbc;
using namespace nlohmann; using namespace nlohmann;
using namespace fuc2;
TEST_CASE("state and actions work", "[ai]") { namespace ai_tests {
void test_state_and_actions() {
enum StateNames { enum StateNames {
ENEMY_IN_RANGE, ENEMY_IN_RANGE,
ENEMY_DEAD ENEMY_DEAD
@ -29,38 +31,38 @@ TEST_CASE("state and actions work", "[ai]") {
move_closer.needs(ENEMY_IN_RANGE, false); move_closer.needs(ENEMY_IN_RANGE, false);
move_closer.effect(ENEMY_IN_RANGE, true); move_closer.effect(ENEMY_IN_RANGE, true);
REQUIRE(move_closer.can_effect(start)); CHECK(move_closer.can_effect(start));
auto after_move_state = move_closer.apply_effect(start); auto after_move_state = move_closer.apply_effect(start);
REQUIRE(start[ENEMY_IN_RANGE] == false); CHECK(start[ENEMY_IN_RANGE] == false);
REQUIRE(after_move_state[ENEMY_IN_RANGE] == true); CHECK(after_move_state[ENEMY_IN_RANGE] == true);
REQUIRE(after_move_state[ENEMY_DEAD] == false); CHECK(after_move_state[ENEMY_DEAD] == false);
// start is clean but after move is dirty // start is clean but after move is dirty
REQUIRE(move_closer.can_effect(start)); CHECK(move_closer.can_effect(start));
REQUIRE(!move_closer.can_effect(after_move_state)); CHECK(!move_closer.can_effect(after_move_state));
REQUIRE(ai::distance_to_goal(start, after_move_state) == 1); CHECK(ai::distance_to_goal(start, after_move_state) == 1);
ai::Action kill_it("kill_it", 10); ai::Action kill_it("kill_it", 10);
kill_it.needs(ENEMY_IN_RANGE, true); kill_it.needs(ENEMY_IN_RANGE, true);
kill_it.needs(ENEMY_DEAD, false); kill_it.needs(ENEMY_DEAD, false);
kill_it.effect(ENEMY_DEAD, true); kill_it.effect(ENEMY_DEAD, true);
REQUIRE(!kill_it.can_effect(start)); CHECK(!kill_it.can_effect(start));
REQUIRE(kill_it.can_effect(after_move_state)); CHECK(kill_it.can_effect(after_move_state));
auto after_kill_state = kill_it.apply_effect(after_move_state); auto after_kill_state = kill_it.apply_effect(after_move_state);
REQUIRE(!kill_it.can_effect(after_kill_state)); CHECK(!kill_it.can_effect(after_kill_state));
REQUIRE(ai::distance_to_goal(after_move_state, after_kill_state) == 1); CHECK(ai::distance_to_goal(after_move_state, after_kill_state) == 1);
kill_it.ignore(ENEMY_IN_RANGE); kill_it.ignore(ENEMY_IN_RANGE);
REQUIRE(kill_it.can_effect(after_move_state)); CHECK(kill_it.can_effect(after_move_state));
actions.push_back(kill_it); actions.push_back(kill_it);
actions.push_back(move_closer); actions.push_back(move_closer);
REQUIRE(start != goal); CHECK(start != goal);
} }
TEST_CASE("basic feature tests", "[ai]") { void test_basic_features() {
enum StateNames { enum StateNames {
ENEMY_IN_RANGE, ENEMY_IN_RANGE,
ENEMY_DEAD ENEMY_DEAD
@ -97,7 +99,7 @@ TEST_CASE("basic feature tests", "[ai]") {
actions.push_back(move_closer); actions.push_back(move_closer);
auto result = ai::plan_actions(actions, start, goal); auto result = ai::plan_actions(actions, start, goal);
REQUIRE(result.complete); CHECK(result.complete);
auto state = start; auto state = start;
@ -105,11 +107,11 @@ TEST_CASE("basic feature tests", "[ai]") {
state = action.apply_effect(state); state = action.apply_effect(state);
} }
REQUIRE(state[ENEMY_DEAD]); CHECK(state[ENEMY_DEAD]);
} }
TEST_CASE("ai as a module like sound/sprites", "[ai]") { void test_ai_module_like_sound() {
ai::reset(); ai::reset();
ai::init("tests/ai_fixture.json"); ai::init("tests/ai_fixture.json");
@ -117,7 +119,7 @@ TEST_CASE("ai as a module like sound/sprites", "[ai]") {
auto goal = ai::load_state("test_goal"); auto goal = ai::load_state("test_goal");
auto a_plan = ai::plan("test1", start, goal); auto a_plan = ai::plan("test1", start, goal);
REQUIRE(a_plan.complete); CHECK(a_plan.complete);
auto state = start; auto state = start;
for(auto& action : a_plan.script) { for(auto& action : a_plan.script) {
@ -125,10 +127,10 @@ TEST_CASE("ai as a module like sound/sprites", "[ai]") {
state = action.apply_effect(state); state = action.apply_effect(state);
} }
REQUIRE(ai::test(state, "target_dead")); CHECK(ai::test(state, "target_dead"));
} }
TEST_CASE("ai autowalker ai test", "[ai]") { void test_ai_autowalker() {
ai::reset(); ai::reset();
ai::init("ai"); ai::init("ai");
auto start = ai::load_state("Host::initial_state"); auto start = ai::load_state("Host::initial_state");
@ -139,11 +141,11 @@ TEST_CASE("ai autowalker ai test", "[ai]") {
// find an enemy and kill them // find an enemy and kill them
auto a_plan = ai::plan("Host::actions", start, goal); auto a_plan = ai::plan("Host::actions", start, goal);
REQUIRE(!a_plan.complete); CHECK(!a_plan.complete);
auto result = ai::dump_script("\n\nWALKER KILL STUFF", start, a_plan.script); auto result = ai::dump_script("\n\nWALKER KILL STUFF", start, a_plan.script);
REQUIRE(ai::test(result, "enemy_found")); CHECK(ai::test(result, "enemy_found"));
REQUIRE(!ai::test(result, "no_more_enemies")); CHECK(!ai::test(result, "no_more_enemies"));
// health is low, go heal // health is low, go heal
ai::set(result, "health_good", false); ai::set(result, "health_good", false);
@ -151,24 +153,24 @@ TEST_CASE("ai autowalker ai test", "[ai]") {
ai::set(result, "enemy_found", false); ai::set(result, "enemy_found", false);
ai::set(result, "have_healing", true); ai::set(result, "have_healing", true);
ai::set(result, "have_item", true); ai::set(result, "have_item", true);
REQUIRE(!ai::test(result, "health_good")); CHECK(!ai::test(result, "health_good"));
auto health_plan = ai::plan("Host::actions", result, goal); auto health_plan = ai::plan("Host::actions", result, goal);
result = ai::dump_script("\n\nWALKER NEED HEALTH", result, health_plan.script); result = ai::dump_script("\n\nWALKER NEED HEALTH", result, health_plan.script);
REQUIRE(!health_plan.complete); CHECK(!health_plan.complete);
REQUIRE(ai::test(result, "health_good")); CHECK(ai::test(result, "health_good"));
// health is good, enemies dead, go get stuff // health is good, enemies dead, go get stuff
ai::set(result, "no_more_enemies", true); ai::set(result, "no_more_enemies", true);
REQUIRE(ai::test(result, "no_more_enemies")); CHECK(ai::test(result, "no_more_enemies"));
auto new_plan = ai::plan("Host::actions", result, goal); auto new_plan = ai::plan("Host::actions", result, goal);
result = ai::dump_script("\n\nWALKER COLLECT ITEMS", result, new_plan.script); result = ai::dump_script("\n\nWALKER COLLECT ITEMS", result, new_plan.script);
REQUIRE(ai::test(result, "no_more_items")); CHECK(ai::test(result, "no_more_items"));
REQUIRE(ai::test(result, "no_more_enemies")); CHECK(ai::test(result, "no_more_enemies"));
} }
TEST_CASE("Confirm EntityAI behaves as expected", "[ai]") { void test_confirm_EntityAI_behaves() {
ai::reset(); ai::reset();
ai::init("ai"); ai::init("ai");
auto ai_start = ai::load_state("Enemy::initial_state"); auto ai_start = ai::load_state("Enemy::initial_state");
@ -178,26 +180,26 @@ TEST_CASE("Confirm EntityAI behaves as expected", "[ai]") {
enemy.set_state("detect_enemy", true); enemy.set_state("detect_enemy", true);
enemy.update(); enemy.update();
REQUIRE(enemy.wants_to("find_enemy")); CHECK(enemy.wants_to("find_enemy"));
enemy.set_state("enemy_found", true); enemy.set_state("enemy_found", true);
enemy.set_state("in_combat", true); enemy.set_state("in_combat", true);
enemy.update(); enemy.update();
REQUIRE(enemy.wants_to("kill_enemy")); CHECK(enemy.wants_to("kill_enemy"));
enemy.set_state("have_item", true); enemy.set_state("have_item", true);
enemy.set_state("have_healing", true); enemy.set_state("have_healing", true);
enemy.set_state("in_combat", false); enemy.set_state("in_combat", false);
enemy.set_state("health_good", false); enemy.set_state("health_good", false);
enemy.update(); enemy.update();
REQUIRE(enemy.wants_to("use_healing")); CHECK(enemy.wants_to("use_healing"));
enemy.set_state("have_healing", false); enemy.set_state("have_healing", false);
enemy.set_state("tough_personality", true); enemy.set_state("tough_personality", true);
enemy.set_state("in_combat", true); enemy.set_state("in_combat", true);
enemy.set_state("health_good", true); enemy.set_state("health_good", true);
enemy.update(); enemy.update();
REQUIRE(enemy.wants_to("kill_enemy")); CHECK(enemy.wants_to("kill_enemy"));
fmt::println("\n\n\n\n=============================\n\n\n\n"); fmt::println("\n\n\n\n=============================\n\n\n\n");
enemy.set_state("have_healing", false); enemy.set_state("have_healing", false);
@ -205,5 +207,17 @@ TEST_CASE("Confirm EntityAI behaves as expected", "[ai]") {
enemy.set_state("in_combat", true); enemy.set_state("in_combat", true);
enemy.set_state("health_good", false); enemy.set_state("health_good", false);
enemy.update(); enemy.update();
REQUIRE(enemy.wants_to("run_away")); CHECK(enemy.wants_to("run_away"));
}
fuc2::Set TESTS{
.name="ai functionality",
.tests={
TEST(test_state_and_actions),
TEST(test_basic_features),
TEST(test_ai_module_like_sound),
TEST(test_ai_autowalker),
TEST(test_confirm_EntityAI_behaves),
}
};
} }

View file

@ -1,5 +1,4 @@
tests = files( tests = files(
'ai.cpp',
'animation.cpp', 'animation.cpp',
'base.cpp', 'base.cpp',
'battle.cpp', 'battle.cpp',
@ -26,3 +25,8 @@ tests = files(
'systems.cpp', 'systems.cpp',
'textures.cpp', 'textures.cpp',
) )
fuc2_tests = files(
'ai.cpp',
'main.cpp'
)

9
wraps/fuc2.wrap Normal file
View file

@ -0,0 +1,9 @@
[wrap-git]
directory=fuc2-0.1.0
url=https://lcthw.dev/cpp/fuc2.git
revision=HEAD
depth=1
method=meson
[provide]
fuc2 = fuc2_dep