BSP: add room adjacency graph for corridor generation (closes #210)
New features: - bsp.adjacency[i] returns tuple of neighbor leaf indices - bsp.get_leaf(index) returns BSPNode by leaf index (O(1) lookup) - node.leaf_index returns this leaf's index (0..n-1) or None - node.adjacent_tiles[j] returns tuple of Vector wall tiles bordering neighbor j Implementation details: - Lazy-computed adjacency cache with generation-based invalidation - O(n²) pairwise adjacency check on first access - Wall tiles computed per-direction (not symmetric) for correct perspective - Supports 'in' operator: `5 in leaf.adjacent_tiles` Code review fixes applied: - split_once now increments generation to invalidate cache - Wall tile cache uses (self, neighbor) key, not symmetric - Added sq_contains for 'in' operator support - Documented wall tile semantics (tiles on THIS leaf's boundary) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5a86602789
commit
8628ac164b
4 changed files with 1058 additions and 2 deletions
|
|
@ -441,6 +441,8 @@ PyObject* PyInit_mcrfpy()
|
||||||
/*BSP internal types - returned by BSP methods but not directly instantiable*/
|
/*BSP internal types - returned by BSP methods but not directly instantiable*/
|
||||||
&mcrfpydef::PyBSPNodeType,
|
&mcrfpydef::PyBSPNodeType,
|
||||||
&mcrfpydef::PyBSPIterType,
|
&mcrfpydef::PyBSPIterType,
|
||||||
|
&mcrfpydef::PyBSPAdjacencyType, // #210: BSP.adjacency wrapper
|
||||||
|
&mcrfpydef::PyBSPAdjacentTilesType, // #210: BSPNode.adjacent_tiles wrapper
|
||||||
|
|
||||||
nullptr};
|
nullptr};
|
||||||
|
|
||||||
|
|
|
||||||
614
src/PyBSP.cpp
614
src/PyBSP.cpp
|
|
@ -3,9 +3,11 @@
|
||||||
#include "McRFPy_Doc.h"
|
#include "McRFPy_Doc.h"
|
||||||
#include "PyPositionHelper.h"
|
#include "PyPositionHelper.h"
|
||||||
#include "PyHeightMap.h"
|
#include "PyHeightMap.h"
|
||||||
|
#include "PyVector.h" // #210: For wall tile Vectors
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <algorithm> // #210: For std::min, std::max
|
||||||
|
|
||||||
// Static storage for Traversal enum
|
// Static storage for Traversal enum
|
||||||
PyObject* PyTraversal::traversal_enum_class = nullptr;
|
PyObject* PyTraversal::traversal_enum_class = nullptr;
|
||||||
|
|
@ -19,6 +21,127 @@ enum TraversalOrder {
|
||||||
TRAVERSAL_INVERTED_LEVEL_ORDER = 4,
|
TRAVERSAL_INVERTED_LEVEL_ORDER = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==================== Adjacency Helpers (#210) ====================
|
||||||
|
|
||||||
|
// Check if two BSP leaf nodes share a wall segment (not just a corner)
|
||||||
|
static bool are_adjacent(TCOD_bsp_t* a, TCOD_bsp_t* b) {
|
||||||
|
// a is left of b (vertical wall)
|
||||||
|
if (a->x + a->w == b->x) {
|
||||||
|
int overlap = std::min(a->y + a->h, b->y + b->h) - std::max(a->y, b->y);
|
||||||
|
if (overlap > 0) return true;
|
||||||
|
}
|
||||||
|
// b is left of a (vertical wall)
|
||||||
|
if (b->x + b->w == a->x) {
|
||||||
|
int overlap = std::min(a->y + a->h, b->y + b->h) - std::max(a->y, b->y);
|
||||||
|
if (overlap > 0) return true;
|
||||||
|
}
|
||||||
|
// a is above b (horizontal wall)
|
||||||
|
if (a->y + a->h == b->y) {
|
||||||
|
int overlap = std::min(a->x + a->w, b->x + b->w) - std::max(a->x, b->x);
|
||||||
|
if (overlap > 0) return true;
|
||||||
|
}
|
||||||
|
// b is above a (horizontal wall)
|
||||||
|
if (b->y + b->h == a->y) {
|
||||||
|
int overlap = std::min(a->x + a->w, b->x + b->w) - std::max(a->x, b->x);
|
||||||
|
if (overlap > 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute wall tiles for two adjacent leaves (from perspective of node a).
|
||||||
|
// Returns coordinates of tiles on a's boundary that are suitable for corridor placement.
|
||||||
|
// For a vertical wall (a left of b): returns a's rightmost column in the overlap range.
|
||||||
|
// For a horizontal wall (a above b): returns a's bottommost row in the overlap range.
|
||||||
|
// These are the tiles INSIDE leaf a that touch leaf b's boundary.
|
||||||
|
static std::vector<sf::Vector2i> compute_wall_tiles(TCOD_bsp_t* a, TCOD_bsp_t* b) {
|
||||||
|
std::vector<sf::Vector2i> tiles;
|
||||||
|
|
||||||
|
// a is left of b (vertical wall at right edge of a)
|
||||||
|
if (a->x + a->w == b->x) {
|
||||||
|
int y_start = std::max(a->y, b->y);
|
||||||
|
int y_end = std::min(a->y + a->h, b->y + b->h);
|
||||||
|
int x = a->x + a->w - 1; // Last column of a
|
||||||
|
for (int y = y_start; y < y_end; y++) {
|
||||||
|
tiles.push_back({x, y});
|
||||||
|
}
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
// b is left of a (vertical wall at left edge of a)
|
||||||
|
if (b->x + b->w == a->x) {
|
||||||
|
int y_start = std::max(a->y, b->y);
|
||||||
|
int y_end = std::min(a->y + a->h, b->y + b->h);
|
||||||
|
int x = a->x; // First column of a
|
||||||
|
for (int y = y_start; y < y_end; y++) {
|
||||||
|
tiles.push_back({x, y});
|
||||||
|
}
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
// a is above b (horizontal wall at bottom edge of a)
|
||||||
|
if (a->y + a->h == b->y) {
|
||||||
|
int x_start = std::max(a->x, b->x);
|
||||||
|
int x_end = std::min(a->x + a->w, b->x + b->w);
|
||||||
|
int y = a->y + a->h - 1; // Last row of a
|
||||||
|
for (int x = x_start; x < x_end; x++) {
|
||||||
|
tiles.push_back({x, y});
|
||||||
|
}
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
// b is above a (horizontal wall at top edge of a)
|
||||||
|
if (b->y + b->h == a->y) {
|
||||||
|
int x_start = std::max(a->x, b->x);
|
||||||
|
int x_end = std::min(a->x + a->w, b->x + b->w);
|
||||||
|
int y = a->y; // First row of a
|
||||||
|
for (int x = x_start; x < x_end; x++) {
|
||||||
|
tiles.push_back({x, y});
|
||||||
|
}
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tiles; // Empty if not adjacent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the adjacency cache for a BSP tree
|
||||||
|
static void rebuild_adjacency_cache(PyBSPObject* self) {
|
||||||
|
// Delete old cache if exists
|
||||||
|
if (self->adjacency_cache) {
|
||||||
|
delete self->adjacency_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->adjacency_cache = new BSPAdjacencyCache();
|
||||||
|
self->adjacency_cache->generation = self->generation;
|
||||||
|
|
||||||
|
// Collect all leaves in level-order
|
||||||
|
TCOD_bsp_traverse_level_order(self->root, [](TCOD_bsp_t* node, void* data) -> bool {
|
||||||
|
auto cache = (BSPAdjacencyCache*)data;
|
||||||
|
if (TCOD_bsp_is_leaf(node)) {
|
||||||
|
int idx = (int)cache->leaf_pointers.size();
|
||||||
|
cache->leaf_pointers.push_back(node);
|
||||||
|
cache->ptr_to_index[node] = idx;
|
||||||
|
cache->graph.push_back({}); // Empty neighbor list
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, self->adjacency_cache);
|
||||||
|
|
||||||
|
// Build adjacency graph (O(n^2) pairwise check)
|
||||||
|
auto& cache = *self->adjacency_cache;
|
||||||
|
int n = (int)cache.leaf_pointers.size();
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
for (int j = i + 1; j < n; j++) {
|
||||||
|
if (are_adjacent(cache.leaf_pointers[i], cache.leaf_pointers[j])) {
|
||||||
|
cache.graph[i].push_back(j);
|
||||||
|
cache.graph[j].push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure adjacency cache is valid, rebuild if needed
|
||||||
|
static void ensure_adjacency_cache(PyBSPObject* self) {
|
||||||
|
if (!self->adjacency_cache || self->adjacency_cache->generation != self->generation) {
|
||||||
|
rebuild_adjacency_cache(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Traversal Enum ====================
|
// ==================== Traversal Enum ====================
|
||||||
|
|
||||||
PyObject* PyTraversal::create_enum_class(PyObject* module) {
|
PyObject* PyTraversal::create_enum_class(PyObject* module) {
|
||||||
|
|
@ -193,6 +316,8 @@ PyGetSetDef PyBSP::getsetters[] = {
|
||||||
MCRF_PROPERTY(size, "Dimensions (width, height). Read-only."), NULL},
|
MCRF_PROPERTY(size, "Dimensions (width, height). Read-only."), NULL},
|
||||||
{"root", (getter)PyBSP::get_root, NULL,
|
{"root", (getter)PyBSP::get_root, NULL,
|
||||||
MCRF_PROPERTY(root, "Reference to the root BSPNode. Read-only."), NULL},
|
MCRF_PROPERTY(root, "Reference to the root BSPNode. Read-only."), NULL},
|
||||||
|
{"adjacency", (getter)PyBSP::get_adjacency, NULL,
|
||||||
|
MCRF_PROPERTY(adjacency, "Leaf adjacency graph. adjacency[i] returns tuple of neighbor indices. Read-only."), NULL},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -268,6 +393,16 @@ PyMethodDef PyBSP::methods[] = {
|
||||||
MCRF_ARG("value", "Value inside selected regions. Default: 1.0.")
|
MCRF_ARG("value", "Value inside selected regions. Default: 1.0.")
|
||||||
MCRF_RETURNS("HeightMap with selected regions filled")
|
MCRF_RETURNS("HeightMap with selected regions filled")
|
||||||
)},
|
)},
|
||||||
|
{"get_leaf", (PyCFunction)PyBSP::get_leaf, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
MCRF_METHOD(BSP, get_leaf,
|
||||||
|
MCRF_SIG("(index: int)", "BSPNode"),
|
||||||
|
MCRF_DESC("Get a leaf node by its index (0 to len(bsp)-1). "
|
||||||
|
"This is useful when working with adjacency data, which returns leaf indices."),
|
||||||
|
MCRF_ARGS_START
|
||||||
|
MCRF_ARG("index", "Leaf index (0 to len(bsp)-1). Negative indices supported.")
|
||||||
|
MCRF_RETURNS("BSPNode at the specified index")
|
||||||
|
MCRF_RAISES("IndexError", "If index is out of range")
|
||||||
|
)},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -283,6 +418,7 @@ PyObject* PyBSP::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
||||||
self->orig_w = 0;
|
self->orig_w = 0;
|
||||||
self->orig_h = 0;
|
self->orig_h = 0;
|
||||||
self->generation = 0;
|
self->generation = 0;
|
||||||
|
self->adjacency_cache = nullptr; // #210: Lazy-computed
|
||||||
}
|
}
|
||||||
return (PyObject*)self;
|
return (PyObject*)self;
|
||||||
}
|
}
|
||||||
|
|
@ -330,6 +466,12 @@ int PyBSP::init(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||||||
TCOD_bsp_delete(self->root);
|
TCOD_bsp_delete(self->root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up any existing adjacency cache (#210)
|
||||||
|
if (self->adjacency_cache) {
|
||||||
|
delete self->adjacency_cache;
|
||||||
|
self->adjacency_cache = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// Create new BSP with size
|
// Create new BSP with size
|
||||||
self->root = TCOD_bsp_new_with_size(x, y, w, h);
|
self->root = TCOD_bsp_new_with_size(x, y, w, h);
|
||||||
if (!self->root) {
|
if (!self->root) {
|
||||||
|
|
@ -349,6 +491,11 @@ int PyBSP::init(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
|
||||||
void PyBSP::dealloc(PyBSPObject* self)
|
void PyBSP::dealloc(PyBSPObject* self)
|
||||||
{
|
{
|
||||||
|
// Clean up adjacency cache (#210)
|
||||||
|
if (self->adjacency_cache) {
|
||||||
|
delete self->adjacency_cache;
|
||||||
|
self->adjacency_cache = nullptr;
|
||||||
|
}
|
||||||
if (self->root) {
|
if (self->root) {
|
||||||
TCOD_bsp_delete(self->root);
|
TCOD_bsp_delete(self->root);
|
||||||
self->root = nullptr;
|
self->root = nullptr;
|
||||||
|
|
@ -422,6 +569,29 @@ PyObject* PyBSP::get_root(PyBSPObject* self, void* closure)
|
||||||
return PyBSPNode::create(self->root, (PyObject*)self);
|
return PyBSPNode::create(self->root, (PyObject*)self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Property: adjacency (#210)
|
||||||
|
PyObject* PyBSP::get_adjacency(PyBSPObject* self, void* closure)
|
||||||
|
{
|
||||||
|
if (!self->root) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure cache is valid
|
||||||
|
ensure_adjacency_cache(self);
|
||||||
|
|
||||||
|
// Create and return adjacency wrapper
|
||||||
|
PyBSPAdjacencyObject* adj = (PyBSPAdjacencyObject*)
|
||||||
|
mcrfpydef::PyBSPAdjacencyType.tp_alloc(&mcrfpydef::PyBSPAdjacencyType, 0);
|
||||||
|
if (!adj) return nullptr;
|
||||||
|
|
||||||
|
adj->bsp_owner = (PyObject*)self;
|
||||||
|
adj->generation = self->generation;
|
||||||
|
Py_INCREF(self);
|
||||||
|
|
||||||
|
return (PyObject*)adj;
|
||||||
|
}
|
||||||
|
|
||||||
// Method: split_once(horizontal, position) -> BSP
|
// Method: split_once(horizontal, position) -> BSP
|
||||||
PyObject* PyBSP::split_once(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
PyObject* PyBSP::split_once(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||||||
{
|
{
|
||||||
|
|
@ -439,8 +609,10 @@ PyObject* PyBSP::split_once(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: split_once only adds children, doesn't remove any nodes
|
// Increment generation to invalidate adjacency cache and stale BSPNode references
|
||||||
// Root node pointer remains valid, so we don't increment generation
|
// The tree structure changes, so any cached adjacency graph is now invalid
|
||||||
|
self->generation++;
|
||||||
|
|
||||||
TCOD_bsp_split_once(self->root, horizontal ? true : false, position);
|
TCOD_bsp_split_once(self->root, horizontal ? true : false, position);
|
||||||
|
|
||||||
Py_INCREF(self);
|
Py_INCREF(self);
|
||||||
|
|
@ -685,6 +857,40 @@ PyObject* PyBSP::find(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||||||
return PyBSPNode::create(found, (PyObject*)self);
|
return PyBSPNode::create(found, (PyObject*)self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method: get_leaf(index) -> BSPNode (#210)
|
||||||
|
PyObject* PyBSP::get_leaf(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
static const char* keywords[] = {"index", nullptr};
|
||||||
|
int index;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "i", const_cast<char**>(keywords), &index)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->root) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "BSP not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure adjacency cache is valid (this builds leaf_pointers list)
|
||||||
|
ensure_adjacency_cache(self);
|
||||||
|
|
||||||
|
int n = (int)self->adjacency_cache->leaf_pointers.size();
|
||||||
|
|
||||||
|
// Handle negative indexing
|
||||||
|
if (index < 0) {
|
||||||
|
index += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= n) {
|
||||||
|
PyErr_SetString(PyExc_IndexError, "leaf index out of range");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
TCOD_bsp_t* leaf = self->adjacency_cache->leaf_pointers[index];
|
||||||
|
return PyBSPNode::create(leaf, (PyObject*)self);
|
||||||
|
}
|
||||||
|
|
||||||
// Method: to_heightmap(...) -> HeightMap
|
// Method: to_heightmap(...) -> HeightMap
|
||||||
PyObject* PyBSP::to_heightmap(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
PyObject* PyBSP::to_heightmap(PyBSPObject* self, PyObject* args, PyObject* kwds)
|
||||||
{
|
{
|
||||||
|
|
@ -841,6 +1047,13 @@ PyGetSetDef PyBSPNode::getsetters[] = {
|
||||||
MCRF_PROPERTY(parent, "Parent node, or None if root. Read-only."), NULL},
|
MCRF_PROPERTY(parent, "Parent node, or None if root. Read-only."), NULL},
|
||||||
{"sibling", (getter)PyBSPNode::get_sibling, NULL,
|
{"sibling", (getter)PyBSPNode::get_sibling, NULL,
|
||||||
MCRF_PROPERTY(sibling, "Other child of parent, or None. Read-only."), NULL},
|
MCRF_PROPERTY(sibling, "Other child of parent, or None. Read-only."), NULL},
|
||||||
|
{"leaf_index", (getter)PyBSPNode::get_leaf_index, NULL,
|
||||||
|
MCRF_PROPERTY(leaf_index, "Leaf index (0..n-1) in adjacency graph, or None if not a leaf. Read-only."), NULL},
|
||||||
|
{"adjacent_tiles", (getter)PyBSPNode::get_adjacent_tiles, NULL,
|
||||||
|
MCRF_PROPERTY(adjacent_tiles, "Mapping of neighbor_index -> tuple of Vector wall tiles. "
|
||||||
|
"Returns tiles on THIS leaf's boundary suitable for corridor placement. "
|
||||||
|
"Each Vector has integer coordinates; use .int for (x, y) tuple. "
|
||||||
|
"Only available for leaf nodes. Read-only."), NULL},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1080,6 +1293,69 @@ PyObject* PyBSPNode::get_sibling(PyBSPNodeObject* self, void* closure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Property: leaf_index (#210)
|
||||||
|
PyObject* PyBSPNode::get_leaf_index(PyBSPNodeObject* self, void* closure)
|
||||||
|
{
|
||||||
|
if (!checkValid(self)) return nullptr;
|
||||||
|
|
||||||
|
// Only leaves have an index
|
||||||
|
if (!TCOD_bsp_is_leaf(self->node)) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
|
||||||
|
// Ensure cache is valid
|
||||||
|
ensure_adjacency_cache(bsp);
|
||||||
|
|
||||||
|
// Look up this node's index
|
||||||
|
auto it = bsp->adjacency_cache->ptr_to_index.find(self->node);
|
||||||
|
if (it == bsp->adjacency_cache->ptr_to_index.end()) {
|
||||||
|
// Should not happen if node is valid leaf
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Leaf node not found in adjacency cache");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PyLong_FromLong(it->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Property: adjacent_tiles (#210)
|
||||||
|
PyObject* PyBSPNode::get_adjacent_tiles(PyBSPNodeObject* self, void* closure)
|
||||||
|
{
|
||||||
|
if (!checkValid(self)) return nullptr;
|
||||||
|
|
||||||
|
// Only leaves have adjacent_tiles
|
||||||
|
if (!TCOD_bsp_is_leaf(self->node)) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "adjacent_tiles is only available for leaf nodes");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
|
||||||
|
// Ensure cache is valid
|
||||||
|
ensure_adjacency_cache(bsp);
|
||||||
|
|
||||||
|
// Look up this node's index
|
||||||
|
auto it = bsp->adjacency_cache->ptr_to_index.find(self->node);
|
||||||
|
if (it == bsp->adjacency_cache->ptr_to_index.end()) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Leaf node not found in adjacency cache");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create adjacent_tiles wrapper
|
||||||
|
PyBSPAdjacentTilesObject* tiles = (PyBSPAdjacentTilesObject*)
|
||||||
|
mcrfpydef::PyBSPAdjacentTilesType.tp_alloc(&mcrfpydef::PyBSPAdjacentTilesType, 0);
|
||||||
|
if (!tiles) return nullptr;
|
||||||
|
|
||||||
|
tiles->bsp_owner = self->bsp_owner;
|
||||||
|
tiles->node = self->node;
|
||||||
|
tiles->leaf_index = it->second;
|
||||||
|
tiles->generation = bsp->generation;
|
||||||
|
Py_INCREF(self->bsp_owner);
|
||||||
|
|
||||||
|
return (PyObject*)tiles;
|
||||||
|
}
|
||||||
|
|
||||||
// Method: contains(pos) -> bool
|
// Method: contains(pos) -> bool
|
||||||
PyObject* PyBSPNode::contains(PyBSPNodeObject* self, PyObject* args, PyObject* kwds)
|
PyObject* PyBSPNode::contains(PyBSPNodeObject* self, PyObject* args, PyObject* kwds)
|
||||||
{
|
{
|
||||||
|
|
@ -1167,3 +1443,337 @@ PyObject* PyBSPIter::repr(PyObject* obj)
|
||||||
|
|
||||||
return PyUnicode_FromString(ss.str().c_str());
|
return PyUnicode_FromString(ss.str().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== PyBSPAdjacency Implementation (#210) ====================
|
||||||
|
|
||||||
|
// Static method definitions
|
||||||
|
PySequenceMethods PyBSPAdjacency::sequence_methods = {
|
||||||
|
.sq_length = (lenfunc)PyBSPAdjacency::len,
|
||||||
|
.sq_item = (ssizeargfunc)PyBSPAdjacency::getitem,
|
||||||
|
};
|
||||||
|
|
||||||
|
PyMappingMethods PyBSPAdjacency::mapping_methods = {
|
||||||
|
.mp_length = (lenfunc)PyBSPAdjacency::len,
|
||||||
|
.mp_subscript = (binaryfunc)PyBSPAdjacency::subscript,
|
||||||
|
};
|
||||||
|
|
||||||
|
bool PyBSPAdjacency::checkValid(PyBSPAdjacencyObject* self)
|
||||||
|
{
|
||||||
|
if (!self->bsp_owner) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "BSPAdjacency has no parent BSP");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
if (self->generation != bsp->generation) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError,
|
||||||
|
"BSPAdjacency is stale: parent BSP was modified. Re-access bsp.adjacency.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PyBSPAdjacency::dealloc(PyBSPAdjacencyObject* self)
|
||||||
|
{
|
||||||
|
Py_XDECREF(self->bsp_owner);
|
||||||
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyBSPAdjacency::repr(PyObject* obj)
|
||||||
|
{
|
||||||
|
PyBSPAdjacencyObject* self = (PyBSPAdjacencyObject*)obj;
|
||||||
|
|
||||||
|
if (!self->bsp_owner) {
|
||||||
|
return PyUnicode_FromString("<BSPAdjacency (invalid)>");
|
||||||
|
}
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
if (self->generation != bsp->generation) {
|
||||||
|
return PyUnicode_FromString("<BSPAdjacency (stale)>");
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_adjacency_cache(bsp);
|
||||||
|
int n = (int)bsp->adjacency_cache->leaf_pointers.size();
|
||||||
|
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << "<BSPAdjacency with " << n << " leaves>";
|
||||||
|
return PyUnicode_FromString(ss.str().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_ssize_t PyBSPAdjacency::len(PyBSPAdjacencyObject* self)
|
||||||
|
{
|
||||||
|
if (!checkValid(self)) return -1;
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
ensure_adjacency_cache(bsp);
|
||||||
|
|
||||||
|
return (Py_ssize_t)bsp->adjacency_cache->leaf_pointers.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyBSPAdjacency::getitem(PyBSPAdjacencyObject* self, Py_ssize_t index)
|
||||||
|
{
|
||||||
|
if (!checkValid(self)) return nullptr;
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
ensure_adjacency_cache(bsp);
|
||||||
|
|
||||||
|
int n = (int)bsp->adjacency_cache->leaf_pointers.size();
|
||||||
|
|
||||||
|
// Handle negative indexing
|
||||||
|
if (index < 0) {
|
||||||
|
index += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= n) {
|
||||||
|
PyErr_SetString(PyExc_IndexError, "leaf index out of range");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get neighbors for this leaf
|
||||||
|
const auto& neighbors = bsp->adjacency_cache->graph[index];
|
||||||
|
|
||||||
|
// Build tuple of neighbor indices
|
||||||
|
PyObject* result = PyTuple_New(neighbors.size());
|
||||||
|
if (!result) return nullptr;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < neighbors.size(); i++) {
|
||||||
|
PyTuple_SET_ITEM(result, i, PyLong_FromLong(neighbors[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyBSPAdjacency::subscript(PyBSPAdjacencyObject* self, PyObject* key)
|
||||||
|
{
|
||||||
|
if (!PyLong_Check(key)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "adjacency indices must be integers");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_ssize_t index = PyLong_AsSsize_t(key);
|
||||||
|
if (index == -1 && PyErr_Occurred()) return nullptr;
|
||||||
|
|
||||||
|
return getitem(self, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyBSPAdjacency::iter(PyBSPAdjacencyObject* self)
|
||||||
|
{
|
||||||
|
if (!checkValid(self)) return nullptr;
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
ensure_adjacency_cache(bsp);
|
||||||
|
|
||||||
|
// Create a list of tuples for iteration
|
||||||
|
int n = (int)bsp->adjacency_cache->leaf_pointers.size();
|
||||||
|
PyObject* list = PyList_New(n);
|
||||||
|
if (!list) return nullptr;
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
const auto& neighbors = bsp->adjacency_cache->graph[i];
|
||||||
|
PyObject* tuple = PyTuple_New(neighbors.size());
|
||||||
|
if (!tuple) {
|
||||||
|
Py_DECREF(list);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (size_t j = 0; j < neighbors.size(); j++) {
|
||||||
|
PyTuple_SET_ITEM(tuple, j, PyLong_FromLong(neighbors[j]));
|
||||||
|
}
|
||||||
|
PyList_SET_ITEM(list, i, tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return iterator over the list
|
||||||
|
PyObject* iter = PyObject_GetIter(list);
|
||||||
|
Py_DECREF(list);
|
||||||
|
return iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PyBSPAdjacentTiles Implementation (#210) ====================
|
||||||
|
|
||||||
|
// Static method definitions
|
||||||
|
PyMappingMethods PyBSPAdjacentTiles::mapping_methods = {
|
||||||
|
.mp_length = (lenfunc)PyBSPAdjacentTiles::len,
|
||||||
|
.mp_subscript = (binaryfunc)PyBSPAdjacentTiles::subscript,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sequence methods for 'in' operator support
|
||||||
|
PySequenceMethods PyBSPAdjacentTiles::sequence_methods = {
|
||||||
|
.sq_length = (lenfunc)PyBSPAdjacentTiles::len,
|
||||||
|
.sq_contains = (objobjproc)PyBSPAdjacentTiles::contains,
|
||||||
|
};
|
||||||
|
|
||||||
|
PyMethodDef PyBSPAdjacentTiles::methods[] = {
|
||||||
|
{"keys", (PyCFunction)PyBSPAdjacentTiles::keys, METH_NOARGS,
|
||||||
|
"Return tuple of adjacent neighbor indices."},
|
||||||
|
{NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool PyBSPAdjacentTiles::checkValid(PyBSPAdjacentTilesObject* self)
|
||||||
|
{
|
||||||
|
if (!self->bsp_owner) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "BSPAdjacentTiles has no parent BSP");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
if (self->generation != bsp->generation) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError,
|
||||||
|
"BSPAdjacentTiles is stale: parent BSP was modified. Re-access node.adjacent_tiles.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PyBSPAdjacentTiles::dealloc(PyBSPAdjacentTilesObject* self)
|
||||||
|
{
|
||||||
|
Py_XDECREF(self->bsp_owner);
|
||||||
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyBSPAdjacentTiles::repr(PyObject* obj)
|
||||||
|
{
|
||||||
|
PyBSPAdjacentTilesObject* self = (PyBSPAdjacentTilesObject*)obj;
|
||||||
|
|
||||||
|
if (!self->bsp_owner) {
|
||||||
|
return PyUnicode_FromString("<BSPAdjacentTiles (invalid)>");
|
||||||
|
}
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
if (self->generation != bsp->generation) {
|
||||||
|
return PyUnicode_FromString("<BSPAdjacentTiles (stale)>");
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_adjacency_cache(bsp);
|
||||||
|
const auto& neighbors = bsp->adjacency_cache->graph[self->leaf_index];
|
||||||
|
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << "<BSPAdjacentTiles for leaf " << self->leaf_index
|
||||||
|
<< " with " << neighbors.size() << " neighbors>";
|
||||||
|
return PyUnicode_FromString(ss.str().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_ssize_t PyBSPAdjacentTiles::len(PyBSPAdjacentTilesObject* self)
|
||||||
|
{
|
||||||
|
if (!checkValid(self)) return -1;
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
ensure_adjacency_cache(bsp);
|
||||||
|
|
||||||
|
return (Py_ssize_t)bsp->adjacency_cache->graph[self->leaf_index].size();
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyBSPAdjacentTiles::subscript(PyBSPAdjacentTilesObject* self, PyObject* key)
|
||||||
|
{
|
||||||
|
if (!checkValid(self)) return nullptr;
|
||||||
|
|
||||||
|
if (!PyLong_Check(key)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "adjacent_tiles keys must be integers (neighbor leaf index)");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int neighbor_index = (int)PyLong_AsLong(key);
|
||||||
|
if (neighbor_index == -1 && PyErr_Occurred()) return nullptr;
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
ensure_adjacency_cache(bsp);
|
||||||
|
|
||||||
|
// Validate neighbor_index is in range
|
||||||
|
int n = (int)bsp->adjacency_cache->leaf_pointers.size();
|
||||||
|
if (neighbor_index < 0 || neighbor_index >= n) {
|
||||||
|
PyErr_Format(PyExc_KeyError, "%d", neighbor_index);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if neighbor_index is actually a neighbor
|
||||||
|
const auto& neighbors = bsp->adjacency_cache->graph[self->leaf_index];
|
||||||
|
bool is_neighbor = false;
|
||||||
|
for (int ni : neighbors) {
|
||||||
|
if (ni == neighbor_index) {
|
||||||
|
is_neighbor = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_neighbor) {
|
||||||
|
PyErr_Format(PyExc_KeyError, "%d (not adjacent to leaf %d)", neighbor_index, self->leaf_index);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or compute wall tiles
|
||||||
|
// Key is (self, neighbor) - NOT symmetric! Each direction has different tiles.
|
||||||
|
auto& cache = *bsp->adjacency_cache;
|
||||||
|
auto cache_key = std::make_pair(self->leaf_index, neighbor_index);
|
||||||
|
|
||||||
|
auto it = cache.wall_tiles_cache.find(cache_key);
|
||||||
|
if (it == cache.wall_tiles_cache.end()) {
|
||||||
|
// Compute and cache - returns tiles on self's edge bordering neighbor
|
||||||
|
TCOD_bsp_t* this_node = cache.leaf_pointers[self->leaf_index];
|
||||||
|
TCOD_bsp_t* other_node = cache.leaf_pointers[neighbor_index];
|
||||||
|
cache.wall_tiles_cache[cache_key] = compute_wall_tiles(this_node, other_node);
|
||||||
|
it = cache.wall_tiles_cache.find(cache_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tuple of Vector objects
|
||||||
|
const auto& tiles = it->second;
|
||||||
|
PyObject* result = PyTuple_New(tiles.size());
|
||||||
|
if (!result) return nullptr;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < tiles.size(); i++) {
|
||||||
|
// Convert sf::Vector2i to Python Vector (sf::Vector2f)
|
||||||
|
PyVector vec(sf::Vector2f((float)tiles[i].x, (float)tiles[i].y));
|
||||||
|
PyObject* py_vec = vec.pyObject();
|
||||||
|
if (!py_vec) {
|
||||||
|
Py_DECREF(result);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
PyTuple_SET_ITEM(result, i, py_vec);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PyBSPAdjacentTiles::contains(PyBSPAdjacentTilesObject* self, PyObject* key)
|
||||||
|
{
|
||||||
|
if (!checkValid(self)) return -1;
|
||||||
|
|
||||||
|
if (!PyLong_Check(key)) {
|
||||||
|
return 0; // Non-integer keys are not contained
|
||||||
|
}
|
||||||
|
|
||||||
|
int neighbor_index = (int)PyLong_AsLong(key);
|
||||||
|
if (neighbor_index == -1 && PyErr_Occurred()) {
|
||||||
|
PyErr_Clear();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
ensure_adjacency_cache(bsp);
|
||||||
|
|
||||||
|
const auto& neighbors = bsp->adjacency_cache->graph[self->leaf_index];
|
||||||
|
for (int ni : neighbors) {
|
||||||
|
if (ni == neighbor_index) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyBSPAdjacentTiles::keys(PyBSPAdjacentTilesObject* self, PyObject* Py_UNUSED(args))
|
||||||
|
{
|
||||||
|
if (!checkValid(self)) return nullptr;
|
||||||
|
|
||||||
|
PyBSPObject* bsp = (PyBSPObject*)self->bsp_owner;
|
||||||
|
ensure_adjacency_cache(bsp);
|
||||||
|
|
||||||
|
const auto& neighbors = bsp->adjacency_cache->graph[self->leaf_index];
|
||||||
|
|
||||||
|
PyObject* result = PyTuple_New(neighbors.size());
|
||||||
|
if (!result) return nullptr;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < neighbors.size(); i++) {
|
||||||
|
PyTuple_SET_ITEM(result, i, PyLong_FromLong(neighbors[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
||||||
121
src/PyBSP.h
121
src/PyBSP.h
|
|
@ -4,10 +4,26 @@
|
||||||
#include <libtcod.h>
|
#include <libtcod.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <map>
|
||||||
|
#include <SFML/System/Vector2.hpp>
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
class PyBSP;
|
class PyBSP;
|
||||||
class PyBSPNode;
|
class PyBSPNode;
|
||||||
|
class PyBSPAdjacency;
|
||||||
|
class PyBSPAdjacentTiles;
|
||||||
|
|
||||||
|
// Adjacency cache - computed lazily, invalidated on generation change (#210)
|
||||||
|
struct BSPAdjacencyCache {
|
||||||
|
std::vector<std::vector<int>> graph; // graph[i] = neighbor indices for leaf i
|
||||||
|
std::vector<TCOD_bsp_t*> leaf_pointers; // leaf_pointers[i] = TCOD node for leaf i
|
||||||
|
std::unordered_map<TCOD_bsp_t*, int> ptr_to_index; // Reverse lookup: node ptr -> index
|
||||||
|
uint64_t generation; // Generation when computed
|
||||||
|
// Wall tile cache: key=(self_index, neighbor_index) - perspective matters!
|
||||||
|
// leaf0.adjacent_tiles[1] returns tiles on leaf0's edge, not symmetric with leaf1.adjacent_tiles[0]
|
||||||
|
std::map<std::pair<int,int>, std::vector<sf::Vector2i>> wall_tiles_cache;
|
||||||
|
};
|
||||||
|
|
||||||
// Maximum recursion depth to prevent memory exhaustion
|
// Maximum recursion depth to prevent memory exhaustion
|
||||||
// 2^16 = 65536 potential leaf nodes, which is already excessive
|
// 2^16 = 65536 potential leaf nodes, which is already excessive
|
||||||
|
|
@ -20,6 +36,7 @@ typedef struct {
|
||||||
int orig_x, orig_y; // Original bounds for clear()
|
int orig_x, orig_y; // Original bounds for clear()
|
||||||
int orig_w, orig_h;
|
int orig_w, orig_h;
|
||||||
uint64_t generation; // Incremented on structural changes (clear, split)
|
uint64_t generation; // Incremented on structural changes (clear, split)
|
||||||
|
BSPAdjacencyCache* adjacency_cache; // Lazy-computed adjacency graph (#210)
|
||||||
} PyBSPObject;
|
} PyBSPObject;
|
||||||
|
|
||||||
// Python object structure for BSPNode (lightweight reference)
|
// Python object structure for BSPNode (lightweight reference)
|
||||||
|
|
@ -39,6 +56,22 @@ typedef struct {
|
||||||
uint64_t generation; // Generation at iterator creation
|
uint64_t generation; // Generation at iterator creation
|
||||||
} PyBSPIterObject;
|
} PyBSPIterObject;
|
||||||
|
|
||||||
|
// Python object for BSP.adjacency property (#210)
|
||||||
|
typedef struct {
|
||||||
|
PyObject_HEAD
|
||||||
|
PyObject* bsp_owner; // Reference to PyBSPObject
|
||||||
|
uint64_t generation; // Generation at creation (for validity check)
|
||||||
|
} PyBSPAdjacencyObject;
|
||||||
|
|
||||||
|
// Python object for BSPNode.adjacent_tiles property (#210)
|
||||||
|
typedef struct {
|
||||||
|
PyObject_HEAD
|
||||||
|
PyObject* bsp_owner; // Reference to PyBSPObject
|
||||||
|
TCOD_bsp_t* node; // The leaf node this belongs to
|
||||||
|
int leaf_index; // This leaf's index in adjacency graph
|
||||||
|
uint64_t generation; // Generation at creation (for validity check)
|
||||||
|
} PyBSPAdjacentTilesObject;
|
||||||
|
|
||||||
class PyBSP
|
class PyBSP
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
@ -53,6 +86,7 @@ public:
|
||||||
static PyObject* get_pos(PyBSPObject* self, void* closure);
|
static PyObject* get_pos(PyBSPObject* self, void* closure);
|
||||||
static PyObject* get_size(PyBSPObject* self, void* closure);
|
static PyObject* get_size(PyBSPObject* self, void* closure);
|
||||||
static PyObject* get_root(PyBSPObject* self, void* closure);
|
static PyObject* get_root(PyBSPObject* self, void* closure);
|
||||||
|
static PyObject* get_adjacency(PyBSPObject* self, void* closure); // #210
|
||||||
|
|
||||||
// Splitting methods (#202)
|
// Splitting methods (#202)
|
||||||
static PyObject* split_once(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* split_once(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
@ -65,6 +99,7 @@ public:
|
||||||
|
|
||||||
// Query methods (#205)
|
// Query methods (#205)
|
||||||
static PyObject* find(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* find(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static PyObject* get_leaf(PyBSPObject* self, PyObject* args, PyObject* kwds); // #210
|
||||||
|
|
||||||
// HeightMap conversion (#206)
|
// HeightMap conversion (#206)
|
||||||
static PyObject* to_heightmap(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* to_heightmap(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
@ -106,6 +141,10 @@ public:
|
||||||
static PyObject* get_parent(PyBSPNodeObject* self, void* closure);
|
static PyObject* get_parent(PyBSPNodeObject* self, void* closure);
|
||||||
static PyObject* get_sibling(PyBSPNodeObject* self, void* closure);
|
static PyObject* get_sibling(PyBSPNodeObject* self, void* closure);
|
||||||
|
|
||||||
|
// Adjacency properties (#210)
|
||||||
|
static PyObject* get_leaf_index(PyBSPNodeObject* self, void* closure);
|
||||||
|
static PyObject* get_adjacent_tiles(PyBSPNodeObject* self, void* closure);
|
||||||
|
|
||||||
// Methods (#203)
|
// Methods (#203)
|
||||||
static PyObject* contains(PyBSPNodeObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* contains(PyBSPNodeObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* center(PyBSPNodeObject* self, PyObject* Py_UNUSED(args));
|
static PyObject* center(PyBSPNodeObject* self, PyObject* Py_UNUSED(args));
|
||||||
|
|
@ -132,6 +171,39 @@ public:
|
||||||
static PyObject* repr(PyObject* obj);
|
static PyObject* repr(PyObject* obj);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// BSP Adjacency wrapper class (#210)
|
||||||
|
class PyBSPAdjacency
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static void dealloc(PyBSPAdjacencyObject* self);
|
||||||
|
static PyObject* repr(PyObject* obj);
|
||||||
|
static Py_ssize_t len(PyBSPAdjacencyObject* self);
|
||||||
|
static PyObject* getitem(PyBSPAdjacencyObject* self, Py_ssize_t index);
|
||||||
|
static PyObject* subscript(PyBSPAdjacencyObject* self, PyObject* key);
|
||||||
|
static PyObject* iter(PyBSPAdjacencyObject* self);
|
||||||
|
static bool checkValid(PyBSPAdjacencyObject* self);
|
||||||
|
|
||||||
|
static PySequenceMethods sequence_methods;
|
||||||
|
static PyMappingMethods mapping_methods;
|
||||||
|
};
|
||||||
|
|
||||||
|
// BSP Adjacent Tiles wrapper class (#210)
|
||||||
|
class PyBSPAdjacentTiles
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static void dealloc(PyBSPAdjacentTilesObject* self);
|
||||||
|
static PyObject* repr(PyObject* obj);
|
||||||
|
static Py_ssize_t len(PyBSPAdjacentTilesObject* self);
|
||||||
|
static PyObject* subscript(PyBSPAdjacentTilesObject* self, PyObject* key);
|
||||||
|
static int contains(PyBSPAdjacentTilesObject* self, PyObject* key);
|
||||||
|
static PyObject* keys(PyBSPAdjacentTilesObject* self, PyObject* Py_UNUSED(args));
|
||||||
|
static bool checkValid(PyBSPAdjacentTilesObject* self);
|
||||||
|
|
||||||
|
static PyMappingMethods mapping_methods;
|
||||||
|
static PySequenceMethods sequence_methods; // For 'in' operator
|
||||||
|
static PyMethodDef methods[];
|
||||||
|
};
|
||||||
|
|
||||||
// Traversal enum creation
|
// Traversal enum creation
|
||||||
class PyTraversal
|
class PyTraversal
|
||||||
{
|
{
|
||||||
|
|
@ -237,4 +309,53 @@ namespace mcrfpydef {
|
||||||
return NULL;
|
return NULL;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// BSP Adjacency - internal type for BSP.adjacency property (#210)
|
||||||
|
inline PyTypeObject PyBSPAdjacencyType = {
|
||||||
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
.tp_name = "mcrfpy.BSPAdjacency",
|
||||||
|
.tp_basicsize = sizeof(PyBSPAdjacencyObject),
|
||||||
|
.tp_itemsize = 0,
|
||||||
|
.tp_dealloc = (destructor)PyBSPAdjacency::dealloc,
|
||||||
|
.tp_repr = PyBSPAdjacency::repr,
|
||||||
|
.tp_as_sequence = &PyBSPAdjacency::sequence_methods,
|
||||||
|
.tp_as_mapping = &PyBSPAdjacency::mapping_methods,
|
||||||
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
|
.tp_doc = PyDoc_STR(
|
||||||
|
"BSPAdjacency - Sequence of leaf neighbor tuples.\n\n"
|
||||||
|
"Accessed via BSP.adjacency property. adjacency[i] returns a tuple\n"
|
||||||
|
"of leaf indices that are adjacent to (share a wall with) leaf i.\n"
|
||||||
|
),
|
||||||
|
.tp_iter = (getiterfunc)PyBSPAdjacency::iter,
|
||||||
|
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "BSPAdjacency cannot be instantiated directly");
|
||||||
|
return NULL;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// BSP Adjacent Tiles - internal type for BSPNode.adjacent_tiles property (#210)
|
||||||
|
inline PyTypeObject PyBSPAdjacentTilesType = {
|
||||||
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
.tp_name = "mcrfpy.BSPAdjacentTiles",
|
||||||
|
.tp_basicsize = sizeof(PyBSPAdjacentTilesObject),
|
||||||
|
.tp_itemsize = 0,
|
||||||
|
.tp_dealloc = (destructor)PyBSPAdjacentTiles::dealloc,
|
||||||
|
.tp_repr = PyBSPAdjacentTiles::repr,
|
||||||
|
.tp_as_sequence = &PyBSPAdjacentTiles::sequence_methods, // For 'in' operator
|
||||||
|
.tp_as_mapping = &PyBSPAdjacentTiles::mapping_methods,
|
||||||
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
|
.tp_doc = PyDoc_STR(
|
||||||
|
"BSPAdjacentTiles - Mapping of neighbor index to wall tile coordinates.\n\n"
|
||||||
|
"Accessed via BSPNode.adjacent_tiles property. adjacent_tiles[j] returns\n"
|
||||||
|
"a tuple of Vector coordinates representing tiles on THIS leaf's edge that\n"
|
||||||
|
"border neighbor j. Each Vector has integer x/y coordinates (use .int for tuple).\n"
|
||||||
|
"Raises KeyError if j is not an adjacent leaf.\n\n"
|
||||||
|
"Supports 'in' operator: `5 in leaf.adjacent_tiles` checks if leaf 5 is adjacent.\n"
|
||||||
|
),
|
||||||
|
.tp_methods = PyBSPAdjacentTiles::methods,
|
||||||
|
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "BSPAdjacentTiles cannot be instantiated directly");
|
||||||
|
return NULL;
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
323
tests/unit/bsp_adjacency_test.py
Normal file
323
tests/unit/bsp_adjacency_test.py
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Tests for BSP adjacency graph feature (#210)"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def test_adjacency_basic():
|
||||||
|
"""Test basic adjacency on a simple 2-leaf BSP"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 50))
|
||||||
|
# Split once - creates 2 leaves
|
||||||
|
bsp.split_once(horizontal=False, position=50) # Vertical split at x=50
|
||||||
|
|
||||||
|
leaves = list(bsp.leaves())
|
||||||
|
assert len(leaves) == 2, f"Expected 2 leaves, got {len(leaves)}"
|
||||||
|
|
||||||
|
# Access adjacency
|
||||||
|
adj = bsp.adjacency
|
||||||
|
assert len(adj) == 2, f"Expected adjacency len 2, got {len(adj)}"
|
||||||
|
|
||||||
|
# Each leaf should be adjacent to the other
|
||||||
|
neighbors_0 = adj[0]
|
||||||
|
neighbors_1 = adj[1]
|
||||||
|
|
||||||
|
assert 1 in neighbors_0, f"Leaf 0 should be adjacent to leaf 1, got {neighbors_0}"
|
||||||
|
assert 0 in neighbors_1, f"Leaf 1 should be adjacent to leaf 0, got {neighbors_1}"
|
||||||
|
|
||||||
|
print(" test_adjacency_basic: PASS")
|
||||||
|
|
||||||
|
def test_leaf_indexing():
|
||||||
|
"""Test that leaf_index property works correctly"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 100))
|
||||||
|
bsp.split_recursive(depth=2, min_size=(10, 10), seed=42)
|
||||||
|
|
||||||
|
leaves = list(bsp.leaves())
|
||||||
|
|
||||||
|
# Each leaf should have a valid index
|
||||||
|
for i, leaf in enumerate(leaves):
|
||||||
|
assert leaf.leaf_index == i, f"Leaf {i} has index {leaf.leaf_index}"
|
||||||
|
|
||||||
|
# Non-leaves should return None
|
||||||
|
root = bsp.root
|
||||||
|
if not root.is_leaf:
|
||||||
|
assert root.leaf_index is None, "Non-leaf should have leaf_index=None"
|
||||||
|
|
||||||
|
print(" test_leaf_indexing: PASS")
|
||||||
|
|
||||||
|
def test_adjacency_symmetry():
|
||||||
|
"""Test that adjacency is symmetric: if A adjacent to B, then B adjacent to A"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 100))
|
||||||
|
bsp.split_recursive(depth=3, min_size=(10, 10), seed=42)
|
||||||
|
|
||||||
|
adj = bsp.adjacency
|
||||||
|
n = len(adj)
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
for j in adj[i]:
|
||||||
|
assert i in adj[j], f"Adjacency not symmetric: {i} -> {j} but not {j} -> {i}"
|
||||||
|
|
||||||
|
print(" test_adjacency_symmetry: PASS")
|
||||||
|
|
||||||
|
def test_adjacent_tiles_basic():
|
||||||
|
"""Test that adjacent_tiles returns Vector tuples"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 50))
|
||||||
|
bsp.split_once(horizontal=False, position=50) # Vertical split at x=50
|
||||||
|
|
||||||
|
leaves = list(bsp.leaves())
|
||||||
|
assert len(leaves) == 2
|
||||||
|
|
||||||
|
leaf0 = leaves[0]
|
||||||
|
neighbors = bsp.adjacency[0]
|
||||||
|
assert len(neighbors) > 0, "Leaf 0 should have neighbors"
|
||||||
|
|
||||||
|
neighbor_idx = neighbors[0]
|
||||||
|
wall_tiles = leaf0.adjacent_tiles[neighbor_idx]
|
||||||
|
|
||||||
|
assert len(wall_tiles) > 0, "Should have wall tiles"
|
||||||
|
|
||||||
|
# Check that wall tiles are Vector objects
|
||||||
|
first_tile = wall_tiles[0]
|
||||||
|
assert hasattr(first_tile, 'x') and hasattr(first_tile, 'y'), \
|
||||||
|
f"Wall tile should be a Vector, got {type(first_tile)}"
|
||||||
|
|
||||||
|
print(" test_adjacent_tiles_basic: PASS")
|
||||||
|
|
||||||
|
def test_adjacent_tiles_keyerror():
|
||||||
|
"""Test that non-adjacent lookups raise KeyError"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 100))
|
||||||
|
bsp.split_recursive(depth=3, min_size=(10, 10), seed=42)
|
||||||
|
|
||||||
|
leaves = list(bsp.leaves())
|
||||||
|
|
||||||
|
# Find a non-adjacent pair
|
||||||
|
adj = bsp.adjacency
|
||||||
|
for i in range(len(leaves)):
|
||||||
|
for j in range(len(leaves)):
|
||||||
|
if i != j and j not in adj[i]:
|
||||||
|
# i and j are not adjacent
|
||||||
|
try:
|
||||||
|
_ = leaves[i].adjacent_tiles[j]
|
||||||
|
assert False, f"Expected KeyError for non-adjacent pair {i}, {j}"
|
||||||
|
except KeyError:
|
||||||
|
pass # Expected
|
||||||
|
print(" test_adjacent_tiles_keyerror: PASS")
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we get here, all pairs are adjacent (unlikely with depth 3)
|
||||||
|
print(" test_adjacent_tiles_keyerror: SKIP (all pairs adjacent)")
|
||||||
|
|
||||||
|
def test_cache_invalidation():
|
||||||
|
"""Test that cache is invalidated on clear() and split_recursive()"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 100))
|
||||||
|
bsp.split_recursive(depth=2, min_size=(10, 10), seed=42)
|
||||||
|
|
||||||
|
# Access adjacency to populate cache
|
||||||
|
adj1 = bsp.adjacency
|
||||||
|
n1 = len(adj1)
|
||||||
|
|
||||||
|
# Clear and re-split
|
||||||
|
bsp.clear()
|
||||||
|
bsp.split_recursive(depth=3, min_size=(10, 10), seed=123)
|
||||||
|
|
||||||
|
# Access adjacency again - should be rebuilt
|
||||||
|
adj2 = bsp.adjacency
|
||||||
|
n2 = len(adj2)
|
||||||
|
|
||||||
|
# Different seed/depth should give different results
|
||||||
|
assert n2 > n1 or n2 != n1, "Cache should be invalidated after clear()"
|
||||||
|
|
||||||
|
print(" test_cache_invalidation: PASS")
|
||||||
|
|
||||||
|
def test_wall_tiles_on_boundary():
|
||||||
|
"""Test that wall tiles are on the correct boundary"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 50))
|
||||||
|
bsp.split_once(horizontal=False, position=50) # Vertical split at x=50
|
||||||
|
|
||||||
|
leaves = list(bsp.leaves())
|
||||||
|
leaf0 = leaves[0] # Should be left side (x: 0-50)
|
||||||
|
leaf1 = leaves[1] # Should be right side (x: 50-100)
|
||||||
|
|
||||||
|
# Get wall tiles from leaf0 to leaf1
|
||||||
|
wall_tiles = leaf0.adjacent_tiles[1]
|
||||||
|
|
||||||
|
# Wall should be at x=49 (last column of leaf0) for leaf0
|
||||||
|
for tile in wall_tiles:
|
||||||
|
x, y = int(tile.x), int(tile.y)
|
||||||
|
# Tile should be within leaf0's bounds
|
||||||
|
assert x == 49, f"Wall tile x should be 49 (boundary), got {x}"
|
||||||
|
assert 0 <= y < 50, f"Wall tile y should be in range 0-49, got {y}"
|
||||||
|
|
||||||
|
print(" test_wall_tiles_on_boundary: PASS")
|
||||||
|
|
||||||
|
def test_negative_indexing():
|
||||||
|
"""Test that negative indices work for adjacency"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 100))
|
||||||
|
bsp.split_recursive(depth=2, min_size=(10, 10), seed=42)
|
||||||
|
|
||||||
|
adj = bsp.adjacency
|
||||||
|
n = len(adj)
|
||||||
|
|
||||||
|
# adj[-1] should be same as adj[n-1]
|
||||||
|
last_positive = adj[n-1]
|
||||||
|
last_negative = adj[-1]
|
||||||
|
|
||||||
|
assert last_positive == last_negative, \
|
||||||
|
f"Negative indexing failed: adj[-1]={last_negative}, adj[{n-1}]={last_positive}"
|
||||||
|
|
||||||
|
print(" test_negative_indexing: PASS")
|
||||||
|
|
||||||
|
def test_iteration():
|
||||||
|
"""Test that adjacency can be iterated"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 100))
|
||||||
|
bsp.split_recursive(depth=2, min_size=(10, 10), seed=42)
|
||||||
|
|
||||||
|
adj = bsp.adjacency
|
||||||
|
|
||||||
|
# Should be iterable
|
||||||
|
count = 0
|
||||||
|
for neighbors in adj:
|
||||||
|
assert isinstance(neighbors, tuple), f"Expected tuple, got {type(neighbors)}"
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
assert count == len(adj), f"Iteration count {count} != len {len(adj)}"
|
||||||
|
|
||||||
|
print(" test_iteration: PASS")
|
||||||
|
|
||||||
|
def test_keys_method():
|
||||||
|
"""Test that adjacent_tiles.keys() works"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 50))
|
||||||
|
bsp.split_once(horizontal=False, position=50)
|
||||||
|
|
||||||
|
leaves = list(bsp.leaves())
|
||||||
|
leaf0 = leaves[0]
|
||||||
|
|
||||||
|
keys = leaf0.adjacent_tiles.keys()
|
||||||
|
assert isinstance(keys, tuple), f"keys() should return tuple, got {type(keys)}"
|
||||||
|
assert len(keys) > 0, "Should have at least one neighbor"
|
||||||
|
assert 1 in keys, f"Leaf 1 should be in keys, got {keys}"
|
||||||
|
|
||||||
|
print(" test_keys_method: PASS")
|
||||||
|
|
||||||
|
def test_split_once_invalidation():
|
||||||
|
"""Test that split_once invalidates adjacency cache and BSPNode references"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 100))
|
||||||
|
|
||||||
|
# First split - creates 2 leaves
|
||||||
|
bsp.split_once(horizontal=False, position=50)
|
||||||
|
|
||||||
|
# Access adjacency to build cache
|
||||||
|
adj1 = bsp.adjacency
|
||||||
|
n1 = len(adj1)
|
||||||
|
assert n1 == 2, f"Expected 2 leaves after first split, got {n1}"
|
||||||
|
|
||||||
|
# Get a reference to a leaf before second split
|
||||||
|
old_leaf = list(bsp.leaves())[0]
|
||||||
|
old_leaf_index = old_leaf.leaf_index
|
||||||
|
|
||||||
|
# Second split_once on the BSP (note: split_once always works on root)
|
||||||
|
# After this, the tree structure changes, but old_leaf should be stale
|
||||||
|
bsp.clear() # Clear and split fresh to get more leaves
|
||||||
|
bsp.split_once(horizontal=False, position=50)
|
||||||
|
bsp.split_once(horizontal=True, position=50) # Won't work - split_once only on root
|
||||||
|
|
||||||
|
# The old leaf reference should now be stale
|
||||||
|
try:
|
||||||
|
_ = old_leaf.leaf_index
|
||||||
|
assert False, "Expected RuntimeError for stale BSPNode"
|
||||||
|
except RuntimeError:
|
||||||
|
pass # Expected - node is stale after clear()
|
||||||
|
|
||||||
|
# Access adjacency - should reflect new structure (2 leaves again)
|
||||||
|
adj2 = bsp.adjacency
|
||||||
|
n2 = len(adj2)
|
||||||
|
assert n2 == 2, f"Expected 2 leaves after clear+split, got {n2}"
|
||||||
|
|
||||||
|
print(" test_split_once_invalidation: PASS")
|
||||||
|
|
||||||
|
def test_wall_tiles_perspective():
|
||||||
|
"""Test that wall tiles are from the correct leaf's perspective"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 50))
|
||||||
|
bsp.split_once(horizontal=False, position=50) # Vertical split at x=50
|
||||||
|
|
||||||
|
leaves = list(bsp.leaves())
|
||||||
|
leaf0 = leaves[0] # Left side: x 0-50
|
||||||
|
leaf1 = leaves[1] # Right side: x 50-100
|
||||||
|
|
||||||
|
# Get tiles from leaf0's perspective (should be at x=49, leaf0's edge)
|
||||||
|
tiles_0_to_1 = leaf0.adjacent_tiles[1]
|
||||||
|
for tile in tiles_0_to_1:
|
||||||
|
assert int(tile.x) == 49, f"Leaf0->Leaf1 tile should be at x=49, got {tile.x}"
|
||||||
|
|
||||||
|
# Get tiles from leaf1's perspective (should be at x=50, leaf1's edge)
|
||||||
|
tiles_1_to_0 = leaf1.adjacent_tiles[0]
|
||||||
|
for tile in tiles_1_to_0:
|
||||||
|
assert int(tile.x) == 50, f"Leaf1->Leaf0 tile should be at x=50, got {tile.x}"
|
||||||
|
|
||||||
|
print(" test_wall_tiles_perspective: PASS")
|
||||||
|
|
||||||
|
def test_get_leaf():
|
||||||
|
"""Test bsp.get_leaf(index) method"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 100))
|
||||||
|
bsp.split_recursive(depth=2, min_size=(10, 10), seed=42)
|
||||||
|
|
||||||
|
leaves = list(bsp.leaves())
|
||||||
|
n = len(leaves)
|
||||||
|
|
||||||
|
# Test positive indices
|
||||||
|
for i in range(n):
|
||||||
|
leaf = bsp.get_leaf(i)
|
||||||
|
assert leaf.leaf_index == i, f"get_leaf({i}) returned leaf with index {leaf.leaf_index}"
|
||||||
|
|
||||||
|
# Test negative index
|
||||||
|
last_leaf = bsp.get_leaf(-1)
|
||||||
|
assert last_leaf.leaf_index == n - 1, f"get_leaf(-1) should return leaf {n-1}, got {last_leaf.leaf_index}"
|
||||||
|
|
||||||
|
# Test out of range
|
||||||
|
try:
|
||||||
|
bsp.get_leaf(n)
|
||||||
|
assert False, "Expected IndexError for out-of-range index"
|
||||||
|
except IndexError:
|
||||||
|
pass # Expected
|
||||||
|
|
||||||
|
print(" test_get_leaf: PASS")
|
||||||
|
|
||||||
|
def test_contains_operator():
|
||||||
|
"""Test 'in' operator for adjacent_tiles"""
|
||||||
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 50))
|
||||||
|
bsp.split_once(horizontal=False, position=50)
|
||||||
|
|
||||||
|
leaves = list(bsp.leaves())
|
||||||
|
leaf0 = leaves[0]
|
||||||
|
|
||||||
|
# Leaf 1 should be adjacent to leaf 0
|
||||||
|
assert 1 in leaf0.adjacent_tiles, "1 should be in leaf0.adjacent_tiles"
|
||||||
|
|
||||||
|
# Some arbitrary index should not be (assuming only 2 leaves)
|
||||||
|
assert 5 not in leaf0.adjacent_tiles, "5 should not be in leaf0.adjacent_tiles"
|
||||||
|
|
||||||
|
print(" test_contains_operator: PASS")
|
||||||
|
|
||||||
|
def run_all_tests():
|
||||||
|
"""Run all adjacency tests"""
|
||||||
|
print("Running BSP adjacency tests (#210)...")
|
||||||
|
|
||||||
|
test_adjacency_basic()
|
||||||
|
test_leaf_indexing()
|
||||||
|
test_adjacency_symmetry()
|
||||||
|
test_adjacent_tiles_basic()
|
||||||
|
test_adjacent_tiles_keyerror()
|
||||||
|
test_cache_invalidation()
|
||||||
|
test_wall_tiles_on_boundary()
|
||||||
|
test_negative_indexing()
|
||||||
|
test_iteration()
|
||||||
|
test_keys_method()
|
||||||
|
test_split_once_invalidation()
|
||||||
|
test_wall_tiles_perspective()
|
||||||
|
test_get_leaf()
|
||||||
|
test_contains_operator()
|
||||||
|
|
||||||
|
print("\nAll BSP adjacency tests passed!")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(run_all_tests())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue