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.
123 lines
3.2 KiB
123 lines
3.2 KiB
#include <catch2/catch_test_macros.hpp>
|
|
#include "dbc.hpp"
|
|
#include "ai.hpp"
|
|
#include <iostream>
|
|
|
|
using namespace dbc;
|
|
using namespace nlohmann;
|
|
|
|
TEST_CASE("worldstate works", "[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);
|
|
|
|
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 != std::nullopt);
|
|
|
|
auto state = start;
|
|
|
|
for(auto& action : *result) {
|
|
state = action.apply_effect(state);
|
|
}
|
|
|
|
REQUIRE(state[ENEMY_DEAD]);
|
|
}
|
|
|
|
|
|
TEST_CASE("ai as a module like sound/sprites", "[ai]") {
|
|
ai::init("tests/ai_fixture.json");
|
|
|
|
auto start = ai::load_state("test_start");
|
|
auto goal = ai::load_state("test_goal");
|
|
|
|
auto script = ai::plan("test1", start, goal);
|
|
REQUIRE(script != std::nullopt);
|
|
|
|
auto state = start;
|
|
for(auto& action : *script) {
|
|
fmt::println("ACTION: {}", action.$name);
|
|
state = action.apply_effect(state);
|
|
}
|
|
|
|
REQUIRE(state[ai::state_id("target_dead")]);
|
|
}
|
|
|