BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206: - BSP class: core tree structure with bounds, split_once, split_recursive, clear - BSPNode class: lightweight node reference with bounds, level, is_leaf, split_horizontal, split_position; navigation via left/right/parent/sibling; contains() and center() methods - Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER - BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes - BSP query: find(pos) returns deepest node containing position - BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap. The HeightMap already has all necessary operations (inverse, threshold, etc.) for procedural generation workflows. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a4b1ab7d68
commit
8699bba9e6
5 changed files with 1703 additions and 0 deletions
|
|
@ -22,6 +22,7 @@
|
|||
#include "PyMouse.h"
|
||||
#include "UIGridPathfinding.h" // AStarPath and DijkstraMap types
|
||||
#include "PyHeightMap.h" // Procedural generation heightmap (#193)
|
||||
#include "PyBSP.h" // Procedural generation BSP (#202-206)
|
||||
#include "McRogueFaceVersion.h"
|
||||
#include "GameEngine.h"
|
||||
#include "ImGuiConsole.h"
|
||||
|
|
@ -418,6 +419,7 @@ PyObject* PyInit_mcrfpy()
|
|||
|
||||
/*procedural generation (#192)*/
|
||||
&mcrfpydef::PyHeightMapType,
|
||||
&mcrfpydef::PyBSPType,
|
||||
|
||||
nullptr};
|
||||
|
||||
|
|
@ -434,6 +436,10 @@ PyObject* PyInit_mcrfpy()
|
|||
/*pathfinding iterator - returned by AStarPath.__iter__() but not directly instantiable*/
|
||||
&mcrfpydef::PyAStarPathIterType,
|
||||
|
||||
/*BSP internal types - returned by BSP methods but not directly instantiable*/
|
||||
&mcrfpydef::PyBSPNodeType,
|
||||
&mcrfpydef::PyBSPIterType,
|
||||
|
||||
nullptr};
|
||||
|
||||
// Set up PyWindowType methods and getsetters before PyType_Ready
|
||||
|
|
@ -448,6 +454,12 @@ PyObject* PyInit_mcrfpy()
|
|||
mcrfpydef::PyHeightMapType.tp_methods = PyHeightMap::methods;
|
||||
mcrfpydef::PyHeightMapType.tp_getset = PyHeightMap::getsetters;
|
||||
|
||||
// Set up PyBSPType and BSPNode methods and getsetters (#202-206)
|
||||
mcrfpydef::PyBSPType.tp_methods = PyBSP::methods;
|
||||
mcrfpydef::PyBSPType.tp_getset = PyBSP::getsetters;
|
||||
mcrfpydef::PyBSPNodeType.tp_methods = PyBSPNode::methods;
|
||||
mcrfpydef::PyBSPNodeType.tp_getset = PyBSPNode::getsetters;
|
||||
|
||||
// Set up weakref support for all types that need it
|
||||
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
|
||||
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
|
||||
|
|
@ -555,6 +567,13 @@ PyObject* PyInit_mcrfpy()
|
|||
PyErr_Clear();
|
||||
}
|
||||
|
||||
// Add Traversal enum class for BSP traversal (uses Python's IntEnum)
|
||||
PyObject* traversal_class = PyTraversal::create_enum_class(m);
|
||||
if (!traversal_class) {
|
||||
// If enum creation fails, continue without it (non-fatal)
|
||||
PyErr_Clear();
|
||||
}
|
||||
|
||||
// Add Key enum class for keyboard input
|
||||
PyObject* key_class = PyKey::create_enum_class(m);
|
||||
if (!key_class) {
|
||||
|
|
|
|||
1047
src/PyBSP.cpp
Normal file
1047
src/PyBSP.cpp
Normal file
File diff suppressed because it is too large
Load diff
201
src/PyBSP.h
Normal file
201
src/PyBSP.h
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <libtcod.h>
|
||||
#include <vector>
|
||||
|
||||
// Forward declarations
|
||||
class PyBSP;
|
||||
class PyBSPNode;
|
||||
|
||||
// Python object structure for BSP tree (root owner)
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
TCOD_bsp_t* root; // libtcod BSP root (owned, will be deleted)
|
||||
int orig_x, orig_y; // Original bounds for clear()
|
||||
int orig_w, orig_h;
|
||||
} PyBSPObject;
|
||||
|
||||
// Python object structure for BSPNode (lightweight reference)
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
TCOD_bsp_t* node; // libtcod BSP node (NOT owned)
|
||||
PyObject* bsp_owner; // Reference to PyBSPObject to prevent dangling
|
||||
} PyBSPNodeObject;
|
||||
|
||||
// BSP iterator for traverse()
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::vector<TCOD_bsp_t*>* nodes; // Pre-collected nodes
|
||||
size_t index;
|
||||
PyObject* bsp_owner; // Reference to PyBSPObject
|
||||
} PyBSPIterObject;
|
||||
|
||||
class PyBSP
|
||||
{
|
||||
public:
|
||||
// Python type interface
|
||||
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
||||
static int init(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
||||
static void dealloc(PyBSPObject* self);
|
||||
static PyObject* repr(PyObject* obj);
|
||||
|
||||
// Properties
|
||||
static PyObject* get_bounds(PyBSPObject* self, void* closure);
|
||||
static PyObject* get_root(PyBSPObject* self, void* closure);
|
||||
|
||||
// Splitting methods (#202)
|
||||
static PyObject* split_once(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* split_recursive(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* clear(PyBSPObject* self, PyObject* Py_UNUSED(args));
|
||||
|
||||
// Iteration methods (#204)
|
||||
static PyObject* leaves(PyBSPObject* self, PyObject* Py_UNUSED(args));
|
||||
static PyObject* traverse(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Query methods (#205)
|
||||
static PyObject* find(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// HeightMap conversion (#206)
|
||||
static PyObject* to_heightmap(PyBSPObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Method and property definitions
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
};
|
||||
|
||||
class PyBSPNode
|
||||
{
|
||||
public:
|
||||
// Python type interface
|
||||
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
||||
static int init(PyBSPNodeObject* self, PyObject* args, PyObject* kwds);
|
||||
static void dealloc(PyBSPNodeObject* self);
|
||||
static PyObject* repr(PyObject* obj);
|
||||
|
||||
// Properties (#203)
|
||||
static PyObject* get_bounds(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_level(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_is_leaf(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_split_horizontal(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_split_position(PyBSPNodeObject* self, void* closure);
|
||||
|
||||
// Navigation properties (#203)
|
||||
static PyObject* get_left(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_right(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_parent(PyBSPNodeObject* self, void* closure);
|
||||
static PyObject* get_sibling(PyBSPNodeObject* self, void* closure);
|
||||
|
||||
// Methods (#203)
|
||||
static PyObject* contains(PyBSPNodeObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* center(PyBSPNodeObject* self, PyObject* Py_UNUSED(args));
|
||||
|
||||
// Helper to create a BSPNode from a TCOD_bsp_t*
|
||||
static PyObject* create(TCOD_bsp_t* node, PyObject* bsp_owner);
|
||||
|
||||
// Method and property definitions
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
};
|
||||
|
||||
// BSP Iterator class
|
||||
class PyBSPIter
|
||||
{
|
||||
public:
|
||||
static void dealloc(PyBSPIterObject* self);
|
||||
static PyObject* iter(PyObject* self);
|
||||
static PyObject* next(PyBSPIterObject* self);
|
||||
static int init(PyBSPIterObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* repr(PyObject* obj);
|
||||
};
|
||||
|
||||
// Traversal enum creation
|
||||
class PyTraversal
|
||||
{
|
||||
public:
|
||||
static PyObject* traversal_enum_class;
|
||||
static PyObject* create_enum_class(PyObject* module);
|
||||
static int from_arg(PyObject* arg, int* out_order);
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
// BSP - user-facing, exported
|
||||
inline PyTypeObject PyBSPType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.BSP",
|
||||
.tp_basicsize = sizeof(PyBSPObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)PyBSP::dealloc,
|
||||
.tp_repr = PyBSP::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR(
|
||||
"BSP(bounds: tuple[tuple[int, int], tuple[int, int]])\n\n"
|
||||
"Binary Space Partitioning tree for procedural dungeon generation.\n\n"
|
||||
"BSP recursively divides a rectangular region into smaller sub-regions, "
|
||||
"creating a tree structure perfect for generating dungeon rooms and corridors.\n\n"
|
||||
"Args:\n"
|
||||
" bounds: ((x, y), (w, h)) - Position and size of the root region.\n\n"
|
||||
"Properties:\n"
|
||||
" bounds ((x, y), (w, h)): Read-only. Root node bounds.\n"
|
||||
" root (BSPNode): Read-only. Reference to the root node.\n\n"
|
||||
"Example:\n"
|
||||
" bsp = mcrfpy.BSP(bounds=((0, 0), (80, 50)))\n"
|
||||
" bsp.split_recursive(depth=4, min_size=(8, 8))\n"
|
||||
" for leaf in bsp.leaves():\n"
|
||||
" print(f'Room at {leaf.bounds}')\n"
|
||||
),
|
||||
.tp_methods = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_getset = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_init = (initproc)PyBSP::init,
|
||||
.tp_new = PyBSP::pynew,
|
||||
};
|
||||
|
||||
// BSPNode - internal type (returned by BSP methods)
|
||||
inline PyTypeObject PyBSPNodeType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.BSPNode",
|
||||
.tp_basicsize = sizeof(PyBSPNodeObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)PyBSPNode::dealloc,
|
||||
.tp_repr = PyBSPNode::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR(
|
||||
"BSPNode - Lightweight reference to a node in a BSP tree.\n\n"
|
||||
"BSPNode provides read-only access to node properties and navigation.\n"
|
||||
"Nodes are created by BSP methods, not directly instantiated.\n\n"
|
||||
"Properties:\n"
|
||||
" bounds ((x, y), (w, h)): Position and size of this node.\n"
|
||||
" level (int): Depth in tree (0 for root).\n"
|
||||
" is_leaf (bool): True if this node has no children.\n"
|
||||
" split_horizontal (bool | None): Split orientation, None if leaf.\n"
|
||||
" split_position (int | None): Split coordinate, None if leaf.\n"
|
||||
" left (BSPNode | None): Left child, or None if leaf.\n"
|
||||
" right (BSPNode | None): Right child, or None if leaf.\n"
|
||||
" parent (BSPNode | None): Parent node, or None if root.\n"
|
||||
" sibling (BSPNode | None): Other child of parent, or None.\n"
|
||||
),
|
||||
.tp_methods = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_getset = nullptr, // Set in McRFPy_API.cpp
|
||||
.tp_init = (initproc)PyBSPNode::init,
|
||||
.tp_new = PyBSPNode::pynew,
|
||||
};
|
||||
|
||||
// BSP Iterator - internal type
|
||||
inline PyTypeObject PyBSPIterType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.BSPIter",
|
||||
.tp_basicsize = sizeof(PyBSPIterObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)PyBSPIter::dealloc,
|
||||
.tp_repr = PyBSPIter::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Iterator for BSP tree traversal."),
|
||||
.tp_iter = PyBSPIter::iter,
|
||||
.tp_iternext = (iternextfunc)PyBSPIter::next,
|
||||
.tp_init = (initproc)PyBSPIter::init,
|
||||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
|
||||
PyErr_SetString(PyExc_TypeError, "BSPIter cannot be instantiated directly");
|
||||
return NULL;
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue