Fix #215: Replace mcrfpy.libtcod with mcrfpy.bresenham()

- Remove mcrfpy.libtcod submodule entirely
- Add mcrfpy.bresenham(start, end, include_start=True, include_end=True)
  - Accepts tuples or Vector objects for positions
  - Returns list of (x, y) tuples along the line
- compute_fov() was redundant with Grid.compute_fov()
- line() functionality now available as top-level bresenham()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-01-20 00:10:13 -05:00
commit 4ead2f25fe
4 changed files with 125 additions and 198 deletions

View file

@ -1,7 +1,7 @@
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "UIDrawable.h" #include "UIDrawable.h"
#include "McRFPy_Automation.h" #include "McRFPy_Automation.h"
#include "McRFPy_Libtcod.h" // Note: McRFPy_Libtcod.h removed in #215 - functionality moved to mcrfpy.bresenham()
#include "McRFPy_Doc.h" #include "McRFPy_Doc.h"
#include "PyTypeCache.h" // Thread-safe cached Python types #include "PyTypeCache.h" // Thread-safe cached Python types
#include "platform.h" #include "platform.h"
@ -26,6 +26,7 @@
#include "PyBSP.h" // Procedural generation BSP (#202-206) #include "PyBSP.h" // Procedural generation BSP (#202-206)
#include "PyNoiseSource.h" // Procedural generation noise (#207-208) #include "PyNoiseSource.h" // Procedural generation noise (#207-208)
#include "PyLock.h" // Thread synchronization (#219) #include "PyLock.h" // Thread synchronization (#219)
#include "PyVector.h" // For bresenham Vector support (#215)
#include "McRogueFaceVersion.h" #include "McRogueFaceVersion.h"
#include "GameEngine.h" #include "GameEngine.h"
#include "ImGuiConsole.h" #include "ImGuiConsole.h"
@ -304,6 +305,21 @@ static PyMethodDef mcrfpyMethods[] = {
"Without this, modifying UI from threads may cause visual glitches or crashes.") "Without this, modifying UI from threads may cause visual glitches or crashes.")
)}, )},
// #215: Bresenham line algorithm (replaces mcrfpy.libtcod.line)
{"bresenham", (PyCFunction)McRFPy_API::_bresenham, METH_VARARGS | METH_KEYWORDS,
MCRF_FUNCTION(bresenham,
MCRF_SIG("(start, end, *, include_start=True, include_end=True)", "list[tuple[int, int]]"),
MCRF_DESC("Compute grid cells along a line using Bresenham's algorithm."),
MCRF_ARGS_START
MCRF_ARG("start", "(x, y) tuple or Vector - starting point")
MCRF_ARG("end", "(x, y) tuple or Vector - ending point")
MCRF_ARG("include_start", "Include the starting point in results (default: True)")
MCRF_ARG("include_end", "Include the ending point in results (default: True)")
MCRF_RETURNS("list[tuple[int, int]]: List of (x, y) grid coordinates along the line")
MCRF_NOTE("Useful for line-of-sight checks, projectile paths, and drawing lines on grids. "
"The algorithm ensures minimal grid traversal between two points.")
)},
{NULL, NULL, 0, NULL} {NULL, NULL, 0, NULL}
}; };
@ -638,16 +654,10 @@ PyObject* PyInit_mcrfpy()
PyObject* sys_modules = PyImport_GetModuleDict(); PyObject* sys_modules = PyImport_GetModuleDict();
PyDict_SetItemString(sys_modules, "mcrfpy.automation", automation_module); PyDict_SetItemString(sys_modules, "mcrfpy.automation", automation_module);
} }
// Add libtcod submodule
PyObject* libtcod_module = McRFPy_Libtcod::init_libtcod_module();
if (libtcod_module != NULL) {
PyModule_AddObject(m, "libtcod", libtcod_module);
// Also add to sys.modules for proper import behavior // Note: mcrfpy.libtcod submodule removed in #215
PyObject* sys_modules = PyImport_GetModuleDict(); // - line() functionality replaced by mcrfpy.bresenham()
PyDict_SetItemString(sys_modules, "mcrfpy.libtcod", libtcod_module); // - compute_fov() redundant with Grid.compute_fov()
}
// Initialize PyTypeCache for thread-safe type lookups // Initialize PyTypeCache for thread-safe type lookups
// This must be done after all types are added to the module // This must be done after all types are added to the module
@ -1524,6 +1534,108 @@ PyObject* McRFPy_API::_setDevConsole(PyObject* self, PyObject* args) {
Py_RETURN_NONE; Py_RETURN_NONE;
} }
// #215: Bresenham line algorithm implementation
// Helper to extract (x, y) from tuple, list, or Vector
static bool extract_point(PyObject* obj, int* x, int* y, const char* arg_name) {
// Try tuple/list first
if (PySequence_Check(obj) && PySequence_Size(obj) == 2) {
PyObject* x_obj = PySequence_GetItem(obj, 0);
PyObject* y_obj = PySequence_GetItem(obj, 1);
bool ok = false;
if (x_obj && y_obj && PyNumber_Check(x_obj) && PyNumber_Check(y_obj)) {
PyObject* x_long = PyNumber_Long(x_obj);
PyObject* y_long = PyNumber_Long(y_obj);
if (x_long && y_long) {
*x = PyLong_AsLong(x_long);
*y = PyLong_AsLong(y_long);
ok = !PyErr_Occurred();
}
Py_XDECREF(x_long);
Py_XDECREF(y_long);
}
Py_XDECREF(x_obj);
Py_XDECREF(y_obj);
if (ok) return true;
}
// Try Vector type
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (vector_type && PyObject_IsInstance(obj, vector_type)) {
Py_DECREF(vector_type);
PyVectorObject* vec = (PyVectorObject*)obj;
*x = static_cast<int>(vec->data.x);
*y = static_cast<int>(vec->data.y);
return true;
}
Py_XDECREF(vector_type);
PyErr_Format(PyExc_TypeError,
"%s: expected (x, y) tuple or Vector, got %s",
arg_name, Py_TYPE(obj)->tp_name);
return false;
}
PyObject* McRFPy_API::_bresenham(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"start", "end", "include_start", "include_end", NULL};
PyObject* start_obj = NULL;
PyObject* end_obj = NULL;
int include_start = 1; // Default: True
int include_end = 1; // Default: True
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO|pp",
const_cast<char**>(kwlist),
&start_obj, &end_obj,
&include_start, &include_end)) {
return NULL;
}
int x1, y1, x2, y2;
if (!extract_point(start_obj, &x1, &y1, "start")) return NULL;
if (!extract_point(end_obj, &x2, &y2, "end")) return NULL;
// Build result list using TCOD's Bresenham implementation
PyObject* result = PyList_New(0);
if (!result) return NULL;
// Add start point if requested
if (include_start) {
PyObject* pos = Py_BuildValue("(ii)", x1, y1);
if (!pos) { Py_DECREF(result); return NULL; }
PyList_Append(result, pos);
Py_DECREF(pos);
}
// Use TCOD's line algorithm for intermediate points
TCODLine::init(x1, y1, x2, y2);
int x, y;
// Step through the line - TCODLine::step returns false until reaching endpoint
while (!TCODLine::step(&x, &y)) {
// Skip start point (already handled above if include_start)
if (x == x1 && y == y1) continue;
// Skip end point (handle below based on include_end)
if (x == x2 && y == y2) continue;
PyObject* pos = Py_BuildValue("(ii)", x, y);
if (!pos) { Py_DECREF(result); return NULL; }
PyList_Append(result, pos);
Py_DECREF(pos);
}
// Add end point if requested (and it's different from start)
if (include_end && (x1 != x2 || y1 != y2)) {
PyObject* pos = Py_BuildValue("(ii)", x2, y2);
if (!pos) { Py_DECREF(result); return NULL; }
PyList_Append(result, pos);
Py_DECREF(pos);
}
return result;
}
// Benchmark logging implementation (#104) // Benchmark logging implementation (#104)
PyObject* McRFPy_API::_startBenchmark(PyObject* self, PyObject* args) { PyObject* McRFPy_API::_startBenchmark(PyObject* self, PyObject* args) {
try { try {

View file

@ -76,6 +76,9 @@ public:
// Developer console // Developer console
static PyObject* _setDevConsole(PyObject*, PyObject*); static PyObject* _setDevConsole(PyObject*, PyObject*);
// #215: Bresenham line algorithm (replaces mcrfpy.libtcod.line)
static PyObject* _bresenham(PyObject*, PyObject*, PyObject*);
// Scene lifecycle management for Python Scene objects // Scene lifecycle management for Python Scene objects
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene); static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
static void updatePythonScenes(float dt); static void updatePythonScenes(float dt);

View file

@ -1,164 +0,0 @@
#include "McRFPy_Libtcod.h"
#include "McRFPy_API.h"
#include "UIGrid.h"
#include <vector>
// Helper function to get UIGrid from Python object
static UIGrid* get_grid_from_pyobject(PyObject* obj) {
auto grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
if (!grid_type) {
PyErr_SetString(PyExc_RuntimeError, "Could not find Grid type");
return nullptr;
}
if (!PyObject_IsInstance(obj, (PyObject*)grid_type)) {
Py_DECREF(grid_type);
PyErr_SetString(PyExc_TypeError, "First argument must be a Grid object");
return nullptr;
}
Py_DECREF(grid_type);
PyUIGridObject* pygrid = (PyUIGridObject*)obj;
return pygrid->data.get();
}
// Field of View computation
static PyObject* McRFPy_Libtcod::compute_fov(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y, radius;
int light_walls = 1;
int algorithm = FOV_BASIC;
if (!PyArg_ParseTuple(args, "Oiii|ii", &grid_obj, &x, &y, &radius,
&light_walls, &algorithm)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// Compute FOV using grid's method
grid->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
// Return list of visible cells
PyObject* visible_list = PyList_New(0);
for (int gy = 0; gy < grid->grid_h; gy++) {
for (int gx = 0; gx < grid->grid_w; gx++) {
if (grid->isInFOV(gx, gy)) {
PyObject* pos = Py_BuildValue("(ii)", gx, gy);
PyList_Append(visible_list, pos);
Py_DECREF(pos);
}
}
}
return visible_list;
}
// Line drawing algorithm
static PyObject* McRFPy_Libtcod::line(PyObject* self, PyObject* args) {
int x1, y1, x2, y2;
if (!PyArg_ParseTuple(args, "iiii", &x1, &y1, &x2, &y2)) {
return NULL;
}
// Use TCOD's line algorithm
TCODLine::init(x1, y1, x2, y2);
PyObject* line_list = PyList_New(0);
int x, y;
// Step through line
while (!TCODLine::step(&x, &y)) {
PyObject* pos = Py_BuildValue("(ii)", x, y);
PyList_Append(line_list, pos);
Py_DECREF(pos);
}
return line_list;
}
// Line iterator (generator-like function)
static PyObject* McRFPy_Libtcod::line_iter(PyObject* self, PyObject* args) {
// For simplicity, just call line() for now
// A proper implementation would create an iterator object
return line(self, args);
}
// Pathfinding functions removed - use Grid.find_path() and Grid.get_dijkstra_map() instead
// These return AStarPath and DijkstraMap objects (see UIGridPathfinding.h)
// Method definitions
static PyMethodDef libtcodMethods[] = {
{"compute_fov", McRFPy_Libtcod::compute_fov, METH_VARARGS,
"compute_fov(grid, x, y, radius, light_walls=True, algorithm=mcrfpy.FOV.BASIC)\n\n"
"Compute field of view from a position.\n\n"
"Args:\n"
" grid: Grid object to compute FOV on\n"
" x, y: Origin position\n"
" radius: Maximum sight radius\n"
" light_walls: Whether walls are lit when in FOV\n"
" algorithm: FOV algorithm (mcrfpy.FOV.BASIC, mcrfpy.FOV.SHADOW, etc.)\n\n"
"Returns:\n"
" List of (x, y) tuples for visible cells"},
{"line", McRFPy_Libtcod::line, METH_VARARGS,
"line(x1, y1, x2, y2)\n\n"
"Get cells along a line using Bresenham's algorithm.\n\n"
"Args:\n"
" x1, y1: Starting position\n"
" x2, y2: Ending position\n\n"
"Returns:\n"
" List of (x, y) tuples along the line"},
{"line_iter", McRFPy_Libtcod::line_iter, METH_VARARGS,
"line_iter(x1, y1, x2, y2)\n\n"
"Iterate over cells along a line.\n\n"
"Args:\n"
" x1, y1: Starting position\n"
" x2, y2: Ending position\n\n"
"Returns:\n"
" Iterator of (x, y) tuples along the line"},
{NULL, NULL, 0, NULL}
};
// Module definition
static PyModuleDef libtcodModule = {
PyModuleDef_HEAD_INIT,
"mcrfpy.libtcod",
"TCOD-compatible algorithms for field of view and line drawing.\n\n"
"This module provides access to TCOD's algorithms integrated with McRogueFace grids.\n"
"Unlike the original TCOD, these functions work directly with Grid objects.\n\n"
"FOV Algorithms (use mcrfpy.FOV enum):\n"
" mcrfpy.FOV.BASIC - Basic circular FOV\n"
" mcrfpy.FOV.SHADOW - Shadow casting (recommended)\n"
" mcrfpy.FOV.DIAMOND - Diamond-shaped FOV\n"
" mcrfpy.FOV.PERMISSIVE_0 through PERMISSIVE_8 - Permissive variants\n"
" mcrfpy.FOV.RESTRICTIVE - Most restrictive FOV\n"
" mcrfpy.FOV.SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\n"
"Pathfinding:\n"
" Use Grid.find_path() for A* pathfinding (returns AStarPath objects)\n"
" Use Grid.get_dijkstra_map() for Dijkstra pathfinding (returns DijkstraMap objects)\n\n"
"Example:\n"
" import mcrfpy\n"
" from mcrfpy import libtcod\n\n"
" grid = mcrfpy.Grid(50, 50)\n"
" visible = libtcod.compute_fov(grid, 25, 25, 10)\n"
" path = grid.find_path((0, 0), (49, 49)) # Returns AStarPath",
-1,
libtcodMethods
};
// Module initialization
PyObject* McRFPy_Libtcod::init_libtcod_module() {
PyObject* m = PyModule_Create(&libtcodModule);
if (m == NULL) {
return NULL;
}
// FOV algorithm constants now provided by mcrfpy.FOV enum (#114)
return m;
}

View file

@ -1,24 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <libtcod.h>
namespace McRFPy_Libtcod
{
// Field of View algorithms
static PyObject* compute_fov(PyObject* self, PyObject* args);
// Pathfinding
static PyObject* find_path(PyObject* self, PyObject* args);
static PyObject* dijkstra_new(PyObject* self, PyObject* args);
static PyObject* dijkstra_compute(PyObject* self, PyObject* args);
static PyObject* dijkstra_get_distance(PyObject* self, PyObject* args);
static PyObject* dijkstra_path_to(PyObject* self, PyObject* args);
// Line algorithms
static PyObject* line(PyObject* self, PyObject* args);
static PyObject* line_iter(PyObject* self, PyObject* args);
// Module initialization
PyObject* init_libtcod_module();
}