Autowalker is now using the GOAP AI system and works way better. Still quite a lot of jank in the code but that'll get removed over time. Next thing is being able to detect when its near an item/enemy and properly react.
This commit is contained in:
parent
ff81c78d13
commit
d15c9b12fd
9 changed files with 84 additions and 47 deletions
10
ai.cpp
10
ai.cpp
|
@ -22,17 +22,17 @@ namespace ai {
|
||||||
Action result(config["name"], config["cost"]);
|
Action result(config["name"], config["cost"]);
|
||||||
|
|
||||||
check(config.contains("needs"),
|
check(config.contains("needs"),
|
||||||
fmt::format("config_action: no 'needs' field", result.$name));
|
fmt::format("config_action: no 'needs' field", result.name));
|
||||||
check(config.contains("effects"),
|
check(config.contains("effects"),
|
||||||
fmt::format("config_action: no 'effects' field", result.$name));
|
fmt::format("config_action: no 'effects' field", result.name));
|
||||||
|
|
||||||
for(auto& [name_key, value] : config["needs"].items()) {
|
for(auto& [name_key, value] : config["needs"].items()) {
|
||||||
check(profile.contains(name_key), fmt::format("config_action: profile does not have name {}", result.$name, name_key));
|
check(profile.contains(name_key), fmt::format("config_action: profile does not have name {}", result.name, name_key));
|
||||||
result.needs(profile.at(name_key), bool(value));
|
result.needs(profile.at(name_key), bool(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
for(auto& [name_key, value] : config["effects"].items()) {
|
for(auto& [name_key, value] : config["effects"].items()) {
|
||||||
check(profile.contains(name_key), fmt::format("config_action: profile does not have name {}", result.$name, name_key));
|
check(profile.contains(name_key), fmt::format("config_action: profile does not have name {}", result.name, name_key));
|
||||||
|
|
||||||
result.effect(profile.at(name_key), bool(value));
|
result.effect(profile.at(name_key), bool(value));
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@ namespace ai {
|
||||||
auto& actions = config["actions"];
|
auto& actions = config["actions"];
|
||||||
for(auto& action_vars : actions) {
|
for(auto& action_vars : actions) {
|
||||||
auto the_action = config_action(AIMGR.profile, action_vars);
|
auto the_action = config_action(AIMGR.profile, action_vars);
|
||||||
AIMGR.actions.insert_or_assign(the_action.$name, the_action);
|
AIMGR.actions.insert_or_assign(the_action.name, the_action);
|
||||||
}
|
}
|
||||||
|
|
||||||
// load all states
|
// load all states
|
||||||
|
|
|
@ -22,7 +22,7 @@ namespace ai {
|
||||||
}
|
}
|
||||||
|
|
||||||
void dump_action(AIProfile& profile, Action& action) {
|
void dump_action(AIProfile& profile, Action& action) {
|
||||||
fmt::println(" --ACTION: {}, cost={}", action.$name, action.$cost);
|
fmt::println(" --ACTION: {}, cost={}", action.name, action.cost);
|
||||||
|
|
||||||
fmt::println(" PRECONDS:");
|
fmt::println(" PRECONDS:");
|
||||||
dump_only(profile, action.$positive_preconds, true, true);
|
dump_only(profile, action.$positive_preconds, true, true);
|
||||||
|
@ -39,7 +39,7 @@ namespace ai {
|
||||||
dump_state(profile, start);
|
dump_state(profile, start);
|
||||||
fmt::print("% ACTIONS PLANNED:");
|
fmt::print("% ACTIONS PLANNED:");
|
||||||
for(auto& action : script) {
|
for(auto& action : script) {
|
||||||
fmt::print("{} ", action.$name);
|
fmt::print("{} ", action.name);
|
||||||
}
|
}
|
||||||
fmt::print("\n");
|
fmt::print("\n");
|
||||||
|
|
||||||
|
|
|
@ -89,9 +89,12 @@ bool Autowalker::path_player(Pathing& paths, Point& target_out) {
|
||||||
bool found = paths.random_walk(target_out, false, PATHING_TOWARD);
|
bool found = paths.random_walk(target_out, false, PATHING_TOWARD);
|
||||||
|
|
||||||
if(!found) {
|
if(!found) {
|
||||||
dbc::log("no neighbor found in any direction, aborting autowalk");
|
// failed to find a linear path, try diagonal
|
||||||
matrix::dump("NO TOWARD", paths.$paths, target_out.x, target_out.y);
|
if(!paths.random_walk(target_out, false, PATHING_TOWARD, MOVE_DIAGONAL)) {
|
||||||
return false;
|
dbc::log("couldn't find a diagonal direction");
|
||||||
|
matrix::dump("MOVE FAIL PATHS", paths.$paths, target_out.x, target_out.y);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!fsm.$level.map->can_move(target_out)) {
|
if(!fsm.$level.map->can_move(target_out)) {
|
||||||
|
@ -111,6 +114,11 @@ void Autowalker::rotate_player(Point current, Point target) {
|
||||||
int facing = fsm.$main_ui.$compass_dir;
|
int facing = fsm.$main_ui.$compass_dir;
|
||||||
int target_facing = 0;
|
int target_facing = 0;
|
||||||
|
|
||||||
|
/* This is a massive pile of garbage. Need a way
|
||||||
|
* to determine player facing direction without
|
||||||
|
* hacking into the compass, and also do accurate
|
||||||
|
* turns.
|
||||||
|
*/
|
||||||
if(delta_x == -1 && delta_y == 0) {
|
if(delta_x == -1 && delta_y == 0) {
|
||||||
// west
|
// west
|
||||||
target_facing = 4;
|
target_facing = 4;
|
||||||
|
@ -123,9 +131,21 @@ void Autowalker::rotate_player(Point current, Point target) {
|
||||||
} else if(delta_x == 0 && delta_y == -1) {
|
} else if(delta_x == 0 && delta_y == -1) {
|
||||||
// north
|
// north
|
||||||
target_facing = 6;
|
target_facing = 6;
|
||||||
|
} else if(delta_x == 1 && delta_y == -1) {
|
||||||
|
// north east
|
||||||
|
target_facing = 5;
|
||||||
|
} else if(delta_x == 1 && delta_y == 1) {
|
||||||
|
// south east
|
||||||
|
target_facing = 1;
|
||||||
|
} else if(delta_x == -1 && delta_y == 1) {
|
||||||
|
// south west
|
||||||
|
target_facing = 3;
|
||||||
|
} else if(delta_x == -1 && delta_y == -1) {
|
||||||
|
// north west
|
||||||
|
target_facing = 5;
|
||||||
} else {
|
} else {
|
||||||
dbc::sentinel(
|
dbc::sentinel(
|
||||||
fmt::format("got more than 4 direction result: "
|
fmt::format("got more than 8 direction result: "
|
||||||
"current={},{} "
|
"current={},{} "
|
||||||
"target={},{} "
|
"target={},{} "
|
||||||
"delta={},{} ",
|
"delta={},{} ",
|
||||||
|
@ -197,31 +217,30 @@ void Autowalker::autowalk() {
|
||||||
|
|
||||||
// need a test for plan complete and only action is END
|
// need a test for plan complete and only action is END
|
||||||
for(auto action : a_plan.script) {
|
for(auto action : a_plan.script) {
|
||||||
if(action.$name == "find_enemy") {
|
if(action.name == "find_enemy") {
|
||||||
// this is where to test if enemy found and update state
|
// this is where to test if enemy found and update state
|
||||||
fmt::println("FINDING AN ENEMY");
|
fmt::println("FINDING AN ENEMY");
|
||||||
auto paths = path_to_enemies();
|
auto paths = path_to_enemies();
|
||||||
auto pos = get_current_position();
|
|
||||||
matrix::dump("FINDING", paths.$paths, pos.x, pos.y);
|
|
||||||
process_move(paths);
|
process_move(paths);
|
||||||
} else if(action.$name == "kill_enemy") {
|
send_event(gui::Event::ATTACK);
|
||||||
|
} else if(action.name == "kill_enemy") {
|
||||||
fmt::println("KILLING ENEMY");
|
fmt::println("KILLING ENEMY");
|
||||||
process_combat();
|
process_combat();
|
||||||
} else if(action.$name == "find_healing") {
|
} else if(action.name == "find_healing") {
|
||||||
fmt::println("FINDING HEALING");
|
fmt::println("FINDING HEALING");
|
||||||
auto paths = path_to_items();
|
auto paths = path_to_items();
|
||||||
process_move(paths);
|
process_move(paths);
|
||||||
// do the path to healing thing
|
// do the path to healing thing
|
||||||
} else if(action.$name == "collect_items") {
|
} else if(action.name == "collect_items") {
|
||||||
fmt::println("COLLECTING ITEMS");
|
fmt::println("COLLECTING ITEMS");
|
||||||
auto paths = path_to_items();
|
auto paths = path_to_items();
|
||||||
process_move(paths);
|
process_move(paths);
|
||||||
// path to the items and get them all
|
// path to the items and get them all
|
||||||
} else if(action.$name == "END") {
|
} else if(action == ai::FINAL_ACTION) {
|
||||||
fmt::println("END STATE, complete? {}", a_plan.complete);
|
fmt::println("END STATE, complete? {}", a_plan.complete);
|
||||||
fsm.autowalking = false;
|
fsm.autowalking = false;
|
||||||
} else {
|
} else {
|
||||||
fmt::println("Unknown action: {}", action.$name);
|
fmt::println("Unknown action: {}", action.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
move_attempts++;
|
move_attempts++;
|
||||||
|
|
26
goap.cpp
26
goap.cpp
|
@ -31,6 +31,11 @@ namespace ai {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Action::ignore(int name) {
|
||||||
|
$positive_preconds[name] = false;
|
||||||
|
$negative_preconds[name] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool Action::can_effect(State& state) {
|
bool Action::can_effect(State& state) {
|
||||||
return ((state & $positive_preconds) == $positive_preconds) &&
|
return ((state & $positive_preconds) == $positive_preconds) &&
|
||||||
|
@ -41,16 +46,15 @@ namespace ai {
|
||||||
return (state | $positive_effects) & ~$negative_effects;
|
return (state | $positive_effects) & ~$negative_effects;
|
||||||
}
|
}
|
||||||
|
|
||||||
int distance_to_goal(State& from, State& to) {
|
int distance_to_goal(State from, State to, Action& action) {
|
||||||
auto result = from ^ to;
|
auto result = from ^ to;
|
||||||
return result.count();
|
return result.count() + action.cost;
|
||||||
}
|
}
|
||||||
|
|
||||||
Script reconstruct_path(std::unordered_map<Action, Action>& came_from, Action& current) {
|
Script reconstruct_path(std::unordered_map<Action, Action>& came_from, Action& current) {
|
||||||
Script total_path{current};
|
Script total_path{current};
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
while(came_from.contains(current) && count++ < 10) {
|
while(came_from.contains(current)) {
|
||||||
current = came_from.at(current);
|
current = came_from.at(current);
|
||||||
if(current != FINAL_ACTION) {
|
if(current != FINAL_ACTION) {
|
||||||
total_path.push_front(current);
|
total_path.push_front(current);
|
||||||
|
@ -60,12 +64,12 @@ namespace ai {
|
||||||
return total_path;
|
return total_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int h(State& start, State& goal) {
|
inline int h(State start, State goal, Action& action) {
|
||||||
return distance_to_goal(start, goal);
|
return distance_to_goal(start, goal, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int d(State& start, State& goal) {
|
inline int d(State start, State goal, Action& action) {
|
||||||
return distance_to_goal(start, goal);
|
return distance_to_goal(start, goal, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionState find_lowest(std::unordered_map<ActionState, int>& open_set) {
|
ActionState find_lowest(std::unordered_map<ActionState, int>& open_set) {
|
||||||
|
@ -90,7 +94,7 @@ namespace ai {
|
||||||
ActionState current{FINAL_ACTION, start};
|
ActionState current{FINAL_ACTION, start};
|
||||||
|
|
||||||
g_score[start] = 0;
|
g_score[start] = 0;
|
||||||
open_set[current] = g_score[start] + h(start, goal);
|
open_set[current] = g_score[start] + h(start, goal, current.action);
|
||||||
|
|
||||||
while(!open_set.empty()) {
|
while(!open_set.empty()) {
|
||||||
current = find_lowest(open_set);
|
current = find_lowest(open_set);
|
||||||
|
@ -109,7 +113,7 @@ namespace ai {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto neighbor = neighbor_action.apply_effect(current.state);
|
auto neighbor = neighbor_action.apply_effect(current.state);
|
||||||
int d_score = d(current.state, neighbor);
|
int d_score = d(current.state, neighbor, current.action);
|
||||||
int tentative_g_score = g_score[current.state] + d_score;
|
int tentative_g_score = g_score[current.state] + d_score;
|
||||||
int neighbor_g_score = g_score.contains(neighbor) ? g_score[neighbor] : SCORE_MAX;
|
int neighbor_g_score = g_score.contains(neighbor) ? g_score[neighbor] : SCORE_MAX;
|
||||||
if(tentative_g_score < neighbor_g_score) {
|
if(tentative_g_score < neighbor_g_score) {
|
||||||
|
@ -118,7 +122,7 @@ namespace ai {
|
||||||
g_score[neighbor] = tentative_g_score;
|
g_score[neighbor] = tentative_g_score;
|
||||||
// open_set gets the fScore
|
// open_set gets the fScore
|
||||||
ActionState neighbor_as{neighbor_action, neighbor};
|
ActionState neighbor_as{neighbor_action, neighbor};
|
||||||
open_set[neighbor_as] = tentative_g_score + h(neighbor, goal);
|
open_set[neighbor_as] = tentative_g_score + h(neighbor, goal, neighbor_as.action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
goap.hpp
14
goap.hpp
|
@ -12,6 +12,7 @@ namespace ai {
|
||||||
using AIProfile = std::unordered_map<std::string, int>;
|
using AIProfile = std::unordered_map<std::string, int>;
|
||||||
|
|
||||||
constexpr const int SCORE_MAX = std::numeric_limits<int>::max();
|
constexpr const int SCORE_MAX = std::numeric_limits<int>::max();
|
||||||
|
|
||||||
constexpr const size_t STATE_MAX = 32;
|
constexpr const size_t STATE_MAX = 32;
|
||||||
|
|
||||||
using State = std::bitset<STATE_MAX>;
|
using State = std::bitset<STATE_MAX>;
|
||||||
|
@ -20,8 +21,8 @@ namespace ai {
|
||||||
const State ALL_ONES = ~ALL_ZERO;
|
const State ALL_ONES = ~ALL_ZERO;
|
||||||
|
|
||||||
struct Action {
|
struct Action {
|
||||||
std::string $name;
|
std::string name;
|
||||||
int $cost = 0;
|
int cost = 0;
|
||||||
|
|
||||||
State $positive_preconds;
|
State $positive_preconds;
|
||||||
State $negative_preconds;
|
State $negative_preconds;
|
||||||
|
@ -30,16 +31,17 @@ namespace ai {
|
||||||
State $negative_effects;
|
State $negative_effects;
|
||||||
|
|
||||||
Action(std::string name, int cost) :
|
Action(std::string name, int cost) :
|
||||||
$name(name), $cost(cost) { }
|
name(name), cost(cost) { }
|
||||||
|
|
||||||
void needs(int name, bool val);
|
void needs(int name, bool val);
|
||||||
void effect(int name, bool val);
|
void effect(int name, bool val);
|
||||||
|
void ignore(int name);
|
||||||
|
|
||||||
bool can_effect(State& state);
|
bool can_effect(State& state);
|
||||||
State apply_effect(State& state);
|
State apply_effect(State& state);
|
||||||
|
|
||||||
bool operator==(const Action& other) const {
|
bool operator==(const Action& other) const {
|
||||||
return other.$name == $name;
|
return other.name == name;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -66,14 +68,14 @@ namespace ai {
|
||||||
|
|
||||||
bool is_subset(State& source, State& target);
|
bool is_subset(State& source, State& target);
|
||||||
|
|
||||||
int distance_to_goal(State& from, State& to);
|
int distance_to_goal(State from, State to, Action& action);
|
||||||
|
|
||||||
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal);
|
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal);
|
||||||
}
|
}
|
||||||
|
|
||||||
template<> struct std::hash<ai::Action> {
|
template<> struct std::hash<ai::Action> {
|
||||||
size_t operator()(const ai::Action& p) const {
|
size_t operator()(const ai::Action& p) const {
|
||||||
return std::hash<std::string>{}(p.$name);
|
return std::hash<std::string>{}(p.name);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,10 @@ namespace matrix {
|
||||||
print("{:x}<", cell);
|
print("{:x}<", cell);
|
||||||
} else if(cell == WALL_PATH_LIMIT) {
|
} else if(cell == WALL_PATH_LIMIT) {
|
||||||
print("# ");
|
print("# ");
|
||||||
} else if(cell > 15) {
|
} else if(cell > 15 && cell < 32) {
|
||||||
print("[{:x}]", cell);
|
print("{:x}+", cell - 16);
|
||||||
|
} else if(cell > 31) {
|
||||||
|
print("* ");
|
||||||
} else {
|
} else {
|
||||||
print("{:x} ", cell);
|
print("{:x} ", cell);
|
||||||
}
|
}
|
||||||
|
|
14
pathing.cpp
14
pathing.cpp
|
@ -74,15 +74,21 @@ void Pathing::clear_target(const Point &at) {
|
||||||
$input[at.y][at.x] = 1;
|
$input[at.y][at.x] = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Pathing::random_walk(Point &out, bool random, int direction) {
|
bool Pathing::random_walk(Point &out, bool random, int direction, size_t dir_count) {
|
||||||
bool zero_found = false;
|
bool zero_found = false;
|
||||||
|
dbc::check(dir_count == 4 || dir_count == 8, "Only 8 or 4 directions allowed.");
|
||||||
|
|
||||||
// just make a list of the four directions
|
// just make a list of the four directions
|
||||||
std::array<Point, 4> dirs{{
|
std::array<Point, 8> dirs{{
|
||||||
{out.x,out.y-1}, // north
|
{out.x,out.y-1}, // north
|
||||||
{out.x+1,out.y}, // east
|
{out.x+1,out.y}, // east
|
||||||
{out.x,out.y+1}, // south
|
{out.x,out.y+1}, // south
|
||||||
{out.x-1,out.y} // west
|
{out.x-1,out.y}, // west
|
||||||
|
|
||||||
|
{out.x+1,out.y-1}, // north east
|
||||||
|
{out.x+1,out.y+1}, // south east
|
||||||
|
{out.x-1,out.y+1}, // south west
|
||||||
|
{out.x-1,out.y-1} // north west
|
||||||
}};
|
}};
|
||||||
|
|
||||||
// get the current dijkstra number
|
// get the current dijkstra number
|
||||||
|
@ -93,7 +99,7 @@ bool Pathing::random_walk(Point &out, bool random, int direction) {
|
||||||
int rand_start = Random::uniform<int>(0, dirs.size());
|
int rand_start = Random::uniform<int>(0, dirs.size());
|
||||||
|
|
||||||
// go through all possible directions
|
// go through all possible directions
|
||||||
for(size_t i = 0; i < dirs.size(); i++) {
|
for(size_t i = 0; i < dir_count; i++) {
|
||||||
// but start at the random start, effectively randomizing
|
// but start at the random start, effectively randomizing
|
||||||
// which valid direction to go
|
// which valid direction to go
|
||||||
// BUG: this might be wrong given the above ranom from 0-size
|
// BUG: this might be wrong given the above ranom from 0-size
|
||||||
|
|
|
@ -7,6 +7,7 @@ using matrix::Matrix;
|
||||||
|
|
||||||
constexpr const int PATHING_TOWARD=1;
|
constexpr const int PATHING_TOWARD=1;
|
||||||
constexpr const int PATHING_AWAY=-1;
|
constexpr const int PATHING_AWAY=-1;
|
||||||
|
constexpr const int MOVE_DIAGONAL=8;
|
||||||
|
|
||||||
class Pathing {
|
class Pathing {
|
||||||
public:
|
public:
|
||||||
|
@ -28,7 +29,7 @@ public:
|
||||||
Matrix &paths() { return $paths; }
|
Matrix &paths() { return $paths; }
|
||||||
Matrix &input() { return $input; }
|
Matrix &input() { return $input; }
|
||||||
int distance(Point to) { return $paths[to.y][to.x];}
|
int distance(Point to) { return $paths[to.y][to.x];}
|
||||||
bool random_walk(Point &out, bool random, int direction);
|
bool random_walk(Point &out, bool random, int direction, size_t dir_count = 4);
|
||||||
|
|
||||||
bool INVARIANT();
|
bool INVARIANT();
|
||||||
};
|
};
|
||||||
|
|
11
tests/ai.cpp
11
tests/ai.cpp
|
@ -7,7 +7,7 @@
|
||||||
using namespace dbc;
|
using namespace dbc;
|
||||||
using namespace nlohmann;
|
using namespace nlohmann;
|
||||||
|
|
||||||
TEST_CASE("worldstate works", "[ai]") {
|
TEST_CASE("state and actions work", "[ai]") {
|
||||||
enum StateNames {
|
enum StateNames {
|
||||||
ENEMY_IN_RANGE,
|
ENEMY_IN_RANGE,
|
||||||
ENEMY_DEAD
|
ENEMY_DEAD
|
||||||
|
@ -36,7 +36,7 @@ TEST_CASE("worldstate works", "[ai]") {
|
||||||
// start is clean but after move is dirty
|
// start is clean but after move is dirty
|
||||||
REQUIRE(move_closer.can_effect(start));
|
REQUIRE(move_closer.can_effect(start));
|
||||||
REQUIRE(!move_closer.can_effect(after_move_state));
|
REQUIRE(!move_closer.can_effect(after_move_state));
|
||||||
REQUIRE(ai::distance_to_goal(start, after_move_state) == 1);
|
REQUIRE(ai::distance_to_goal(start, after_move_state, move_closer) == 11);
|
||||||
|
|
||||||
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);
|
||||||
|
@ -48,7 +48,10 @@ TEST_CASE("worldstate works", "[ai]") {
|
||||||
|
|
||||||
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));
|
REQUIRE(!kill_it.can_effect(after_kill_state));
|
||||||
REQUIRE(ai::distance_to_goal(after_move_state, after_kill_state) == 1);
|
REQUIRE(ai::distance_to_goal(after_move_state, after_kill_state, kill_it) == 11);
|
||||||
|
|
||||||
|
kill_it.ignore(ENEMY_IN_RANGE);
|
||||||
|
REQUIRE(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);
|
||||||
|
@ -117,7 +120,7 @@ TEST_CASE("ai as a module like sound/sprites", "[ai]") {
|
||||||
|
|
||||||
auto state = start;
|
auto state = start;
|
||||||
for(auto& action : a_plan.script) {
|
for(auto& action : a_plan.script) {
|
||||||
fmt::println("ACTION: {}", action.$name);
|
fmt::println("ACTION: {}", action.name);
|
||||||
state = action.apply_effect(state);
|
state = action.apply_effect(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue