diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 0199b37..5b35d79 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -5,6 +5,7 @@ #include "UITestScene.h" #include "Resources.h" #include "Animation.h" +#include GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{}) { @@ -35,7 +36,8 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) // Initialize the game view gameView.setSize(static_cast(gameResolution.x), static_cast(gameResolution.y)); - gameView.setCenter(gameResolution.x / 2.0f, gameResolution.y / 2.0f); + // Use integer center coordinates for pixel-perfect rendering + gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f)); updateViewport(); scene = "uitest"; scenes["uitest"] = new UITestScene(this); @@ -417,7 +419,8 @@ void GameEngine::setFramerateLimit(unsigned int limit) void GameEngine::setGameResolution(unsigned int width, unsigned int height) { gameResolution = sf::Vector2u(width, height); gameView.setSize(static_cast(width), static_cast(height)); - gameView.setCenter(width / 2.0f, height / 2.0f); + // Use integer center coordinates for pixel-perfect rendering + gameView.setCenter(std::floor(width / 2.0f), std::floor(height / 2.0f)); updateViewport(); } @@ -446,8 +449,9 @@ void GameEngine::updateViewport() { float viewportWidth = std::min(static_cast(gameResolution.x), static_cast(windowSize.x)); float viewportHeight = std::min(static_cast(gameResolution.y), static_cast(windowSize.y)); - float offsetX = (windowSize.x - viewportWidth) / 2.0f; - float offsetY = (windowSize.y - viewportHeight) / 2.0f; + // Floor offsets to ensure integer pixel alignment + float offsetX = std::floor((windowSize.x - viewportWidth) / 2.0f); + float offsetY = std::floor((windowSize.y - viewportHeight) / 2.0f); gameView.setViewport(sf::FloatRect( offsetX / windowSize.x, @@ -474,13 +478,21 @@ void GameEngine::updateViewport() { if (windowAspect > gameAspect) { // Window is wider - black bars on sides + // Calculate viewport size in pixels and floor for pixel-perfect scaling + float pixelHeight = static_cast(windowSize.y); + float pixelWidth = std::floor(pixelHeight * gameAspect); + viewportHeight = 1.0f; - viewportWidth = gameAspect / windowAspect; + viewportWidth = pixelWidth / windowSize.x; offsetX = (1.0f - viewportWidth) / 2.0f; } else { // Window is taller - black bars on top/bottom + // Calculate viewport size in pixels and floor for pixel-perfect scaling + float pixelWidth = static_cast(windowSize.x); + float pixelHeight = std::floor(pixelWidth / gameAspect); + viewportWidth = 1.0f; - viewportHeight = windowAspect / gameAspect; + viewportHeight = pixelHeight / windowSize.y; offsetY = (1.0f - viewportHeight) / 2.0f; } diff --git a/src/PyTexture.cpp b/src/PyTexture.cpp index d4ea3f3..631d8af 100644 --- a/src/PyTexture.cpp +++ b/src/PyTexture.cpp @@ -2,10 +2,15 @@ #include "McRFPy_API.h" PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h) -: source(filename), sprite_width(sprite_w), sprite_height(sprite_h) +: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0) { texture = sf::Texture(); - texture.loadFromFile(source); + if (!texture.loadFromFile(source)) { + // Failed to load texture - leave sheet dimensions as 0 + // This will be checked in init() + return; + } + texture.setSmooth(false); // Disable smoothing for pixel art auto size = texture.getSize(); sheet_width = (size.x / sprite_width); sheet_height = (size.y / sprite_height); @@ -18,6 +23,12 @@ PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h) sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s) { + // Protect against division by zero if texture failed to load + if (sheet_width == 0 || sheet_height == 0) { + // Return an empty sprite + return sf::Sprite(); + } + int tx = index % sheet_width, ty = index / sheet_width; auto ir = sf::IntRect(tx * sprite_width, ty * sprite_height, sprite_width, sprite_height); auto sprite = sf::Sprite(texture, ir); @@ -71,7 +82,16 @@ int PyTexture::init(PyTextureObject* self, PyObject* args, PyObject* kwds) int sprite_width, sprite_height; if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii", const_cast(keywords), &filename, &sprite_width, &sprite_height)) return -1; + + // Create the texture object self->data = std::make_shared(filename, sprite_width, sprite_height); + + // Check if the texture failed to load (sheet dimensions will be 0) + if (self->data->sheet_width == 0 || self->data->sheet_height == 0) { + PyErr_Format(PyExc_IOError, "Failed to load texture from file: %s", filename); + return -1; + } + return 0; } diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 222477c..c8a053b 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -9,16 +9,52 @@ #include "UIEntityPyMethods.h" + UIEntity::UIEntity() : self(nullptr), grid(nullptr), position(0.0f, 0.0f) { // Initialize sprite with safe defaults (sprite has its own safe constructor now) - // gridstate vector starts empty since we don't know grid dimensions + // gridstate vector starts empty - will be lazily initialized when needed } -UIEntity::UIEntity(UIGrid& grid) -: gridstate(grid.grid_x * grid.grid_y) +// Removed UIEntity(UIGrid&) constructor - using lazy initialization instead + +void UIEntity::updateVisibility() { + if (!grid) return; + + // Lazy initialize gridstate if needed + if (gridstate.size() == 0) { + gridstate.resize(grid->grid_x * grid->grid_y); + // Initialize all cells as not visible/discovered + for (auto& state : gridstate) { + state.visible = false; + state.discovered = false; + } + } + + // First, mark all cells as not visible + for (auto& state : gridstate) { + state.visible = false; + } + + // Compute FOV from entity's position + int x = static_cast(position.x); + int y = static_cast(position.y); + + // Use default FOV radius of 10 (can be made configurable later) + grid->computeFOV(x, y, 10); + + // Update visible cells based on FOV computation + for (int gy = 0; gy < grid->grid_y; gy++) { + for (int gx = 0; gx < grid->grid_x; gx++) { + int idx = gy * grid->grid_x + gx; + if (grid->isInFOV(gx, gy)) { + gridstate[idx].visible = true; + gridstate[idx].discovered = true; // Once seen, always discovered + } + } + } } PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) { @@ -32,17 +68,29 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) { PyErr_SetString(PyExc_ValueError, "Entity cannot access surroundings because it is not associated with a grid"); return NULL; } - /* - PyUIGridPointStateObject* obj = (PyUIGridPointStateObject*)((&mcrfpydef::PyUIGridPointStateType)->tp_alloc(&mcrfpydef::PyUIGridPointStateType, 0)); - */ + + // Lazy initialize gridstate if needed + if (self->data->gridstate.size() == 0) { + self->data->gridstate.resize(self->data->grid->grid_x * self->data->grid->grid_y); + // Initialize all cells as not visible/discovered + for (auto& state : self->data->gridstate) { + state.visible = false; + state.discovered = false; + } + } + + // Bounds check + if (x < 0 || x >= self->data->grid->grid_x || y < 0 || y >= self->data->grid->grid_y) { + PyErr_Format(PyExc_IndexError, "Grid coordinates (%d, %d) out of bounds", x, y); + return NULL; + } + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState"); auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0); - //auto target = std::static_pointer_cast(target); - obj->data = &(self->data->gridstate[y + self->data->grid->grid_x * x]); + obj->data = &(self->data->gridstate[y * self->data->grid->grid_x + x]); obj->grid = self->data->grid; obj->entity = self->data; return (PyObject*)obj; - } PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) { @@ -166,10 +214,8 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { return -1; } - if (grid_obj == NULL) - self->data = std::make_shared(); - else - self->data = std::make_shared(*((PyUIGridObject*)grid_obj)->data); + // Always use default constructor for lazy initialization + self->data = std::make_shared(); // Store reference to Python object self->data->self = (PyObject*)self; @@ -191,6 +237,9 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { self->data->grid = pygrid->data; // todone - on creation of Entity with Grid assignment, also append it to the entity list pygrid->data->entities->push_back(self->data); + + // Don't initialize gridstate here - lazy initialization to support large numbers of entities + // gridstate will be initialized when visibility is updated or accessed } return 0; } @@ -237,11 +286,26 @@ sf::Vector2i PyObject_to_sfVector2i(PyObject* obj) { return sf::Vector2i(static_cast(vec->data.x), static_cast(vec->data.y)); } -// TODO - deprecate / remove this helper PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state) { - // This function is incomplete - it creates an empty object without setting state data - // Should use PyObjectUtils::createGridPointState() instead - return PyObjectUtils::createPyObjectGeneric("GridPointState"); + // Create a new GridPointState Python object + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState"); + if (!type) { + return NULL; + } + + auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0); + if (!obj) { + Py_DECREF(type); + return NULL; + } + + // Allocate new data and copy values + obj->data = new UIGridPointState(); + obj->data->visible = state.visible; + obj->data->discovered = state.discovered; + + Py_DECREF(type); + return (PyObject*)obj; } PyObject* UIGridPointStateVector_to_PyList(const std::vector& vec) { @@ -434,11 +498,18 @@ PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kw return path_list; } +PyObject* UIEntity::update_visibility(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) +{ + self->data->updateVisibility(); + Py_RETURN_NONE; +} + PyMethodDef UIEntity::methods[] = { {"at", (PyCFunction)UIEntity::at, METH_O}, {"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"}, {"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"}, {"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, "Find path from entity to target position using Dijkstra pathfinding"}, + {"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, "Update entity's visibility state based on current FOV"}, {NULL, NULL, 0, NULL} }; @@ -452,6 +523,7 @@ PyMethodDef UIEntity_all_methods[] = { {"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"}, {"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"}, {"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, "Find path from entity to target position using Dijkstra pathfinding"}, + {"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, "Update entity's visibility state based on current FOV"}, {NULL} // Sentinel }; @@ -485,15 +557,12 @@ PyObject* UIEntity::repr(PyUIEntityObject* self) { bool UIEntity::setProperty(const std::string& name, float value) { if (name == "x") { position.x = value; - // Update sprite position based on grid position - // Note: This is a simplified version - actual grid-to-pixel conversion depends on grid properties - sprite.setPosition(sf::Vector2f(position.x, position.y)); + // Don't update sprite position here - UIGrid::render() handles the pixel positioning return true; } else if (name == "y") { position.y = value; - // Update sprite position based on grid position - sprite.setPosition(sf::Vector2f(position.x, position.y)); + // Don't update sprite position here - UIGrid::render() handles the pixel positioning return true; } else if (name == "sprite_scale") { diff --git a/src/UIEntity.h b/src/UIEntity.h index 8d470f6..dfd155e 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -27,10 +27,10 @@ class UIGrid; //} PyUIEntityObject; // helper methods with no namespace requirement -static PyObject* sfVector2f_to_PyObject(sf::Vector2f vector); -static sf::Vector2f PyObject_to_sfVector2f(PyObject* obj); -static PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state); -static PyObject* UIGridPointStateVector_to_PyList(const std::vector& vec); +PyObject* sfVector2f_to_PyObject(sf::Vector2f vector); +sf::Vector2f PyObject_to_sfVector2f(PyObject* obj); +PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state); +PyObject* UIGridPointStateVector_to_PyList(const std::vector& vec); // TODO: make UIEntity a drawable class UIEntity//: public UIDrawable @@ -44,7 +44,9 @@ public: //void render(sf::Vector2f); //override final; UIEntity(); - UIEntity(UIGrid&); + + // Visibility methods + void updateVisibility(); // Update gridstate from current FOV // Property system for animations bool setProperty(const std::string& name, float value); @@ -60,6 +62,7 @@ public: static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds); + static PyObject* update_visibility(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds); static PyObject* get_position(PyUIEntityObject* self, void* closure); diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index a37d1c0..e65901e 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -7,7 +7,8 @@ UIGrid::UIGrid() : grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr), - fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr) // Default dark gray background + fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr), + perspective(-1) // Default to omniscient view { // Initialize entities list entities = std::make_shared>>(); @@ -34,7 +35,8 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr _ptex, sf::Vector2f _x : grid_x(gx), grid_y(gy), zoom(1.0f), ptex(_ptex), points(gx * gy), - fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr) // Default dark gray background + fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr), + perspective(-1) // Default to omniscient view { // Use texture dimensions if available, otherwise use defaults int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH; @@ -70,6 +72,9 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr _ptex, sf::Vector2f _x // Create TCOD dijkstra pathfinder tcod_dijkstra = new TCODDijkstra(tcod_map); + // Create TCOD A* pathfinder + tcod_path = new TCODPath(tcod_map); + // Initialize grid points with parent reference for (int y = 0; y < gy; y++) { for (int x = 0; x < gx; x++) { @@ -183,43 +188,55 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) } - // top layer - opacity for discovered / visible status (debug, basically) - /* // Disabled until I attach a "perspective" - for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); - x < x_limit; //x < view_width; - x+=1) - { - //for (float y = (top_edge >= 0 ? top_edge : 0); - for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); - y < y_limit; //y < view_height; - y+=1) + // top layer - opacity for discovered / visible status based on perspective + // Only render visibility overlay if perspective is set (not omniscient) + if (perspective >= 0 && perspective < static_cast(entities->size())) { + // Get the entity whose perspective we're using + auto it = entities->begin(); + std::advance(it, perspective); + auto& entity = *it; + + // Create rectangle for overlays + sf::RectangleShape overlay; + overlay.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom)); + + for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); + x < x_limit; + x+=1) { + for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); + y < y_limit; + y+=1) + { + // Skip out-of-bounds cells + if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue; + + auto pixel_pos = sf::Vector2f( + (x*cell_width - left_spritepixels) * zoom, + (y*cell_height - top_spritepixels) * zoom ); - auto pixel_pos = sf::Vector2f( - (x*itex->grid_size - left_spritepixels) * zoom, - (y*itex->grid_size - top_spritepixels) * zoom ); - - auto gridpoint = at(std::floor(x), std::floor(y)); - - sprite.setPosition(pixel_pos); - - r.setPosition(pixel_pos); - - // visible & discovered layers for testing purposes - if (!gridpoint.discovered) { - r.setFillColor(sf::Color(16, 16, 20, 192)); // 255 opacity for actual blackout - renderTexture.draw(r); - } else if (!gridpoint.visible) { - r.setFillColor(sf::Color(32, 32, 40, 128)); - renderTexture.draw(r); + // Get visibility state from entity's perspective + int idx = y * grid_x + x; + if (idx >= 0 && idx < static_cast(entity->gridstate.size())) { + const auto& state = entity->gridstate[idx]; + + overlay.setPosition(pixel_pos); + + // Three overlay colors as specified: + if (!state.discovered) { + // Never seen - black + overlay.setFillColor(sf::Color(0, 0, 0, 255)); + renderTexture.draw(overlay); + } else if (!state.visible) { + // Discovered but not currently visible - dark gray + overlay.setFillColor(sf::Color(32, 32, 40, 192)); + renderTexture.draw(overlay); + } + // If visible and discovered, no overlay (fully visible) + } } - - // overlay - - // uisprite } } - */ // grid lines for testing & validation /* @@ -255,6 +272,10 @@ UIGridPoint& UIGrid::at(int x, int y) UIGrid::~UIGrid() { + if (tcod_path) { + delete tcod_path; + tcod_path = nullptr; + } if (tcod_dijkstra) { delete tcod_dijkstra; tcod_dijkstra = nullptr; @@ -363,6 +384,41 @@ std::vector> UIGrid::getDijkstraPath(int x, int y) const return path; } +// A* pathfinding implementation +std::vector> UIGrid::computeAStarPath(int x1, int y1, int x2, int y2, float diagonalCost) +{ + std::vector> path; + + // Validate inputs + if (!tcod_map || !tcod_path || + x1 < 0 || x1 >= grid_x || y1 < 0 || y1 >= grid_y || + x2 < 0 || x2 >= grid_x || y2 < 0 || y2 >= grid_y) { + return path; // Return empty path + } + + // Set diagonal cost (TCODPath doesn't take it as parameter to compute) + // Instead, diagonal cost is set during TCODPath construction + // For now, we'll use the default diagonal cost from the constructor + + // Compute the path + bool success = tcod_path->compute(x1, y1, x2, y2); + + if (success) { + // Get the computed path + int pathSize = tcod_path->size(); + path.reserve(pathSize); + + // TCOD path includes the starting position, so we start from index 0 + for (int i = 0; i < pathSize; i++) { + int px, py; + tcod_path->get(i, &px, &py); + path.push_back(std::make_pair(px, py)); + } + } + + return path; +} + // Phase 1 implementations sf::FloatRect UIGrid::get_bounds() const { @@ -876,6 +932,38 @@ int UIGrid::set_fill_color(PyUIGridObject* self, PyObject* value, void* closure) return 0; } +PyObject* UIGrid::get_perspective(PyUIGridObject* self, void* closure) +{ + return PyLong_FromLong(self->data->perspective); +} + +int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure) +{ + long perspective = PyLong_AsLong(value); + if (PyErr_Occurred()) { + return -1; + } + + // Validate perspective (-1 for omniscient, or valid entity index) + if (perspective < -1) { + PyErr_SetString(PyExc_ValueError, "perspective must be -1 (omniscient) or a valid entity index"); + return -1; + } + + // Check if entity index is valid (if not omniscient) + if (perspective >= 0 && self->data->entities) { + int entity_count = self->data->entities->size(); + if (perspective >= entity_count) { + PyErr_Format(PyExc_IndexError, "perspective index %ld out of range (grid has %d entities)", + perspective, entity_count); + return -1; + } + } + + self->data->perspective = perspective; + return 0; +} + // Python API implementations for TCOD functionality PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds) { @@ -980,6 +1068,31 @@ PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args) return path_list; } +PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds) +{ + int x1, y1, x2, y2; + float diagonal_cost = 1.41f; + + static char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", kwlist, + &x1, &y1, &x2, &y2, &diagonal_cost)) { + return NULL; + } + + // Compute A* path + std::vector> path = self->data->computeAStarPath(x1, y1, x2, y2, diagonal_cost); + + // Convert to Python list + PyObject* path_list = PyList_New(path.size()); + for (size_t i = 0; i < path.size(); i++) { + PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second); + PyList_SetItem(path_list, i, pos); // Steals reference + } + + return path_list; +} + PyMethodDef UIGrid::methods[] = { {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, @@ -994,6 +1107,8 @@ PyMethodDef UIGrid::methods[] = { "Get distance from Dijkstra root to position. Args: x, y. Returns float or None if invalid."}, {"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS, "Get path from position to Dijkstra root. Args: x, y. Returns list of (x,y) tuples."}, + {"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS, + "Compute A* path between two points. Args: x1, y1, x2, y2, diagonal_cost=1.41. Returns list of (x,y) tuples. Note: diagonal_cost is currently ignored (uses default 1.41)."}, {NULL, NULL, 0, NULL} }; @@ -1016,6 +1131,8 @@ PyMethodDef UIGrid_all_methods[] = { "Get distance from Dijkstra root to position. Args: x, y. Returns float or None if invalid."}, {"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS, "Get path from position to Dijkstra root. Args: x, y. Returns list of (x,y) tuples."}, + {"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS, + "Compute A* path between two points. Args: x1, y1, x2, y2, diagonal_cost=1.41. Returns list of (x,y) tuples. Note: diagonal_cost is currently ignored (uses default 1.41)."}, {NULL} // Sentinel }; @@ -1044,6 +1161,7 @@ PyGetSetDef UIGrid::getsetters[] = { {"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5 {"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL}, + {"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective, "Entity perspective index (-1 for omniscient view)", NULL}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID}, UIDRAWABLE_GETSETTERS, @@ -1386,6 +1504,16 @@ PyObject* UIEntityCollection::append(PyUIEntityCollectionObject* self, PyObject* PyUIEntityObject* entity = (PyUIEntityObject*)o; self->data->push_back(entity->data); entity->data->grid = self->grid; + + // Initialize gridstate if not already done + if (entity->data->gridstate.size() == 0 && self->grid) { + entity->data->gridstate.resize(self->grid->grid_x * self->grid->grid_y); + // Initialize all cells as not visible/discovered + for (auto& state : entity->data->gridstate) { + state.visible = false; + state.discovered = false; + } + } Py_INCREF(Py_None); return Py_None; diff --git a/src/UIGrid.h b/src/UIGrid.h index ce46703..96f41ed 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -28,6 +28,7 @@ private: static constexpr int DEFAULT_CELL_HEIGHT = 16; TCODMap* tcod_map; // TCOD map for FOV and pathfinding TCODDijkstra* tcod_dijkstra; // Dijkstra pathfinding + TCODPath* tcod_path; // A* pathfinding public: UIGrid(); @@ -53,6 +54,9 @@ public: float getDijkstraDistance(int x, int y) const; std::vector> getDijkstraPath(int x, int y) const; + // A* pathfinding methods + std::vector> computeAStarPath(int x1, int y1, int x2, int y2, float diagonalCost = 1.41f); + // Phase 1 virtual method implementations sf::FloatRect get_bounds() const override; void move(float dx, float dy) override; @@ -73,6 +77,9 @@ public: // Background rendering sf::Color fill_color; + // Perspective system - which entity's view to render (-1 = omniscient/default) + int perspective; + // Property system for animations bool setProperty(const std::string& name, float value) override; bool setProperty(const std::string& name, const sf::Vector2f& value) override; @@ -94,6 +101,8 @@ public: static PyObject* get_texture(PyUIGridObject* self, void* closure); static PyObject* get_fill_color(PyUIGridObject* self, void* closure); static int set_fill_color(PyUIGridObject* self, PyObject* value, void* closure); + static PyObject* get_perspective(PyUIGridObject* self, void* closure); + static int set_perspective(PyUIGridObject* self, PyObject* value, void* closure); static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args); @@ -101,6 +110,7 @@ public: static PyObject* py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args); static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args); + static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyMethodDef methods[]; static PyGetSetDef getsetters[]; static PyObject* get_children(PyUIGridObject* self, void* closure); diff --git a/src/UITestScene.cpp b/src/UITestScene.cpp index d3d5ff9..f505b75 100644 --- a/src/UITestScene.cpp +++ b/src/UITestScene.cpp @@ -121,7 +121,7 @@ UITestScene::UITestScene(GameEngine* g) : Scene(g) //UIEntity test: // asdf // TODO - reimplement UISprite style rendering within UIEntity class. Entities don't have a screen pixel position, they have a grid position, and grid sets zoom when rendering them. - auto e5a = std::make_shared(*e5); // this basic constructor sucks: sprite position + zoom are irrelevant for UIEntity. + auto e5a = std::make_shared(); // Default constructor - lazy initialization e5a->grid = e5; //auto e5as = UISprite(indextex, 85, sf::Vector2f(0, 0), 1.0); //e5a->sprite = e5as; // will copy constructor even exist for UISprite...? diff --git a/src/scripts/example_text_widgets.py b/src/scripts/example_text_widgets.py new file mode 100644 index 0000000..913e913 --- /dev/null +++ b/src/scripts/example_text_widgets.py @@ -0,0 +1,48 @@ +from text_input_widget_improved import FocusManager, TextInput + +# Create focus manager +focus_mgr = FocusManager() + +# Create input field +name_input = TextInput( + x=50, y=100, + width=300, + label="Name:", + placeholder="Enter your name", + on_change=lambda text: print(f"Name changed to: {text}") +) + +tags_input = TextInput( + x=50, y=160, + width=300, + label="Tags:", + placeholder="door,chest,floor,wall", + on_change=lambda text: print(f"Text: {text}") +) + +# Register with focus manager +name_input._focus_manager = focus_mgr +focus_mgr.register(name_input) + + +# Create demo scene +import mcrfpy + +mcrfpy.createScene("text_example") +mcrfpy.setScene("text_example") + +ui = mcrfpy.sceneUI("text_example") +# Add to scene +#ui.append(name_input) # don't do this, only the internal Frame class can go into the UI; have to manage derived objects "carefully" (McRogueFace alpha anti-feature) +name_input.add_to_scene(ui) +tags_input.add_to_scene(ui) + +# Handle keyboard events +def handle_keys(key, state): + if not focus_mgr.handle_key(key, state): + if key == "Tab" and state == "start": + focus_mgr.focus_next() + +# McRogueFace alpha anti-feature: only the active scene can be given a keypress callback +mcrfpy.keypressScene(handle_keys) + diff --git a/src/scripts/text_input_widget.py b/src/scripts/text_input_widget.py new file mode 100644 index 0000000..396d82c --- /dev/null +++ b/src/scripts/text_input_widget.py @@ -0,0 +1,201 @@ +""" +Text Input Widget System for McRogueFace +A reusable module for text input fields with focus management +""" + +import mcrfpy + + +class FocusManager: + """Manages focus across multiple widgets""" + def __init__(self): + self.widgets = [] + self.focused_widget = None + self.focus_index = -1 + + def register(self, widget): + """Register a widget""" + self.widgets.append(widget) + if self.focused_widget is None: + self.focus(widget) + + def focus(self, widget): + """Set focus to widget""" + if self.focused_widget: + self.focused_widget.on_blur() + + self.focused_widget = widget + self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1 + + if widget: + widget.on_focus() + + def focus_next(self): + """Focus next widget""" + if not self.widgets: + return + self.focus_index = (self.focus_index + 1) % len(self.widgets) + self.focus(self.widgets[self.focus_index]) + + def focus_prev(self): + """Focus previous widget""" + if not self.widgets: + return + self.focus_index = (self.focus_index - 1) % len(self.widgets) + self.focus(self.widgets[self.focus_index]) + + def handle_key(self, key): + """Send key to focused widget""" + if self.focused_widget: + return self.focused_widget.handle_key(key) + return False + + +class TextInput: + """Text input field widget""" + def __init__(self, x, y, width, height=24, label="", placeholder="", on_change=None): + self.x = x + self.y = y + self.width = width + self.height = height + self.label = label + self.placeholder = placeholder + self.on_change = on_change + + # Text state + self.text = "" + self.cursor_pos = 0 + self.focused = False + + # Visual elements + self._create_ui() + + def _create_ui(self): + """Create UI components""" + # Background frame + self.frame = mcrfpy.Frame(self.x, self.y, self.width, self.height) + self.frame.fill_color = (255, 255, 255, 255) + self.frame.outline_color = (128, 128, 128, 255) + self.frame.outline = 2 + + # Label (above input) + if self.label: + self.label_text = mcrfpy.Caption(self.label, self.x, self.y - 20) + self.label_text.fill_color = (255, 255, 255, 255) + + # Text content + self.text_display = mcrfpy.Caption("", self.x + 4, self.y + 4) + self.text_display.fill_color = (0, 0, 0, 255) + + # Placeholder text + if self.placeholder: + self.placeholder_text = mcrfpy.Caption(self.placeholder, self.x + 4, self.y + 4) + self.placeholder_text.fill_color = (180, 180, 180, 255) + + # Cursor + self.cursor = mcrfpy.Frame(self.x + 4, self.y + 4, 2, self.height - 8) + self.cursor.fill_color = (0, 0, 0, 255) + self.cursor.visible = False + + # Click handler + self.frame.click = self._on_click + + def _on_click(self, x, y, button, state): + """Handle mouse clicks""" + print(self, x, y, button, state) + if button == "left" and hasattr(self, '_focus_manager'): + self._focus_manager.focus(self) + + def on_focus(self): + """Called when focused""" + self.focused = True + self.frame.outline_color = (0, 120, 255, 255) + self.frame.outline = 3 + self.cursor.visible = True + self._update_display() + + def on_blur(self): + """Called when focus lost""" + self.focused = False + self.frame.outline_color = (128, 128, 128, 255) + self.frame.outline = 2 + self.cursor.visible = False + self._update_display() + + def handle_key(self, key): + """Process keyboard input""" + if not self.focused: + return False + + old_text = self.text + handled = True + + # Navigation and editing keys + if key == "BackSpace": + if self.cursor_pos > 0: + self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:] + self.cursor_pos -= 1 + elif key == "Delete": + if self.cursor_pos < len(self.text): + self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:] + elif key == "Left": + self.cursor_pos = max(0, self.cursor_pos - 1) + elif key == "Right": + self.cursor_pos = min(len(self.text), self.cursor_pos + 1) + elif key == "Home": + self.cursor_pos = 0 + elif key == "End": + self.cursor_pos = len(self.text) + elif key in ("Tab", "Return"): + handled = False # Let parent handle + elif len(key) == 1 and key.isprintable(): + self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:] + self.cursor_pos += 1 + else: + handled = False + + # Update if changed + if old_text != self.text: + self._update_display() + if self.on_change: + self.on_change(self.text) + elif handled: + self._update_cursor() + + return handled + + def _update_display(self): + """Update visual state""" + # Show/hide placeholder + if hasattr(self, 'placeholder_text'): + self.placeholder_text.visible = (self.text == "" and not self.focused) + + # Update text + self.text_display.text = self.text + self._update_cursor() + + def _update_cursor(self): + """Update cursor position""" + if self.focused: + # Estimate position (10 pixels per character) + self.cursor.x = self.x + 4 + (self.cursor_pos * 10) + + def set_text(self, text): + """Set text programmatically""" + self.text = text + self.cursor_pos = len(text) + self._update_display() + + def get_text(self): + """Get current text""" + return self.text + + def add_to_scene(self, scene): + """Add all components to scene""" + scene.append(self.frame) + if hasattr(self, 'label_text'): + scene.append(self.label_text) + if hasattr(self, 'placeholder_text'): + scene.append(self.placeholder_text) + scene.append(self.text_display) + scene.append(self.cursor) diff --git a/src/scripts/text_input_widget_improved.py b/src/scripts/text_input_widget_improved.py new file mode 100644 index 0000000..7f7f7b6 --- /dev/null +++ b/src/scripts/text_input_widget_improved.py @@ -0,0 +1,265 @@ +""" +Improved Text Input Widget System for McRogueFace +Uses proper parent-child frame structure and handles keyboard input correctly +""" + +import mcrfpy + + +class FocusManager: + """Manages focus across multiple widgets""" + def __init__(self): + self.widgets = [] + self.focused_widget = None + self.focus_index = -1 + # Global keyboard state + self.shift_pressed = False + self.caps_lock = False + + def register(self, widget): + """Register a widget""" + self.widgets.append(widget) + if self.focused_widget is None: + self.focus(widget) + + def focus(self, widget): + """Set focus to widget""" + if self.focused_widget: + self.focused_widget.on_blur() + + self.focused_widget = widget + self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1 + + if widget: + widget.on_focus() + + def focus_next(self): + """Focus next widget""" + if not self.widgets: + return + self.focus_index = (self.focus_index + 1) % len(self.widgets) + self.focus(self.widgets[self.focus_index]) + + def focus_prev(self): + """Focus previous widget""" + if not self.widgets: + return + self.focus_index = (self.focus_index - 1) % len(self.widgets) + self.focus(self.widgets[self.focus_index]) + + def handle_key(self, key, state): + """Send key to focused widget""" + # Track shift state + if key == "LShift" or key == "RShift": + self.shift_pressed = True + return True + elif key == "start": # Key release for shift + self.shift_pressed = False + return True + elif key == "CapsLock": + self.caps_lock = not self.caps_lock + return True + + if self.focused_widget: + return self.focused_widget.handle_key(key, self.shift_pressed, self.caps_lock) + return False + + +class TextInput: + """Text input field widget with proper parent-child structure""" + def __init__(self, x, y, width, height=24, label="", placeholder="", on_change=None): + self.x = x + self.y = y + self.width = width + self.height = height + self.label = label + self.placeholder = placeholder + self.on_change = on_change + + # Text state + self.text = "" + self.cursor_pos = 0 + self.focused = False + + # Create the widget structure + self._create_ui() + + def _create_ui(self): + """Create UI components with proper parent-child structure""" + # Parent frame that contains everything + self.parent_frame = mcrfpy.Frame(self.x, self.y - (20 if self.label else 0), + self.width, self.height + (20 if self.label else 0)) + self.parent_frame.fill_color = (0, 0, 0, 0) # Transparent parent + + # Input frame (relative to parent) + self.frame = mcrfpy.Frame(0, 20 if self.label else 0, self.width, self.height) + self.frame.fill_color = (255, 255, 255, 255) + self.frame.outline_color = (128, 128, 128, 255) + self.frame.outline = 2 + + # Label (relative to parent) + if self.label: + self.label_text = mcrfpy.Caption(self.label, 0, 0) + self.label_text.fill_color = (255, 255, 255, 255) + self.parent_frame.children.append(self.label_text) + + # Text content (relative to input frame) + self.text_display = mcrfpy.Caption("", 4, 4) + self.text_display.fill_color = (0, 0, 0, 255) + + # Placeholder text (relative to input frame) + if self.placeholder: + self.placeholder_text = mcrfpy.Caption(self.placeholder, 4, 4) + self.placeholder_text.fill_color = (180, 180, 180, 255) + self.frame.children.append(self.placeholder_text) + + # Cursor (relative to input frame) + # Experiment: replacing cursor frame with an inline text character + #self.cursor = mcrfpy.Frame(4, 4, 2, self.height - 8) + #self.cursor.fill_color = (0, 0, 0, 255) + #self.cursor.visible = False + + # Add children to input frame + self.frame.children.append(self.text_display) + #self.frame.children.append(self.cursor) + + # Add input frame to parent + self.parent_frame.children.append(self.frame) + + # Click handler on the input frame + self.frame.click = self._on_click + + def _on_click(self, x, y, button, state): + """Handle mouse clicks""" + print(f"{x=} {y=} {button=} {state=}") + if button == "left" and hasattr(self, '_focus_manager'): + self._focus_manager.focus(self) + + def on_focus(self): + """Called when focused""" + self.focused = True + self.frame.outline_color = (0, 120, 255, 255) + self.frame.outline = 3 + #self.cursor.visible = True + self._update_display() + + def on_blur(self): + """Called when focus lost""" + self.focused = False + self.frame.outline_color = (128, 128, 128, 255) + self.frame.outline = 2 + #self.cursor.visible = False + self._update_display() + + def handle_key(self, key, shift_pressed, caps_lock): + """Process keyboard input with shift state""" + if not self.focused: + return False + + old_text = self.text + handled = True + + # Special key mappings for shifted characters + shift_map = { + "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", + "6": "^", "7": "&", "8": "*", "9": "(", "0": ")", + "-": "_", "=": "+", "[": "{", "]": "}", "\\": "|", + ";": ":", "'": '"', ",": "<", ".": ">", "/": "?", + "`": "~" + } + + # Navigation and editing keys + if key == "BackSpace": + if self.cursor_pos > 0: + self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:] + self.cursor_pos -= 1 + elif key == "Delete": + if self.cursor_pos < len(self.text): + self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:] + elif key == "Left": + self.cursor_pos = max(0, self.cursor_pos - 1) + elif key == "Right": + self.cursor_pos = min(len(self.text), self.cursor_pos + 1) + elif key == "Home": + self.cursor_pos = 0 + elif key == "End": + self.cursor_pos = len(self.text) + elif key == "Space": + self._insert_at_cursor(" ") + elif key in ("Tab", "Return"): + handled = False # Let parent handle + # Handle number keys with "Num" prefix + elif key.startswith("Num") and len(key) == 4: + num = key[3] # Get the digit after "Num" + if shift_pressed and num in shift_map: + self._insert_at_cursor(shift_map[num]) + else: + self._insert_at_cursor(num) + # Handle single character keys + elif len(key) == 1: + char = key + # Apply shift transformations + if shift_pressed: + if char in shift_map: + char = shift_map[char] + elif char.isalpha(): + char = char.upper() + else: + # Apply caps lock for letters + if char.isalpha(): + if caps_lock: + char = char.upper() + else: + char = char.lower() + self._insert_at_cursor(char) + else: + # Unhandled key - print for debugging + print(f"[TextInput] Unhandled key: '{key}' (shift={shift_pressed}, caps={caps_lock})") + handled = False + + # Update if changed + if old_text != self.text: + self._update_display() + if self.on_change: + self.on_change(self.text) + elif handled: + self._update_cursor() + + return handled + + def _insert_at_cursor(self, char): + """Insert a character at the cursor position""" + self.text = self.text[:self.cursor_pos] + char + self.text[self.cursor_pos:] + self.cursor_pos += 1 + + def _update_display(self): + """Update visual state""" + # Show/hide placeholder + if hasattr(self, 'placeholder_text'): + self.placeholder_text.visible = (self.text == "" and not self.focused) + + # Update text + self.text_display.text = self.text[:self.cursor_pos] + "|" + self.text[self.cursor_pos:] + self._update_cursor() + + def _update_cursor(self): + """Update cursor position""" + if self.focused: + # Estimate position (10 pixels per character) + #self.cursor.x = 4 + (self.cursor_pos * 10) + self.text_display.text = self.text[:self.cursor_pos] + "|" + self.text[self.cursor_pos:] + pass + + def set_text(self, text): + """Set text programmatically""" + self.text = text + self.cursor_pos = len(text) + self._update_display() + + def get_text(self): + """Get current text""" + return self.text + + def add_to_scene(self, scene): + """Add only the parent frame to scene""" + scene.append(self.parent_frame) diff --git a/tests/animation_demo.py b/tests/animation_demo.py index f12fc70..716cded 100644 --- a/tests/animation_demo.py +++ b/tests/animation_demo.py @@ -1,165 +1,208 @@ #!/usr/bin/env python3 -"""Animation System Demo - Shows all animation capabilities""" +""" +Animation Demo: Grid Center & Entity Movement +============================================= + +Demonstrates: +- Animated grid centering following entity +- Smooth entity movement along paths +- Perspective shifts with zoom transitions +- Field of view updates +""" import mcrfpy -import math +import sys -# Create main scene -mcrfpy.createScene("animation_demo") -ui = mcrfpy.sceneUI("animation_demo") -mcrfpy.setScene("animation_demo") +# Setup scene +mcrfpy.createScene("anim_demo") -# Title -title = mcrfpy.Caption((400, 30), "McRogueFace Animation System Demo", mcrfpy.default_font) -title.size = 24 -title.fill_color = (255, 255, 255) -# Note: centered property doesn't exist for Caption +# Create grid +grid = mcrfpy.Grid(grid_x=30, grid_y=20) +grid.fill_color = mcrfpy.Color(20, 20, 30) + +# Simple map +for y in range(20): + for x in range(30): + cell = grid.at(x, y) + # Create walls around edges and some obstacles + if x == 0 or x == 29 or y == 0 or y == 19: + cell.walkable = False + cell.transparent = False + cell.color = mcrfpy.Color(40, 30, 30) + elif (x == 10 and 5 <= y <= 15) or (y == 10 and 5 <= x <= 25): + cell.walkable = False + cell.transparent = False + cell.color = mcrfpy.Color(60, 40, 40) + else: + cell.walkable = True + cell.transparent = True + cell.color = mcrfpy.Color(80, 80, 100) + +# Create entities +player = mcrfpy.Entity(5, 5, grid=grid) +player.sprite_index = 64 # @ + +enemy = mcrfpy.Entity(25, 15, grid=grid) +enemy.sprite_index = 69 # E + +# Update visibility +player.update_visibility() +enemy.update_visibility() + +# UI setup +ui = mcrfpy.sceneUI("anim_demo") +ui.append(grid) +grid.position = (100, 100) +grid.size = (600, 400) + +title = mcrfpy.Caption("Animation Demo - Grid Center & Entity Movement", 200, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) ui.append(title) -# 1. Position Animation Demo -pos_frame = mcrfpy.Frame(50, 100, 80, 80) -pos_frame.fill_color = (255, 100, 100) -pos_frame.outline = 2 -ui.append(pos_frame) +status = mcrfpy.Caption("Press 1: Move Player | 2: Move Enemy | 3: Perspective Shift | Q: Quit", 100, 50) +status.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(status) -pos_label = mcrfpy.Caption((50, 80), "Position Animation", mcrfpy.default_font) -pos_label.fill_color = (200, 200, 200) -ui.append(pos_label) - -# 2. Size Animation Demo -size_frame = mcrfpy.Frame(200, 100, 50, 50) -size_frame.fill_color = (100, 255, 100) -size_frame.outline = 2 -ui.append(size_frame) - -size_label = mcrfpy.Caption((200, 80), "Size Animation", mcrfpy.default_font) -size_label.fill_color = (200, 200, 200) -ui.append(size_label) - -# 3. Color Animation Demo -color_frame = mcrfpy.Frame(350, 100, 80, 80) -color_frame.fill_color = (255, 0, 0) -ui.append(color_frame) - -color_label = mcrfpy.Caption((350, 80), "Color Animation", mcrfpy.default_font) -color_label.fill_color = (200, 200, 200) -ui.append(color_label) - -# 4. Easing Functions Demo -easing_y = 250 -easing_frames = [] -easings = ["linear", "easeIn", "easeOut", "easeInOut", "easeInElastic", "easeOutBounce"] - -for i, easing in enumerate(easings): - x = 50 + i * 120 - - frame = mcrfpy.Frame(x, easing_y, 20, 20) - frame.fill_color = (100, 150, 255) - ui.append(frame) - easing_frames.append((frame, easing)) - - label = mcrfpy.Caption((x, easing_y - 20), easing, mcrfpy.default_font) - label.size = 12 - label.fill_color = (200, 200, 200) - ui.append(label) - -# 5. Complex Animation Demo -complex_frame = mcrfpy.Frame(300, 350, 100, 100) -complex_frame.fill_color = (128, 128, 255) -complex_frame.outline = 3 -ui.append(complex_frame) - -complex_label = mcrfpy.Caption((300, 330), "Complex Multi-Property", mcrfpy.default_font) -complex_label.fill_color = (200, 200, 200) -ui.append(complex_label) - -# Start animations -def start_animations(runtime): - # 1. Position animation - back and forth - x_anim = mcrfpy.Animation("x", 500.0, 3.0, "easeInOut") - x_anim.start(pos_frame) - - # 2. Size animation - pulsing - w_anim = mcrfpy.Animation("w", 150.0, 2.0, "easeInOut") - h_anim = mcrfpy.Animation("h", 150.0, 2.0, "easeInOut") - w_anim.start(size_frame) - h_anim.start(size_frame) - - # 3. Color animation - rainbow cycle - color_anim = mcrfpy.Animation("fill_color", (0, 255, 255, 255), 2.0, "linear") - color_anim.start(color_frame) - - # 4. Easing demos - all move up with different easings - for frame, easing in easing_frames: - y_anim = mcrfpy.Animation("y", 150.0, 2.0, easing) - y_anim.start(frame) - - # 5. Complex animation - multiple properties - cx_anim = mcrfpy.Animation("x", 500.0, 4.0, "easeInOut") - cy_anim = mcrfpy.Animation("y", 400.0, 4.0, "easeOut") - cw_anim = mcrfpy.Animation("w", 150.0, 4.0, "easeInElastic") - ch_anim = mcrfpy.Animation("h", 150.0, 4.0, "easeInElastic") - outline_anim = mcrfpy.Animation("outline", 10.0, 4.0, "linear") - - cx_anim.start(complex_frame) - cy_anim.start(complex_frame) - cw_anim.start(complex_frame) - ch_anim.start(complex_frame) - outline_anim.start(complex_frame) - - # Individual color component animations - r_anim = mcrfpy.Animation("fill_color.r", 255.0, 4.0, "easeInOut") - g_anim = mcrfpy.Animation("fill_color.g", 100.0, 4.0, "easeInOut") - b_anim = mcrfpy.Animation("fill_color.b", 50.0, 4.0, "easeInOut") - - r_anim.start(complex_frame) - g_anim.start(complex_frame) - b_anim.start(complex_frame) - - print("All animations started!") - -# Reverse some animations -def reverse_animations(runtime): - # Position back - x_anim = mcrfpy.Animation("x", 50.0, 3.0, "easeInOut") - x_anim.start(pos_frame) - - # Size back - w_anim = mcrfpy.Animation("w", 50.0, 2.0, "easeInOut") - h_anim = mcrfpy.Animation("h", 50.0, 2.0, "easeInOut") - w_anim.start(size_frame) - h_anim.start(size_frame) - - # Color cycle continues - color_anim = mcrfpy.Animation("fill_color", (255, 0, 255, 255), 2.0, "linear") - color_anim.start(color_frame) - - # Easing frames back down - for frame, easing in easing_frames: - y_anim = mcrfpy.Animation("y", 250.0, 2.0, easing) - y_anim.start(frame) - -# Continue color cycle -def cycle_colors(runtime): - color_anim = mcrfpy.Animation("fill_color", (255, 255, 0, 255), 2.0, "linear") - color_anim.start(color_frame) - -# Info text -info = mcrfpy.Caption((400, 550), "Watch as different properties animate with various easing functions!", mcrfpy.default_font) -info.fill_color = (255, 255, 200) -# Note: centered property doesn't exist for Caption +info = mcrfpy.Caption("Perspective: Player", 500, 70) +info.fill_color = mcrfpy.Color(100, 255, 100) ui.append(info) -# Schedule animations -mcrfpy.setTimer("start", start_animations, 500) -mcrfpy.setTimer("reverse", reverse_animations, 4000) -mcrfpy.setTimer("cycle", cycle_colors, 2500) +# Movement functions +def move_player_demo(): + """Demo player movement with camera follow""" + # Calculate path to a destination + path = player.path_to(20, 10) + if not path: + status.text = "No path available!" + return + + status.text = f"Moving player along {len(path)} steps..." + + # Animate along path + for i, (x, y) in enumerate(path[:5]): # First 5 steps + delay = i * 500 # 500ms between steps + + # Schedule movement + def move_step(dt, px=x, py=y): + # Animate entity position + anim_x = mcrfpy.Animation("x", float(px), 0.4, "easeInOut") + anim_y = mcrfpy.Animation("y", float(py), 0.4, "easeInOut") + anim_x.start(player) + anim_y.start(player) + + # Update visibility + player.update_visibility() + + # Animate camera to follow + center_x = px * 16 # Assuming 16x16 tiles + center_y = py * 16 + cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.4, "easeOut") + cam_anim.start(grid) + + mcrfpy.setTimer(f"player_move_{i}", move_step, delay) -# Exit handler -def on_key(key): - if key == "Escape": - mcrfpy.exit() +def move_enemy_demo(): + """Demo enemy movement""" + # Calculate path + path = enemy.path_to(10, 5) + if not path: + status.text = "Enemy has no path!" + return + + status.text = f"Moving enemy along {len(path)} steps..." + + # Animate along path + for i, (x, y) in enumerate(path[:5]): # First 5 steps + delay = i * 500 + + def move_step(dt, ex=x, ey=y): + anim_x = mcrfpy.Animation("x", float(ex), 0.4, "easeInOut") + anim_y = mcrfpy.Animation("y", float(ey), 0.4, "easeInOut") + anim_x.start(enemy) + anim_y.start(enemy) + enemy.update_visibility() + + # If following enemy, update camera + if grid.perspective == 1: + center_x = ex * 16 + center_y = ey * 16 + cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.4, "easeOut") + cam_anim.start(grid) + + mcrfpy.setTimer(f"enemy_move_{i}", move_step, delay) -mcrfpy.keypressScene(on_key) +def perspective_shift_demo(): + """Demo dramatic perspective shift""" + status.text = "Perspective shift in progress..." + + # Phase 1: Zoom out + zoom_out = mcrfpy.Animation("zoom", 0.5, 1.5, "easeInExpo") + zoom_out.start(grid) + + # Phase 2: Switch perspective at peak + def switch_perspective(dt): + if grid.perspective == 0: + grid.perspective = 1 + info.text = "Perspective: Enemy" + info.fill_color = mcrfpy.Color(255, 100, 100) + target = enemy + else: + grid.perspective = 0 + info.text = "Perspective: Player" + info.fill_color = mcrfpy.Color(100, 255, 100) + target = player + + # Update camera to new target + center_x = target.x * 16 + center_y = target.y * 16 + cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.5, "linear") + cam_anim.start(grid) + + mcrfpy.setTimer("switch_persp", switch_perspective, 1600) + + # Phase 3: Zoom back in + def zoom_in(dt): + zoom_in_anim = mcrfpy.Animation("zoom", 1.0, 1.5, "easeOutExpo") + zoom_in_anim.start(grid) + status.text = "Perspective shift complete!" + + mcrfpy.setTimer("zoom_in", zoom_in, 2100) -print("Animation demo started! Press Escape to exit.") \ No newline at end of file +# Input handler +def handle_input(key, state): + if state != "start": + return + + if key == "q": + print("Exiting demo...") + sys.exit(0) + elif key == "1": + move_player_demo() + elif key == "2": + move_enemy_demo() + elif key == "3": + perspective_shift_demo() + +# Set scene +mcrfpy.setScene("anim_demo") +mcrfpy.keypressScene(handle_input) + +# Initial setup +grid.perspective = 0 +grid.zoom = 1.0 + +# Center on player initially +center_x = player.x * 16 +center_y = player.y * 16 +initial_cam = mcrfpy.Animation("center", (center_x, center_y), 0.5, "easeOut") +initial_cam.start(grid) + +print("Animation Demo Started!") +print("======================") +print("Press 1: Animate player movement with camera follow") +print("Press 2: Animate enemy movement") +print("Press 3: Dramatic perspective shift with zoom") +print("Press Q: Quit") +print() +print("Watch how the grid center smoothly follows entities") +print("and how perspective shifts create cinematic effects!") \ No newline at end of file diff --git a/tests/astar_vs_dijkstra.py b/tests/astar_vs_dijkstra.py new file mode 100644 index 0000000..5b93c99 --- /dev/null +++ b/tests/astar_vs_dijkstra.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +""" +A* vs Dijkstra Visual Comparison +================================= + +Shows the difference between A* (single target) and Dijkstra (multi-target). +""" + +import mcrfpy +import sys + +# Colors +WALL_COLOR = mcrfpy.Color(40, 20, 20) +FLOOR_COLOR = mcrfpy.Color(60, 60, 80) +ASTAR_COLOR = mcrfpy.Color(0, 255, 0) # Green for A* +DIJKSTRA_COLOR = mcrfpy.Color(0, 150, 255) # Blue for Dijkstra +START_COLOR = mcrfpy.Color(255, 100, 100) # Red for start +END_COLOR = mcrfpy.Color(255, 255, 100) # Yellow for end + +# Global state +grid = None +mode = "ASTAR" +start_pos = (5, 10) +end_pos = (27, 10) # Changed from 25 to 27 to avoid the wall + +def create_map(): + """Create a map with obstacles to show pathfinding differences""" + global grid + + mcrfpy.createScene("pathfinding_comparison") + + # Create grid + grid = mcrfpy.Grid(grid_x=30, grid_y=20) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Initialize all as floor + for y in range(20): + for x in range(30): + grid.at(x, y).walkable = True + grid.at(x, y).color = FLOOR_COLOR + + # Create obstacles that make A* and Dijkstra differ + obstacles = [ + # Vertical wall with gaps + [(15, y) for y in range(3, 17) if y not in [8, 12]], + # Horizontal walls + [(x, 5) for x in range(10, 20)], + [(x, 15) for x in range(10, 20)], + # Maze-like structure + [(x, 10) for x in range(20, 25)], + [(25, y) for y in range(5, 15)], + ] + + for obstacle_group in obstacles: + for x, y in obstacle_group: + grid.at(x, y).walkable = False + grid.at(x, y).color = WALL_COLOR + + # Mark start and end + grid.at(start_pos[0], start_pos[1]).color = START_COLOR + grid.at(end_pos[0], end_pos[1]).color = END_COLOR + +def clear_paths(): + """Clear path highlighting""" + for y in range(20): + for x in range(30): + cell = grid.at(x, y) + if cell.walkable: + cell.color = FLOOR_COLOR + + # Restore start and end colors + grid.at(start_pos[0], start_pos[1]).color = START_COLOR + grid.at(end_pos[0], end_pos[1]).color = END_COLOR + +def show_astar(): + """Show A* path""" + clear_paths() + + # Compute A* path + path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1]) + + # Color the path + for i, (x, y) in enumerate(path): + if (x, y) != start_pos and (x, y) != end_pos: + grid.at(x, y).color = ASTAR_COLOR + + status_text.text = f"A* Path: {len(path)} steps (optimized for single target)" + status_text.fill_color = ASTAR_COLOR + +def show_dijkstra(): + """Show Dijkstra exploration""" + clear_paths() + + # Compute Dijkstra from start + grid.compute_dijkstra(start_pos[0], start_pos[1]) + + # Color cells by distance (showing exploration) + max_dist = 40.0 + for y in range(20): + for x in range(30): + if grid.at(x, y).walkable: + dist = grid.get_dijkstra_distance(x, y) + if dist is not None and dist < max_dist: + # Color based on distance + intensity = int(255 * (1 - dist / max_dist)) + grid.at(x, y).color = mcrfpy.Color(0, intensity // 2, intensity) + + # Get the actual path + path = grid.get_dijkstra_path(end_pos[0], end_pos[1]) + + # Highlight the actual path more brightly + for x, y in path: + if (x, y) != start_pos and (x, y) != end_pos: + grid.at(x, y).color = DIJKSTRA_COLOR + + # Restore start and end + grid.at(start_pos[0], start_pos[1]).color = START_COLOR + grid.at(end_pos[0], end_pos[1]).color = END_COLOR + + status_text.text = f"Dijkstra: {len(path)} steps (explores all directions)" + status_text.fill_color = DIJKSTRA_COLOR + +def show_both(): + """Show both paths overlaid""" + clear_paths() + + # Get both paths + astar_path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1]) + grid.compute_dijkstra(start_pos[0], start_pos[1]) + dijkstra_path = grid.get_dijkstra_path(end_pos[0], end_pos[1]) + + print(astar_path, dijkstra_path) + + # Color Dijkstra path first (blue) + for x, y in dijkstra_path: + if (x, y) != start_pos and (x, y) != end_pos: + grid.at(x, y).color = DIJKSTRA_COLOR + + # Then A* path (green) - will overwrite shared cells + for x, y in astar_path: + if (x, y) != start_pos and (x, y) != end_pos: + grid.at(x, y).color = ASTAR_COLOR + + # Mark differences + different_cells = [] + for cell in dijkstra_path: + if cell not in astar_path: + different_cells.append(cell) + + status_text.text = f"Both paths: A*={len(astar_path)} steps, Dijkstra={len(dijkstra_path)} steps" + if different_cells: + info_text.text = f"Paths differ at {len(different_cells)} cells" + else: + info_text.text = "Paths are identical" + +def handle_keypress(key_str, state): + """Handle keyboard input""" + global mode + if state == "end": return + print(key_str) + if key_str == "Esc" or key_str == "Q": + print("\nExiting...") + sys.exit(0) + elif key_str == "A" or key_str == "1": + mode = "ASTAR" + show_astar() + elif key_str == "D" or key_str == "2": + mode = "DIJKSTRA" + show_dijkstra() + elif key_str == "B" or key_str == "3": + mode = "BOTH" + show_both() + elif key_str == "Space": + # Refresh current mode + if mode == "ASTAR": + show_astar() + elif mode == "DIJKSTRA": + show_dijkstra() + else: + show_both() + +# Create the demo +print("A* vs Dijkstra Pathfinding Comparison") +print("=====================================") +print("Controls:") +print(" A or 1 - Show A* path (green)") +print(" D or 2 - Show Dijkstra (blue gradient)") +print(" B or 3 - Show both paths") +print(" Q/ESC - Quit") +print() +print("A* is optimized for single-target pathfinding") +print("Dijkstra explores in all directions (good for multiple targets)") + +create_map() + +# Set up UI +ui = mcrfpy.sceneUI("pathfinding_comparison") +ui.append(grid) + +# Scale and position +grid.size = (600, 400) # 30*20, 20*20 +grid.position = (100, 100) + +# Add title +title = mcrfpy.Caption("A* vs Dijkstra Pathfinding", 250, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add status +status_text = mcrfpy.Caption("Press A for A*, D for Dijkstra, B for Both", 100, 60) +status_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(status_text) + +# Add info +info_text = mcrfpy.Caption("", 100, 520) +info_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(info_text) + +# Add legend +legend1 = mcrfpy.Caption("Red=Start, Yellow=End, Green=A*, Blue=Dijkstra", 100, 540) +legend1.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend1) + +legend2 = mcrfpy.Caption("Dark=Walls, Light=Floor", 100, 560) +legend2.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend2) + +# Set scene and input +mcrfpy.setScene("pathfinding_comparison") +mcrfpy.keypressScene(handle_keypress) + +# Show initial A* path +show_astar() + +print("\nDemo ready!") diff --git a/tests/debug_astar_demo.py b/tests/debug_astar_demo.py new file mode 100644 index 0000000..3c26d3c --- /dev/null +++ b/tests/debug_astar_demo.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Debug the astar_vs_dijkstra demo issue""" + +import mcrfpy +import sys + +# Same setup as the demo +start_pos = (5, 10) +end_pos = (25, 10) + +print("Debugging A* vs Dijkstra demo...") +print(f"Start: {start_pos}, End: {end_pos}") + +# Create scene and grid +mcrfpy.createScene("debug") +grid = mcrfpy.Grid(grid_x=30, grid_y=20) + +# Initialize all as floor +print("\nInitializing 30x20 grid...") +for y in range(20): + for x in range(30): + grid.at(x, y).walkable = True + +# Test path before obstacles +print("\nTest 1: Path with no obstacles") +path1 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1]) +print(f" Path: {path1[:5]}...{path1[-3:] if len(path1) > 5 else ''}") +print(f" Length: {len(path1)}") + +# Add obstacles from the demo +obstacles = [ + # Vertical wall with gaps + [(15, y) for y in range(3, 17) if y not in [8, 12]], + # Horizontal walls + [(x, 5) for x in range(10, 20)], + [(x, 15) for x in range(10, 20)], + # Maze-like structure + [(x, 10) for x in range(20, 25)], + [(25, y) for y in range(5, 15)], +] + +print("\nAdding obstacles...") +wall_count = 0 +for obstacle_group in obstacles: + for x, y in obstacle_group: + grid.at(x, y).walkable = False + wall_count += 1 + if wall_count <= 5: + print(f" Wall at ({x}, {y})") + +print(f" Total walls added: {wall_count}") + +# Check specific cells +print(f"\nChecking key positions:") +print(f" Start ({start_pos[0]}, {start_pos[1]}): walkable={grid.at(start_pos[0], start_pos[1]).walkable}") +print(f" End ({end_pos[0]}, {end_pos[1]}): walkable={grid.at(end_pos[0], end_pos[1]).walkable}") + +# Check if path is blocked +print(f"\nChecking horizontal line at y=10:") +blocked_x = [] +for x in range(30): + if not grid.at(x, 10).walkable: + blocked_x.append(x) + +print(f" Blocked x positions: {blocked_x}") + +# Test path with obstacles +print("\nTest 2: Path with obstacles") +path2 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1]) +print(f" Path: {path2}") +print(f" Length: {len(path2)}") + +# Check if there's any path at all +if not path2: + print("\n No path found! Checking why...") + + # Check if we can reach the vertical wall gap + print("\n Testing path to wall gap at (15, 8):") + path_to_gap = grid.compute_astar_path(start_pos[0], start_pos[1], 15, 8) + print(f" Path to gap: {path_to_gap}") + + # Check from gap to end + print("\n Testing path from gap (15, 8) to end:") + path_from_gap = grid.compute_astar_path(15, 8, end_pos[0], end_pos[1]) + print(f" Path from gap: {path_from_gap}") + +# Check walls more carefully +print("\nDetailed wall analysis:") +print(" Walls at x=25 (blocking end?):") +for y in range(5, 15): + print(f" ({25}, {y}): walkable={grid.at(25, y).walkable}") + +def timer_cb(dt): + sys.exit(0) + +ui = mcrfpy.sceneUI("debug") +ui.append(grid) +mcrfpy.setScene("debug") +mcrfpy.setTimer("exit", timer_cb, 100) \ No newline at end of file diff --git a/tests/debug_empty_paths.py b/tests/debug_empty_paths.py new file mode 100644 index 0000000..1485177 --- /dev/null +++ b/tests/debug_empty_paths.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Debug empty paths issue""" + +import mcrfpy +import sys + +print("Debugging empty paths...") + +# Create scene and grid +mcrfpy.createScene("debug") +grid = mcrfpy.Grid(grid_x=10, grid_y=10) + +# Initialize grid - all walkable +print("\nInitializing grid...") +for y in range(10): + for x in range(10): + grid.at(x, y).walkable = True + +# Test simple path +print("\nTest 1: Simple path from (0,0) to (5,5)") +path = grid.compute_astar_path(0, 0, 5, 5) +print(f" A* path: {path}") +print(f" Path length: {len(path)}") + +# Test with Dijkstra +print("\nTest 2: Same path with Dijkstra") +grid.compute_dijkstra(0, 0) +dpath = grid.get_dijkstra_path(5, 5) +print(f" Dijkstra path: {dpath}") +print(f" Path length: {len(dpath)}") + +# Check if grid is properly initialized +print("\nTest 3: Checking grid cells") +for y in range(3): + for x in range(3): + cell = grid.at(x, y) + print(f" Cell ({x},{y}): walkable={cell.walkable}") + +# Test with walls +print("\nTest 4: Path with wall") +grid.at(2, 2).walkable = False +grid.at(3, 2).walkable = False +grid.at(4, 2).walkable = False +print(" Added wall at y=2, x=2,3,4") + +path2 = grid.compute_astar_path(0, 0, 5, 5) +print(f" A* path with wall: {path2}") +print(f" Path length: {len(path2)}") + +# Test invalid paths +print("\nTest 5: Path to blocked cell") +grid.at(9, 9).walkable = False +path3 = grid.compute_astar_path(0, 0, 9, 9) +print(f" Path to blocked cell: {path3}") + +# Check TCOD map sync +print("\nTest 6: Verify TCOD map is synced") +# Try to force a sync +print(" Checking if syncTCODMap exists...") +if hasattr(grid, 'sync_tcod_map'): + print(" Calling sync_tcod_map()") + grid.sync_tcod_map() +else: + print(" No sync_tcod_map method found") + +# Try path again +print("\nTest 7: Path after potential sync") +path4 = grid.compute_astar_path(0, 0, 5, 5) +print(f" A* path: {path4}") + +def timer_cb(dt): + sys.exit(0) + +# Quick UI setup +ui = mcrfpy.sceneUI("debug") +ui.append(grid) +mcrfpy.setScene("debug") +mcrfpy.setTimer("exit", timer_cb, 100) + +print("\nStarting timer...") \ No newline at end of file diff --git a/tests/debug_visibility.py b/tests/debug_visibility.py new file mode 100644 index 0000000..da0bd60 --- /dev/null +++ b/tests/debug_visibility.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Debug visibility crash""" + +import mcrfpy +import sys + +print("Debug visibility...") + +# Create scene and grid +mcrfpy.createScene("debug") +grid = mcrfpy.Grid(grid_x=5, grid_y=5) + +# Initialize grid +print("Initializing grid...") +for y in range(5): + for x in range(5): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + +# Create entity +print("Creating entity...") +entity = mcrfpy.Entity(2, 2) +entity.sprite_index = 64 +grid.entities.append(entity) +print(f"Entity at ({entity.x}, {entity.y})") + +# Check gridstate +print(f"\nGridstate length: {len(entity.gridstate)}") +print(f"Expected: {5 * 5}") + +# Try to access gridstate +print("\nChecking gridstate access...") +try: + if len(entity.gridstate) > 0: + state = entity.gridstate[0] + print(f"First state: visible={state.visible}, discovered={state.discovered}") +except Exception as e: + print(f"Error accessing gridstate: {e}") + +# Try update_visibility +print("\nTrying update_visibility...") +try: + entity.update_visibility() + print("update_visibility succeeded") +except Exception as e: + print(f"Error in update_visibility: {e}") + +# Try perspective +print("\nTesting perspective...") +print(f"Initial perspective: {grid.perspective}") +try: + grid.perspective = 0 + print(f"Set perspective to 0: {grid.perspective}") +except Exception as e: + print(f"Error setting perspective: {e}") + +print("\nTest complete") +sys.exit(0) \ No newline at end of file diff --git a/tests/dijkstra_all_paths.py b/tests/dijkstra_all_paths.py new file mode 100644 index 0000000..e205f08 --- /dev/null +++ b/tests/dijkstra_all_paths.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +""" +Dijkstra Demo - Shows ALL Path Combinations (Including Invalid) +=============================================================== + +Cycles through every possible entity pair to demonstrate both +valid paths and properly handled invalid paths (empty lists). +""" + +import mcrfpy +import sys + +# High contrast colors +WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown +FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray +PATH_COLOR = mcrfpy.Color(0, 255, 0) # Bright green +START_COLOR = mcrfpy.Color(255, 100, 100) # Light red +END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue +NO_PATH_COLOR = mcrfpy.Color(255, 0, 0) # Pure red for unreachable + +# Global state +grid = None +entities = [] +current_combo_index = 0 +all_combinations = [] # All possible pairs +current_path = [] + +def create_map(): + """Create the map with entities""" + global grid, entities, all_combinations + + mcrfpy.createScene("dijkstra_all") + + # Create grid + grid = mcrfpy.Grid(grid_x=14, grid_y=10) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Map layout - Entity 1 is intentionally trapped! + map_layout = [ + "..............", # Row 0 + "..W.....WWWW..", # Row 1 + "..W.W...W.EW..", # Row 2 - Entity 1 TRAPPED at (10,2) + "..W.....W..W..", # Row 3 + "..W...E.WWWW..", # Row 4 - Entity 2 at (6,4) + "E.W...........", # Row 5 - Entity 3 at (0,5) + "..W...........", # Row 6 + "..W...........", # Row 7 + "..W.WWW.......", # Row 8 + "..............", # Row 9 + ] + + # Create the map + entity_positions = [] + for y, row in enumerate(map_layout): + for x, char in enumerate(row): + cell = grid.at(x, y) + + if char == 'W': + cell.walkable = False + cell.color = WALL_COLOR + else: + cell.walkable = True + cell.color = FLOOR_COLOR + + if char == 'E': + entity_positions.append((x, y)) + + # Create entities + entities = [] + for i, (x, y) in enumerate(entity_positions): + entity = mcrfpy.Entity(x, y) + entity.sprite_index = 49 + i # '1', '2', '3' + grid.entities.append(entity) + entities.append(entity) + + print("Map Analysis:") + print("=============") + for i, (x, y) in enumerate(entity_positions): + print(f"Entity {i+1} at ({x}, {y})") + + # Generate ALL combinations (including invalid ones) + all_combinations = [] + for i in range(len(entities)): + for j in range(len(entities)): + if i != j: # Skip self-paths + all_combinations.append((i, j)) + + print(f"\nTotal path combinations to test: {len(all_combinations)}") + +def clear_path_colors(): + """Reset all floor tiles to original color""" + global current_path + + for y in range(grid.grid_y): + for x in range(grid.grid_x): + cell = grid.at(x, y) + if cell.walkable: + cell.color = FLOOR_COLOR + + current_path = [] + +def show_combination(index): + """Show a specific path combination (valid or invalid)""" + global current_combo_index, current_path + + current_combo_index = index % len(all_combinations) + from_idx, to_idx = all_combinations[current_combo_index] + + # Clear previous path + clear_path_colors() + + # Get entities + e_from = entities[from_idx] + e_to = entities[to_idx] + + # Calculate path + path = e_from.path_to(int(e_to.x), int(e_to.y)) + current_path = path if path else [] + + # Always color start and end positions + grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR + grid.at(int(e_to.x), int(e_to.y)).color = NO_PATH_COLOR if not path else END_COLOR + + # Color the path if it exists + if path: + # Color intermediate steps + for i, (x, y) in enumerate(path): + if i > 0 and i < len(path) - 1: + grid.at(x, y).color = PATH_COLOR + + status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = {len(path)} steps" + status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green for valid + + # Show path steps + path_display = [] + for i, (x, y) in enumerate(path[:5]): + path_display.append(f"({x},{y})") + if len(path) > 5: + path_display.append("...") + path_text.text = "Path: " + " → ".join(path_display) + else: + status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = NO PATH!" + status_text.fill_color = mcrfpy.Color(255, 100, 100) # Red for invalid + path_text.text = "Path: [] (No valid path exists)" + + # Update info + info_text.text = f"From: Entity {from_idx+1} at ({int(e_from.x)}, {int(e_from.y)}) | To: Entity {to_idx+1} at ({int(e_to.x)}, {int(e_to.y)})" + +def handle_keypress(key_str, state): + """Handle keyboard input""" + global current_combo_index + if state == "end": return + + if key_str == "Esc" or key_str == "Q": + print("\nExiting...") + sys.exit(0) + elif key_str == "Space" or key_str == "N": + show_combination(current_combo_index + 1) + elif key_str == "P": + show_combination(current_combo_index - 1) + elif key_str == "R": + show_combination(current_combo_index) + elif key_str in "123456": + combo_num = int(key_str) - 1 # 0-based index + if combo_num < len(all_combinations): + show_combination(combo_num) + +# Create the demo +print("Dijkstra All Paths Demo") +print("=======================") +print("Shows ALL path combinations including invalid ones") +print("Entity 1 is trapped - paths to/from it will be empty!") +print() + +create_map() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_all") +ui.append(grid) + +# Scale and position +grid.size = (560, 400) +grid.position = (120, 100) + +# Add title +title = mcrfpy.Caption("Dijkstra - All Paths (Valid & Invalid)", 200, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add status (will change color based on validity) +status_text = mcrfpy.Caption("Ready", 120, 60) +status_text.fill_color = mcrfpy.Color(255, 255, 100) +ui.append(status_text) + +# Add info +info_text = mcrfpy.Caption("", 120, 80) +info_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(info_text) + +# Add path display +path_text = mcrfpy.Caption("Path: None", 120, 520) +path_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(path_text) + +# Add controls +controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, 1-6=Jump to path, Q=Quit", 120, 540) +controls.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(controls) + +# Add legend +legend = mcrfpy.Caption("Red Start→Blue End (valid) | Red Start→Red End (invalid)", 120, 560) +legend.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend) + +# Expected results info +expected = mcrfpy.Caption("Entity 1 is trapped: paths 1→2, 1→3, 2→1, 3→1 will fail", 120, 580) +expected.fill_color = mcrfpy.Color(255, 150, 150) +ui.append(expected) + +# Set scene first, then set up input handler +mcrfpy.setScene("dijkstra_all") +mcrfpy.keypressScene(handle_keypress) + +# Show first combination +show_combination(0) + +print("\nDemo ready!") +print("Expected results:") +print(" Path 1: Entity 1→2 = NO PATH (Entity 1 is trapped)") +print(" Path 2: Entity 1→3 = NO PATH (Entity 1 is trapped)") +print(" Path 3: Entity 2→1 = NO PATH (Entity 1 is trapped)") +print(" Path 4: Entity 2→3 = Valid path") +print(" Path 5: Entity 3→1 = NO PATH (Entity 1 is trapped)") +print(" Path 6: Entity 3→2 = Valid path") \ No newline at end of file diff --git a/tests/dijkstra_cycle_paths.py b/tests/dijkstra_cycle_paths.py new file mode 100644 index 0000000..201219c --- /dev/null +++ b/tests/dijkstra_cycle_paths.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +""" +Dijkstra Demo - Cycles Through Different Path Combinations +========================================================== + +Shows paths between different entity pairs, skipping impossible paths. +""" + +import mcrfpy +import sys + +# High contrast colors +WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown +FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray +PATH_COLOR = mcrfpy.Color(0, 255, 0) # Bright green +START_COLOR = mcrfpy.Color(255, 100, 100) # Light red +END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue + +# Global state +grid = None +entities = [] +current_path_index = 0 +path_combinations = [] +current_path = [] + +def create_map(): + """Create the map with entities""" + global grid, entities + + mcrfpy.createScene("dijkstra_cycle") + + # Create grid + grid = mcrfpy.Grid(grid_x=14, grid_y=10) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Map layout + map_layout = [ + "..............", # Row 0 + "..W.....WWWW..", # Row 1 + "..W.W...W.EW..", # Row 2 - Entity 1 at (10,2) is TRAPPED! + "..W.....W..W..", # Row 3 + "..W...E.WWWW..", # Row 4 - Entity 2 at (6,4) + "E.W...........", # Row 5 - Entity 3 at (0,5) + "..W...........", # Row 6 + "..W...........", # Row 7 + "..W.WWW.......", # Row 8 + "..............", # Row 9 + ] + + # Create the map + entity_positions = [] + for y, row in enumerate(map_layout): + for x, char in enumerate(row): + cell = grid.at(x, y) + + if char == 'W': + cell.walkable = False + cell.color = WALL_COLOR + else: + cell.walkable = True + cell.color = FLOOR_COLOR + + if char == 'E': + entity_positions.append((x, y)) + + # Create entities + entities = [] + for i, (x, y) in enumerate(entity_positions): + entity = mcrfpy.Entity(x, y) + entity.sprite_index = 49 + i # '1', '2', '3' + grid.entities.append(entity) + entities.append(entity) + + print("Entities created:") + for i, (x, y) in enumerate(entity_positions): + print(f" Entity {i+1} at ({x}, {y})") + + # Check which entity is trapped + print("\nChecking accessibility:") + for i, e in enumerate(entities): + # Try to path to each other entity + can_reach = [] + for j, other in enumerate(entities): + if i != j: + path = e.path_to(int(other.x), int(other.y)) + if path: + can_reach.append(j+1) + + if not can_reach: + print(f" Entity {i+1} at ({int(e.x)}, {int(e.y)}) is TRAPPED!") + else: + print(f" Entity {i+1} can reach entities: {can_reach}") + + # Generate valid path combinations (excluding trapped entity) + global path_combinations + path_combinations = [] + + # Only paths between entities 2 and 3 (indices 1 and 2) will work + # since entity 1 (index 0) is trapped + if len(entities) >= 3: + # Entity 2 to Entity 3 + path = entities[1].path_to(int(entities[2].x), int(entities[2].y)) + if path: + path_combinations.append((1, 2, path)) + + # Entity 3 to Entity 2 + path = entities[2].path_to(int(entities[1].x), int(entities[1].y)) + if path: + path_combinations.append((2, 1, path)) + + print(f"\nFound {len(path_combinations)} valid paths") + +def clear_path_colors(): + """Reset all floor tiles to original color""" + global current_path + + for y in range(grid.grid_y): + for x in range(grid.grid_x): + cell = grid.at(x, y) + if cell.walkable: + cell.color = FLOOR_COLOR + + current_path = [] + +def show_path(index): + """Show a specific path combination""" + global current_path_index, current_path + + if not path_combinations: + status_text.text = "No valid paths available (Entity 1 is trapped!)" + return + + current_path_index = index % len(path_combinations) + from_idx, to_idx, path = path_combinations[current_path_index] + + # Clear previous path + clear_path_colors() + + # Get entities + e_from = entities[from_idx] + e_to = entities[to_idx] + + # Color the path + current_path = path + if path: + # Color start and end + grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR + grid.at(int(e_to.x), int(e_to.y)).color = END_COLOR + + # Color intermediate steps + for i, (x, y) in enumerate(path): + if i > 0 and i < len(path) - 1: + grid.at(x, y).color = PATH_COLOR + + # Update status + status_text.text = f"Path {current_path_index + 1}/{len(path_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} ({len(path)} steps)" + + # Update path display + path_display = [] + for i, (x, y) in enumerate(path[:5]): # Show first 5 steps + path_display.append(f"({x},{y})") + if len(path) > 5: + path_display.append("...") + path_text.text = "Path: " + " → ".join(path_display) if path_display else "Path: None" + +def handle_keypress(key_str, state): + """Handle keyboard input""" + global current_path_index + if state == "end": return + if key_str == "Esc": + print("\nExiting...") + sys.exit(0) + elif key_str == "N" or key_str == "Space": + show_path(current_path_index + 1) + elif key_str == "P": + show_path(current_path_index - 1) + elif key_str == "R": + show_path(current_path_index) + +# Create the demo +print("Dijkstra Path Cycling Demo") +print("==========================") +print("Note: Entity 1 is trapped by walls!") +print() + +create_map() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_cycle") +ui.append(grid) + +# Scale and position +grid.size = (560, 400) +grid.position = (120, 100) + +# Add title +title = mcrfpy.Caption("Dijkstra Pathfinding - Cycle Paths", 200, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add status +status_text = mcrfpy.Caption("Press SPACE to cycle paths", 120, 60) +status_text.fill_color = mcrfpy.Color(255, 255, 100) +ui.append(status_text) + +# Add path display +path_text = mcrfpy.Caption("Path: None", 120, 520) +path_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(path_text) + +# Add controls +controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, R=Refresh, Q=Quit", 120, 540) +controls.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(controls) + +# Add legend +legend = mcrfpy.Caption("Red=Start, Blue=End, Green=Path, Dark=Wall", 120, 560) +legend.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend) + +# Show first valid path +mcrfpy.setScene("dijkstra_cycle") +mcrfpy.keypressScene(handle_keypress) + +# Display initial path +if path_combinations: + show_path(0) +else: + status_text.text = "No valid paths! Entity 1 is trapped!" + +print("\nDemo ready!") +print("Controls:") +print(" SPACE or N - Next path") +print(" P - Previous path") +print(" R - Refresh current path") +print(" Q - Quit") diff --git a/tests/dijkstra_debug.py b/tests/dijkstra_debug.py new file mode 100644 index 0000000..fd182b8 --- /dev/null +++ b/tests/dijkstra_debug.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +""" +Debug version of Dijkstra pathfinding to diagnose visualization issues +""" + +import mcrfpy +import sys + +# Colors +WALL_COLOR = mcrfpy.Color(60, 30, 30) +FLOOR_COLOR = mcrfpy.Color(200, 200, 220) +PATH_COLOR = mcrfpy.Color(200, 250, 220) +ENTITY_COLORS = [ + mcrfpy.Color(255, 100, 100), # Entity 1 - Red + mcrfpy.Color(100, 255, 100), # Entity 2 - Green + mcrfpy.Color(100, 100, 255), # Entity 3 - Blue +] + +# Global state +grid = None +entities = [] +first_point = None +second_point = None + +def create_simple_map(): + """Create a simple test map""" + global grid, entities + + mcrfpy.createScene("dijkstra_debug") + + # Small grid for easy debugging + grid = mcrfpy.Grid(grid_x=10, grid_y=10) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + print("Initializing 10x10 grid...") + + # Initialize all as floor + for y in range(10): + for x in range(10): + grid.at(x, y).walkable = True + grid.at(x, y).transparent = True + grid.at(x, y).color = FLOOR_COLOR + + # Add a simple wall + print("Adding walls at:") + walls = [(5, 2), (5, 3), (5, 4), (5, 5), (5, 6)] + for x, y in walls: + print(f" Wall at ({x}, {y})") + grid.at(x, y).walkable = False + grid.at(x, y).color = WALL_COLOR + + # Create 3 entities + entity_positions = [(2, 5), (8, 5), (5, 8)] + entities = [] + + print("\nCreating entities at:") + for i, (x, y) in enumerate(entity_positions): + print(f" Entity {i+1} at ({x}, {y})") + entity = mcrfpy.Entity(x, y) + entity.sprite_index = 49 + i # '1', '2', '3' + grid.entities.append(entity) + entities.append(entity) + + return grid + +def test_path_highlighting(): + """Test path highlighting with debug output""" + print("\n" + "="*50) + print("Testing path highlighting...") + + # Select first two entities + e1 = entities[0] + e2 = entities[1] + + print(f"\nEntity 1 position: ({e1.x}, {e1.y})") + print(f"Entity 2 position: ({e2.x}, {e2.y})") + + # Use entity.path_to() + print("\nCalling entity.path_to()...") + path = e1.path_to(int(e2.x), int(e2.y)) + + print(f"Path returned: {path}") + print(f"Path length: {len(path)} steps") + + if path: + print("\nHighlighting path cells:") + for i, (x, y) in enumerate(path): + print(f" Step {i}: ({x}, {y})") + # Get current color for debugging + cell = grid.at(x, y) + old_color = (cell.color.r, cell.color.g, cell.color.b) + + # Set new color + cell.color = PATH_COLOR + new_color = (cell.color.r, cell.color.g, cell.color.b) + + print(f" Color changed from {old_color} to {new_color}") + print(f" Walkable: {cell.walkable}") + + # Also test grid's Dijkstra methods + print("\n" + "-"*30) + print("Testing grid Dijkstra methods...") + + grid.compute_dijkstra(int(e1.x), int(e1.y)) + grid_path = grid.get_dijkstra_path(int(e2.x), int(e2.y)) + distance = grid.get_dijkstra_distance(int(e2.x), int(e2.y)) + + print(f"Grid path: {grid_path}") + print(f"Grid distance: {distance}") + + # Verify colors were set + print("\nVerifying cell colors after highlighting:") + for x, y in path[:3]: # Check first 3 cells + cell = grid.at(x, y) + color = (cell.color.r, cell.color.g, cell.color.b) + expected = (PATH_COLOR.r, PATH_COLOR.g, PATH_COLOR.b) + match = color == expected + print(f" Cell ({x}, {y}): color={color}, expected={expected}, match={match}") + +def handle_keypress(scene_name, keycode): + """Simple keypress handler""" + if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC + print("\nExiting debug...") + sys.exit(0) + elif keycode == 32: # Space + print("\nSpace pressed - retesting path highlighting...") + test_path_highlighting() + +# Create the map +print("Dijkstra Debug Test") +print("===================") +grid = create_simple_map() + +# Initial path test +test_path_highlighting() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_debug") +ui.append(grid) + +# Position and scale +grid.position = (50, 50) +grid.size = (400, 400) # 10*40 + +# Add title +title = mcrfpy.Caption("Dijkstra Debug - Press SPACE to retest, Q to quit", 50, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add debug info +info = mcrfpy.Caption("Check console for debug output", 50, 470) +info.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(info) + +# Set up scene +mcrfpy.keypressScene(handle_keypress) +mcrfpy.setScene("dijkstra_debug") + +print("\nScene ready. The path should be highlighted in cyan.") +print("If you don't see the path, there may be a rendering issue.") +print("Press SPACE to retest, Q to quit.") \ No newline at end of file diff --git a/tests/dijkstra_demo_working.py b/tests/dijkstra_demo_working.py new file mode 100644 index 0000000..91efc51 --- /dev/null +++ b/tests/dijkstra_demo_working.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +Working Dijkstra Demo with Clear Visual Feedback +================================================ + +This demo shows pathfinding with high-contrast colors. +""" + +import mcrfpy +import sys + +# High contrast colors +WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown for walls +FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray for floors +PATH_COLOR = mcrfpy.Color(0, 255, 0) # Pure green for paths +START_COLOR = mcrfpy.Color(255, 0, 0) # Red for start +END_COLOR = mcrfpy.Color(0, 0, 255) # Blue for end + +print("Dijkstra Demo - High Contrast") +print("==============================") + +# Create scene +mcrfpy.createScene("dijkstra_demo") + +# Create grid with exact layout from user +grid = mcrfpy.Grid(grid_x=14, grid_y=10) +grid.fill_color = mcrfpy.Color(0, 0, 0) + +# Map layout +map_layout = [ + "..............", # Row 0 + "..W.....WWWW..", # Row 1 + "..W.W...W.EW..", # Row 2 + "..W.....W..W..", # Row 3 + "..W...E.WWWW..", # Row 4 + "E.W...........", # Row 5 + "..W...........", # Row 6 + "..W...........", # Row 7 + "..W.WWW.......", # Row 8 + "..............", # Row 9 +] + +# Create the map +entity_positions = [] +for y, row in enumerate(map_layout): + for x, char in enumerate(row): + cell = grid.at(x, y) + + if char == 'W': + cell.walkable = False + cell.color = WALL_COLOR + else: + cell.walkable = True + cell.color = FLOOR_COLOR + + if char == 'E': + entity_positions.append((x, y)) + +print(f"Map created: {grid.grid_x}x{grid.grid_y}") +print(f"Entity positions: {entity_positions}") + +# Create entities +entities = [] +for i, (x, y) in enumerate(entity_positions): + entity = mcrfpy.Entity(x, y) + entity.sprite_index = 49 + i # '1', '2', '3' + grid.entities.append(entity) + entities.append(entity) + print(f"Entity {i+1} at ({x}, {y})") + +# Highlight a path immediately +if len(entities) >= 2: + e1, e2 = entities[0], entities[1] + print(f"\nCalculating path from Entity 1 ({e1.x}, {e1.y}) to Entity 2 ({e2.x}, {e2.y})...") + + path = e1.path_to(int(e2.x), int(e2.y)) + print(f"Path found: {path}") + print(f"Path length: {len(path)} steps") + + if path: + print("\nHighlighting path in bright green...") + # Color start and end specially + grid.at(int(e1.x), int(e1.y)).color = START_COLOR + grid.at(int(e2.x), int(e2.y)).color = END_COLOR + + # Color the path + for i, (x, y) in enumerate(path): + if i > 0 and i < len(path) - 1: # Skip start and end + grid.at(x, y).color = PATH_COLOR + print(f" Colored ({x}, {y}) green") + +# Keypress handler +def handle_keypress(scene_name, keycode): + if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC + print("\nExiting...") + sys.exit(0) + elif keycode == 32: # Space + print("\nRefreshing path colors...") + # Re-color the path to ensure it's visible + if len(entities) >= 2 and path: + for x, y in path[1:-1]: + grid.at(x, y).color = PATH_COLOR + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_demo") +ui.append(grid) + +# Scale grid +grid.size = (560, 400) # 14*40, 10*40 +grid.position = (120, 100) + +# Add title +title = mcrfpy.Caption("Dijkstra Pathfinding - High Contrast", 200, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add legend +legend1 = mcrfpy.Caption("Red=Start, Blue=End, Green=Path", 120, 520) +legend1.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(legend1) + +legend2 = mcrfpy.Caption("Press Q to quit, SPACE to refresh", 120, 540) +legend2.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend2) + +# Entity info +info = mcrfpy.Caption(f"Path: Entity 1 to 2 = {len(path) if 'path' in locals() else 0} steps", 120, 60) +info.fill_color = mcrfpy.Color(255, 255, 100) +ui.append(info) + +# Set up input +mcrfpy.keypressScene(handle_keypress) +mcrfpy.setScene("dijkstra_demo") + +print("\nDemo ready! The path should be clearly visible in bright green.") +print("Red = Start, Blue = End, Green = Path") +print("Press SPACE to refresh colors if needed.") \ No newline at end of file diff --git a/tests/dijkstra_interactive.py b/tests/dijkstra_interactive.py index e358c00..fdf2176 100644 --- a/tests/dijkstra_interactive.py +++ b/tests/dijkstra_interactive.py @@ -17,10 +17,10 @@ The path between selected entities is automatically highlighted. import mcrfpy import sys -# Colors +# Colors - using more distinct values WALL_COLOR = mcrfpy.Color(60, 30, 30) -FLOOR_COLOR = mcrfpy.Color(200, 200, 220) -PATH_COLOR = mcrfpy.Color(200, 250, 220) +FLOOR_COLOR = mcrfpy.Color(100, 100, 120) # Darker floor for better contrast +PATH_COLOR = mcrfpy.Color(50, 255, 50) # Bright green for path ENTITY_COLORS = [ mcrfpy.Color(255, 100, 100), # Entity 1 - Red mcrfpy.Color(100, 255, 100), # Entity 2 - Green diff --git a/tests/interactive_visibility.py b/tests/interactive_visibility.py new file mode 100644 index 0000000..3d7aef8 --- /dev/null +++ b/tests/interactive_visibility.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +Interactive Visibility Demo +========================== + +Controls: + - WASD: Move the player (green @) + - Arrow keys: Move enemy (red E) + - Tab: Cycle perspective (Omniscient → Player → Enemy → Omniscient) + - Space: Update visibility for current entity + - R: Reset positions +""" + +import mcrfpy +import sys + +# Create scene and grid +mcrfpy.createScene("visibility_demo") +grid = mcrfpy.Grid(grid_x=30, grid_y=20) +grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background + +# Initialize grid - all walkable and transparent +for y in range(20): + for x in range(30): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + cell.color = mcrfpy.Color(100, 100, 120) # Floor color + +# Create walls +walls = [ + # Central cross + [(15, y) for y in range(8, 12)], + [(x, 10) for x in range(13, 18)], + + # Rooms + # Top-left room + [(x, 5) for x in range(2, 8)] + [(8, y) for y in range(2, 6)], + [(2, y) for y in range(2, 6)] + [(x, 2) for x in range(2, 8)], + + # Top-right room + [(x, 5) for x in range(22, 28)] + [(22, y) for y in range(2, 6)], + [(28, y) for y in range(2, 6)] + [(x, 2) for x in range(22, 28)], + + # Bottom-left room + [(x, 15) for x in range(2, 8)] + [(8, y) for y in range(15, 18)], + [(2, y) for y in range(15, 18)] + [(x, 18) for x in range(2, 8)], + + # Bottom-right room + [(x, 15) for x in range(22, 28)] + [(22, y) for y in range(15, 18)], + [(28, y) for y in range(15, 18)] + [(x, 18) for x in range(22, 28)], +] + +for wall_group in walls: + for x, y in wall_group: + if 0 <= x < 30 and 0 <= y < 20: + cell = grid.at(x, y) + cell.walkable = False + cell.transparent = False + cell.color = mcrfpy.Color(40, 20, 20) # Wall color + +# Create entities +player = mcrfpy.Entity(5, 10, grid=grid) +player.sprite_index = 64 # @ +enemy = mcrfpy.Entity(25, 10, grid=grid) +enemy.sprite_index = 69 # E + +# Update initial visibility +player.update_visibility() +enemy.update_visibility() + +# Global state +current_perspective = -1 +perspective_names = ["Omniscient", "Player", "Enemy"] + +# UI Setup +ui = mcrfpy.sceneUI("visibility_demo") +ui.append(grid) +grid.position = (50, 100) +grid.size = (900, 600) # 30*30, 20*30 + +# Title +title = mcrfpy.Caption("Interactive Visibility Demo", 350, 20) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Info displays +perspective_label = mcrfpy.Caption("Perspective: Omniscient", 50, 50) +perspective_label.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(perspective_label) + +controls = mcrfpy.Caption("WASD: Move player | Arrows: Move enemy | Tab: Cycle perspective | Space: Update visibility | R: Reset", 50, 730) +controls.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(controls) + +player_info = mcrfpy.Caption("Player: (5, 10)", 700, 50) +player_info.fill_color = mcrfpy.Color(100, 255, 100) +ui.append(player_info) + +enemy_info = mcrfpy.Caption("Enemy: (25, 10)", 700, 70) +enemy_info.fill_color = mcrfpy.Color(255, 100, 100) +ui.append(enemy_info) + +# Helper functions +def move_entity(entity, dx, dy): + """Move entity if target is walkable""" + new_x = int(entity.x + dx) + new_y = int(entity.y + dy) + + if 0 <= new_x < 30 and 0 <= new_y < 20: + cell = grid.at(new_x, new_y) + if cell.walkable: + entity.x = new_x + entity.y = new_y + entity.update_visibility() + return True + return False + +def update_info(): + """Update info displays""" + player_info.text = f"Player: ({int(player.x)}, {int(player.y)})" + enemy_info.text = f"Enemy: ({int(enemy.x)}, {int(enemy.y)})" + +def cycle_perspective(): + """Cycle through perspectives""" + global current_perspective + + # Cycle: -1 → 0 → 1 → -1 + current_perspective = (current_perspective + 2) % 3 - 1 + + grid.perspective = current_perspective + name = perspective_names[current_perspective + 1] + perspective_label.text = f"Perspective: {name}" + +# Key handlers +def handle_keys(key, state): + """Handle keyboard input""" + if state == "end": return + key = key.lower() + # Player movement (WASD) + if key == "w": + move_entity(player, 0, -1) + elif key == "s": + move_entity(player, 0, 1) + elif key == "a": + move_entity(player, -1, 0) + elif key == "d": + move_entity(player, 1, 0) + + # Enemy movement (Arrows) + elif key == "up": + move_entity(enemy, 0, -1) + elif key == "down": + move_entity(enemy, 0, 1) + elif key == "left": + move_entity(enemy, -1, 0) + elif key == "right": + move_entity(enemy, 1, 0) + + # Tab to cycle perspective + elif key == "tab": + cycle_perspective() + + # Space to update visibility + elif key == "space": + player.update_visibility() + enemy.update_visibility() + print("Updated visibility for both entities") + + # R to reset + elif key == "r": + player.x, player.y = 5, 10 + enemy.x, enemy.y = 25, 10 + player.update_visibility() + enemy.update_visibility() + update_info() + print("Reset positions") + + # Q to quit + elif key == "q": + print("Exiting...") + sys.exit(0) + + update_info() + +# Set scene first +mcrfpy.setScene("visibility_demo") + +# Register key handler (operates on current scene) +mcrfpy.keypressScene(handle_keys) + +print("Interactive Visibility Demo") +print("===========================") +print("WASD: Move player (green @)") +print("Arrows: Move enemy (red E)") +print("Tab: Cycle perspective") +print("Space: Update visibility") +print("R: Reset positions") +print("Q: Quit") +print("\nCurrent perspective: Omniscient (shows all)") +print("Try moving entities and switching perspectives!") diff --git a/tests/path_vision_fixed.py b/tests/path_vision_fixed.py new file mode 100644 index 0000000..ee4c804 --- /dev/null +++ b/tests/path_vision_fixed.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python3 +""" +Path & Vision Sizzle Reel (Fixed) +================================= + +Fixed version with proper animation chaining to prevent glitches. +""" + +import mcrfpy +import sys + +class PathAnimator: + """Handles step-by-step animation with proper completion tracking""" + + def __init__(self, entity, name="animator"): + self.entity = entity + self.name = name + self.path = [] + self.current_index = 0 + self.step_duration = 0.4 + self.animating = False + self.on_step = None + self.on_complete = None + + def set_path(self, path): + """Set the path to animate along""" + self.path = path + self.current_index = 0 + + def start(self): + """Start animating""" + if not self.path: + return + + self.animating = True + self.current_index = 0 + self._move_to_next() + + def stop(self): + """Stop animating""" + self.animating = False + mcrfpy.delTimer(f"{self.name}_check") + + def _move_to_next(self): + """Move to next position in path""" + if not self.animating or self.current_index >= len(self.path): + self.animating = False + if self.on_complete: + self.on_complete() + return + + # Get next position + x, y = self.path[self.current_index] + + # Create animations + anim_x = mcrfpy.Animation("x", float(x), self.step_duration, "easeInOut") + anim_y = mcrfpy.Animation("y", float(y), self.step_duration, "easeInOut") + + anim_x.start(self.entity) + anim_y.start(self.entity) + + # Update visibility + self.entity.update_visibility() + + # Callback for each step + if self.on_step: + self.on_step(self.current_index, x, y) + + # Schedule next move + delay = int(self.step_duration * 1000) + 50 # Add small buffer + mcrfpy.setTimer(f"{self.name}_next", self._handle_next, delay) + + def _handle_next(self, dt): + """Timer callback to move to next position""" + self.current_index += 1 + mcrfpy.delTimer(f"{self.name}_next") + self._move_to_next() + +# Global state +grid = None +player = None +enemy = None +player_animator = None +enemy_animator = None +demo_phase = 0 + +def create_scene(): + """Create the demo environment""" + global grid, player, enemy + + mcrfpy.createScene("fixed_demo") + + # Create grid + grid = mcrfpy.Grid(grid_x=30, grid_y=20) + grid.fill_color = mcrfpy.Color(20, 20, 30) + + # Simple dungeon layout + map_layout = [ + "##############################", + "#......#########.....#########", + "#......#########.....#########", + "#......#.........#...#########", + "#......#.........#...#########", + "####.###.........#.###########", + "####.............#.###########", + "####.............#.###########", + "####.###.........#.###########", + "#......#.........#...#########", + "#......#.........#...#########", + "#......#########.#...........#", + "#......#########.#...........#", + "#......#########.#...........#", + "#......#########.#############", + "####.###########.............#", + "####.........................#", + "####.###########.............#", + "#......#########.............#", + "##############################", + ] + + # Build map + for y, row in enumerate(map_layout): + for x, char in enumerate(row): + cell = grid.at(x, y) + if char == '#': + cell.walkable = False + cell.transparent = False + cell.color = mcrfpy.Color(40, 30, 30) + else: + cell.walkable = True + cell.transparent = True + cell.color = mcrfpy.Color(80, 80, 100) + + # Create entities + player = mcrfpy.Entity(3, 3, grid=grid) + player.sprite_index = 64 # @ + + enemy = mcrfpy.Entity(26, 16, grid=grid) + enemy.sprite_index = 69 # E + + # Initial visibility + player.update_visibility() + enemy.update_visibility() + + # Set initial perspective + grid.perspective = 0 + +def setup_ui(): + """Create UI elements""" + ui = mcrfpy.sceneUI("fixed_demo") + ui.append(grid) + + grid.position = (50, 80) + grid.size = (700, 500) + + title = mcrfpy.Caption("Path & Vision Demo (Fixed)", 300, 20) + title.fill_color = mcrfpy.Color(255, 255, 255) + ui.append(title) + + global status_text, perspective_text + status_text = mcrfpy.Caption("Initializing...", 50, 50) + status_text.fill_color = mcrfpy.Color(200, 200, 200) + ui.append(status_text) + + perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50) + perspective_text.fill_color = mcrfpy.Color(100, 255, 100) + ui.append(perspective_text) + + controls = mcrfpy.Caption("Space: Start/Pause | R: Restart | Q: Quit", 250, 600) + controls.fill_color = mcrfpy.Color(150, 150, 150) + ui.append(controls) + +def update_camera_smooth(target, duration=0.3): + """Smoothly move camera to entity""" + center_x = target.x * 23 # Approximate pixel size + center_y = target.y * 23 + + cam_anim = mcrfpy.Animation("center", (center_x, center_y), duration, "easeOut") + cam_anim.start(grid) + +def start_demo(): + """Start the demo sequence""" + global demo_phase, player_animator, enemy_animator + + demo_phase = 1 + status_text.text = "Phase 1: Player movement with camera follow" + + # Player path + player_path = [ + (3, 3), (3, 6), (4, 6), (7, 6), (7, 8), + (10, 8), (13, 8), (16, 8), (16, 10), + (16, 13), (16, 16), (20, 16), (24, 16) + ] + + # Setup player animator + player_animator = PathAnimator(player, "player") + player_animator.set_path(player_path) + player_animator.step_duration = 0.5 + + def on_player_step(index, x, y): + """Called for each player step""" + status_text.text = f"Player step {index+1}/{len(player_path)}" + if grid.perspective == 0: + update_camera_smooth(player, 0.4) + + def on_player_complete(): + """Called when player path is complete""" + start_phase_2() + + player_animator.on_step = on_player_step + player_animator.on_complete = on_player_complete + player_animator.start() + +def start_phase_2(): + """Start enemy movement phase""" + global demo_phase + + demo_phase = 2 + status_text.text = "Phase 2: Enemy movement (may enter player's view)" + + # Enemy path + enemy_path = [ + (26, 16), (22, 16), (18, 16), (16, 16), + (16, 13), (16, 10), (16, 8), (13, 8), + (10, 8), (7, 8), (7, 6), (4, 6) + ] + + # Setup enemy animator + enemy_animator.set_path(enemy_path) + enemy_animator.step_duration = 0.4 + + def on_enemy_step(index, x, y): + """Check if enemy is visible to player""" + if grid.perspective == 0: + # Check if enemy is in player's view + enemy_idx = int(y) * grid.grid_x + int(x) + if enemy_idx < len(player.gridstate) and player.gridstate[enemy_idx].visible: + status_text.text = "Enemy spotted in player's view!" + + def on_enemy_complete(): + """Start perspective transition""" + start_phase_3() + + enemy_animator.on_step = on_enemy_step + enemy_animator.on_complete = on_enemy_complete + enemy_animator.start() + +def start_phase_3(): + """Dramatic perspective shift""" + global demo_phase + + demo_phase = 3 + status_text.text = "Phase 3: Perspective shift..." + + # Stop any ongoing animations + player_animator.stop() + enemy_animator.stop() + + # Zoom out + zoom_out = mcrfpy.Animation("zoom", 0.6, 2.0, "easeInExpo") + zoom_out.start(grid) + + # Schedule perspective switch + mcrfpy.setTimer("switch_persp", switch_perspective, 2100) + +def switch_perspective(dt): + """Switch to enemy perspective""" + grid.perspective = 1 + perspective_text.text = "Perspective: Enemy" + perspective_text.fill_color = mcrfpy.Color(255, 100, 100) + + # Update camera + update_camera_smooth(enemy, 0.5) + + # Zoom back in + zoom_in = mcrfpy.Animation("zoom", 1.0, 2.0, "easeOutExpo") + zoom_in.start(grid) + + status_text.text = "Now following enemy perspective" + + # Clean up timer + mcrfpy.delTimer("switch_persp") + + # Continue enemy movement after transition + mcrfpy.setTimer("continue_enemy", continue_enemy_movement, 2500) + +def continue_enemy_movement(dt): + """Continue enemy movement after perspective shift""" + mcrfpy.delTimer("continue_enemy") + + # Continue path + enemy_path_2 = [ + (4, 6), (3, 6), (3, 3), (3, 2), (3, 1) + ] + + enemy_animator.set_path(enemy_path_2) + + def on_step(index, x, y): + update_camera_smooth(enemy, 0.4) + status_text.text = f"Following enemy: step {index+1}" + + def on_complete(): + status_text.text = "Demo complete! Press R to restart" + + enemy_animator.on_step = on_step + enemy_animator.on_complete = on_complete + enemy_animator.start() + +# Control state +running = False + +def handle_keys(key, state): + """Handle keyboard input""" + global running + + if state != "start": + return + + key = key.lower() + + if key == "q": + sys.exit(0) + elif key == "space": + if not running: + running = True + start_demo() + else: + running = False + player_animator.stop() + enemy_animator.stop() + status_text.text = "Paused" + elif key == "r": + # Reset everything + player.x, player.y = 3, 3 + enemy.x, enemy.y = 26, 16 + grid.perspective = 0 + perspective_text.text = "Perspective: Player" + perspective_text.fill_color = mcrfpy.Color(100, 255, 100) + grid.zoom = 1.0 + update_camera_smooth(player, 0.5) + + if running: + player_animator.stop() + enemy_animator.stop() + running = False + + status_text.text = "Reset - Press SPACE to start" + +# Initialize +create_scene() +setup_ui() + +# Setup animators +player_animator = PathAnimator(player, "player") +enemy_animator = PathAnimator(enemy, "enemy") + +# Set scene +mcrfpy.setScene("fixed_demo") +mcrfpy.keypressScene(handle_keys) + +# Initial camera +grid.zoom = 1.0 +update_camera_smooth(player, 0.5) + +print("Path & Vision Demo (Fixed)") +print("==========================") +print("This version properly chains animations to prevent glitches.") +print() +print("The demo will:") +print("1. Move player with camera following") +print("2. Move enemy (may enter player's view)") +print("3. Dramatic perspective shift to enemy") +print("4. Continue following enemy") +print() +print("Press SPACE to start, Q to quit") \ No newline at end of file diff --git a/tests/path_vision_sizzle_reel.py b/tests/path_vision_sizzle_reel.py new file mode 100644 index 0000000..b067b6c --- /dev/null +++ b/tests/path_vision_sizzle_reel.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +""" +Path & Vision Sizzle Reel +========================= + +A choreographed demo showing: +- Smooth entity movement along paths +- Camera following with grid center animation +- Field of view updates as entities move +- Dramatic perspective transitions with zoom effects +""" + +import mcrfpy +import sys + +# Colors +WALL_COLOR = mcrfpy.Color(40, 30, 30) +FLOOR_COLOR = mcrfpy.Color(80, 80, 100) +PATH_COLOR = mcrfpy.Color(120, 120, 180) +DARK_FLOOR = mcrfpy.Color(40, 40, 50) + +# Global state +grid = None +player = None +enemy = None +sequence_step = 0 +player_path = [] +enemy_path = [] +player_path_index = 0 +enemy_path_index = 0 + +def create_scene(): + """Create the demo environment""" + global grid, player, enemy + + mcrfpy.createScene("path_vision_demo") + + # Create larger grid for more dramatic movement + grid = mcrfpy.Grid(grid_x=40, grid_y=25) + grid.fill_color = mcrfpy.Color(20, 20, 30) + + # Map layout - interconnected rooms with corridors + map_layout = [ + "########################################", # 0 + "#......##########......################", # 1 + "#......##########......################", # 2 + "#......##########......################", # 3 + "#......#.........#.....################", # 4 + "#......#.........#.....################", # 5 + "####.###.........####.#################", # 6 + "####.....................##############", # 7 + "####.....................##############", # 8 + "####.###.........####.#################", # 9 + "#......#.........#.....################", # 10 + "#......#.........#.....################", # 11 + "#......#.........#.....################", # 12 + "#......###.....###.....################", # 13 + "#......###.....###.....################", # 14 + "#......###.....###.....#########......#", # 15 + "#......###.....###.....#########......#", # 16 + "#......###.....###.....#########......#", # 17 + "#####.############.#############......#", # 18 + "#####...........................#.....#", # 19 + "#####...........................#.....#", # 20 + "#####.############.#############......#", # 21 + "#......###########.##########.........#", # 22 + "#......###########.##########.........#", # 23 + "########################################", # 24 + ] + + # Build the map + for y, row in enumerate(map_layout): + for x, char in enumerate(row): + cell = grid.at(x, y) + if char == '#': + cell.walkable = False + cell.transparent = False + cell.color = WALL_COLOR + else: + cell.walkable = True + cell.transparent = True + cell.color = FLOOR_COLOR + + # Create player in top-left room + player = mcrfpy.Entity(3, 3, grid=grid) + player.sprite_index = 64 # @ + + # Create enemy in bottom-right area + enemy = mcrfpy.Entity(35, 20, grid=grid) + enemy.sprite_index = 69 # E + + # Initial visibility + player.update_visibility() + enemy.update_visibility() + + # Set initial perspective to player + grid.perspective = 0 + +def setup_paths(): + """Define the paths for entities""" + global player_path, enemy_path + + # Player path: Top-left room → corridor → middle room + player_waypoints = [ + (3, 3), # Start + (3, 8), # Move down + (7, 8), # Enter corridor + (16, 8), # Through corridor + (16, 12), # Enter middle room + (12, 12), # Move in room + (12, 16), # Move down + (16, 16), # Move right + (16, 19), # Exit room + (25, 19), # Move right + (30, 19), # Continue + (35, 19), # Near enemy start + ] + + # Enemy path: Bottom-right → around → approach player area + enemy_waypoints = [ + (35, 20), # Start + (30, 20), # Move left + (25, 20), # Continue + (20, 20), # Continue + (16, 20), # Corridor junction + (16, 16), # Move up (might see player) + (16, 12), # Continue up + (16, 8), # Top corridor + (10, 8), # Move left + (7, 8), # Continue + (3, 8), # Player's area + (3, 12), # Move down + ] + + # Calculate full paths using pathfinding + player_path = [] + for i in range(len(player_waypoints) - 1): + x1, y1 = player_waypoints[i] + x2, y2 = player_waypoints[i + 1] + + # Use grid's A* pathfinding + segment = grid.compute_astar_path(x1, y1, x2, y2) + if segment: + # Add segment (avoiding duplicates) + if not player_path or segment[0] != player_path[-1]: + player_path.extend(segment) + else: + player_path.extend(segment[1:]) + + enemy_path = [] + for i in range(len(enemy_waypoints) - 1): + x1, y1 = enemy_waypoints[i] + x2, y2 = enemy_waypoints[i + 1] + + segment = grid.compute_astar_path(x1, y1, x2, y2) + if segment: + if not enemy_path or segment[0] != enemy_path[-1]: + enemy_path.extend(segment) + else: + enemy_path.extend(segment[1:]) + + print(f"Player path: {len(player_path)} steps") + print(f"Enemy path: {len(enemy_path)} steps") + +def setup_ui(): + """Create UI elements""" + ui = mcrfpy.sceneUI("path_vision_demo") + ui.append(grid) + + # Position and size grid + grid.position = (50, 80) + grid.size = (700, 500) # Adjust based on zoom + + # Title + title = mcrfpy.Caption("Path & Vision Sizzle Reel", 300, 20) + title.fill_color = mcrfpy.Color(255, 255, 255) + ui.append(title) + + # Status + global status_text, perspective_text + status_text = mcrfpy.Caption("Starting demo...", 50, 50) + status_text.fill_color = mcrfpy.Color(200, 200, 200) + ui.append(status_text) + + perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50) + perspective_text.fill_color = mcrfpy.Color(100, 255, 100) + ui.append(perspective_text) + + # Controls + controls = mcrfpy.Caption("Space: Pause/Resume | R: Restart | Q: Quit", 250, 600) + controls.fill_color = mcrfpy.Color(150, 150, 150) + ui.append(controls) + +# Animation control +paused = False +move_timer = 0 +zoom_transition = False + +def move_entity_smooth(entity, target_x, target_y, duration=0.3): + """Smoothly animate entity to position""" + # Create position animation + anim_x = mcrfpy.Animation("x", float(target_x), duration, "easeInOut") + anim_y = mcrfpy.Animation("y", float(target_y), duration, "easeInOut") + + anim_x.start(entity) + anim_y.start(entity) + +def update_camera_smooth(center_x, center_y, duration=0.3): + """Smoothly move camera center""" + # Convert grid coords to pixel coords (assuming 16x16 tiles) + pixel_x = center_x * 16 + pixel_y = center_y * 16 + + anim = mcrfpy.Animation("center", (pixel_x, pixel_y), duration, "easeOut") + anim.start(grid) + +def start_perspective_transition(): + """Begin the dramatic perspective shift""" + global zoom_transition, sequence_step + zoom_transition = True + sequence_step = 100 # Special sequence number + + status_text.text = "Perspective shift: Zooming out..." + + # Zoom out with elastic easing + zoom_out = mcrfpy.Animation("zoom", 0.5, 2.0, "easeInExpo") + zoom_out.start(grid) + + # Schedule the perspective switch + mcrfpy.setTimer("switch_perspective", switch_perspective, 2100) + +def switch_perspective(dt): + """Switch perspective at the peak of zoom""" + global sequence_step + + # Switch to enemy perspective + grid.perspective = 1 + perspective_text.text = "Perspective: Enemy" + perspective_text.fill_color = mcrfpy.Color(255, 100, 100) + + status_text.text = "Perspective shift: Following enemy..." + + # Update camera to enemy position + update_camera_smooth(enemy.x, enemy.y, 0.1) + + # Zoom back in + zoom_in = mcrfpy.Animation("zoom", 1.2, 2.0, "easeOutExpo") + zoom_in.start(grid) + + # Resume sequence + mcrfpy.setTimer("resume_enemy", resume_enemy_sequence, 2100) + + # Cancel this timer + mcrfpy.delTimer("switch_perspective") + +def resume_enemy_sequence(dt): + """Resume following enemy after perspective shift""" + global sequence_step, zoom_transition + zoom_transition = False + sequence_step = 101 # Continue with enemy movement + mcrfpy.delTimer("resume_enemy") + +def sequence_tick(dt): + """Main sequence controller""" + global sequence_step, player_path_index, enemy_path_index, move_timer + + if paused or zoom_transition: + return + + move_timer += dt + if move_timer < 400: # Move every 400ms + return + move_timer = 0 + + if sequence_step < 50: + # Phase 1: Follow player movement + if player_path_index < len(player_path): + x, y = player_path[player_path_index] + move_entity_smooth(player, x, y) + player.update_visibility() + + # Camera follows player + if grid.perspective == 0: + update_camera_smooth(player.x, player.y) + + player_path_index += 1 + status_text.text = f"Player moving... Step {player_path_index}/{len(player_path)}" + + # Start enemy movement after player has moved a bit + if player_path_index == 10: + sequence_step = 1 # Enable enemy movement + else: + # Player reached destination, start perspective transition + start_perspective_transition() + + if sequence_step >= 1 and sequence_step < 50: + # Phase 2: Enemy movement (concurrent with player) + if enemy_path_index < len(enemy_path): + x, y = enemy_path[enemy_path_index] + move_entity_smooth(enemy, x, y) + enemy.update_visibility() + + # Check if enemy is visible to player + if grid.perspective == 0: + enemy_cell_idx = int(enemy.y) * grid.grid_x + int(enemy.x) + if enemy_cell_idx < len(player.gridstate) and player.gridstate[enemy_cell_idx].visible: + status_text.text = "Enemy spotted!" + + enemy_path_index += 1 + + elif sequence_step == 101: + # Phase 3: Continue following enemy after perspective shift + if enemy_path_index < len(enemy_path): + x, y = enemy_path[enemy_path_index] + move_entity_smooth(enemy, x, y) + enemy.update_visibility() + + # Camera follows enemy + update_camera_smooth(enemy.x, enemy.y) + + enemy_path_index += 1 + status_text.text = f"Following enemy... Step {enemy_path_index}/{len(enemy_path)}" + else: + status_text.text = "Demo complete! Press R to restart" + sequence_step = 200 # Done + +def handle_keys(key, state): + """Handle keyboard input""" + global paused, sequence_step, player_path_index, enemy_path_index, move_timer + key = key.lower() + if state != "start": + return + + if key == "q": + print("Exiting sizzle reel...") + sys.exit(0) + elif key == "space": + paused = not paused + status_text.text = "PAUSED" if paused else "Running..." + elif key == "r": + # Reset everything + player.x, player.y = 3, 3 + enemy.x, enemy.y = 35, 20 + player.update_visibility() + enemy.update_visibility() + grid.perspective = 0 + perspective_text.text = "Perspective: Player" + perspective_text.fill_color = mcrfpy.Color(100, 255, 100) + sequence_step = 0 + player_path_index = 0 + enemy_path_index = 0 + move_timer = 0 + update_camera_smooth(player.x, player.y, 0.5) + + # Reset zoom + zoom_reset = mcrfpy.Animation("zoom", 1.2, 0.5, "easeOut") + zoom_reset.start(grid) + + status_text.text = "Demo restarted!" + +# Initialize everything +print("Path & Vision Sizzle Reel") +print("=========================") +print("Demonstrating:") +print("- Smooth entity movement along calculated paths") +print("- Camera following with animated grid centering") +print("- Field of view updates as entities move") +print("- Dramatic perspective transitions with zoom effects") +print() + +create_scene() +setup_paths() +setup_ui() + +# Set scene and input +mcrfpy.setScene("path_vision_demo") +mcrfpy.keypressScene(handle_keys) + +# Initial camera setup +grid.zoom = 1.2 +update_camera_smooth(player.x, player.y, 0.1) + +# Start the sequence +mcrfpy.setTimer("sequence", sequence_tick, 50) # Tick every 50ms + +print("Demo started!") +print("- Player (@) will navigate through rooms") +print("- Enemy (E) will move on a different path") +print("- Watch for the dramatic perspective shift!") +print() +print("Controls: Space=Pause, R=Restart, Q=Quit") diff --git a/tests/simple_interactive_visibility.py b/tests/simple_interactive_visibility.py new file mode 100644 index 0000000..fd95d5a --- /dev/null +++ b/tests/simple_interactive_visibility.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Simple interactive visibility test""" + +import mcrfpy +import sys + +# Create scene and grid +print("Creating scene...") +mcrfpy.createScene("vis_test") + +print("Creating grid...") +grid = mcrfpy.Grid(grid_x=10, grid_y=10) + +# Initialize grid +print("Initializing grid...") +for y in range(10): + for x in range(10): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + cell.color = mcrfpy.Color(100, 100, 120) + +# Create entity +print("Creating entity...") +entity = mcrfpy.Entity(5, 5, grid=grid) +entity.sprite_index = 64 + +print("Updating visibility...") +entity.update_visibility() + +# Set up UI +print("Setting up UI...") +ui = mcrfpy.sceneUI("vis_test") +ui.append(grid) +grid.position = (50, 50) +grid.size = (300, 300) + +# Test perspective +print("Testing perspective...") +grid.perspective = -1 # Omniscient +print(f"Perspective set to: {grid.perspective}") + +print("Setting scene...") +mcrfpy.setScene("vis_test") + +print("Ready!") \ No newline at end of file diff --git a/tests/simple_visibility_test.py b/tests/simple_visibility_test.py new file mode 100644 index 0000000..5c20758 --- /dev/null +++ b/tests/simple_visibility_test.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Simple visibility test without entity append""" + +import mcrfpy +import sys + +print("Simple visibility test...") + +# Create scene and grid +mcrfpy.createScene("simple") +print("Scene created") + +grid = mcrfpy.Grid(grid_x=5, grid_y=5) +print("Grid created") + +# Create entity without appending +entity = mcrfpy.Entity(2, 2, grid=grid) +print(f"Entity created at ({entity.x}, {entity.y})") + +# Check if gridstate is initialized +print(f"Gridstate length: {len(entity.gridstate)}") + +# Try to access at method +try: + state = entity.at(0, 0) + print(f"at(0,0) returned: {state}") + print(f"visible: {state.visible}, discovered: {state.discovered}") +except Exception as e: + print(f"Error in at(): {e}") + +# Try update_visibility +try: + entity.update_visibility() + print("update_visibility() succeeded") +except Exception as e: + print(f"Error in update_visibility(): {e}") + +print("Test complete") +sys.exit(0) \ No newline at end of file