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,209 +1,223 @@
#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 {
enum StateNames { void test_state_and_actions() {
ENEMY_IN_RANGE, enum StateNames {
ENEMY_DEAD ENEMY_IN_RANGE,
}; ENEMY_DEAD
};
ai::State goal; ai::State goal;
ai::State start; ai::State start;
std::vector<ai::Action> actions; std::vector<ai::Action> actions;
// start off enemy not dead and not in range // start off enemy not dead and not in range
start[ENEMY_DEAD] = false; start[ENEMY_DEAD] = false;
start[ENEMY_IN_RANGE] = false; start[ENEMY_IN_RANGE] = false;
// end goal is enemy is dead // end goal is enemy is dead
goal[ENEMY_DEAD] = true; goal[ENEMY_DEAD] = true;
ai::Action move_closer("move_closer", 10); ai::Action move_closer("move_closer", 10);
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]") {
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]); void test_basic_features() {
} enum StateNames {
ENEMY_IN_RANGE,
ENEMY_DEAD
};
ai::State goal;
ai::State start;
std::vector<ai::Action> actions;
TEST_CASE("ai as a module like sound/sprites", "[ai]") { // start off enemy not dead and not in range
ai::reset(); start[ENEMY_DEAD] = false;
ai::init("tests/ai_fixture.json"); start[ENEMY_IN_RANGE] = false;
auto start = ai::load_state("test_start"); // end goal is enemy is dead
auto goal = ai::load_state("test_goal"); goal[ENEMY_DEAD] = true;
auto a_plan = ai::plan("test1", start, goal); ai::Action move_closer("move_closer", 10);
REQUIRE(a_plan.complete); move_closer.needs(ENEMY_IN_RANGE, false);
move_closer.effect(ENEMY_IN_RANGE, true);
auto state = start; ai::Action kill_it("kill_it", 10);
for(auto& action : a_plan.script) { kill_it.needs(ENEMY_IN_RANGE, true);
fmt::println("ACTION: {}", action.name); // this is duplicated on purpose to confirm that setting
state = action.apply_effect(state); // 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);
CHECK(result.complete);
auto state = start;
for(auto& action : result.script) {
state = action.apply_effect(state);
}
CHECK(state[ENEMY_DEAD]);
} }
REQUIRE(ai::test(state, "target_dead"));
} void test_ai_module_like_sound() {
ai::reset();
TEST_CASE("ai autowalker ai test", "[ai]") { ai::init("tests/ai_fixture.json");
ai::reset();
ai::init("ai"); auto start = ai::load_state("test_start");
auto start = ai::load_state("Host::initial_state"); auto goal = ai::load_state("test_goal");
auto goal = ai::load_state("Host::final_state");
int enemy_count = 5; auto a_plan = ai::plan("test1", start, goal);
CHECK(a_plan.complete);
ai::set(start, "no_more_enemies", enemy_count == 0);
auto state = start;
// find an enemy and kill them for(auto& action : a_plan.script) {
auto a_plan = ai::plan("Host::actions", start, goal); fmt::println("ACTION: {}", action.name);
REQUIRE(!a_plan.complete); state = action.apply_effect(state);
}
auto result = ai::dump_script("\n\nWALKER KILL STUFF", start, a_plan.script);
REQUIRE(ai::test(result, "enemy_found")); CHECK(ai::test(state, "target_dead"));
REQUIRE(!ai::test(result, "no_more_enemies")); }
// health is low, go heal void test_ai_autowalker() {
ai::set(result, "health_good", false); ai::reset();
ai::set(result, "in_combat", false); ai::init("ai");
ai::set(result, "enemy_found", false); auto start = ai::load_state("Host::initial_state");
ai::set(result, "have_healing", true); auto goal = ai::load_state("Host::final_state");
ai::set(result, "have_item", true); int enemy_count = 5;
REQUIRE(!ai::test(result, "health_good"));
ai::set(start, "no_more_enemies", enemy_count == 0);
auto health_plan = ai::plan("Host::actions", result, goal);
result = ai::dump_script("\n\nWALKER NEED HEALTH", result, health_plan.script); // find an enemy and kill them
REQUIRE(!health_plan.complete); auto a_plan = ai::plan("Host::actions", start, goal);
REQUIRE(ai::test(result, "health_good")); CHECK(!a_plan.complete);
// health is good, enemies dead, go get stuff auto result = ai::dump_script("\n\nWALKER KILL STUFF", start, a_plan.script);
ai::set(result, "no_more_enemies", true); CHECK(ai::test(result, "enemy_found"));
REQUIRE(ai::test(result, "no_more_enemies")); CHECK(!ai::test(result, "no_more_enemies"));
auto new_plan = ai::plan("Host::actions", result, goal); // health is low, go heal
result = ai::dump_script("\n\nWALKER COLLECT ITEMS", result, new_plan.script); ai::set(result, "health_good", false);
REQUIRE(ai::test(result, "no_more_items")); ai::set(result, "in_combat", false);
REQUIRE(ai::test(result, "no_more_enemies")); ai::set(result, "enemy_found", false);
} ai::set(result, "have_healing", true);
ai::set(result, "have_item", true);
TEST_CASE("Confirm EntityAI behaves as expected", "[ai]") { CHECK(!ai::test(result, "health_good"));
ai::reset();
ai::init("ai"); auto health_plan = ai::plan("Host::actions", result, goal);
auto ai_start = ai::load_state("Enemy::initial_state"); result = ai::dump_script("\n\nWALKER NEED HEALTH", result, health_plan.script);
auto ai_goal = ai::load_state("Enemy::final_state"); CHECK(!health_plan.complete);
CHECK(ai::test(result, "health_good"));
ai::EntityAI enemy("Enemy::actions", ai_start, ai_goal);
// health is good, enemies dead, go get stuff
enemy.set_state("detect_enemy", true); ai::set(result, "no_more_enemies", true);
enemy.update(); CHECK(ai::test(result, "no_more_enemies"));
REQUIRE(enemy.wants_to("find_enemy"));
auto new_plan = ai::plan("Host::actions", result, goal);
enemy.set_state("enemy_found", true); result = ai::dump_script("\n\nWALKER COLLECT ITEMS", result, new_plan.script);
enemy.set_state("in_combat", true); CHECK(ai::test(result, "no_more_items"));
enemy.update(); CHECK(ai::test(result, "no_more_enemies"));
REQUIRE(enemy.wants_to("kill_enemy")); }
enemy.set_state("have_item", true); void test_confirm_EntityAI_behaves() {
enemy.set_state("have_healing", true); ai::reset();
enemy.set_state("in_combat", false); ai::init("ai");
enemy.set_state("health_good", false); auto ai_start = ai::load_state("Enemy::initial_state");
enemy.update(); auto ai_goal = ai::load_state("Enemy::final_state");
REQUIRE(enemy.wants_to("use_healing"));
ai::EntityAI enemy("Enemy::actions", ai_start, ai_goal);
enemy.set_state("have_healing", false);
enemy.set_state("tough_personality", true); enemy.set_state("detect_enemy", true);
enemy.set_state("in_combat", true); enemy.update();
enemy.set_state("health_good", true); CHECK(enemy.wants_to("find_enemy"));
enemy.update();
REQUIRE(enemy.wants_to("kill_enemy")); enemy.set_state("enemy_found", true);
enemy.set_state("in_combat", true);
fmt::println("\n\n\n\n=============================\n\n\n\n"); enemy.update();
enemy.set_state("have_healing", false); CHECK(enemy.wants_to("kill_enemy"));
enemy.set_state("tough_personality", false);
enemy.set_state("in_combat", true); enemy.set_state("have_item", true);
enemy.set_state("health_good", false); enemy.set_state("have_healing", true);
enemy.update(); enemy.set_state("in_combat", false);
REQUIRE(enemy.wants_to("run_away")); enemy.set_state("health_good", false);
enemy.update();
CHECK(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();
CHECK(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();
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