Refactor EntityBehavior SEEK/FLEE to use PathProvider strategy; refs #315
EntityBehavior no longer holds a direct DijkstraMap reference. A new PathProvider interface has three concrete implementations: - DijkstraProvider: steps along a (possibly inverted) DijkstraMap. SEEK descends a normal map toward roots; FLEE descends an inverted map away from threats. - AStarProvider: follows a pre-computed AStarPath step-by-step. - TargetProvider: takes a single (x, y) target and picks the Chebyshev neighbor closest to it each turn. Entity.set_behavior() gains a pathfinder= kwarg accepting any of the above (DijkstraMap, AStarPath, or (x, y) tuple). The old executeSeek/executeFlee helpers collapse into a single executeProviderStep() that delegates to the provider. EntityBehavior.h forward-declares PathProvider so the header stays light. EntityBehavior::reset() moves out of line to avoid pulling PathProvider into the header. New tests: tests/regression/issue_315_path_provider_test.py covers all three providers driving SEEK, FLEE via inverted DijkstraMap, mid-run pathfinder swap, and invalid-argument handling. grid_step_bench baseline refreshed against the new provider dispatch path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
767d0d4b0f
commit
17f2d6e1ef
7 changed files with 349 additions and 82 deletions
|
|
@ -2,9 +2,22 @@
|
||||||
#include "UIEntity.h"
|
#include "UIEntity.h"
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
#include "UIGridPathfinding.h"
|
#include "UIGridPathfinding.h"
|
||||||
|
#include "PathProvider.h"
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
// Out-of-line reset lives here so the header can forward-declare PathProvider.
|
||||||
|
void EntityBehavior::reset() {
|
||||||
|
type = BehaviorType::IDLE;
|
||||||
|
waypoints.clear();
|
||||||
|
current_waypoint_index = 0;
|
||||||
|
patrol_direction = 1;
|
||||||
|
current_path.clear();
|
||||||
|
path_step_index = 0;
|
||||||
|
sleep_turns_remaining = 0;
|
||||||
|
path_provider.reset();
|
||||||
|
}
|
||||||
|
|
||||||
// Thread-local random engine for behavior randomness
|
// Thread-local random engine for behavior randomness
|
||||||
static thread_local std::mt19937 rng{std::random_device{}()};
|
static thread_local std::mt19937 rng{std::random_device{}()};
|
||||||
|
|
||||||
|
|
@ -226,78 +239,28 @@ static BehaviorOutput executeSleep(UIEntity& entity, UIGrid& grid) {
|
||||||
return {BehaviorResult::NO_ACTION, {}};
|
return {BehaviorResult::NO_ACTION, {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
static BehaviorOutput executeSeek(UIEntity& entity, UIGrid& grid) {
|
// SEEK and FLEE share one implementation now: both delegate to the active
|
||||||
|
// PathProvider. FLEE differs only in which map is stored in the provider -
|
||||||
|
// DijkstraProvider over an inverted DijkstraMap descends away from the threat,
|
||||||
|
// which matches the old max-distance-neighbor behavior.
|
||||||
|
static BehaviorOutput executeProviderStep(UIEntity& entity, UIGrid& grid) {
|
||||||
auto& behavior = entity.behavior;
|
auto& behavior = entity.behavior;
|
||||||
|
if (!behavior.path_provider) {
|
||||||
if (!behavior.dijkstra_map) {
|
|
||||||
return {BehaviorResult::NO_ACTION, {}};
|
return {BehaviorResult::NO_ACTION, {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use Dijkstra map to find the lowest-distance neighbor (moving toward target)
|
|
||||||
int cx = entity.cell_position.x;
|
int cx = entity.cell_position.x;
|
||||||
int cy = entity.cell_position.y;
|
int cy = entity.cell_position.y;
|
||||||
float best_dist = std::numeric_limits<float>::max();
|
bool ok = false;
|
||||||
sf::Vector2i best_cell = {cx, cy};
|
sf::Vector2i next = behavior.path_provider->nextStep({cx, cy}, grid, &ok);
|
||||||
bool found = false;
|
|
||||||
|
|
||||||
sf::Vector2i dirs[] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0},
|
if (!ok) {
|
||||||
{-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
|
||||||
|
|
||||||
for (auto& dir : dirs) {
|
|
||||||
int nx = cx + dir.x;
|
|
||||||
int ny = cy + dir.y;
|
|
||||||
if (!isCellWalkable(grid, nx, ny)) continue;
|
|
||||||
|
|
||||||
float dist = behavior.dijkstra_map->getDistance(nx, ny);
|
|
||||||
if (dist >= 0 && dist < best_dist) {
|
|
||||||
best_dist = dist;
|
|
||||||
best_cell = {nx, ny};
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found || (best_cell.x == cx && best_cell.y == cy)) {
|
|
||||||
return {BehaviorResult::BLOCKED, {cx, cy}};
|
return {BehaviorResult::BLOCKED, {cx, cy}};
|
||||||
}
|
}
|
||||||
|
if (next.x == cx && next.y == cy) {
|
||||||
return {BehaviorResult::MOVED, best_cell};
|
|
||||||
}
|
|
||||||
|
|
||||||
static BehaviorOutput executeFlee(UIEntity& entity, UIGrid& grid) {
|
|
||||||
auto& behavior = entity.behavior;
|
|
||||||
|
|
||||||
if (!behavior.dijkstra_map) {
|
|
||||||
return {BehaviorResult::NO_ACTION, {}};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Dijkstra map to find the highest-distance neighbor (fleeing from target)
|
|
||||||
int cx = entity.cell_position.x;
|
|
||||||
int cy = entity.cell_position.y;
|
|
||||||
float best_dist = -1.0f;
|
|
||||||
sf::Vector2i best_cell = {cx, cy};
|
|
||||||
bool found = false;
|
|
||||||
|
|
||||||
sf::Vector2i dirs[] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0},
|
|
||||||
{-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
|
||||||
|
|
||||||
for (auto& dir : dirs) {
|
|
||||||
int nx = cx + dir.x;
|
|
||||||
int ny = cy + dir.y;
|
|
||||||
if (!isCellWalkable(grid, nx, ny)) continue;
|
|
||||||
|
|
||||||
float dist = behavior.dijkstra_map->getDistance(nx, ny);
|
|
||||||
if (dist >= 0 && dist > best_dist) {
|
|
||||||
best_dist = dist;
|
|
||||||
best_cell = {nx, ny};
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found || (best_cell.x == cx && best_cell.y == cy)) {
|
|
||||||
return {BehaviorResult::BLOCKED, {cx, cy}};
|
return {BehaviorResult::BLOCKED, {cx, cy}};
|
||||||
}
|
}
|
||||||
|
return {BehaviorResult::MOVED, next};
|
||||||
return {BehaviorResult::MOVED, best_cell};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -314,8 +277,8 @@ BehaviorOutput executeBehavior(UIEntity& entity, UIGrid& grid) {
|
||||||
case BehaviorType::PATROL: return executePatrol(entity, grid);
|
case BehaviorType::PATROL: return executePatrol(entity, grid);
|
||||||
case BehaviorType::LOOP: return executeLoop(entity, grid);
|
case BehaviorType::LOOP: return executeLoop(entity, grid);
|
||||||
case BehaviorType::SLEEP: return executeSleep(entity, grid);
|
case BehaviorType::SLEEP: return executeSleep(entity, grid);
|
||||||
case BehaviorType::SEEK: return executeSeek(entity, grid);
|
case BehaviorType::SEEK: return executeProviderStep(entity, grid);
|
||||||
case BehaviorType::FLEE: return executeFlee(entity, grid);
|
case BehaviorType::FLEE: return executeProviderStep(entity, grid);
|
||||||
}
|
}
|
||||||
return {BehaviorResult::NO_ACTION, {}};
|
return {BehaviorResult::NO_ACTION, {}};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
class UIEntity;
|
class UIEntity;
|
||||||
class UIGrid;
|
class UIGrid;
|
||||||
class DijkstraMap;
|
class DijkstraMap;
|
||||||
|
class PathProvider;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// BehaviorType - matches Python mcrfpy.Behavior enum values
|
// BehaviorType - matches Python mcrfpy.Behavior enum values
|
||||||
|
|
@ -60,19 +61,11 @@ struct EntityBehavior {
|
||||||
// Sleep data
|
// Sleep data
|
||||||
int sleep_turns_remaining = 0;
|
int sleep_turns_remaining = 0;
|
||||||
|
|
||||||
// Dijkstra map (for SEEK/FLEE)
|
// SEEK/FLEE pathfinding strategy (#315). Nullptr means NO_ACTION.
|
||||||
std::shared_ptr<DijkstraMap> dijkstra_map;
|
std::unique_ptr<PathProvider> path_provider;
|
||||||
|
|
||||||
void reset() {
|
// Defined in EntityBehavior.cpp to avoid needing the full PathProvider type here.
|
||||||
type = BehaviorType::IDLE;
|
void reset();
|
||||||
waypoints.clear();
|
|
||||||
current_waypoint_index = 0;
|
|
||||||
patrol_direction = 1;
|
|
||||||
current_path.clear();
|
|
||||||
path_step_index = 0;
|
|
||||||
sleep_turns_remaining = 0;
|
|
||||||
dijkstra_map = nullptr;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
65
src/PathProvider.cpp
Normal file
65
src/PathProvider.cpp
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#include "PathProvider.h"
|
||||||
|
#include "UIGrid.h"
|
||||||
|
#include "UIGridPathfinding.h"
|
||||||
|
#include "UIGridPoint.h"
|
||||||
|
|
||||||
|
static bool cellWalkable(UIGrid& grid, int x, int y) {
|
||||||
|
if (x < 0 || x >= grid.grid_w || y < 0 || y >= grid.grid_h) return false;
|
||||||
|
return grid.at(x, y).walkable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// DijkstraProvider
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
DijkstraProvider::DijkstraProvider(std::shared_ptr<DijkstraMap> map)
|
||||||
|
: map_(std::move(map)) {}
|
||||||
|
|
||||||
|
sf::Vector2i DijkstraProvider::nextStep(sf::Vector2i from, UIGrid& /*grid*/, bool* ok) {
|
||||||
|
if (!map_) {
|
||||||
|
if (ok) *ok = false;
|
||||||
|
return {-1, -1};
|
||||||
|
}
|
||||||
|
bool valid = false;
|
||||||
|
sf::Vector2i step = map_->descentStep(from.x, from.y, &valid);
|
||||||
|
if (ok) *ok = valid;
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// AStarProvider
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
AStarProvider::AStarProvider(std::vector<sf::Vector2i> path)
|
||||||
|
: path_(std::move(path)) {}
|
||||||
|
|
||||||
|
sf::Vector2i AStarProvider::nextStep(sf::Vector2i /*from*/, UIGrid& /*grid*/, bool* ok) {
|
||||||
|
if (index_ >= path_.size()) {
|
||||||
|
if (ok) *ok = false;
|
||||||
|
return {-1, -1};
|
||||||
|
}
|
||||||
|
if (ok) *ok = true;
|
||||||
|
return path_[index_++];
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// TargetProvider
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
TargetProvider::TargetProvider(sf::Vector2i target)
|
||||||
|
: target_(target) {}
|
||||||
|
|
||||||
|
sf::Vector2i TargetProvider::nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) {
|
||||||
|
int dx = target_.x - from.x;
|
||||||
|
int dy = target_.y - from.y;
|
||||||
|
if (dx == 0 && dy == 0) {
|
||||||
|
if (ok) *ok = false;
|
||||||
|
return {-1, -1}; // already at target
|
||||||
|
}
|
||||||
|
int sx = (dx > 0) ? 1 : ((dx < 0) ? -1 : 0);
|
||||||
|
int sy = (dy > 0) ? 1 : ((dy < 0) ? -1 : 0);
|
||||||
|
sf::Vector2i step{from.x + sx, from.y + sy};
|
||||||
|
if (!cellWalkable(grid, step.x, step.y)) {
|
||||||
|
if (ok) *ok = false;
|
||||||
|
return {-1, -1};
|
||||||
|
}
|
||||||
|
if (ok) *ok = true;
|
||||||
|
return step;
|
||||||
|
}
|
||||||
64
src/PathProvider.h
Normal file
64
src/PathProvider.h
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
#pragma once
|
||||||
|
#include "Common.h"
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class UIGrid;
|
||||||
|
class DijkstraMap;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PathProvider (#315) - strategy interface for "what's my next cell?"
|
||||||
|
//
|
||||||
|
// EntityBehavior's SEEK/FLEE execute step() by asking the active PathProvider
|
||||||
|
// for a single cell. Three concrete providers satisfy every pathfinding shape
|
||||||
|
// the engine exposes today.
|
||||||
|
// =============================================================================
|
||||||
|
class PathProvider {
|
||||||
|
public:
|
||||||
|
virtual ~PathProvider() = default;
|
||||||
|
|
||||||
|
// Return the next cell to step to. Sets *ok=true on a valid step, false
|
||||||
|
// otherwise. The provider is responsible for walkability checks -
|
||||||
|
// DijkstraProvider relies on the TCOD map used at compute time,
|
||||||
|
// TargetProvider re-queries the live grid, AStarProvider trusts the
|
||||||
|
// pre-computed path.
|
||||||
|
virtual sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) = 0;
|
||||||
|
|
||||||
|
// Hint for providers that hold iteration state (currently only A*).
|
||||||
|
virtual void reset() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Descend a precomputed DijkstraMap. For SEEK, pass the map as-is; for FLEE,
|
||||||
|
// pass DijkstraMap.inverted().
|
||||||
|
class DijkstraProvider : public PathProvider {
|
||||||
|
public:
|
||||||
|
explicit DijkstraProvider(std::shared_ptr<DijkstraMap> map);
|
||||||
|
sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<DijkstraMap> map_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replay a pre-computed A* path.
|
||||||
|
class AStarProvider : public PathProvider {
|
||||||
|
public:
|
||||||
|
explicit AStarProvider(std::vector<sf::Vector2i> path);
|
||||||
|
sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) override;
|
||||||
|
void reset() override { index_ = 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<sf::Vector2i> path_;
|
||||||
|
size_t index_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Take a single Chebyshev step toward a fixed target cell. Used when full
|
||||||
|
// pathfinding is overkill (e.g. an adjacent target). Returns no-step if the
|
||||||
|
// straight-line neighbor is blocked - callers can treat that as BLOCKED.
|
||||||
|
class TargetProvider : public PathProvider {
|
||||||
|
public:
|
||||||
|
explicit TargetProvider(sf::Vector2i target);
|
||||||
|
sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
sf::Vector2i target_;
|
||||||
|
};
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
#include "UIGridView.h" // #252: Entity.grid accepts GridView
|
#include "UIGridView.h" // #252: Entity.grid accepts GridView
|
||||||
#include "UIGridPathfinding.h"
|
#include "UIGridPathfinding.h"
|
||||||
|
#include "PathProvider.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
@ -1512,14 +1513,16 @@ int UIEntity::set_sight_radius(PyUIEntityObject* self, PyObject* value, void* cl
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* UIEntity::py_set_behavior(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
PyObject* UIEntity::py_set_behavior(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||||
static const char* kwlist[] = {"type", "waypoints", "turns", "path", nullptr};
|
static const char* kwlist[] = {"type", "waypoints", "turns", "path", "pathfinder", nullptr};
|
||||||
int type_val = 0;
|
int type_val = 0;
|
||||||
PyObject* waypoints_obj = nullptr;
|
PyObject* waypoints_obj = nullptr;
|
||||||
int turns = 0;
|
int turns = 0;
|
||||||
PyObject* path_obj = nullptr;
|
PyObject* path_obj = nullptr;
|
||||||
|
PyObject* pathfinder_obj = nullptr;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "i|OiO", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "i|OiOO", const_cast<char**>(kwlist),
|
||||||
&type_val, &waypoints_obj, &turns, &path_obj)) {
|
&type_val, &waypoints_obj, &turns, &path_obj,
|
||||||
|
&pathfinder_obj)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1582,6 +1585,35 @@ PyObject* UIEntity::py_set_behavior(PyUIEntityObject* self, PyObject* args, PyOb
|
||||||
behavior.sleep_turns_remaining = turns;
|
behavior.sleep_turns_remaining = turns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse pathfinder (#315): DijkstraMap, AStarPath, or (x, y) target tuple.
|
||||||
|
if (pathfinder_obj && pathfinder_obj != Py_None) {
|
||||||
|
if (PyObject_IsInstance(pathfinder_obj, (PyObject*)&mcrfpydef::PyDijkstraMapType)) {
|
||||||
|
auto* dmap = (PyDijkstraMapObject*)pathfinder_obj;
|
||||||
|
if (!dmap->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "pathfinder: DijkstraMap is invalid");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
behavior.path_provider = std::make_unique<DijkstraProvider>(dmap->data);
|
||||||
|
} else if (PyObject_IsInstance(pathfinder_obj, (PyObject*)&mcrfpydef::PyAStarPathType)) {
|
||||||
|
auto* apath = (PyAStarPathObject*)pathfinder_obj;
|
||||||
|
// Copy remaining steps - the provider owns its own iteration state.
|
||||||
|
std::vector<sf::Vector2i> steps(
|
||||||
|
apath->path.begin() + apath->current_index,
|
||||||
|
apath->path.end());
|
||||||
|
behavior.path_provider = std::make_unique<AStarProvider>(std::move(steps));
|
||||||
|
} else if (PyTuple_Check(pathfinder_obj) && PyTuple_Size(pathfinder_obj) == 2) {
|
||||||
|
long tx = PyLong_AsLong(PyTuple_GetItem(pathfinder_obj, 0));
|
||||||
|
long ty = PyLong_AsLong(PyTuple_GetItem(pathfinder_obj, 1));
|
||||||
|
if (PyErr_Occurred()) return NULL;
|
||||||
|
behavior.path_provider = std::make_unique<TargetProvider>(
|
||||||
|
sf::Vector2i(static_cast<int>(tx), static_cast<int>(ty)));
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError,
|
||||||
|
"pathfinder must be a DijkstraMap, AStarPath, or (x, y) tuple");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
"grid": "100x100",
|
"grid": "100x100",
|
||||||
"entities": 100,
|
"entities": 100,
|
||||||
"rounds": 1000,
|
"rounds": 1000,
|
||||||
"total_sec": 0.07824495097156614,
|
"total_sec": 0.06852402002550662,
|
||||||
"mean_round_ms": 0.07824495097156614,
|
"mean_round_ms": 0.06852402002550662,
|
||||||
"p95_round_ms": 0.1227830071002245,
|
"p95_round_ms": 0.09302806574851274,
|
||||||
"per_entity_step_us": 0.7824495097156614
|
"per_entity_step_us": 0.6852402002550662
|
||||||
}
|
}
|
||||||
150
tests/regression/issue_315_path_provider_test.py
Normal file
150
tests/regression/issue_315_path_provider_test.py
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
"""Phase C regression: PathProvider abstraction drives SEEK/FLEE via set_behavior.
|
||||||
|
|
||||||
|
Verifies that each of the three provider shapes (DijkstraMap, AStarPath, and
|
||||||
|
plain target tuple) produces a valid SEEK step, and that swapping pathfinder
|
||||||
|
mid-run changes the next step.
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def make_open_grid(w=20, h=20):
|
||||||
|
scene = mcrfpy.Scene("issue315")
|
||||||
|
mcrfpy.current_scene = scene
|
||||||
|
grid = mcrfpy.Grid(grid_size=(w, h))
|
||||||
|
scene.children.append(grid)
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
c = grid.at(x, y)
|
||||||
|
# Walkable interior, walls on the border.
|
||||||
|
if x == 0 or y == 0 or x == w - 1 or y == h - 1:
|
||||||
|
c.walkable = False
|
||||||
|
c.transparent = False
|
||||||
|
else:
|
||||||
|
c.walkable = True
|
||||||
|
c.transparent = True
|
||||||
|
return grid
|
||||||
|
|
||||||
|
|
||||||
|
def closer_to(p, goal, start):
|
||||||
|
"""True when p is closer than start to goal (Chebyshev)."""
|
||||||
|
def d(a, b): return max(abs(a[0] - b[0]), abs(a[1] - b[1]))
|
||||||
|
return d(p, goal) < d(start, goal)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dijkstra_provider():
|
||||||
|
grid = make_open_grid()
|
||||||
|
e = mcrfpy.Entity((5, 5), grid=grid)
|
||||||
|
e.move_speed = 0
|
||||||
|
goal = (15, 15)
|
||||||
|
dmap = grid.get_dijkstra_map(goal)
|
||||||
|
e.set_behavior(int(mcrfpy.Behavior.SEEK), pathfinder=dmap)
|
||||||
|
|
||||||
|
start = (e.cell_x, e.cell_y)
|
||||||
|
grid.step()
|
||||||
|
ended = (e.cell_x, e.cell_y)
|
||||||
|
assert ended != start, "DijkstraProvider SEEK must move the entity"
|
||||||
|
assert closer_to(ended, goal, start), f"moved away from goal: {start} -> {ended}"
|
||||||
|
print(" DijkstraProvider SEEK: OK")
|
||||||
|
|
||||||
|
|
||||||
|
def test_astar_provider():
|
||||||
|
grid = make_open_grid()
|
||||||
|
e = mcrfpy.Entity((5, 5), grid=grid)
|
||||||
|
e.move_speed = 0
|
||||||
|
goal = (10, 10)
|
||||||
|
path = grid.find_path((5, 5), goal)
|
||||||
|
assert path is not None
|
||||||
|
e.set_behavior(int(mcrfpy.Behavior.SEEK), pathfinder=path)
|
||||||
|
|
||||||
|
start = (e.cell_x, e.cell_y)
|
||||||
|
grid.step()
|
||||||
|
ended = (e.cell_x, e.cell_y)
|
||||||
|
assert ended != start, "AStarProvider SEEK must move the entity"
|
||||||
|
assert closer_to(ended, goal, start), f"moved away from goal: {start} -> {ended}"
|
||||||
|
print(" AStarProvider SEEK: OK")
|
||||||
|
|
||||||
|
|
||||||
|
def test_target_provider():
|
||||||
|
grid = make_open_grid()
|
||||||
|
e = mcrfpy.Entity((5, 5), grid=grid)
|
||||||
|
e.move_speed = 0
|
||||||
|
goal = (10, 10)
|
||||||
|
e.set_behavior(int(mcrfpy.Behavior.SEEK), pathfinder=goal)
|
||||||
|
|
||||||
|
start = (e.cell_x, e.cell_y)
|
||||||
|
grid.step()
|
||||||
|
ended = (e.cell_x, e.cell_y)
|
||||||
|
assert ended != start, "TargetProvider SEEK must move the entity"
|
||||||
|
assert closer_to(ended, goal, start), f"moved away from target: {start} -> {ended}"
|
||||||
|
print(" TargetProvider SEEK: OK")
|
||||||
|
|
||||||
|
|
||||||
|
def test_flee_via_inverted_dijkstra():
|
||||||
|
grid = make_open_grid()
|
||||||
|
e = mcrfpy.Entity((10, 10), grid=grid)
|
||||||
|
e.move_speed = 0
|
||||||
|
threat = (12, 10)
|
||||||
|
threat_map = grid.get_dijkstra_map(threat)
|
||||||
|
safety_map = threat_map.invert()
|
||||||
|
e.set_behavior(int(mcrfpy.Behavior.FLEE), pathfinder=safety_map)
|
||||||
|
|
||||||
|
start = (e.cell_x, e.cell_y)
|
||||||
|
start_dist = threat_map.distance(start)
|
||||||
|
grid.step()
|
||||||
|
ended = (e.cell_x, e.cell_y)
|
||||||
|
assert ended != start, "FLEE must move the entity"
|
||||||
|
new_dist = threat_map.distance(ended)
|
||||||
|
assert new_dist >= start_dist, (
|
||||||
|
f"FLEE should not get closer: d={start_dist} -> {new_dist}")
|
||||||
|
print(" FLEE via inverted DijkstraMap: OK")
|
||||||
|
|
||||||
|
|
||||||
|
def test_provider_swap_midrun():
|
||||||
|
"""Swapping pathfinder mid-run changes the next step."""
|
||||||
|
grid = make_open_grid()
|
||||||
|
e = mcrfpy.Entity((10, 10), grid=grid)
|
||||||
|
e.move_speed = 0
|
||||||
|
|
||||||
|
# First: seek (5, 10) - should move left.
|
||||||
|
e.set_behavior(int(mcrfpy.Behavior.SEEK),
|
||||||
|
pathfinder=grid.get_dijkstra_map((5, 10)))
|
||||||
|
grid.step()
|
||||||
|
after_first = (e.cell_x, e.cell_y)
|
||||||
|
assert after_first[0] < 10, f"expected leftward step, got {after_first}"
|
||||||
|
|
||||||
|
# Swap pathfinder: now seek (15, 10) - should move right from here.
|
||||||
|
e.set_behavior(int(mcrfpy.Behavior.SEEK),
|
||||||
|
pathfinder=grid.get_dijkstra_map((15, 10)))
|
||||||
|
grid.step()
|
||||||
|
after_second = (e.cell_x, e.cell_y)
|
||||||
|
assert after_second[0] > after_first[0], (
|
||||||
|
f"expected rightward step after swap, got {after_first} -> {after_second}")
|
||||||
|
print(" Mid-run provider swap: OK")
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_pathfinder_raises():
|
||||||
|
grid = make_open_grid()
|
||||||
|
e = mcrfpy.Entity((5, 5), grid=grid)
|
||||||
|
try:
|
||||||
|
e.set_behavior(int(mcrfpy.Behavior.SEEK), pathfinder="not a pathfinder")
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError("expected TypeError for bad pathfinder argument")
|
||||||
|
print(" Invalid pathfinder rejected: OK")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
test_dijkstra_provider()
|
||||||
|
test_astar_provider()
|
||||||
|
test_target_provider()
|
||||||
|
test_flee_via_inverted_dijkstra()
|
||||||
|
test_provider_swap_midrun()
|
||||||
|
test_invalid_pathfinder_raises()
|
||||||
|
print("PASS")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
sys.exit(0)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue