diff --git a/src/EntityBehavior.cpp b/src/EntityBehavior.cpp index 98371f4..e4c41a6 100644 --- a/src/EntityBehavior.cpp +++ b/src/EntityBehavior.cpp @@ -36,7 +36,7 @@ static BehaviorOutput executeCustom(UIEntity& entity, UIGrid& grid) { static bool isCellWalkable(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; + return grid.isWalkable(x, y); // #332 } static BehaviorOutput executeNoise(UIEntity& entity, UIGrid& grid, bool include_diagonals) { diff --git a/src/GridChunk.cpp b/src/GridChunk.cpp deleted file mode 100644 index dd72d42..0000000 --- a/src/GridChunk.cpp +++ /dev/null @@ -1,174 +0,0 @@ -#include "GridChunk.h" -#include "UIGrid.h" -#include "PyTexture.h" -#include -#include - -// ============================================================================= -// GridChunk implementation -// ============================================================================= - -GridChunk::GridChunk(int chunk_x, int chunk_y, int width, int height, - int world_x, int world_y, GridData* parent) - : chunk_x(chunk_x), chunk_y(chunk_y), - width(width), height(height), - world_x(world_x), world_y(world_y), - cells(width * height), - dirty(true), - parent_grid(parent) -{} - -UIGridPoint& GridChunk::at(int local_x, int local_y) { - return cells[local_y * width + local_x]; -} - -const UIGridPoint& GridChunk::at(int local_x, int local_y) const { - return cells[local_y * width + local_x]; -} - -void GridChunk::markDirty() { - dirty = true; -} - -// #150 - Removed ensureTexture/renderToTexture - base layer rendering removed -// GridChunk now only provides data storage for GridPoints - -bool GridChunk::isVisible(float left_edge, float top_edge, - float right_edge, float bottom_edge) const { - // Check if chunk's cell range overlaps with viewport's cell range - float chunk_right = world_x + width; - float chunk_bottom = world_y + height; - - return !(world_x >= right_edge || chunk_right <= left_edge || - world_y >= bottom_edge || chunk_bottom <= top_edge); -} - -// ============================================================================= -// ChunkManager implementation -// ============================================================================= - -ChunkManager::ChunkManager(int grid_x, int grid_y, GridData* parent) - : grid_x(grid_x), grid_y(grid_y), parent_grid(parent) -{ - // Calculate number of chunks needed - chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE; - chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE; - - chunks.reserve(chunks_x * chunks_y); - - // Create chunks - for (int cy = 0; cy < chunks_y; ++cy) { - for (int cx = 0; cx < chunks_x; ++cx) { - // Calculate world position - int world_x = cx * GridChunk::CHUNK_SIZE; - int world_y = cy * GridChunk::CHUNK_SIZE; - - // Calculate actual size (may be smaller at edges) - int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x); - int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y); - - chunks.push_back(std::make_unique( - cx, cy, chunk_width, chunk_height, world_x, world_y, parent - )); - } - } -} - -GridChunk* ChunkManager::getChunkForCell(int x, int y) { - if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) { - return nullptr; - } - - int chunk_x = x / GridChunk::CHUNK_SIZE; - int chunk_y = y / GridChunk::CHUNK_SIZE; - return getChunk(chunk_x, chunk_y); -} - -const GridChunk* ChunkManager::getChunkForCell(int x, int y) const { - if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) { - return nullptr; - } - - int chunk_x = x / GridChunk::CHUNK_SIZE; - int chunk_y = y / GridChunk::CHUNK_SIZE; - return getChunk(chunk_x, chunk_y); -} - -GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) { - if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) { - return nullptr; - } - return chunks[chunk_y * chunks_x + chunk_x].get(); -} - -const GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) const { - if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) { - return nullptr; - } - return chunks[chunk_y * chunks_x + chunk_x].get(); -} - -UIGridPoint& ChunkManager::at(int x, int y) { - GridChunk* chunk = getChunkForCell(x, y); - if (!chunk) { - // Return a static dummy point for out-of-bounds access - // This matches the original behavior of UIGrid::at() - static UIGridPoint dummy; - return dummy; - } - - // Convert to local coordinates within chunk - int local_x = x % GridChunk::CHUNK_SIZE; - int local_y = y % GridChunk::CHUNK_SIZE; - - // Mark chunk dirty when accessed for modification - chunk->markDirty(); - - return chunk->at(local_x, local_y); -} - -const UIGridPoint& ChunkManager::at(int x, int y) const { - const GridChunk* chunk = getChunkForCell(x, y); - if (!chunk) { - static UIGridPoint dummy; - return dummy; - } - - int local_x = x % GridChunk::CHUNK_SIZE; - int local_y = y % GridChunk::CHUNK_SIZE; - - return chunk->at(local_x, local_y); -} - -void ChunkManager::resize(int new_grid_x, int new_grid_y) { - // For now, simple rebuild - could be optimized to preserve data - grid_x = new_grid_x; - grid_y = new_grid_y; - - chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE; - chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE; - - chunks.clear(); - chunks.reserve(chunks_x * chunks_y); - - for (int cy = 0; cy < chunks_y; ++cy) { - for (int cx = 0; cx < chunks_x; ++cx) { - int world_x = cx * GridChunk::CHUNK_SIZE; - int world_y = cy * GridChunk::CHUNK_SIZE; - int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x); - int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y); - - chunks.push_back(std::make_unique( - cx, cy, chunk_width, chunk_height, world_x, world_y, parent_grid - )); - } - } -} - -int ChunkManager::dirtyChunks() const { - int count = 0; - for (const auto& chunk : chunks) { - if (chunk->dirty) ++count; - } - return count; -} diff --git a/src/GridChunk.h b/src/GridChunk.h deleted file mode 100644 index 909e4f0..0000000 --- a/src/GridChunk.h +++ /dev/null @@ -1,98 +0,0 @@ -#pragma once -#include "Common.h" -#include -#include -#include "UIGridPoint.h" - -// Forward declarations -class UIGrid; -class GridData; -class PyTexture; - -/** - * #123 - Grid chunk for sub-grid data storage - * #150 - Rendering removed; layers now handle all rendering - * - * Each chunk represents a CHUNK_SIZE x CHUNK_SIZE portion of the grid. - * Chunks store GridPoint data for pathfinding and game logic. - */ -class GridChunk { -public: - // Compile-time configurable chunk size (power of 2 recommended) - static constexpr int CHUNK_SIZE = 64; - - // Position of this chunk in chunk coordinates - int chunk_x, chunk_y; - - // Actual dimensions (may be less than CHUNK_SIZE at grid edges) - int width, height; - - // World position (in cell coordinates) - int world_x, world_y; - - // Cell data for this chunk (pathfinding properties only) - std::vector cells; - - // Dirty flag (for layer sync if needed) - bool dirty; - - // Parent grid reference - GridData* parent_grid; - - // Constructor - GridChunk(int chunk_x, int chunk_y, int width, int height, - int world_x, int world_y, GridData* parent); - - // Access cell at local chunk coordinates - UIGridPoint& at(int local_x, int local_y); - const UIGridPoint& at(int local_x, int local_y) const; - - // Mark chunk as dirty - void markDirty(); - - // Check if chunk overlaps with viewport - bool isVisible(float left_edge, float top_edge, - float right_edge, float bottom_edge) const; -}; - -/** - * Manages a 2D array of chunks for a grid - */ -class ChunkManager { -public: - // Dimensions in chunks - int chunks_x, chunks_y; - - // Grid dimensions in cells - int grid_x, grid_y; - - // All chunks (row-major order) - std::vector> chunks; - - // Parent grid - GridData* parent_grid; - - // Constructor - creates chunks for given grid dimensions - ChunkManager(int grid_x, int grid_y, GridData* parent); - - // Get chunk containing cell (x, y) - GridChunk* getChunkForCell(int x, int y); - const GridChunk* getChunkForCell(int x, int y) const; - - // Get chunk at chunk coordinates - GridChunk* getChunk(int chunk_x, int chunk_y); - const GridChunk* getChunk(int chunk_x, int chunk_y) const; - - // Access cell at grid coordinates (routes through chunk) - UIGridPoint& at(int x, int y); - const UIGridPoint& at(int x, int y) const; - - // Resize grid (rebuilds chunks) - void resize(int new_grid_x, int new_grid_y); - - // Get total number of chunks - int totalChunks() const { return chunks_x * chunks_y; } - - // Get number of dirty chunks - int dirtyChunks() const; -}; diff --git a/src/GridData.cpp b/src/GridData.cpp index 0c40b15..4f91215 100644 --- a/src/GridData.cpp +++ b/src/GridData.cpp @@ -39,23 +39,8 @@ GridData::~GridData() if (layer) layer->parent_grid = nullptr; } - // #271: Null out parent_grid in all grid points (flat storage) - for (auto& p : points) { - p.parent_grid = nullptr; - } - - // #277: Null out parent_grid in chunks and chunk manager - if (chunk_manager) { - for (auto& chunk : chunk_manager->chunks) { - if (chunk) { - chunk->parent_grid = nullptr; - for (auto& cell : chunk->cells) { - cell.parent_grid = nullptr; - } - } - } - chunk_manager->parent_grid = nullptr; - } + // #332: cell storage is now plain uint8 planes (no per-cell back-pointers + // to null out; they free with the vectors). cleanupTCOD(); } @@ -73,59 +58,26 @@ void GridData::initStorage(int gx, int gy, GridData* parent_ref) { grid_w = gx; grid_h = gy; - use_chunks = (gx > CHUNK_THRESHOLD || gy > CHUNK_THRESHOLD); + (void)parent_ref; // #332 - planes need no per-cell back-pointer if (tcod_map) delete tcod_map; tcod_map = new TCODMap(gx, gy); - if (use_chunks) { - chunk_manager = std::make_unique(gx, gy, parent_ref); - for (int cy = 0; cy < chunk_manager->chunks_y; ++cy) { - for (int cx = 0; cx < chunk_manager->chunks_x; ++cx) { - GridChunk* chunk = chunk_manager->getChunk(cx, cy); - if (!chunk) continue; - for (int ly = 0; ly < chunk->height; ++ly) { - for (int lx = 0; lx < chunk->width; ++lx) { - auto& cell = chunk->at(lx, ly); - cell.grid_x = chunk->world_x + lx; - cell.grid_y = chunk->world_y + ly; - cell.parent_grid = parent_ref; - } - } - } - } - } else { - points.resize(gx * gy); - for (int y = 0; y < gy; y++) { - for (int x = 0; x < gx; x++) { - int idx = y * gx + x; - points[idx].grid_x = x; - points[idx].grid_y = y; - points[idx].parent_grid = parent_ref; - } - } - } + // #332 - dense uint8 planes; default cells are not walkable / not + // transparent (matches the former UIGridPoint() default). + walkable_plane.assign((size_t)gx * (size_t)gy, 0); + transparent_plane.assign((size_t)gx * (size_t)gy, 0); syncTCODMap(); } -// Cell access -UIGridPoint& GridData::at(int x, int y) -{ - if (use_chunks && chunk_manager) { - return chunk_manager->at(x, y); - } - return points[y * grid_w + x]; -} - // TCOD integration void GridData::syncTCODMap() { if (!tcod_map) return; for (int y = 0; y < grid_h; y++) { for (int x = 0; x < grid_w; x++) { - const UIGridPoint& point = at(x, y); - tcod_map->setProperties(x, y, point.transparent, point.walkable); + tcod_map->setProperties(x, y, isTransparent(x, y), isWalkable(x, y)); } } fov_dirty = true; @@ -135,8 +87,7 @@ void GridData::syncTCODMap() void GridData::syncTCODMapCell(int x, int y) { if (!tcod_map || x < 0 || x >= grid_w || y < 0 || y >= grid_h) return; - const UIGridPoint& point = at(x, y); - tcod_map->setProperties(x, y, point.transparent, point.walkable); + tcod_map->setProperties(x, y, isTransparent(x, y), isWalkable(x, y)); fov_dirty = true; transparency_generation++; } diff --git a/src/GridData.h b/src/GridData.h index 27de55f..c56fd68 100644 --- a/src/GridData.h +++ b/src/GridData.h @@ -20,7 +20,6 @@ #include "UIGridPoint.h" #include "SpatialHash.h" #include "GridLayers.h" -#include "GridChunk.h" // Forward declarations class DijkstraMap; @@ -47,16 +46,20 @@ public: int cell_width() const { return cell_width_px; } int cell_height() const { return cell_height_px; } - // #123 - Chunk-based storage for large grid support - std::unique_ptr chunk_manager; - // Legacy flat storage (kept for small grids or compatibility) - std::vector points; - // Use chunks for grids larger than this threshold - static constexpr int CHUNK_THRESHOLD = 64; - bool use_chunks = false; + // #332 - Logic cell storage as two dense row-major uint8 planes (SoA). + // Replaces the 24-byte-per-cell array-of-UIGridPoint (walkable/transparent + // were only 2 bytes of payload; grid_x/grid_y/parent_grid were derivable or + // back-reference bookkeeping). 12x smaller, cache-friendly, and contiguous + // at ANY size -- chunking is dropped for logic data (render caches still + // chunk independently in GridLayers). Indexed y*grid_w + x. The Python + // GridPoint wrapper holds (grid, x, y) and reads/writes via these accessors. + std::vector walkable_plane; + std::vector transparent_plane; - // Cell access (handles both flat and chunked storage) - UIGridPoint& at(int x, int y); + bool isWalkable(int x, int y) const { return walkable_plane[(size_t)y * grid_w + x] != 0; } + bool isTransparent(int x, int y) const { return transparent_plane[(size_t)y * grid_w + x] != 0; } + void setWalkable(int x, int y, bool v) { walkable_plane[(size_t)y * grid_w + x] = v ? 1 : 0; } + void setTransparent(int x, int y, bool v) { transparent_plane[(size_t)y * grid_w + x] = v ? 1 : 0; } // ========================================================================= // Entity management diff --git a/src/GridLayers.h b/src/GridLayers.h index 00da248..28b837d 100644 --- a/src/GridLayers.h +++ b/src/GridLayers.h @@ -25,7 +25,8 @@ enum class GridLayerType { // Abstract base class for grid layers class GridLayer { public: - // Chunk size for per-chunk dirty tracking (matches GridChunk::CHUNK_SIZE) + // Chunk size for per-chunk render-cache dirty tracking (render-only; logic + // cell storage is dense planes on GridData since #332) static constexpr int CHUNK_SIZE = 64; GridLayerType type; diff --git a/src/PathProvider.cpp b/src/PathProvider.cpp index d697b09..50999aa 100644 --- a/src/PathProvider.cpp +++ b/src/PathProvider.cpp @@ -5,7 +5,7 @@ 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; + return grid.isWalkable(x, y); // #332 } // ----------------------------------------------------------------------------- diff --git a/src/UIGrid.h b/src/UIGrid.h index 7371f7f..78ff351 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -23,7 +23,6 @@ #include "UIDrawable.h" #include "UIBase.h" #include "GridLayers.h" -#include "GridChunk.h" #include "SpatialHash.h" #include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid) #include "GridData.h" // #252 - Data layer base class diff --git a/src/UIGridPoint.cpp b/src/UIGridPoint.cpp index bc67d14..8c7b4be 100644 --- a/src/UIGridPoint.cpp +++ b/src/UIGridPoint.cpp @@ -5,9 +5,9 @@ #include "McRFPy_Doc.h" // #177 - for MCRF_PROPERTY macro #include // #150 - for strcmp -UIGridPoint::UIGridPoint() -: walkable(false), transparent(false), grid_x(-1), grid_y(-1), parent_grid(nullptr) -{} +// #332 - cell storage moved to GridData's dense uint8 planes; UIGridPoint no +// longer holds per-cell data. The Python GridPoint wrapper reaches walkable/ +// transparent through the grid's plane accessors keyed by (x, y). // Utility function to convert sf::Color to PyObject* PyObject* sfColor_to_PyObject(sf::Color color) { @@ -40,56 +40,46 @@ sf::Color PyObject_to_sfColor(PyObject* obj) { // #150 - Removed get_color/set_color - now handled by layers -// Helper to safely get the GridPoint data from coordinates -// Routes through UIGrid::at() which handles both flat and chunked storage -static UIGridPoint* getGridPointData(PyUIGridPointObject* self) { - if (!self->grid) return nullptr; - if (self->x < 0 || self->x >= self->grid->grid_w || - self->y < 0 || self->y >= self->grid->grid_h) return nullptr; - return &self->grid->at(self->x, self->y); +// #332 - validate the wrapper's (grid, x, y) still name a live in-bounds cell. +static bool gridPointValid(PyUIGridPointObject* self) { + return self->grid && + self->x >= 0 && self->x < self->grid->grid_w && + self->y >= 0 && self->y < self->grid->grid_h; } PyObject* UIGridPoint::get_bool_member(PyUIGridPointObject* self, void* closure) { - auto* data = getGridPointData(self); - if (!data) { + if (!gridPointValid(self)) { PyErr_SetString(PyExc_RuntimeError, "GridPoint data is no longer valid"); return NULL; } if (reinterpret_cast(closure) == 0) { // walkable - return PyBool_FromLong(data->walkable); + return PyBool_FromLong(self->grid->isWalkable(self->x, self->y)); } else { // transparent - return PyBool_FromLong(data->transparent); + return PyBool_FromLong(self->grid->isTransparent(self->x, self->y)); } } int UIGridPoint::set_bool_member(PyUIGridPointObject* self, PyObject* value, void* closure) { - auto* data = getGridPointData(self); - if (!data) { + if (!gridPointValid(self)) { PyErr_SetString(PyExc_RuntimeError, "GridPoint data is no longer valid"); return -1; } - if (value == Py_True) { - if (reinterpret_cast(closure) == 0) { // walkable - data->walkable = true; - } else { // transparent - data->transparent = true; - } - } else if (value == Py_False) { - if (reinterpret_cast(closure) == 0) { // walkable - data->walkable = false; - } else { // transparent - data->transparent = false; - } - } else { + bool bv; + if (value == Py_True) bv = true; + else if (value == Py_False) bv = false; + else { PyErr_SetString(PyExc_ValueError, "Expected a boolean value"); return -1; } - // Sync with TCOD map if parent grid exists - if (data->parent_grid && data->grid_x >= 0 && data->grid_y >= 0) { - data->parent_grid->syncTCODMapCell(data->grid_x, data->grid_y); + if (reinterpret_cast(closure) == 0) { // walkable + self->grid->setWalkable(self->x, self->y, bv); + } else { // transparent + self->grid->setTransparent(self->x, self->y, bv); } + // Per-cell TCOD sync (unchanged behavior; #333 will make this lazy) + self->grid->syncTCODMapCell(self->x, self->y); return 0; } @@ -149,11 +139,10 @@ PyGetSetDef UIGridPoint::getsetters[] = { PyObject* UIGridPoint::repr(PyUIGridPointObject* self) { std::ostringstream ss; - auto* gp = getGridPointData(self); - if (!gp) ss << ""; + if (!gridPointValid(self)) ss << ""; else { - ss << "x << ", " << self->y << ")>"; } std::string repr_str = ss.str(); diff --git a/src/UIGridPoint.h b/src/UIGridPoint.h index 0ac07a7..827bf2d 100644 --- a/src/UIGridPoint.h +++ b/src/UIGridPoint.h @@ -26,16 +26,14 @@ typedef struct { int x, y; // Grid coordinates - compute data pointer on access } PyUIGridPointObject; -// UIGridPoint - grid cell data for pathfinding and layer access -// #150 - Layer-related properties (color, tilesprite, etc.) removed; now handled by layers +// UIGridPoint - namespaces the Python GridPoint type's accessors. +// #150 - Layer-related properties (color, tilesprite, etc.) removed; now handled by layers. +// #332 - per-cell data (walkable/transparent) moved to GridData's dense uint8 +// planes (SoA); this class no longer stores cell state. The Python wrapper +// (PyUIGridPointObject) holds (grid, x, y) and reads/writes via GridData accessors. class UIGridPoint { public: - bool walkable, transparent; // Pathfinding/FOV properties - int grid_x, grid_y; // Position in parent grid - GridData* parent_grid; // Parent grid reference for TCOD sync (#252) - UIGridPoint(); - // Built-in property accessors (walkable, transparent only) static PyGetSetDef getsetters[]; static int set_bool_member(PyUIGridPointObject* self, PyObject* value, void* closure); diff --git a/src/UIGridPyMethods.cpp b/src/UIGridPyMethods.cpp index 531d2b8..a4c173e 100644 --- a/src/UIGridPyMethods.cpp +++ b/src/UIGridPyMethods.cpp @@ -515,12 +515,11 @@ PyObject* UIGrid::py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObj for (int x = 0; x < self->data->grid_w; x++) { float value = TCOD_heightmap_get_value(hmap->heightmap, x, y); if (value >= range_min && value <= range_max) { - UIGridPoint& point = self->data->at(x, y); if (set_walkable) { - point.walkable = walkable_value; + self->data->setWalkable(x, y, walkable_value); } if (set_transparent) { - point.transparent = transparent_value; + self->data->setTransparent(x, y, transparent_value); } } } @@ -623,15 +622,14 @@ PyObject* UIGrid::py_apply_ranges(PyUIGridObject* self, PyObject* args) { for (int y = 0; y < self->data->grid_h; y++) { for (int x = 0; x < self->data->grid_w; x++) { float value = TCOD_heightmap_get_value(hmap->heightmap, x, y); - UIGridPoint& point = self->data->at(x, y); for (const auto& re : entries) { if (value >= re.min && value <= re.max) { if (re.set_walkable) { - point.walkable = re.walkable_value; + self->data->setWalkable(x, y, re.walkable_value); } if (re.set_transparent) { - point.transparent = re.transparent_value; + self->data->setTransparent(x, y, re.transparent_value); } break; } diff --git a/src/UITestScene.cpp b/src/UITestScene.cpp index d76b644..eb5ffe0 100644 --- a/src/UITestScene.cpp +++ b/src/UITestScene.cpp @@ -113,8 +113,8 @@ UITestScene::UITestScene(GameEngine* g) : Scene(g) // Use layers for visual rendering; GridPoint only has walkable/transparent // The default "tilesprite" TileLayer is created automatically // Example: e5->layers[0]->at(x, y) = tile_index for TileLayer - e5->points[0].walkable = true; - e5->points[0].transparent = true; + e5->setWalkable(0, 0, true); // #332 + e5->setTransparent(0, 0, true); ui_elements->push_back(e5); diff --git a/tests/regression/issue_332_soa_storage_test.py b/tests/regression/issue_332_soa_storage_test.py new file mode 100644 index 0000000..6f7277e --- /dev/null +++ b/tests/regression/issue_332_soa_storage_test.py @@ -0,0 +1,76 @@ +"""Regression test for #332 - GridData SoA plane storage. + +Cell walkable/transparent moved from an array-of-UIGridPoint (24 B/cell, and +chunked into non-contiguous 64x64 blocks above size 64) to two dense row-major +uint8 planes (2 B/cell, contiguous at ANY size). This verifies behavior is +preserved -- especially on LARGE grids that formerly used chunked storage, at +and across the old 64-cell chunk boundary -- and that the coordinate-keyed +GridPoint wrapper round-trips writes both directions. + +Direct-execution style. +""" +import mcrfpy +import sys +import os +import gc + + +def rss_bytes(): + with open("/proc/self/statm") as f: + return int(f.read().split()[1]) * os.sysconf("SC_PAGE_SIZE") + + +def main(): + # Large grid: formerly chunked (>64 in both dims). Exercise cells at and + # across the old 64-cell chunk boundary -- the storage seam SoA removed. + W, H = 200, 160 + g = mcrfpy.Grid(grid_size=(W, H)) + cells = [(0, 0), (63, 63), (64, 64), (64, 0), (0, 64), (65, 65), + (127, 127), (128, 120), (W - 1, H - 1)] + + for (x, y) in cells: + g.at(x, y).walkable = True + g.at(x, y).transparent = True + + for (x, y) in cells: + gp = g.at(x, y) + assert gp.walkable is True, f"walkable round-trip failed at {(x, y)}" + assert gp.transparent is True, f"transparent round-trip failed at {(x, y)}" + + # A cell never set keeps the default (False, False) + assert g.at(100, 100).walkable is False, "default walkable should be False" + assert g.at(100, 100).transparent is False, "default transparent should be False" + + # Toggling back works (write False) + g.at(64, 64).walkable = False + assert g.at(64, 64).walkable is False, "toggle-off failed" + + # Wrapper's coordinate-derived properties still work + assert g.at(5, 7).grid_pos == (5, 7), "grid_pos" + assert isinstance(g.at(5, 7).entities, list), "entities list" + + # Subscript form reaches the same storage + g[10, 11].walkable = True + assert g.at(10, 11).walkable is True, "subscript vs at() disagree" + + print(" large-grid boundary round-trip OK") + + # Memory: per-cell footprint far below the old 24 B array-of-struct cell. + # Total includes the TCOD map + spatial hash, so this is a loose gross-regression + # guard, not the isolated 2 B/cell plane cost. + gc.collect() + base = rss_bytes() + N, gs = 40, 100 + grids = [mcrfpy.Grid(grid_size=(gs, gs)) for _ in range(N)] + after = rss_bytes() + per_cell = (after - base) / (N * gs * gs) + print(f" ~{per_cell:.1f} bytes/cell total ({N} grids of {gs}x{gs}, incl. TCOD map)") + assert per_cell < 40, f"per-cell {per_cell:.1f} B too high -- storage regression?" + assert len(grids) == N + + print("PASS") + sys.exit(0) + + +if __name__ == "__main__": + main()