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 "UIGrid.h"
|
||||
#include "UIGridPathfinding.h"
|
||||
#include "PathProvider.h"
|
||||
#include <random>
|
||||
#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
|
||||
static thread_local std::mt19937 rng{std::random_device{}()};
|
||||
|
||||
|
|
@ -226,78 +239,28 @@ static BehaviorOutput executeSleep(UIEntity& entity, UIGrid& grid) {
|
|||
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;
|
||||
|
||||
if (!behavior.dijkstra_map) {
|
||||
if (!behavior.path_provider) {
|
||||
return {BehaviorResult::NO_ACTION, {}};
|
||||
}
|
||||
|
||||
// Use Dijkstra map to find the lowest-distance neighbor (moving toward target)
|
||||
int cx = entity.cell_position.x;
|
||||
int cy = entity.cell_position.y;
|
||||
float best_dist = std::numeric_limits<float>::max();
|
||||
sf::Vector2i best_cell = {cx, cy};
|
||||
bool found = false;
|
||||
bool ok = false;
|
||||
sf::Vector2i next = behavior.path_provider->nextStep({cx, cy}, grid, &ok);
|
||||
|
||||
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)) {
|
||||
if (!ok) {
|
||||
return {BehaviorResult::BLOCKED, {cx, 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)) {
|
||||
if (next.x == cx && next.y == cy) {
|
||||
return {BehaviorResult::BLOCKED, {cx, cy}};
|
||||
}
|
||||
|
||||
return {BehaviorResult::MOVED, best_cell};
|
||||
return {BehaviorResult::MOVED, next};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -314,8 +277,8 @@ BehaviorOutput executeBehavior(UIEntity& entity, UIGrid& grid) {
|
|||
case BehaviorType::PATROL: return executePatrol(entity, grid);
|
||||
case BehaviorType::LOOP: return executeLoop(entity, grid);
|
||||
case BehaviorType::SLEEP: return executeSleep(entity, grid);
|
||||
case BehaviorType::SEEK: return executeSeek(entity, grid);
|
||||
case BehaviorType::FLEE: return executeFlee(entity, grid);
|
||||
case BehaviorType::SEEK: return executeProviderStep(entity, grid);
|
||||
case BehaviorType::FLEE: return executeProviderStep(entity, grid);
|
||||
}
|
||||
return {BehaviorResult::NO_ACTION, {}};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
class UIEntity;
|
||||
class UIGrid;
|
||||
class DijkstraMap;
|
||||
class PathProvider;
|
||||
|
||||
// =============================================================================
|
||||
// BehaviorType - matches Python mcrfpy.Behavior enum values
|
||||
|
|
@ -60,19 +61,11 @@ struct EntityBehavior {
|
|||
// Sleep data
|
||||
int sleep_turns_remaining = 0;
|
||||
|
||||
// Dijkstra map (for SEEK/FLEE)
|
||||
std::shared_ptr<DijkstraMap> dijkstra_map;
|
||||
// SEEK/FLEE pathfinding strategy (#315). Nullptr means NO_ACTION.
|
||||
std::unique_ptr<PathProvider> path_provider;
|
||||
|
||||
void reset() {
|
||||
type = BehaviorType::IDLE;
|
||||
waypoints.clear();
|
||||
current_waypoint_index = 0;
|
||||
patrol_direction = 1;
|
||||
current_path.clear();
|
||||
path_step_index = 0;
|
||||
sleep_turns_remaining = 0;
|
||||
dijkstra_map = nullptr;
|
||||
}
|
||||
// Defined in EntityBehavior.cpp to avoid needing the full PathProvider type here.
|
||||
void reset();
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
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 "UIGridView.h" // #252: Entity.grid accepts GridView
|
||||
#include "UIGridPathfinding.h"
|
||||
#include "PathProvider.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include <algorithm>
|
||||
#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) {
|
||||
static const char* kwlist[] = {"type", "waypoints", "turns", "path", nullptr};
|
||||
static const char* kwlist[] = {"type", "waypoints", "turns", "path", "pathfinder", nullptr};
|
||||
int type_val = 0;
|
||||
PyObject* waypoints_obj = nullptr;
|
||||
int turns = 0;
|
||||
PyObject* path_obj = nullptr;
|
||||
PyObject* pathfinder_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "i|OiO", const_cast<char**>(kwlist),
|
||||
&type_val, &waypoints_obj, &turns, &path_obj)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "i|OiOO", const_cast<char**>(kwlist),
|
||||
&type_val, &waypoints_obj, &turns, &path_obj,
|
||||
&pathfinder_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -1582,6 +1585,35 @@ PyObject* UIEntity::py_set_behavior(PyUIEntityObject* self, PyObject* args, PyOb
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"grid": "100x100",
|
||||
"entities": 100,
|
||||
"rounds": 1000,
|
||||
"total_sec": 0.07824495097156614,
|
||||
"mean_round_ms": 0.07824495097156614,
|
||||
"p95_round_ms": 0.1227830071002245,
|
||||
"per_entity_step_us": 0.7824495097156614
|
||||
"total_sec": 0.06852402002550662,
|
||||
"mean_round_ms": 0.06852402002550662,
|
||||
"p95_round_ms": 0.09302806574851274,
|
||||
"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