Exploring raycasters and possibly make a little "doom like" game based on it.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
raycaster/tests/ai.cpp

232 lines
6.5 KiB

#include <catch2/catch_test_macros.hpp>
#include "dbc.hpp"
#include "ai.hpp"
#include <iostream>
#include "ai_debug.hpp"
#include "rituals.hpp"
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("assets/ai.json");
auto start = ai::load_state("Walker::initial_state");
auto goal = ai::load_state("Walker::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("Walker::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, "enemy_dead"));
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("Walker::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("Walker::actions", result, goal);
result = ai::dump_script("\n\nWALKER COMPLETE", result, new_plan.script);
REQUIRE(new_plan.complete);
REQUIRE(ai::test(result, "enemy_found"));
REQUIRE(ai::test(result, "enemy_dead"));
REQUIRE(ai::test(result, "no_more_enemies"));
}
TEST_CASE("Confirm EntityAI behaves as expected", "[ai]") {
ai::reset();
ai::init("assets/ai.json");
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.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"));
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"));
}
TEST_CASE("confirm that cycles are avoided/detected", "[ai]") {
ai::reset();
ai::init("tests/cyclic_rituals.json");
auto start = ai::load_state("initial");
auto goal = ai::load_state("final");
RitualAI ritual("actions", start, goal);
ritual.reset();
ritual.set_state("has_magick", true);
ritual.set_state("cursed_item", true);
ritual.set_state("shiny_bauble", true);
bool it_throws = false;
try { ritual.update(); } catch(...) { it_throws = true; }
REQUIRE(it_throws);
fmt::println("\n\n------------ CYCLES AVOIDED");
ritual.dump();
}