parent
a079f882df
commit
b2c1b220ac
@ -0,0 +1,85 @@ |
||||
{ |
||||
"profile": { |
||||
"target_acquired": 0, |
||||
"target_lost": 1, |
||||
"target_in_warhead_range": 2, |
||||
"target_dead": 3 |
||||
}, |
||||
"actions": [ |
||||
{ |
||||
"name": "searchSpiral", |
||||
"cost": 10, |
||||
"needs": { |
||||
"target_acquired": false, |
||||
"target_lost": true |
||||
}, |
||||
"effects": { |
||||
"target_acquired": true |
||||
} |
||||
}, |
||||
{ |
||||
"name": "searchSerpentine", |
||||
"cost": 5, |
||||
"needs": { |
||||
"target_acquired": false, |
||||
"target_lost": false |
||||
}, |
||||
"effects": { |
||||
"target_acquired": true |
||||
} |
||||
}, |
||||
{ |
||||
"name": "searchSpiral", |
||||
"cost": 5, |
||||
"needs": { |
||||
"target_acquired": false, |
||||
"target_lost": true |
||||
}, |
||||
"effects": { |
||||
"target_acquired": true |
||||
} |
||||
}, |
||||
{ |
||||
"name": "interceptTarget", |
||||
"cost": 5, |
||||
"needs": { |
||||
"target_acquired": true, |
||||
"target_dead": false |
||||
}, |
||||
"effects": { |
||||
"target_in_warhead_range": true |
||||
} |
||||
}, |
||||
{ |
||||
"name": "detonateNearTarget", |
||||
"cost": 5, |
||||
"needs": { |
||||
"target_in_warhead_range": true, |
||||
"target_acquired": true, |
||||
"target_dead": false |
||||
}, |
||||
"effects": { |
||||
"target_dead": true |
||||
} |
||||
} |
||||
], |
||||
"states": { |
||||
"test_start": { |
||||
"target_acquired": false, |
||||
"target_lost": true, |
||||
"target_in_warhead_range": false, |
||||
"target_dead": false |
||||
}, |
||||
"test_goal": { |
||||
"target_dead": true |
||||
} |
||||
}, |
||||
"scripts": { |
||||
"test1": [ |
||||
"searchSpiral", |
||||
"searchSerpentine", |
||||
"searchSpiral", |
||||
"interceptTarget", |
||||
"detonateNearTarget"] |
||||
} |
||||
} |
@ -0,0 +1,220 @@ |
||||
#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("wargame test from cppAI", "[ai]") { |
||||
std::vector<ai::Action> actions; |
||||
auto profile = R"({ |
||||
"target_acquired": 0, |
||||
"target_lost": 1, |
||||
"target_in_warhead_range": 2, |
||||
"target_dead": 3 |
||||
})"_json; |
||||
|
||||
// Now establish all the possible actions for the action pool
|
||||
// In this example we're providing the AI some different FPS actions
|
||||
auto config = R"({ |
||||
"name": "searchSpiral", |
||||
"cost": 5, |
||||
"needs": { |
||||
"target_acquired": false, |
||||
"target_lost": true |
||||
}, |
||||
"effects": { |
||||
"target_acquired": true |
||||
} |
||||
})"_json; |
||||
auto spiral = ai::config_action(profile, config); |
||||
actions.push_back(spiral); |
||||
|
||||
config = R"({ |
||||
"name": "searchSerpentine", |
||||
"cost": 5, |
||||
"needs": { |
||||
"target_acquired": false, |
||||
"target_lost": false |
||||
}, |
||||
"effects": { |
||||
"target_acquired": true |
||||
} |
||||
})"_json; |
||||
auto serpentine = ai::config_action(profile, config); |
||||
actions.push_back(serpentine); |
||||
|
||||
config = R"({ |
||||
"name": "interceptTarget", |
||||
"cost": 5, |
||||
"needs": { |
||||
"target_acquired": true, |
||||
"target_dead": false |
||||
}, |
||||
"effects": { |
||||
"target_in_warhead_range": true |
||||
} |
||||
})"_json; |
||||
auto intercept = ai::config_action(profile, config); |
||||
actions.push_back(intercept); |
||||
|
||||
config = R"({ |
||||
"name": "detonateNearTarget", |
||||
"cost": 5, |
||||
"needs": { |
||||
"target_in_warhead_range": true, |
||||
"target_acquired": true, |
||||
"target_dead": false |
||||
}, |
||||
"effects": { |
||||
"target_dead": true |
||||
} |
||||
})"_json; |
||||
|
||||
auto detonateNearTarget = ai::config_action(profile, config); |
||||
actions.push_back(detonateNearTarget); |
||||
|
||||
// Here's the initial state...
|
||||
config = R"({ |
||||
"target_acquired": false, |
||||
"target_lost": true, |
||||
"target_in_warhead_range": false, |
||||
"target_dead": false |
||||
})"_json; |
||||
auto initial_state = ai::config_state(profile, config); |
||||
|
||||
// ...and the goal state
|
||||
config = R"({ |
||||
"target_dead": true |
||||
})"_json; |
||||
auto goal_target_dead = ai::config_state(profile, config); |
||||
|
||||
auto result = ai::plan_actions(actions, initial_state, goal_target_dead); |
||||
REQUIRE(result != std::nullopt); |
||||
|
||||
auto state = initial_state; |
||||
|
||||
for(auto& action : *result) { |
||||
fmt::println("ACTION: {}", action.$name); |
||||
state = action.apply_effect(state); |
||||
} |
||||
|
||||
REQUIRE(state[profile["target_dead"]]); |
||||
} |
||||
|
||||
TEST_CASE("ai as a module like sound/sprites", "[ai]") { |
||||
ai::init(); |
||||
|
||||
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")]); |
||||
|
||||
} |
Loading…
Reference in new issue