diff --git a/.gitignore b/.gitignore index a00ca39..174f159 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ obj build lib obj +__pycache__ .cache/ 7DRL2025 Release/ @@ -27,3 +28,5 @@ forest_fire_CA.py mcrogueface.github.io scripts/ test_* + +tcod_reference diff --git a/ROADMAP.md b/ROADMAP.md index d5040b6..4d00996 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -22,16 +22,19 @@ - Mass entity choreography (100+ entities) - Performance stress test with 1000+ entities -#### 2. TCOD Integration Sprint -- [ ] **UIGrid TCOD Integration** (8 hours) - - Add TCODMap* to UIGrid constructor - - Implement mcrfpy.libtcod.compute_fov() - - Add batch operations for NumPy-style access - - Create CellView for ergonomic .at((x,y)) access -- [ ] **UIEntity Pathfinding** (4 hours) - - Add path_to(target) method using A* - - Implement Dijkstra maps for multiple targets - - Cache paths in UIEntity for performance +#### 2. TCOD Integration Sprint ✅ COMPLETE! +- [x] **UIGrid TCOD Integration** (8 hours) ✅ COMPLETED! + - ✅ Add TCODMap* to UIGrid constructor with proper lifecycle + - ✅ Implement complete Dijkstra pathfinding system + - ✅ Create mcrfpy.libtcod submodule with Python bindings + - ✅ Fix critical PyArg bug preventing Color object assignments + - ✅ Implement FOV with perspective rendering + - [ ] Add batch operations for NumPy-style access (deferred) + - [ ] Create CellView for ergonomic .at((x,y)) access (deferred) +- [x] **UIEntity Pathfinding** (4 hours) ✅ COMPLETED! + - ✅ Implement Dijkstra maps for multiple targets in UIGrid + - ✅ Add path_to(target) method using A* to UIEntity + - ✅ Cache paths in UIEntity for performance #### 3. Performance Critical Path - [ ] **Implement SpatialHash** for 10,000+ entities (2 hours) @@ -121,6 +124,44 @@ entity.can_see(other_entity) # FOV check ## Recent Achievements +### 2025-07-10: Complete FOV, A* Pathfinding & GUI Text Widgets! 👁️🗺️⌨️ +**Engine Feature Sprint - Major Capabilities Added** +- ✅ Complete FOV (Field of View) system with perspective rendering + - UIGrid.perspective property controls which entity's view to render + - Three-layer overlay system: unexplored (black), explored (dark), visible (normal) + - Per-entity visibility state tracking with UIGridPointState + - Perfect knowledge updates - only explored areas persist +- ✅ A* Pathfinding implementation + - Entity.path_to(x, y) method for direct pathfinding + - UIGrid compute_astar() and get_astar_path() methods + - Path caching in entities for performance + - Complete test suite comparing A* vs Dijkstra performance +- ✅ GUI Text Input Widget System + - Full-featured TextInputWidget class with cursor, selection, scrolling + - Improved widget with proper text rendering and multi-line support + - Example showcase demonstrating multiple input fields + - Foundation for in-game consoles, chat systems, and text entry +- ✅ Sizzle Reel Demos + - path_vision_sizzle_reel.py combines pathfinding with FOV + - Interactive visibility demos showing real-time FOV updates + - Performance demonstrations with multiple entities + +### 2025-07-09: Dijkstra Pathfinding & Critical Bug Fix! 🗺️ +**TCOD Integration Sprint - Major Progress** +- ✅ Complete Dijkstra pathfinding implementation in UIGrid + - compute_dijkstra(), get_dijkstra_distance(), get_dijkstra_path() methods + - Full TCODMap and TCODDijkstra integration with proper memory management + - Comprehensive test suite with both headless and interactive demos +- ✅ **CRITICAL FIX**: PyArg bug in UIGridPoint color setter + - Now supports both mcrfpy.Color objects and (r,g,b,a) tuples + - Eliminated mysterious "SystemError: new style getargs format" crashes + - Proper error handling and exception propagation +- ✅ mcrfpy.libtcod submodule with Python bindings + - dijkstra_compute(), dijkstra_get_distance(), dijkstra_get_path() + - line() function for corridor generation + - Foundation ready for FOV implementation +- ✅ Test consolidation: 6 broken demos → 2 clean, working versions + ### 2025-07-08: PyArgHelpers Infrastructure Complete! 🔧 **Standardized Python API Argument Parsing** - Unified position handling: (x, y) tuples or separate x, y args diff --git a/dijkstra_working.png b/dijkstra_working.png new file mode 100644 index 0000000..d33326e Binary files /dev/null and b/dijkstra_working.png differ diff --git a/docs/visibility_tracking_example.cpp b/docs/visibility_tracking_example.cpp new file mode 100644 index 0000000..aefe50b --- /dev/null +++ b/docs/visibility_tracking_example.cpp @@ -0,0 +1,342 @@ +/** + * Example implementation demonstrating the proposed visibility tracking system + * This shows how UIGridPoint, UIGridPointState, and libtcod maps work together + */ + +#include +#include +#include +#include + +// Forward declarations +class UIGrid; +class UIEntity; +class TCODMap; + +/** + * UIGridPoint - The "ground truth" of a grid cell + * This represents the actual state of the world + */ +class UIGridPoint { +public: + // Core properties + bool walkable = true; // Can entities move through this cell? + bool transparent = true; // Does this cell block line of sight? + int tilesprite = 0; // What tile to render + + // Visual properties + sf::Color color; + sf::Color color_overlay; + + // Grid position + int grid_x, grid_y; + UIGrid* parent_grid; + + // When these change, sync with TCOD map + void setWalkable(bool value) { + walkable = value; + if (parent_grid) syncTCODMapCell(); + } + + void setTransparent(bool value) { + transparent = value; + if (parent_grid) syncTCODMapCell(); + } + +private: + void syncTCODMapCell(); // Update TCOD map when properties change +}; + +/** + * UIGridPointState - What an entity knows about a grid cell + * Each entity maintains one of these for each cell it has encountered + */ +class UIGridPointState { +public: + // Visibility state + bool visible = false; // Currently in entity's FOV? + bool discovered = false; // Has entity ever seen this cell? + + // When the entity last saw this cell (for fog of war effects) + int last_seen_turn = -1; + + // What the entity remembers about this cell + // (may be outdated if cell changed after entity saw it) + bool remembered_walkable = true; + bool remembered_transparent = true; + int remembered_tilesprite = 0; + + // Update remembered state from actual grid point + void updateFromTruth(const UIGridPoint& truth, int current_turn) { + if (visible) { + discovered = true; + last_seen_turn = current_turn; + remembered_walkable = truth.walkable; + remembered_transparent = truth.transparent; + remembered_tilesprite = truth.tilesprite; + } + } +}; + +/** + * EntityGridKnowledge - Manages an entity's knowledge across multiple grids + * This allows entities to remember explored areas even when changing levels + */ +class EntityGridKnowledge { +private: + // Map from grid ID to the entity's knowledge of that grid + std::unordered_map> grid_knowledge; + +public: + // Get or create knowledge vector for a specific grid + std::vector& getGridKnowledge(const std::string& grid_id, int grid_size) { + auto& knowledge = grid_knowledge[grid_id]; + if (knowledge.empty()) { + knowledge.resize(grid_size); + } + return knowledge; + } + + // Check if entity has visited this grid before + bool hasGridKnowledge(const std::string& grid_id) const { + return grid_knowledge.find(grid_id) != grid_knowledge.end(); + } + + // Clear knowledge of a specific grid (e.g., for memory-wiping effects) + void forgetGrid(const std::string& grid_id) { + grid_knowledge.erase(grid_id); + } + + // Get total number of grids this entity knows about + size_t getKnownGridCount() const { + return grid_knowledge.size(); + } +}; + +/** + * Enhanced UIEntity with visibility tracking + */ +class UIEntity { +private: + // Entity properties + float x, y; // Position + UIGrid* current_grid; // Current grid entity is on + EntityGridKnowledge knowledge; // Multi-grid knowledge storage + int sight_radius = 10; // How far entity can see + bool omniscient = false; // Does entity know everything? + +public: + // Update entity's FOV and visibility knowledge + void updateFOV(int radius = -1) { + if (!current_grid) return; + if (radius < 0) radius = sight_radius; + + // Get entity's knowledge of current grid + auto& grid_knowledge = knowledge.getGridKnowledge( + current_grid->getGridId(), + current_grid->getGridSize() + ); + + // Reset visibility for all cells + for (auto& cell_knowledge : grid_knowledge) { + cell_knowledge.visible = false; + } + + if (omniscient) { + // Omniscient entities see everything + for (int i = 0; i < grid_knowledge.size(); i++) { + grid_knowledge[i].visible = true; + grid_knowledge[i].discovered = true; + grid_knowledge[i].updateFromTruth( + current_grid->getPointAt(i), + current_grid->getCurrentTurn() + ); + } + } else { + // Normal FOV calculation using TCOD + current_grid->computeFOVForEntity(this, (int)x, (int)y, radius); + + // Update visibility states based on TCOD FOV results + for (int gy = 0; gy < current_grid->getHeight(); gy++) { + for (int gx = 0; gx < current_grid->getWidth(); gx++) { + int idx = gy * current_grid->getWidth() + gx; + + if (current_grid->isCellInFOV(gx, gy)) { + grid_knowledge[idx].visible = true; + grid_knowledge[idx].updateFromTruth( + current_grid->getPointAt(idx), + current_grid->getCurrentTurn() + ); + } + } + } + } + } + + // Check if entity can see a specific position + bool canSeePosition(int gx, int gy) const { + if (!current_grid) return false; + + auto& grid_knowledge = const_cast(knowledge).getGridKnowledge( + current_grid->getGridId(), + current_grid->getGridSize() + ); + + int idx = gy * current_grid->getWidth() + gx; + return idx >= 0 && idx < grid_knowledge.size() && grid_knowledge[idx].visible; + } + + // Check if entity has ever discovered a position + bool hasDiscoveredPosition(int gx, int gy) const { + if (!current_grid) return false; + + auto& grid_knowledge = const_cast(knowledge).getGridKnowledge( + current_grid->getGridId(), + current_grid->getGridSize() + ); + + int idx = gy * current_grid->getWidth() + gx; + return idx >= 0 && idx < grid_knowledge.size() && grid_knowledge[idx].discovered; + } + + // Find path using only discovered/remembered terrain + std::vector> findKnownPath(int dest_x, int dest_y) { + if (!current_grid) return {}; + + // Create a TCOD map based on entity's knowledge + auto knowledge_map = current_grid->createKnowledgeMapForEntity(this); + + // Use A* on the knowledge map + auto path = knowledge_map->computePath((int)x, (int)y, dest_x, dest_y); + + delete knowledge_map; + return path; + } + + // Move to a new grid, preserving knowledge of the old one + void moveToGrid(UIGrid* new_grid) { + if (current_grid) { + // Knowledge is automatically preserved in the knowledge map + current_grid->removeEntity(this); + } + + current_grid = new_grid; + if (new_grid) { + new_grid->addEntity(this); + // If we've been here before, we still remember it + updateFOV(); + } + } +}; + +/** + * Example use cases + */ + +// Use Case 1: Player exploring a dungeon +void playerExploration() { + auto player = std::make_shared(); + auto dungeon_level1 = std::make_shared("dungeon_level_1", 50, 50); + + // Player starts with no knowledge + player->moveToGrid(dungeon_level1.get()); + player->updateFOV(10); // Can see 10 tiles in each direction + + // Only render what player can see + dungeon_level1->renderWithEntityPerspective(player.get()); + + // Player tries to path to unexplored area + auto path = player->findKnownPath(45, 45); + if (path.empty()) { + // "You haven't explored that area yet!" + } +} + +// Use Case 2: Entity with perfect knowledge +void omniscientEntity() { + auto guardian = std::make_shared(); + guardian->setOmniscient(true); // Knows everything about any grid it enters + + auto temple = std::make_shared("temple", 30, 30); + guardian->moveToGrid(temple.get()); + + // Guardian immediately knows entire layout + auto path = guardian->findKnownPath(29, 29); // Can path anywhere +} + +// Use Case 3: Entity returning to previously explored area +void returningToArea() { + auto scout = std::make_shared(); + auto forest = std::make_shared("forest", 40, 40); + auto cave = std::make_shared("cave", 20, 20); + + // Scout explores forest + scout->moveToGrid(forest.get()); + scout->updateFOV(15); + // ... scout moves around, discovering ~50% of forest ... + + // Scout enters cave + scout->moveToGrid(cave.get()); + scout->updateFOV(8); // Darker in cave, reduced vision + + // Later, scout returns to forest + scout->moveToGrid(forest.get()); + // Scout still remembers the areas previously explored! + // Can immediately path through known areas + auto path = scout->findKnownPath(10, 10); // Works if area was explored before +} + +// Use Case 4: Fog of war - remembered vs current state +void fogOfWar() { + auto player = std::make_shared(); + auto dungeon = std::make_shared("dungeon", 50, 50); + + player->moveToGrid(dungeon.get()); + player->setPosition(25, 25); + player->updateFOV(10); + + // Player sees a door at (30, 25) - it's open + auto& door_point = dungeon->at(30, 25); + door_point.walkable = true; + door_point.transparent = true; + + // Player moves away + player->setPosition(10, 10); + player->updateFOV(10); + + // While player is gone, door closes + door_point.walkable = false; + door_point.transparent = false; + + // Player's memory still thinks door is open + auto& player_knowledge = player->getKnowledgeAt(30, 25); + // player_knowledge.remembered_walkable is still true! + + // Player tries to path through the door based on memory + auto path = player->findKnownPath(35, 25); + // Path planning succeeds based on remembered state + + // But when player gets close enough to see it again... + player->setPosition(25, 25); + player->updateFOV(10); + // Knowledge updates - door is actually closed! +} + +/** + * Proper use of each component: + * + * UIGridPoint: + * - Stores the actual, current state of the world + * - Used by the game logic to determine what really happens + * - Syncs with TCOD map for consistent pathfinding/FOV + * + * UIGridPointState: + * - Stores what an entity believes/remembers about a cell + * - May be outdated if world changed since last seen + * - Used for rendering fog of war and entity decision-making + * + * TCOD Map: + * - Provides efficient FOV and pathfinding algorithms + * - Can be created from either ground truth or entity knowledge + * - Multiple maps can exist (one for truth, one per entity for knowledge-based pathfinding) + */ \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/game.py new file mode 100644 index 0000000..00c9de2 --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/game.py @@ -0,0 +1,33 @@ +import mcrfpy + +# Create a new scene called "hello" +mcrfpy.createScene("hello") + +# Switch to our new scene +mcrfpy.setScene("hello") + +# Get the UI container for our scene +ui = mcrfpy.sceneUI("hello") + +# Create a text caption +caption = mcrfpy.Caption("Hello Roguelike!", 400, 300) +caption.font_size = 32 +caption.fill_color = mcrfpy.Color(255, 255, 255) # White text + +# Add the caption to our scene +ui.append(caption) + +# Create a smaller instruction caption +instruction = mcrfpy.Caption("Press ESC to exit", 400, 350) +instruction.font_size = 16 +instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray +ui.append(instruction) + +# Set up a simple key handler +def handle_keys(key, state): + if state == "start" and key == "Escape": + mcrfpy.setScene(None) # This exits the game + +mcrfpy.keypressScene(handle_keys) + +print("Hello Roguelike is running!") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/setup_test.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/setup_test.py new file mode 100644 index 0000000..0b39a49 --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/setup_test.py @@ -0,0 +1,55 @@ +import mcrfpy + +# Create our test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") +ui = mcrfpy.sceneUI("test") + +# Create a background frame +background = mcrfpy.Frame(0, 0, 1024, 768) +background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray +ui.append(background) + +# Title text +title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100) +title.font_size = 36 +title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow +ui.append(title) + +# Status text that will update +status_text = mcrfpy.Caption("Press any key to test input...", 512, 300) +status_text.font_size = 20 +status_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(status_text) + +# Instructions +instructions = [ + "Arrow Keys: Test movement input", + "Space: Test action input", + "Mouse Click: Test mouse input", + "ESC: Exit" +] + +y_offset = 400 +for instruction in instructions: + inst_caption = mcrfpy.Caption(instruction, 512, y_offset) + inst_caption.font_size = 16 + inst_caption.fill_color = mcrfpy.Color(150, 150, 150) + ui.append(inst_caption) + y_offset += 30 + +# Input handler +def handle_input(key, state): + if state != "start": + return + + if key == "Escape": + mcrfpy.setScene(None) + else: + status_text.text = f"You pressed: {key}" + status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green + +# Set up input handling +mcrfpy.keypressScene(handle_input) + +print("Setup test is running! Try pressing different keys.") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_1/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_1/code/game.py new file mode 100644 index 0000000..2f0c157 --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_1/code/game.py @@ -0,0 +1,162 @@ +import mcrfpy + +# Window configuration +mcrfpy.createScene("game") +mcrfpy.setScene("game") + +window = mcrfpy.Window.get() +window.title = "McRogueFace Roguelike - Part 1" + +# Get the UI container for our scene +ui = mcrfpy.sceneUI("game") + +# Create a dark background +background = mcrfpy.Frame(0, 0, 1024, 768) +background.fill_color = mcrfpy.Color(0, 0, 0) +ui.append(background) + +# Load the ASCII tileset +tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + +# Create the game grid +GRID_WIDTH = 50 +GRID_HEIGHT = 30 + +grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset) +grid.position = (100, 100) +grid.size = (800, 480) +ui.append(grid) + +def create_room(): + """Create a room with walls around the edges""" + # Fill everything with floor tiles first + for y in range(GRID_HEIGHT): + for x in range(GRID_WIDTH): + cell = grid.at(x, y) + cell.walkable = True + cell.transparent = True + cell.sprite_index = 46 # '.' character + cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor + + # Create walls around the edges + for x in range(GRID_WIDTH): + # Top wall + cell = grid.at(x, 0) + cell.walkable = False + cell.transparent = False + cell.sprite_index = 35 # '#' character + cell.color = mcrfpy.Color(100, 100, 100) # Gray walls + + # Bottom wall + cell = grid.at(x, GRID_HEIGHT - 1) + cell.walkable = False + cell.transparent = False + cell.sprite_index = 35 # '#' character + cell.color = mcrfpy.Color(100, 100, 100) + + for y in range(GRID_HEIGHT): + # Left wall + cell = grid.at(0, y) + cell.walkable = False + cell.transparent = False + cell.sprite_index = 35 # '#' character + cell.color = mcrfpy.Color(100, 100, 100) + + # Right wall + cell = grid.at(GRID_WIDTH - 1, y) + cell.walkable = False + cell.transparent = False + cell.sprite_index = 35 # '#' character + cell.color = mcrfpy.Color(100, 100, 100) + +# Create the room +create_room() + +# Create the player entity +player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid) +player.sprite_index = 64 # '@' character +player.color = mcrfpy.Color(255, 255, 255) # White + +def move_player(dx, dy): + """Move the player if the destination is walkable""" + # Calculate new position + new_x = player.x + dx + new_y = player.y + dy + + # Check bounds + if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT: + return + + # Check if the destination is walkable + destination = grid.at(new_x, new_y) + if destination.walkable: + # Move the player + player.x = new_x + player.y = new_y + +def handle_input(key, state): + """Handle keyboard input for player movement""" + # Only process key presses, not releases + if state != "start": + return + + # Movement deltas + dx, dy = 0, 0 + + # Arrow keys + if key == "Up": + dy = -1 + elif key == "Down": + dy = 1 + elif key == "Left": + dx = -1 + elif key == "Right": + dx = 1 + + # Numpad movement (for true roguelike feel!) + elif key == "Num7": # Northwest + dx, dy = -1, -1 + elif key == "Num8": # North + dy = -1 + elif key == "Num9": # Northeast + dx, dy = 1, -1 + elif key == "Num4": # West + dx = -1 + elif key == "Num6": # East + dx = 1 + elif key == "Num1": # Southwest + dx, dy = -1, 1 + elif key == "Num2": # South + dy = 1 + elif key == "Num3": # Southeast + dx, dy = 1, 1 + + # Escape to quit + elif key == "Escape": + mcrfpy.setScene(None) + return + + # If there's movement, try to move the player + if dx != 0 or dy != 0: + move_player(dx, dy) + +# Register the input handler +mcrfpy.keypressScene(handle_input) + +# Add UI elements +title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30) +title.font_size = 24 +title.fill_color = mcrfpy.Color(255, 255, 100) +ui.append(title) + +instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60) +instructions.font_size = 16 +instructions.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(instructions) + +status = mcrfpy.Caption("@ You", 100, 600) +status.font_size = 18 +status.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(status) + +print("Part 1: The @ symbol moves!") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_2/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_2/code/game.py new file mode 100644 index 0000000..38eef78 --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_2/code/game.py @@ -0,0 +1,217 @@ +import mcrfpy + +class GameObject: + """Base class for all game objects (player, monsters, items)""" + + def __init__(self, x, y, sprite_index, color, name, blocks=False): + self.x = x + self.y = y + self.sprite_index = sprite_index + self.color = color + self.name = name + self.blocks = blocks + self._entity = None + self.grid = None + + def attach_to_grid(self, grid): + """Attach this game object to a McRogueFace grid""" + self.grid = grid + self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid) + self._entity.sprite_index = self.sprite_index + self._entity.color = mcrfpy.Color(*self.color) + + def move(self, dx, dy): + """Move by the given amount if possible""" + if not self.grid: + return + + new_x = self.x + dx + new_y = self.y + dy + + self.x = new_x + self.y = new_y + + if self._entity: + self._entity.x = new_x + self._entity.y = new_y + +class GameMap: + """Manages the game world""" + + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = None + self.entities = [] + + def create_grid(self, tileset): + """Create the McRogueFace grid""" + self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset) + self.grid.position = (100, 100) + self.grid.size = (800, 480) + self.fill_with_walls() + return self.grid + + def fill_with_walls(self): + """Fill the entire map with wall tiles""" + for y in range(self.height): + for x in range(self.width): + self.set_tile(x, y, walkable=False, transparent=False, + sprite_index=35, color=(100, 100, 100)) + + def set_tile(self, x, y, walkable, transparent, sprite_index, color): + """Set properties for a specific tile""" + if 0 <= x < self.width and 0 <= y < self.height: + cell = self.grid.at(x, y) + cell.walkable = walkable + cell.transparent = transparent + cell.sprite_index = sprite_index + cell.color = mcrfpy.Color(*color) + + def create_room(self, x1, y1, x2, y2): + """Carve out a room in the map""" + x1, x2 = min(x1, x2), max(x1, x2) + y1, y2 = min(y1, y2), max(y1, y2) + + for y in range(y1, y2 + 1): + for x in range(x1, x2 + 1): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, color=(50, 50, 50)) + + def create_tunnel_h(self, x1, x2, y): + """Create a horizontal tunnel""" + for x in range(min(x1, x2), max(x1, x2) + 1): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, color=(50, 50, 50)) + + def create_tunnel_v(self, y1, y2, x): + """Create a vertical tunnel""" + for y in range(min(y1, y2), max(y1, y2) + 1): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, color=(50, 50, 50)) + + def is_blocked(self, x, y): + """Check if a tile blocks movement""" + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return True + + if not self.grid.at(x, y).walkable: + return True + + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return True + + return False + + def add_entity(self, entity): + """Add a GameObject to the map""" + self.entities.append(entity) + entity.attach_to_grid(self.grid) + + def get_blocking_entity_at(self, x, y): + """Return any blocking entity at the given position""" + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return entity + return None + +class Engine: + """Main game engine that manages game state""" + + def __init__(self): + self.game_map = None + self.player = None + self.entities = [] + + mcrfpy.createScene("game") + mcrfpy.setScene("game") + + window = mcrfpy.Window.get() + window.title = "McRogueFace Roguelike - Part 2" + + self.ui = mcrfpy.sceneUI("game") + + background = mcrfpy.Frame(0, 0, 1024, 768) + background.fill_color = mcrfpy.Color(0, 0, 0) + self.ui.append(background) + + self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + + self.setup_game() + self.setup_input() + self.setup_ui() + + def setup_game(self): + """Initialize the game world""" + self.game_map = GameMap(50, 30) + grid = self.game_map.create_grid(self.tileset) + self.ui.append(grid) + + self.game_map.create_room(10, 10, 20, 20) + self.game_map.create_room(30, 15, 40, 25) + self.game_map.create_room(15, 22, 25, 28) + + self.game_map.create_tunnel_h(20, 30, 15) + self.game_map.create_tunnel_v(20, 22, 20) + + self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True) + self.game_map.add_entity(self.player) + + npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True) + self.game_map.add_entity(npc) + self.entities.append(npc) + + potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False) + self.game_map.add_entity(potion) + self.entities.append(potion) + + def handle_movement(self, dx, dy): + """Handle player movement""" + new_x = self.player.x + dx + new_y = self.player.y + dy + + if not self.game_map.is_blocked(new_x, new_y): + self.player.move(dx, dy) + else: + target = self.game_map.get_blocking_entity_at(new_x, new_y) + if target: + print(f"You bump into the {target.name}!") + + def setup_input(self): + """Setup keyboard input handling""" + def handle_keys(key, state): + if state != "start": + return + + movement = { + "Up": (0, -1), "Down": (0, 1), + "Left": (-1, 0), "Right": (1, 0), + "Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1), + "Num4": (-1, 0), "Num6": (1, 0), + "Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1), + } + + if key in movement: + dx, dy = movement[key] + self.handle_movement(dx, dy) + elif key == "Escape": + mcrfpy.setScene(None) + + mcrfpy.keypressScene(handle_keys) + + def setup_ui(self): + """Setup UI elements""" + title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30) + title.font_size = 24 + title.fill_color = mcrfpy.Color(255, 255, 100) + self.ui.append(title) + + instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60) + instructions.font_size = 16 + instructions.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(instructions) + +# Create and run the game +engine = Engine() +print("Part 2: Entities and Maps!") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_3/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_3/code/game.py new file mode 100644 index 0000000..1256ef9 --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_3/code/game.py @@ -0,0 +1,312 @@ +import mcrfpy +import random + +class GameObject: + """Base class for all game objects""" + def __init__(self, x, y, sprite_index, color, name, blocks=False): + self.x = x + self.y = y + self.sprite_index = sprite_index + self.color = color + self.name = name + self.blocks = blocks + self._entity = None + self.grid = None + + def attach_to_grid(self, grid): + """Attach this game object to a McRogueFace grid""" + self.grid = grid + self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid) + self._entity.sprite_index = self.sprite_index + self._entity.color = mcrfpy.Color(*self.color) + + def move(self, dx, dy): + """Move by the given amount""" + if not self.grid: + return + self.x += dx + self.y += dy + if self._entity: + self._entity.x = self.x + self._entity.y = self.y + +class RectangularRoom: + """A rectangular room with its position and size""" + + def __init__(self, x, y, width, height): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self): + """Return the center coordinates of the room""" + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self): + """Return the inner area of the room""" + return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1 + + def intersects(self, other): + """Return True if this room overlaps with another""" + return ( + self.x1 <= other.x2 + and self.x2 >= other.x1 + and self.y1 <= other.y2 + and self.y2 >= other.y1 + ) + +def tunnel_between(start, end): + """Return an L-shaped tunnel between two points""" + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + corner_x = x2 + corner_y = y1 + else: + corner_x = x1 + corner_y = y2 + + # Generate the coordinates + for x in range(min(x1, corner_x), max(x1, corner_x) + 1): + yield x, y1 + for y in range(min(y1, corner_y), max(y1, corner_y) + 1): + yield corner_x, y + for x in range(min(corner_x, x2), max(corner_x, x2) + 1): + yield x, corner_y + for y in range(min(corner_y, y2), max(corner_y, y2) + 1): + yield x2, y + +class GameMap: + """Manages the game world""" + + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = None + self.entities = [] + self.rooms = [] + + def create_grid(self, tileset): + """Create the McRogueFace grid""" + self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset) + self.grid.position = (100, 100) + self.grid.size = (800, 480) + return self.grid + + def fill_with_walls(self): + """Fill the entire map with wall tiles""" + for y in range(self.height): + for x in range(self.width): + self.set_tile(x, y, walkable=False, transparent=False, + sprite_index=35, color=(100, 100, 100)) + + def set_tile(self, x, y, walkable, transparent, sprite_index, color): + """Set properties for a specific tile""" + if 0 <= x < self.width and 0 <= y < self.height: + cell = self.grid.at(x, y) + cell.walkable = walkable + cell.transparent = transparent + cell.sprite_index = sprite_index + cell.color = mcrfpy.Color(*color) + + def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player): + """Generate a new dungeon map""" + self.fill_with_walls() + + for r in range(max_rooms): + room_width = random.randint(room_min_size, room_max_size) + room_height = random.randint(room_min_size, room_max_size) + + x = random.randint(0, self.width - room_width - 1) + y = random.randint(0, self.height - room_height - 1) + + new_room = RectangularRoom(x, y, room_width, room_height) + + if any(new_room.intersects(other_room) for other_room in self.rooms): + continue + + self.carve_room(new_room) + + if len(self.rooms) == 0: + player.x, player.y = new_room.center + if player._entity: + player._entity.x, player._entity.y = new_room.center + else: + self.carve_tunnel(self.rooms[-1].center, new_room.center) + + self.rooms.append(new_room) + + def carve_room(self, room): + """Carve out a room""" + inner_x1, inner_y1, inner_x2, inner_y2 = room.inner + + for y in range(inner_y1, inner_y2): + for x in range(inner_x1, inner_x2): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, color=(50, 50, 50)) + + def carve_tunnel(self, start, end): + """Carve a tunnel between two points""" + for x, y in tunnel_between(start, end): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, color=(30, 30, 40)) + + def is_blocked(self, x, y): + """Check if a tile blocks movement""" + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return True + if not self.grid.at(x, y).walkable: + return True + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return True + return False + + def add_entity(self, entity): + """Add a GameObject to the map""" + self.entities.append(entity) + entity.attach_to_grid(self.grid) + +class Engine: + """Main game engine""" + + def __init__(self): + self.game_map = None + self.player = None + self.entities = [] + + mcrfpy.createScene("game") + mcrfpy.setScene("game") + + window = mcrfpy.Window.get() + window.title = "McRogueFace Roguelike - Part 3" + + self.ui = mcrfpy.sceneUI("game") + + background = mcrfpy.Frame(0, 0, 1024, 768) + background.fill_color = mcrfpy.Color(0, 0, 0) + self.ui.append(background) + + self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + + self.setup_game() + self.setup_input() + self.setup_ui() + + def setup_game(self): + """Initialize the game world""" + self.game_map = GameMap(80, 45) + grid = self.game_map.create_grid(self.tileset) + self.ui.append(grid) + + # Create player (before dungeon generation) + self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True) + + # Generate the dungeon + self.game_map.generate_dungeon( + max_rooms=30, + room_min_size=6, + room_max_size=10, + player=self.player + ) + + # Add player to map + self.game_map.add_entity(self.player) + + # Add some monsters in random rooms + for i in range(5): + if i < len(self.game_map.rooms) - 1: # Don't spawn in first room + room = self.game_map.rooms[i + 1] + x, y = room.center + + # Create an orc + orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True) + self.game_map.add_entity(orc) + self.entities.append(orc) + + def handle_movement(self, dx, dy): + """Handle player movement""" + new_x = self.player.x + dx + new_y = self.player.y + dy + + if not self.game_map.is_blocked(new_x, new_y): + self.player.move(dx, dy) + + def setup_input(self): + """Setup keyboard input handling""" + def handle_keys(key, state): + if state != "start": + return + + movement = { + "Up": (0, -1), "Down": (0, 1), + "Left": (-1, 0), "Right": (1, 0), + "Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1), + "Num4": (-1, 0), "Num6": (1, 0), + "Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1), + } + + if key in movement: + dx, dy = movement[key] + self.handle_movement(dx, dy) + elif key == "Escape": + mcrfpy.setScene(None) + elif key == "Space": + # Regenerate the dungeon + self.regenerate_dungeon() + + mcrfpy.keypressScene(handle_keys) + + def regenerate_dungeon(self): + """Generate a new dungeon""" + # Clear existing entities + self.game_map.entities.clear() + self.game_map.rooms.clear() + self.entities.clear() + + # Clear the entity list in the grid + if self.game_map.grid: + self.game_map.grid.entities.clear() + + # Regenerate + self.game_map.generate_dungeon( + max_rooms=30, + room_min_size=6, + room_max_size=10, + player=self.player + ) + + # Re-add player + self.game_map.add_entity(self.player) + + # Add new monsters + for i in range(5): + if i < len(self.game_map.rooms) - 1: + room = self.game_map.rooms[i + 1] + x, y = room.center + orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True) + self.game_map.add_entity(orc) + self.entities.append(orc) + + def setup_ui(self): + """Setup UI elements""" + title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30) + title.font_size = 24 + title.fill_color = mcrfpy.Color(255, 255, 100) + self.ui.append(title) + + instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60) + instructions.font_size = 16 + instructions.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(instructions) + +# Create and run the game +engine = Engine() +print("Part 3: Procedural Dungeon Generation!") +print("Press SPACE to generate a new dungeon") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_4/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_4/code/game.py new file mode 100644 index 0000000..e5c23da --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_4/code/game.py @@ -0,0 +1,334 @@ +import mcrfpy +import random + +# Color configurations for visibility +COLORS_VISIBLE = { + 'wall': (100, 100, 100), + 'floor': (50, 50, 50), + 'tunnel': (30, 30, 40), +} + +class GameObject: + """Base class for all game objects""" + def __init__(self, x, y, sprite_index, color, name, blocks=False): + self.x = x + self.y = y + self.sprite_index = sprite_index + self.color = color + self.name = name + self.blocks = blocks + self._entity = None + self.grid = None + + def attach_to_grid(self, grid): + """Attach this game object to a McRogueFace grid""" + self.grid = grid + self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid) + self._entity.sprite_index = self.sprite_index + self._entity.color = mcrfpy.Color(*self.color) + + def move(self, dx, dy): + """Move by the given amount""" + if not self.grid: + return + self.x += dx + self.y += dy + if self._entity: + self._entity.x = self.x + self._entity.y = self.y + # Update FOV when player moves + if self.name == "Player": + self.update_fov() + + def update_fov(self): + """Update field of view from this entity's position""" + if self._entity and self.grid: + self._entity.update_fov(radius=8) + +class RectangularRoom: + """A rectangular room with its position and size""" + + def __init__(self, x, y, width, height): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self): + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self): + return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1 + + def intersects(self, other): + return ( + self.x1 <= other.x2 + and self.x2 >= other.x1 + and self.y1 <= other.y2 + and self.y2 >= other.y1 + ) + +def tunnel_between(start, end): + """Return an L-shaped tunnel between two points""" + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + corner_x = x2 + corner_y = y1 + else: + corner_x = x1 + corner_y = y2 + + for x in range(min(x1, corner_x), max(x1, corner_x) + 1): + yield x, y1 + for y in range(min(y1, corner_y), max(y1, corner_y) + 1): + yield corner_x, y + for x in range(min(corner_x, x2), max(corner_x, x2) + 1): + yield x, corner_y + for y in range(min(corner_y, y2), max(corner_y, y2) + 1): + yield x2, y + +class GameMap: + """Manages the game world""" + + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = None + self.entities = [] + self.rooms = [] + + def create_grid(self, tileset): + """Create the McRogueFace grid""" + self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset) + self.grid.position = (100, 100) + self.grid.size = (800, 480) + + # Enable perspective rendering (0 = first entity = player) + self.grid.perspective = 0 + + return self.grid + + def fill_with_walls(self): + """Fill the entire map with wall tiles""" + for y in range(self.height): + for x in range(self.width): + self.set_tile(x, y, walkable=False, transparent=False, + sprite_index=35, tile_type='wall') + + def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type): + """Set properties for a specific tile""" + if 0 <= x < self.width and 0 <= y < self.height: + cell = self.grid.at(x, y) + cell.walkable = walkable + cell.transparent = transparent + cell.sprite_index = sprite_index + cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type]) + + def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player): + """Generate a new dungeon map""" + self.fill_with_walls() + + for r in range(max_rooms): + room_width = random.randint(room_min_size, room_max_size) + room_height = random.randint(room_min_size, room_max_size) + + x = random.randint(0, self.width - room_width - 1) + y = random.randint(0, self.height - room_height - 1) + + new_room = RectangularRoom(x, y, room_width, room_height) + + if any(new_room.intersects(other_room) for other_room in self.rooms): + continue + + self.carve_room(new_room) + + if len(self.rooms) == 0: + player.x, player.y = new_room.center + if player._entity: + player._entity.x, player._entity.y = new_room.center + else: + self.carve_tunnel(self.rooms[-1].center, new_room.center) + + self.rooms.append(new_room) + + def carve_room(self, room): + """Carve out a room""" + inner_x1, inner_y1, inner_x2, inner_y2 = room.inner + + for y in range(inner_y1, inner_y2): + for x in range(inner_x1, inner_x2): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='floor') + + def carve_tunnel(self, start, end): + """Carve a tunnel between two points""" + for x, y in tunnel_between(start, end): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='tunnel') + + def is_blocked(self, x, y): + """Check if a tile blocks movement""" + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return True + if not self.grid.at(x, y).walkable: + return True + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return True + return False + + def add_entity(self, entity): + """Add a GameObject to the map""" + self.entities.append(entity) + entity.attach_to_grid(self.grid) + +class Engine: + """Main game engine""" + + def __init__(self): + self.game_map = None + self.player = None + self.entities = [] + self.fov_radius = 8 + + mcrfpy.createScene("game") + mcrfpy.setScene("game") + + window = mcrfpy.Window.get() + window.title = "McRogueFace Roguelike - Part 4" + + self.ui = mcrfpy.sceneUI("game") + + background = mcrfpy.Frame(0, 0, 1024, 768) + background.fill_color = mcrfpy.Color(0, 0, 0) + self.ui.append(background) + + self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + + self.setup_game() + self.setup_input() + self.setup_ui() + + def setup_game(self): + """Initialize the game world""" + self.game_map = GameMap(80, 45) + grid = self.game_map.create_grid(self.tileset) + self.ui.append(grid) + + # Create player + self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True) + + # Generate the dungeon + self.game_map.generate_dungeon( + max_rooms=30, + room_min_size=6, + room_max_size=10, + player=self.player + ) + + # Add player to map + self.game_map.add_entity(self.player) + + # Add monsters in random rooms + for i in range(10): + if i < len(self.game_map.rooms) - 1: + room = self.game_map.rooms[i + 1] + x, y = room.center + + # Randomly offset from center + x += random.randint(-2, 2) + y += random.randint(-2, 2) + + # Make sure position is walkable + if self.game_map.grid.at(x, y).walkable: + if i % 2 == 0: + # Create an orc + orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True) + self.game_map.add_entity(orc) + self.entities.append(orc) + else: + # Create a troll + troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True) + self.game_map.add_entity(troll) + self.entities.append(troll) + + # Initial FOV calculation + self.player.update_fov() + + def handle_movement(self, dx, dy): + """Handle player movement""" + new_x = self.player.x + dx + new_y = self.player.y + dy + + if not self.game_map.is_blocked(new_x, new_y): + self.player.move(dx, dy) + + def setup_input(self): + """Setup keyboard input handling""" + def handle_keys(key, state): + if state != "start": + return + + movement = { + "Up": (0, -1), "Down": (0, 1), + "Left": (-1, 0), "Right": (1, 0), + "Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1), + "Num4": (-1, 0), "Num6": (1, 0), + "Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1), + } + + if key in movement: + dx, dy = movement[key] + self.handle_movement(dx, dy) + elif key == "Escape": + mcrfpy.setScene(None) + elif key == "v": + # Toggle FOV on/off + if self.game_map.grid.perspective == 0: + self.game_map.grid.perspective = -1 # Omniscient + print("FOV disabled - omniscient view") + else: + self.game_map.grid.perspective = 0 # Player perspective + print("FOV enabled - player perspective") + elif key == "Plus" or key == "Equals": + # Increase FOV radius + self.fov_radius = min(self.fov_radius + 1, 20) + self.player._entity.update_fov(radius=self.fov_radius) + print(f"FOV radius: {self.fov_radius}") + elif key == "Minus": + # Decrease FOV radius + self.fov_radius = max(self.fov_radius - 1, 3) + self.player._entity.update_fov(radius=self.fov_radius) + print(f"FOV radius: {self.fov_radius}") + + mcrfpy.keypressScene(handle_keys) + + def setup_ui(self): + """Setup UI elements""" + title = mcrfpy.Caption("Field of View", 512, 30) + title.font_size = 24 + title.fill_color = mcrfpy.Color(255, 255, 100) + self.ui.append(title) + + instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60) + instructions.font_size = 16 + instructions.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(instructions) + + # FOV indicator + self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100) + self.fov_text.font_size = 14 + self.fov_text.fill_color = mcrfpy.Color(150, 200, 255) + self.ui.append(self.fov_text) + +# Create and run the game +engine = Engine() +print("Part 4: Field of View!") +print("Press V to toggle FOV on/off") +print("Press +/- to adjust FOV radius") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_5/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_5/code/game.py new file mode 100644 index 0000000..3e5947f --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_5/code/game.py @@ -0,0 +1,388 @@ +import mcrfpy +import random + +# Color configurations +COLORS_VISIBLE = { + 'wall': (100, 100, 100), + 'floor': (50, 50, 50), + 'tunnel': (30, 30, 40), +} + +# Actions +class Action: + """Base class for all actions""" + pass + +class MovementAction(Action): + """Action for moving an entity""" + def __init__(self, dx, dy): + self.dx = dx + self.dy = dy + +class WaitAction(Action): + """Action for waiting/skipping turn""" + pass + +class GameObject: + """Base class for all game objects""" + def __init__(self, x, y, sprite_index, color, name, blocks=False): + self.x = x + self.y = y + self.sprite_index = sprite_index + self.color = color + self.name = name + self.blocks = blocks + self._entity = None + self.grid = None + + def attach_to_grid(self, grid): + """Attach this game object to a McRogueFace grid""" + self.grid = grid + self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid) + self._entity.sprite_index = self.sprite_index + self._entity.color = mcrfpy.Color(*self.color) + + def move(self, dx, dy): + """Move by the given amount""" + if not self.grid: + return + self.x += dx + self.y += dy + if self._entity: + self._entity.x = self.x + self._entity.y = self.y + # Update FOV when player moves + if self.name == "Player": + self.update_fov() + + def update_fov(self): + """Update field of view from this entity's position""" + if self._entity and self.grid: + self._entity.update_fov(radius=8) + +class RectangularRoom: + """A rectangular room with its position and size""" + + def __init__(self, x, y, width, height): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self): + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self): + return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1 + + def intersects(self, other): + return ( + self.x1 <= other.x2 + and self.x2 >= other.x1 + and self.y1 <= other.y2 + and self.y2 >= other.y1 + ) + +def tunnel_between(start, end): + """Return an L-shaped tunnel between two points""" + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + corner_x = x2 + corner_y = y1 + else: + corner_x = x1 + corner_y = y2 + + for x in range(min(x1, corner_x), max(x1, corner_x) + 1): + yield x, y1 + for y in range(min(y1, corner_y), max(y1, corner_y) + 1): + yield corner_x, y + for x in range(min(corner_x, x2), max(corner_x, x2) + 1): + yield x, corner_y + for y in range(min(corner_y, y2), max(corner_y, y2) + 1): + yield x2, y + +def spawn_enemies_in_room(room, game_map, max_enemies=2): + """Spawn between 0 and max_enemies in a room""" + number_of_enemies = random.randint(0, max_enemies) + + enemies_spawned = [] + + for i in range(number_of_enemies): + # Try to find a valid position + attempts = 10 + while attempts > 0: + # Random position within room bounds + x = random.randint(room.x1 + 1, room.x2 - 1) + y = random.randint(room.y1 + 1, room.y2 - 1) + + # Check if position is valid + if not game_map.is_blocked(x, y): + # 80% chance for orc, 20% for troll + if random.random() < 0.8: + enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True) + else: + enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True) + + game_map.add_entity(enemy) + enemies_spawned.append(enemy) + break + + attempts -= 1 + + return enemies_spawned + +class GameMap: + """Manages the game world""" + + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = None + self.entities = [] + self.rooms = [] + + def create_grid(self, tileset): + """Create the McRogueFace grid""" + self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset) + self.grid.position = (100, 100) + self.grid.size = (800, 480) + + # Enable perspective rendering + self.grid.perspective = 0 + + return self.grid + + def fill_with_walls(self): + """Fill the entire map with wall tiles""" + for y in range(self.height): + for x in range(self.width): + self.set_tile(x, y, walkable=False, transparent=False, + sprite_index=35, tile_type='wall') + + def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type): + """Set properties for a specific tile""" + if 0 <= x < self.width and 0 <= y < self.height: + cell = self.grid.at(x, y) + cell.walkable = walkable + cell.transparent = transparent + cell.sprite_index = sprite_index + cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type]) + + def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room): + """Generate a new dungeon map""" + self.fill_with_walls() + + for r in range(max_rooms): + room_width = random.randint(room_min_size, room_max_size) + room_height = random.randint(room_min_size, room_max_size) + + x = random.randint(0, self.width - room_width - 1) + y = random.randint(0, self.height - room_height - 1) + + new_room = RectangularRoom(x, y, room_width, room_height) + + if any(new_room.intersects(other_room) for other_room in self.rooms): + continue + + self.carve_room(new_room) + + if len(self.rooms) == 0: + # First room - place player + player.x, player.y = new_room.center + if player._entity: + player._entity.x, player._entity.y = new_room.center + else: + # All other rooms - add tunnel and enemies + self.carve_tunnel(self.rooms[-1].center, new_room.center) + spawn_enemies_in_room(new_room, self, max_enemies_per_room) + + self.rooms.append(new_room) + + def carve_room(self, room): + """Carve out a room""" + inner_x1, inner_y1, inner_x2, inner_y2 = room.inner + + for y in range(inner_y1, inner_y2): + for x in range(inner_x1, inner_x2): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='floor') + + def carve_tunnel(self, start, end): + """Carve a tunnel between two points""" + for x, y in tunnel_between(start, end): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='tunnel') + + def get_blocking_entity_at(self, x, y): + """Return any blocking entity at the given position""" + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return entity + return None + + def is_blocked(self, x, y): + """Check if a tile blocks movement""" + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return True + + if not self.grid.at(x, y).walkable: + return True + + if self.get_blocking_entity_at(x, y): + return True + + return False + + def add_entity(self, entity): + """Add a GameObject to the map""" + self.entities.append(entity) + entity.attach_to_grid(self.grid) + +class Engine: + """Main game engine""" + + def __init__(self): + self.game_map = None + self.player = None + self.entities = [] + + mcrfpy.createScene("game") + mcrfpy.setScene("game") + + window = mcrfpy.Window.get() + window.title = "McRogueFace Roguelike - Part 5" + + self.ui = mcrfpy.sceneUI("game") + + background = mcrfpy.Frame(0, 0, 1024, 768) + background.fill_color = mcrfpy.Color(0, 0, 0) + self.ui.append(background) + + self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + + self.setup_game() + self.setup_input() + self.setup_ui() + + def setup_game(self): + """Initialize the game world""" + self.game_map = GameMap(80, 45) + grid = self.game_map.create_grid(self.tileset) + self.ui.append(grid) + + # Create player + self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True) + + # Generate the dungeon + self.game_map.generate_dungeon( + max_rooms=30, + room_min_size=6, + room_max_size=10, + player=self.player, + max_enemies_per_room=2 + ) + + # Add player to map + self.game_map.add_entity(self.player) + + # Store reference to all entities + self.entities = [e for e in self.game_map.entities if e != self.player] + + # Initial FOV calculation + self.player.update_fov() + + def handle_player_turn(self, action): + """Process the player's action""" + if isinstance(action, MovementAction): + dest_x = self.player.x + action.dx + dest_y = self.player.y + action.dy + + # Check what's at the destination + target = self.game_map.get_blocking_entity_at(dest_x, dest_y) + + if target: + # We bumped into something! + print(f"You kick the {target.name} in the shins, much to its annoyance!") + self.status_text.text = f"You kick the {target.name}!" + elif not self.game_map.is_blocked(dest_x, dest_y): + # Move the player + self.player.move(action.dx, action.dy) + self.status_text.text = "" + else: + # Bumped into a wall + self.status_text.text = "Blocked!" + + elif isinstance(action, WaitAction): + self.status_text.text = "You wait..." + + def setup_input(self): + """Setup keyboard input handling""" + def handle_keys(key, state): + if state != "start": + return + + action = None + + # Movement keys + movement = { + "Up": (0, -1), "Down": (0, 1), + "Left": (-1, 0), "Right": (1, 0), + "Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1), + "Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0), + "Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1), + } + + if key in movement: + dx, dy = movement[key] + if dx == 0 and dy == 0: + action = WaitAction() + else: + action = MovementAction(dx, dy) + elif key == "Period": + action = WaitAction() + elif key == "Escape": + mcrfpy.setScene(None) + return + + # Process the action + if action: + self.handle_player_turn(action) + + mcrfpy.keypressScene(handle_keys) + + def setup_ui(self): + """Setup UI elements""" + title = mcrfpy.Caption("Placing Enemies", 512, 30) + title.font_size = 24 + title.fill_color = mcrfpy.Color(255, 255, 100) + self.ui.append(title) + + instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60) + instructions.font_size = 16 + instructions.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(instructions) + + # Status text + self.status_text = mcrfpy.Caption("", 512, 600) + self.status_text.font_size = 18 + self.status_text.fill_color = mcrfpy.Color(255, 200, 200) + self.ui.append(self.status_text) + + # Entity count + entity_count = len(self.entities) + count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100) + count_text.font_size = 14 + count_text.fill_color = mcrfpy.Color(150, 150, 255) + self.ui.append(count_text) + +# Create and run the game +engine = Engine() +print("Part 5: Placing Enemies!") +print("Try bumping into enemies - combat coming in Part 6!") \ No newline at end of file diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py new file mode 100644 index 0000000..b738dcc --- /dev/null +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py @@ -0,0 +1,568 @@ +import mcrfpy +import random + +# Color configurations +COLORS_VISIBLE = { + 'wall': (100, 100, 100), + 'floor': (50, 50, 50), + 'tunnel': (30, 30, 40), +} + +# Message colors +COLOR_PLAYER_ATK = (230, 230, 230) +COLOR_ENEMY_ATK = (255, 200, 200) +COLOR_PLAYER_DIE = (255, 100, 100) +COLOR_ENEMY_DIE = (255, 165, 0) + +# Actions +class Action: + """Base class for all actions""" + pass + +class MovementAction(Action): + """Action for moving an entity""" + def __init__(self, dx, dy): + self.dx = dx + self.dy = dy + +class MeleeAction(Action): + """Action for melee attacks""" + def __init__(self, attacker, target): + self.attacker = attacker + self.target = target + + def perform(self): + """Execute the attack""" + if not self.target.is_alive: + return None + + damage = self.attacker.power - self.target.defense + + if damage > 0: + attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!" + self.target.take_damage(damage) + + # Choose color based on attacker + if self.attacker.name == "Player": + color = COLOR_PLAYER_ATK + else: + color = COLOR_ENEMY_ATK + + return attack_desc, color + else: + attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage." + return attack_desc, (150, 150, 150) + +class WaitAction(Action): + """Action for waiting/skipping turn""" + pass + +class GameObject: + """Base class for all game objects""" + def __init__(self, x, y, sprite_index, color, name, + blocks=False, hp=0, defense=0, power=0): + self.x = x + self.y = y + self.sprite_index = sprite_index + self.color = color + self.name = name + self.blocks = blocks + self._entity = None + self.grid = None + + # Combat stats + self.max_hp = hp + self.hp = hp + self.defense = defense + self.power = power + + @property + def is_alive(self): + """Returns True if this entity can act""" + return self.hp > 0 + + def attach_to_grid(self, grid): + """Attach this game object to a McRogueFace grid""" + self.grid = grid + self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid) + self._entity.sprite_index = self.sprite_index + self._entity.color = mcrfpy.Color(*self.color) + + def move(self, dx, dy): + """Move by the given amount""" + if not self.grid: + return + self.x += dx + self.y += dy + if self._entity: + self._entity.x = self.x + self._entity.y = self.y + # Update FOV when player moves + if self.name == "Player": + self.update_fov() + + def update_fov(self): + """Update field of view from this entity's position""" + if self._entity and self.grid: + self._entity.update_fov(radius=8) + + def take_damage(self, amount): + """Apply damage to this entity""" + self.hp -= amount + + # Check for death + if self.hp <= 0: + self.die() + + def die(self): + """Handle entity death""" + if self.name == "Player": + # Player death + self.sprite_index = 64 # Stay as @ + self.color = (127, 0, 0) # Dark red + if self._entity: + self._entity.color = mcrfpy.Color(127, 0, 0) + else: + # Enemy death + self.sprite_index = 37 # % character for corpse + self.color = (127, 0, 0) # Dark red + self.blocks = False # Corpses don't block + self.name = f"remains of {self.name}" + + if self._entity: + self._entity.sprite_index = 37 + self._entity.color = mcrfpy.Color(127, 0, 0) + +# Entity factories +def create_player(x, y): + """Create the player entity""" + return GameObject( + x=x, y=y, + sprite_index=64, # @ + color=(255, 255, 255), + name="Player", + blocks=True, + hp=30, + defense=2, + power=5 + ) + +def create_orc(x, y): + """Create an orc enemy""" + return GameObject( + x=x, y=y, + sprite_index=111, # o + color=(63, 127, 63), + name="Orc", + blocks=True, + hp=10, + defense=0, + power=3 + ) + +def create_troll(x, y): + """Create a troll enemy""" + return GameObject( + x=x, y=y, + sprite_index=84, # T + color=(0, 127, 0), + name="Troll", + blocks=True, + hp=16, + defense=1, + power=4 + ) + +class RectangularRoom: + """A rectangular room with its position and size""" + + def __init__(self, x, y, width, height): + self.x1 = x + self.y1 = y + self.x2 = x + width + self.y2 = y + height + + @property + def center(self): + center_x = (self.x1 + self.x2) // 2 + center_y = (self.y1 + self.y2) // 2 + return center_x, center_y + + @property + def inner(self): + return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1 + + def intersects(self, other): + return ( + self.x1 <= other.x2 + and self.x2 >= other.x1 + and self.y1 <= other.y2 + and self.y2 >= other.y1 + ) + +def tunnel_between(start, end): + """Return an L-shaped tunnel between two points""" + x1, y1 = start + x2, y2 = end + + if random.random() < 0.5: + corner_x = x2 + corner_y = y1 + else: + corner_x = x1 + corner_y = y2 + + for x in range(min(x1, corner_x), max(x1, corner_x) + 1): + yield x, y1 + for y in range(min(y1, corner_y), max(y1, corner_y) + 1): + yield corner_x, y + for x in range(min(corner_x, x2), max(corner_x, x2) + 1): + yield x, corner_y + for y in range(min(corner_y, y2), max(corner_y, y2) + 1): + yield x2, y + +def spawn_enemies_in_room(room, game_map, max_enemies=2): + """Spawn between 0 and max_enemies in a room""" + number_of_enemies = random.randint(0, max_enemies) + + enemies_spawned = [] + + for i in range(number_of_enemies): + attempts = 10 + while attempts > 0: + x = random.randint(room.x1 + 1, room.x2 - 1) + y = random.randint(room.y1 + 1, room.y2 - 1) + + if not game_map.is_blocked(x, y): + # 80% chance for orc, 20% for troll + if random.random() < 0.8: + enemy = create_orc(x, y) + else: + enemy = create_troll(x, y) + + game_map.add_entity(enemy) + enemies_spawned.append(enemy) + break + + attempts -= 1 + + return enemies_spawned + +class GameMap: + """Manages the game world""" + + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = None + self.entities = [] + self.rooms = [] + + def create_grid(self, tileset): + """Create the McRogueFace grid""" + self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset) + self.grid.position = (100, 100) + self.grid.size = (800, 480) + + # Enable perspective rendering + self.grid.perspective = 0 + + return self.grid + + def fill_with_walls(self): + """Fill the entire map with wall tiles""" + for y in range(self.height): + for x in range(self.width): + self.set_tile(x, y, walkable=False, transparent=False, + sprite_index=35, tile_type='wall') + + def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type): + """Set properties for a specific tile""" + if 0 <= x < self.width and 0 <= y < self.height: + cell = self.grid.at(x, y) + cell.walkable = walkable + cell.transparent = transparent + cell.sprite_index = sprite_index + cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type]) + + def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room): + """Generate a new dungeon map""" + self.fill_with_walls() + + for r in range(max_rooms): + room_width = random.randint(room_min_size, room_max_size) + room_height = random.randint(room_min_size, room_max_size) + + x = random.randint(0, self.width - room_width - 1) + y = random.randint(0, self.height - room_height - 1) + + new_room = RectangularRoom(x, y, room_width, room_height) + + if any(new_room.intersects(other_room) for other_room in self.rooms): + continue + + self.carve_room(new_room) + + if len(self.rooms) == 0: + # First room - place player + player.x, player.y = new_room.center + if player._entity: + player._entity.x, player._entity.y = new_room.center + else: + # All other rooms - add tunnel and enemies + self.carve_tunnel(self.rooms[-1].center, new_room.center) + spawn_enemies_in_room(new_room, self, max_enemies_per_room) + + self.rooms.append(new_room) + + def carve_room(self, room): + """Carve out a room""" + inner_x1, inner_y1, inner_x2, inner_y2 = room.inner + + for y in range(inner_y1, inner_y2): + for x in range(inner_x1, inner_x2): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='floor') + + def carve_tunnel(self, start, end): + """Carve a tunnel between two points""" + for x, y in tunnel_between(start, end): + self.set_tile(x, y, walkable=True, transparent=True, + sprite_index=46, tile_type='tunnel') + + def get_blocking_entity_at(self, x, y): + """Return any blocking entity at the given position""" + for entity in self.entities: + if entity.blocks and entity.x == x and entity.y == y: + return entity + return None + + def is_blocked(self, x, y): + """Check if a tile blocks movement""" + if x < 0 or x >= self.width or y < 0 or y >= self.height: + return True + + if not self.grid.at(x, y).walkable: + return True + + if self.get_blocking_entity_at(x, y): + return True + + return False + + def add_entity(self, entity): + """Add a GameObject to the map""" + self.entities.append(entity) + entity.attach_to_grid(self.grid) + +class Engine: + """Main game engine""" + + def __init__(self): + self.game_map = None + self.player = None + self.entities = [] + self.messages = [] # Simple message log + self.max_messages = 5 + + mcrfpy.createScene("game") + mcrfpy.setScene("game") + + window = mcrfpy.Window.get() + window.title = "McRogueFace Roguelike - Part 6" + + self.ui = mcrfpy.sceneUI("game") + + background = mcrfpy.Frame(0, 0, 1024, 768) + background.fill_color = mcrfpy.Color(0, 0, 0) + self.ui.append(background) + + self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16) + + self.setup_game() + self.setup_input() + self.setup_ui() + + def add_message(self, text, color=(255, 255, 255)): + """Add a message to the log""" + self.messages.append((text, color)) + if len(self.messages) > self.max_messages: + self.messages.pop(0) + self.update_message_display() + + def update_message_display(self): + """Update the message display""" + # Clear old messages + for caption in self.message_captions: + # Remove from UI (McRogueFace doesn't have remove, so we hide it) + caption.text = "" + + # Display current messages + for i, (text, color) in enumerate(self.messages): + if i < len(self.message_captions): + self.message_captions[i].text = text + self.message_captions[i].fill_color = mcrfpy.Color(*color) + + def setup_game(self): + """Initialize the game world""" + self.game_map = GameMap(80, 45) + grid = self.game_map.create_grid(self.tileset) + self.ui.append(grid) + + # Create player + self.player = create_player(0, 0) + + # Generate the dungeon + self.game_map.generate_dungeon( + max_rooms=30, + room_min_size=6, + room_max_size=10, + player=self.player, + max_enemies_per_room=2 + ) + + # Add player to map + self.game_map.add_entity(self.player) + + # Store reference to all entities + self.entities = [e for e in self.game_map.entities if e != self.player] + + # Initial FOV calculation + self.player.update_fov() + + # Welcome message + self.add_message("Welcome to the dungeon!", (100, 100, 255)) + + def handle_player_turn(self, action): + """Process the player's action""" + if not self.player.is_alive: + return + + if isinstance(action, MovementAction): + dest_x = self.player.x + action.dx + dest_y = self.player.y + action.dy + + # Check what's at the destination + target = self.game_map.get_blocking_entity_at(dest_x, dest_y) + + if target: + # Attack! + attack = MeleeAction(self.player, target) + result = attack.perform() + if result: + text, color = result + self.add_message(text, color) + + # Check if target died + if not target.is_alive: + death_msg = f"The {target.name.replace('remains of ', '')} is dead!" + self.add_message(death_msg, COLOR_ENEMY_DIE) + + elif not self.game_map.is_blocked(dest_x, dest_y): + # Move the player + self.player.move(action.dx, action.dy) + + elif isinstance(action, WaitAction): + pass # Do nothing + + # Enemy turns + self.handle_enemy_turns() + + def handle_enemy_turns(self): + """Let all enemies take their turn""" + for entity in self.entities: + if entity.is_alive: + # Simple AI: if player is adjacent, attack. Otherwise, do nothing. + dx = entity.x - self.player.x + dy = entity.y - self.player.y + distance = abs(dx) + abs(dy) + + if distance == 1: # Adjacent to player + attack = MeleeAction(entity, self.player) + result = attack.perform() + if result: + text, color = result + self.add_message(text, color) + + # Check if player died + if not self.player.is_alive: + self.add_message("You have died!", COLOR_PLAYER_DIE) + + def setup_input(self): + """Setup keyboard input handling""" + def handle_keys(key, state): + if state != "start": + return + + action = None + + # Movement keys + movement = { + "Up": (0, -1), "Down": (0, 1), + "Left": (-1, 0), "Right": (1, 0), + "Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1), + "Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0), + "Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1), + } + + if key in movement: + dx, dy = movement[key] + if dx == 0 and dy == 0: + action = WaitAction() + else: + action = MovementAction(dx, dy) + elif key == "Period": + action = WaitAction() + elif key == "Escape": + mcrfpy.setScene(None) + return + + # Process the action + if action: + self.handle_player_turn(action) + + mcrfpy.keypressScene(handle_keys) + + def setup_ui(self): + """Setup UI elements""" + title = mcrfpy.Caption("Combat System", 512, 30) + title.font_size = 24 + title.fill_color = mcrfpy.Color(255, 255, 100) + self.ui.append(title) + + instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60) + instructions.font_size = 16 + instructions.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(instructions) + + # Player stats + self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100) + self.hp_text.font_size = 18 + self.hp_text.fill_color = mcrfpy.Color(255, 100, 100) + self.ui.append(self.hp_text) + + # Message log + self.message_captions = [] + for i in range(self.max_messages): + caption = mcrfpy.Caption("", 50, 620 + i * 20) + caption.font_size = 14 + caption.fill_color = mcrfpy.Color(200, 200, 200) + self.ui.append(caption) + self.message_captions.append(caption) + + # Timer to update HP display + def update_stats(dt): + self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}" + if self.player.hp <= 0: + self.hp_text.fill_color = mcrfpy.Color(127, 0, 0) + elif self.player.hp < self.player.max_hp // 3: + self.hp_text.fill_color = mcrfpy.Color(255, 100, 100) + else: + self.hp_text.fill_color = mcrfpy.Color(0, 255, 0) + + mcrfpy.setTimer("update_stats", update_stats, 100) + +# Create and run the game +engine = Engine() +print("Part 6: Combat System!") +print("Attack enemies to defeat them, but watch your HP!") \ No newline at end of file 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/McRFPy_API.cpp b/src/McRFPy_API.cpp index f759b0a..2aa7905 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -1,5 +1,6 @@ #include "McRFPy_API.h" #include "McRFPy_Automation.h" +#include "McRFPy_Libtcod.h" #include "platform.h" #include "PyAnimation.h" #include "PyDrawable.h" @@ -12,6 +13,7 @@ #include "PyScene.h" #include #include +#include std::vector* McRFPy_API::soundbuffers = nullptr; sf::Music* McRFPy_API::music = nullptr; @@ -287,6 +289,21 @@ PyObject* PyInit_mcrfpy() PyModule_AddObject(m, "default_font", Py_None); PyModule_AddObject(m, "default_texture", Py_None); + // Add TCOD FOV algorithm constants + PyModule_AddIntConstant(m, "FOV_BASIC", FOV_BASIC); + PyModule_AddIntConstant(m, "FOV_DIAMOND", FOV_DIAMOND); + PyModule_AddIntConstant(m, "FOV_SHADOW", FOV_SHADOW); + PyModule_AddIntConstant(m, "FOV_PERMISSIVE_0", FOV_PERMISSIVE_0); + PyModule_AddIntConstant(m, "FOV_PERMISSIVE_1", FOV_PERMISSIVE_1); + PyModule_AddIntConstant(m, "FOV_PERMISSIVE_2", FOV_PERMISSIVE_2); + PyModule_AddIntConstant(m, "FOV_PERMISSIVE_3", FOV_PERMISSIVE_3); + PyModule_AddIntConstant(m, "FOV_PERMISSIVE_4", FOV_PERMISSIVE_4); + PyModule_AddIntConstant(m, "FOV_PERMISSIVE_5", FOV_PERMISSIVE_5); + PyModule_AddIntConstant(m, "FOV_PERMISSIVE_6", FOV_PERMISSIVE_6); + PyModule_AddIntConstant(m, "FOV_PERMISSIVE_7", FOV_PERMISSIVE_7); + PyModule_AddIntConstant(m, "FOV_PERMISSIVE_8", FOV_PERMISSIVE_8); + PyModule_AddIntConstant(m, "FOV_RESTRICTIVE", FOV_RESTRICTIVE); + // Add automation submodule PyObject* automation_module = McRFPy_Automation::init_automation_module(); if (automation_module != NULL) { @@ -297,6 +314,16 @@ PyObject* PyInit_mcrfpy() PyDict_SetItemString(sys_modules, "mcrfpy.automation", automation_module); } + // Add libtcod submodule + PyObject* libtcod_module = McRFPy_Libtcod::init_libtcod_module(); + if (libtcod_module != NULL) { + PyModule_AddObject(m, "libtcod", libtcod_module); + + // Also add to sys.modules for proper import behavior + PyObject* sys_modules = PyImport_GetModuleDict(); + PyDict_SetItemString(sys_modules, "mcrfpy.libtcod", libtcod_module); + } + //McRFPy_API::mcrf_module = m; return m; } diff --git a/src/McRFPy_Libtcod.cpp b/src/McRFPy_Libtcod.cpp new file mode 100644 index 0000000..bb5de49 --- /dev/null +++ b/src/McRFPy_Libtcod.cpp @@ -0,0 +1,324 @@ +#include "McRFPy_Libtcod.h" +#include "McRFPy_API.h" +#include "UIGrid.h" +#include + +// Helper function to get UIGrid from Python object +static UIGrid* get_grid_from_pyobject(PyObject* obj) { + auto grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"); + if (!grid_type) { + PyErr_SetString(PyExc_RuntimeError, "Could not find Grid type"); + return nullptr; + } + + if (!PyObject_IsInstance(obj, (PyObject*)grid_type)) { + Py_DECREF(grid_type); + PyErr_SetString(PyExc_TypeError, "First argument must be a Grid object"); + return nullptr; + } + + Py_DECREF(grid_type); + PyUIGridObject* pygrid = (PyUIGridObject*)obj; + return pygrid->data.get(); +} + +// Field of View computation +static PyObject* McRFPy_Libtcod::compute_fov(PyObject* self, PyObject* args) { + PyObject* grid_obj; + int x, y, radius; + int light_walls = 1; + int algorithm = FOV_BASIC; + + if (!PyArg_ParseTuple(args, "Oiii|ii", &grid_obj, &x, &y, &radius, + &light_walls, &algorithm)) { + return NULL; + } + + UIGrid* grid = get_grid_from_pyobject(grid_obj); + if (!grid) return NULL; + + // Compute FOV using grid's method + grid->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm); + + // Return list of visible cells + PyObject* visible_list = PyList_New(0); + for (int gy = 0; gy < grid->grid_y; gy++) { + for (int gx = 0; gx < grid->grid_x; gx++) { + if (grid->isInFOV(gx, gy)) { + PyObject* pos = Py_BuildValue("(ii)", gx, gy); + PyList_Append(visible_list, pos); + Py_DECREF(pos); + } + } + } + + return visible_list; +} + +// A* Pathfinding +static PyObject* McRFPy_Libtcod::find_path(PyObject* self, PyObject* args) { + PyObject* grid_obj; + int x1, y1, x2, y2; + float diagonal_cost = 1.41f; + + if (!PyArg_ParseTuple(args, "Oiiii|f", &grid_obj, &x1, &y1, &x2, &y2, &diagonal_cost)) { + return NULL; + } + + UIGrid* grid = get_grid_from_pyobject(grid_obj); + if (!grid) return NULL; + + // Get path from grid + std::vector> path = grid->findPath(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; +} + +// Line drawing algorithm +static PyObject* McRFPy_Libtcod::line(PyObject* self, PyObject* args) { + int x1, y1, x2, y2; + + if (!PyArg_ParseTuple(args, "iiii", &x1, &y1, &x2, &y2)) { + return NULL; + } + + // Use TCOD's line algorithm + TCODLine::init(x1, y1, x2, y2); + + PyObject* line_list = PyList_New(0); + int x, y; + + // Step through line + while (!TCODLine::step(&x, &y)) { + PyObject* pos = Py_BuildValue("(ii)", x, y); + PyList_Append(line_list, pos); + Py_DECREF(pos); + } + + return line_list; +} + +// Line iterator (generator-like function) +static PyObject* McRFPy_Libtcod::line_iter(PyObject* self, PyObject* args) { + // For simplicity, just call line() for now + // A proper implementation would create an iterator object + return line(self, args); +} + +// Dijkstra pathfinding +static PyObject* McRFPy_Libtcod::dijkstra_new(PyObject* self, PyObject* args) { + PyObject* grid_obj; + float diagonal_cost = 1.41f; + + if (!PyArg_ParseTuple(args, "O|f", &grid_obj, &diagonal_cost)) { + return NULL; + } + + UIGrid* grid = get_grid_from_pyobject(grid_obj); + if (!grid) return NULL; + + // For now, just return the grid object since Dijkstra is part of the grid + Py_INCREF(grid_obj); + return grid_obj; +} + +static PyObject* McRFPy_Libtcod::dijkstra_compute(PyObject* self, PyObject* args) { + PyObject* grid_obj; + int root_x, root_y; + + if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &root_x, &root_y)) { + return NULL; + } + + UIGrid* grid = get_grid_from_pyobject(grid_obj); + if (!grid) return NULL; + + grid->computeDijkstra(root_x, root_y); + Py_RETURN_NONE; +} + +static PyObject* McRFPy_Libtcod::dijkstra_get_distance(PyObject* self, PyObject* args) { + PyObject* grid_obj; + int x, y; + + if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) { + return NULL; + } + + UIGrid* grid = get_grid_from_pyobject(grid_obj); + if (!grid) return NULL; + + float distance = grid->getDijkstraDistance(x, y); + if (distance < 0) { + Py_RETURN_NONE; + } + + return PyFloat_FromDouble(distance); +} + +static PyObject* McRFPy_Libtcod::dijkstra_path_to(PyObject* self, PyObject* args) { + PyObject* grid_obj; + int x, y; + + if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) { + return NULL; + } + + UIGrid* grid = get_grid_from_pyobject(grid_obj); + if (!grid) return NULL; + + std::vector> path = grid->getDijkstraPath(x, y); + + 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; +} + +// Add FOV algorithm constants to module +static PyObject* McRFPy_Libtcod::add_fov_constants(PyObject* module) { + // FOV algorithms + PyModule_AddIntConstant(module, "FOV_BASIC", FOV_BASIC); + PyModule_AddIntConstant(module, "FOV_DIAMOND", FOV_DIAMOND); + PyModule_AddIntConstant(module, "FOV_SHADOW", FOV_SHADOW); + PyModule_AddIntConstant(module, "FOV_PERMISSIVE_0", FOV_PERMISSIVE_0); + PyModule_AddIntConstant(module, "FOV_PERMISSIVE_1", FOV_PERMISSIVE_1); + PyModule_AddIntConstant(module, "FOV_PERMISSIVE_2", FOV_PERMISSIVE_2); + PyModule_AddIntConstant(module, "FOV_PERMISSIVE_3", FOV_PERMISSIVE_3); + PyModule_AddIntConstant(module, "FOV_PERMISSIVE_4", FOV_PERMISSIVE_4); + PyModule_AddIntConstant(module, "FOV_PERMISSIVE_5", FOV_PERMISSIVE_5); + PyModule_AddIntConstant(module, "FOV_PERMISSIVE_6", FOV_PERMISSIVE_6); + PyModule_AddIntConstant(module, "FOV_PERMISSIVE_7", FOV_PERMISSIVE_7); + PyModule_AddIntConstant(module, "FOV_PERMISSIVE_8", FOV_PERMISSIVE_8); + PyModule_AddIntConstant(module, "FOV_RESTRICTIVE", FOV_RESTRICTIVE); + PyModule_AddIntConstant(module, "FOV_SYMMETRIC_SHADOWCAST", FOV_SYMMETRIC_SHADOWCAST); + + return module; +} + +// Method definitions +static PyMethodDef libtcodMethods[] = { + {"compute_fov", McRFPy_Libtcod::compute_fov, METH_VARARGS, + "compute_fov(grid, x, y, radius, light_walls=True, algorithm=FOV_BASIC)\n\n" + "Compute field of view from a position.\n\n" + "Args:\n" + " grid: Grid object to compute FOV on\n" + " x, y: Origin position\n" + " radius: Maximum sight radius\n" + " light_walls: Whether walls are lit when in FOV\n" + " algorithm: FOV algorithm to use (FOV_BASIC, FOV_SHADOW, etc.)\n\n" + "Returns:\n" + " List of (x, y) tuples for visible cells"}, + + {"find_path", McRFPy_Libtcod::find_path, METH_VARARGS, + "find_path(grid, x1, y1, x2, y2, diagonal_cost=1.41)\n\n" + "Find shortest path between two points using A*.\n\n" + "Args:\n" + " grid: Grid object to pathfind on\n" + " x1, y1: Starting position\n" + " x2, y2: Target position\n" + " diagonal_cost: Cost of diagonal movement\n\n" + "Returns:\n" + " List of (x, y) tuples representing the path, or empty list if no path exists"}, + + {"line", McRFPy_Libtcod::line, METH_VARARGS, + "line(x1, y1, x2, y2)\n\n" + "Get cells along a line using Bresenham's algorithm.\n\n" + "Args:\n" + " x1, y1: Starting position\n" + " x2, y2: Ending position\n\n" + "Returns:\n" + " List of (x, y) tuples along the line"}, + + {"line_iter", McRFPy_Libtcod::line_iter, METH_VARARGS, + "line_iter(x1, y1, x2, y2)\n\n" + "Iterate over cells along a line.\n\n" + "Args:\n" + " x1, y1: Starting position\n" + " x2, y2: Ending position\n\n" + "Returns:\n" + " Iterator of (x, y) tuples along the line"}, + + {"dijkstra_new", McRFPy_Libtcod::dijkstra_new, METH_VARARGS, + "dijkstra_new(grid, diagonal_cost=1.41)\n\n" + "Create a Dijkstra pathfinding context for a grid.\n\n" + "Args:\n" + " grid: Grid object to use for pathfinding\n" + " diagonal_cost: Cost of diagonal movement\n\n" + "Returns:\n" + " Grid object configured for Dijkstra pathfinding"}, + + {"dijkstra_compute", McRFPy_Libtcod::dijkstra_compute, METH_VARARGS, + "dijkstra_compute(grid, root_x, root_y)\n\n" + "Compute Dijkstra distance map from root position.\n\n" + "Args:\n" + " grid: Grid object with Dijkstra context\n" + " root_x, root_y: Root position to compute distances from"}, + + {"dijkstra_get_distance", McRFPy_Libtcod::dijkstra_get_distance, METH_VARARGS, + "dijkstra_get_distance(grid, x, y)\n\n" + "Get distance from root to a position.\n\n" + "Args:\n" + " grid: Grid object with computed Dijkstra map\n" + " x, y: Position to get distance for\n\n" + "Returns:\n" + " Float distance or None if position is invalid/unreachable"}, + + {"dijkstra_path_to", McRFPy_Libtcod::dijkstra_path_to, METH_VARARGS, + "dijkstra_path_to(grid, x, y)\n\n" + "Get shortest path from position to Dijkstra root.\n\n" + "Args:\n" + " grid: Grid object with computed Dijkstra map\n" + " x, y: Starting position\n\n" + "Returns:\n" + " List of (x, y) tuples representing the path to root"}, + + {NULL, NULL, 0, NULL} +}; + +// Module definition +static PyModuleDef libtcodModule = { + PyModuleDef_HEAD_INIT, + "mcrfpy.libtcod", + "TCOD-compatible algorithms for field of view, pathfinding, and line drawing.\n\n" + "This module provides access to TCOD's algorithms integrated with McRogueFace grids.\n" + "Unlike the original TCOD, these functions work directly with Grid objects.\n\n" + "FOV Algorithms:\n" + " FOV_BASIC - Basic circular FOV\n" + " FOV_SHADOW - Shadow casting (recommended)\n" + " FOV_DIAMOND - Diamond-shaped FOV\n" + " FOV_PERMISSIVE_0 through FOV_PERMISSIVE_8 - Permissive variants\n" + " FOV_RESTRICTIVE - Most restrictive FOV\n" + " FOV_SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\n" + "Example:\n" + " import mcrfpy\n" + " from mcrfpy import libtcod\n\n" + " grid = mcrfpy.Grid(50, 50)\n" + " visible = libtcod.compute_fov(grid, 25, 25, 10)\n" + " path = libtcod.find_path(grid, 0, 0, 49, 49)", + -1, + libtcodMethods +}; + +// Module initialization +PyObject* McRFPy_Libtcod::init_libtcod_module() { + PyObject* m = PyModule_Create(&libtcodModule); + if (m == NULL) { + return NULL; + } + + // Add FOV algorithm constants + add_fov_constants(m); + + return m; +} \ No newline at end of file diff --git a/src/McRFPy_Libtcod.h b/src/McRFPy_Libtcod.h new file mode 100644 index 0000000..8aad75c --- /dev/null +++ b/src/McRFPy_Libtcod.h @@ -0,0 +1,27 @@ +#pragma once +#include "Common.h" +#include "Python.h" +#include + +namespace McRFPy_Libtcod +{ + // Field of View algorithms + static PyObject* compute_fov(PyObject* self, PyObject* args); + + // Pathfinding + static PyObject* find_path(PyObject* self, PyObject* args); + static PyObject* dijkstra_new(PyObject* self, PyObject* args); + static PyObject* dijkstra_compute(PyObject* self, PyObject* args); + static PyObject* dijkstra_get_distance(PyObject* self, PyObject* args); + static PyObject* dijkstra_path_to(PyObject* self, PyObject* args); + + // Line algorithms + static PyObject* line(PyObject* self, PyObject* args); + static PyObject* line_iter(PyObject* self, PyObject* args); + + // FOV algorithm constants + static PyObject* add_fov_constants(PyObject* module); + + // Module initialization + PyObject* init_libtcod_module(); +} \ No newline at end of file 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 e001db7..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) { @@ -377,10 +441,75 @@ PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) Py_RETURN_NONE; } +PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { + static const char* keywords[] = {"target_x", "target_y", "x", "y", nullptr}; + int target_x = -1, target_y = -1; + + // Parse arguments - support both target_x/target_y and x/y parameter names + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast(keywords), + &target_x, &target_y)) { + PyErr_Clear(); + // Try alternative parameter names + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiii", const_cast(keywords), + &target_x, &target_y, &target_x, &target_y)) { + PyErr_SetString(PyExc_TypeError, "path_to() requires target_x and target_y integer arguments"); + return NULL; + } + } + + // Check if entity has a grid + if (!self->data || !self->data->grid) { + PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to compute paths"); + return NULL; + } + + // Get current position + int current_x = static_cast(self->data->position.x); + int current_y = static_cast(self->data->position.y); + + // Validate target position + auto grid = self->data->grid; + if (target_x < 0 || target_x >= grid->grid_x || target_y < 0 || target_y >= grid->grid_y) { + PyErr_Format(PyExc_ValueError, "Target position (%d, %d) is out of grid bounds (0-%d, 0-%d)", + target_x, target_y, grid->grid_x - 1, grid->grid_y - 1); + return NULL; + } + + // Use the grid's Dijkstra implementation + grid->computeDijkstra(current_x, current_y); + auto path = grid->getDijkstraPath(target_x, target_y); + + // Convert path to Python list of tuples + PyObject* path_list = PyList_New(path.size()); + if (!path_list) return PyErr_NoMemory(); + + for (size_t i = 0; i < path.size(); ++i) { + PyObject* coord_tuple = PyTuple_New(2); + if (!coord_tuple) { + Py_DECREF(path_list); + return PyErr_NoMemory(); + } + + PyTuple_SetItem(coord_tuple, 0, PyLong_FromLong(path[i].first)); + PyTuple_SetItem(coord_tuple, 1, PyLong_FromLong(path[i].second)); + PyList_SetItem(path_list, i, coord_tuple); + } + + 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} }; @@ -393,6 +522,8 @@ PyMethodDef UIEntity_all_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} // Sentinel }; @@ -426,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 86b7e92..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); @@ -59,6 +61,8 @@ public: static PyObject* at(PyUIEntityObject* self, PyObject* o); 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 fe6eec7..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) // 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>>(); @@ -27,13 +28,15 @@ UIGrid::UIGrid() output.setTexture(renderTexture.getTexture()); // Points vector starts empty (grid_x * grid_y = 0) + // TCOD map will be created when grid is resized } UIGrid::UIGrid(int gx, int gy, std::shared_ptr _ptex, sf::Vector2f _xy, sf::Vector2f _wh) : grid_x(gx), grid_y(gy), zoom(1.0f), ptex(_ptex), points(gx * gy), - fill_color(8, 8, 8, 255) // 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; @@ -63,6 +66,27 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr _ptex, sf::Vector2f _x // textures are upside-down inside renderTexture output.setTexture(renderTexture.getTexture()); + // Create TCOD map + tcod_map = new TCODMap(gx, gy); + + // 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++) { + int idx = y * gx + x; + points[idx].grid_x = x; + points[idx].grid_y = y; + points[idx].parent_grid = this; + } + } + + // Initial sync of TCOD map + syncTCODMap(); } void UIGrid::update() {} @@ -164,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 /* @@ -234,11 +270,155 @@ UIGridPoint& UIGrid::at(int x, int y) return points[y * grid_x + x]; } +UIGrid::~UIGrid() +{ + if (tcod_path) { + delete tcod_path; + tcod_path = nullptr; + } + if (tcod_dijkstra) { + delete tcod_dijkstra; + tcod_dijkstra = nullptr; + } + if (tcod_map) { + delete tcod_map; + tcod_map = nullptr; + } +} + PyObjectsEnum UIGrid::derived_type() { return PyObjectsEnum::UIGRID; } +// TCOD integration methods +void UIGrid::syncTCODMap() +{ + if (!tcod_map) return; + + for (int y = 0; y < grid_y; y++) { + for (int x = 0; x < grid_x; x++) { + const UIGridPoint& point = at(x, y); + tcod_map->setProperties(x, y, point.transparent, point.walkable); + } + } +} + +void UIGrid::syncTCODMapCell(int x, int y) +{ + if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return; + + const UIGridPoint& point = at(x, y); + tcod_map->setProperties(x, y, point.transparent, point.walkable); +} + +void UIGrid::computeFOV(int x, int y, int radius, bool light_walls, TCOD_fov_algorithm_t algo) +{ + if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return; + + tcod_map->computeFov(x, y, radius, light_walls, algo); +} + +bool UIGrid::isInFOV(int x, int y) const +{ + if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return false; + + return tcod_map->isInFov(x, y); +} + +std::vector> UIGrid::findPath(int x1, int y1, int x2, int y2, float diagonalCost) +{ + std::vector> path; + + if (!tcod_map || x1 < 0 || x1 >= grid_x || y1 < 0 || y1 >= grid_y || + x2 < 0 || x2 >= grid_x || y2 < 0 || y2 >= grid_y) { + return path; + } + + TCODPath tcod_path(tcod_map, diagonalCost); + if (tcod_path.compute(x1, y1, x2, y2)) { + for (int i = 0; i < tcod_path.size(); i++) { + int x, y; + tcod_path.get(i, &x, &y); + path.push_back(std::make_pair(x, y)); + } + } + + return path; +} + +void UIGrid::computeDijkstra(int rootX, int rootY, float diagonalCost) +{ + if (!tcod_map || !tcod_dijkstra || rootX < 0 || rootX >= grid_x || rootY < 0 || rootY >= grid_y) return; + + // Compute the Dijkstra map from the root position + tcod_dijkstra->compute(rootX, rootY); +} + +float UIGrid::getDijkstraDistance(int x, int y) const +{ + if (!tcod_dijkstra || x < 0 || x >= grid_x || y < 0 || y >= grid_y) { + return -1.0f; // Invalid position + } + + return tcod_dijkstra->getDistance(x, y); +} + +std::vector> UIGrid::getDijkstraPath(int x, int y) const +{ + std::vector> path; + + if (!tcod_dijkstra || x < 0 || x >= grid_x || y < 0 || y >= grid_y) { + return path; // Empty path for invalid position + } + + // Set the destination + if (tcod_dijkstra->setPath(x, y)) { + // Walk the path and collect points + int px, py; + while (tcod_dijkstra->walk(&px, &py)) { + path.push_back(std::make_pair(px, py)); + } + } + + 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 { @@ -338,35 +518,53 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point) int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - // Try parsing with PyArgHelpers - int arg_idx = 0; - auto grid_size_result = PyArgHelpers::parseGridSize(args, kwds, &arg_idx); - auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); - auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx); - // Default values int grid_x = 0, grid_y = 0; float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; PyObject* textureObj = nullptr; - // Case 1: Got grid size and position from helpers (tuple format) - if (grid_size_result.valid) { + // Check if first argument is a tuple (for tuple-based initialization) + bool has_tuple_first_arg = false; + if (args && PyTuple_Size(args) > 0) { + PyObject* first_arg = PyTuple_GetItem(args, 0); + if (PyTuple_Check(first_arg)) { + has_tuple_first_arg = true; + } + } + + // Try tuple-based parsing if we have a tuple as first argument + if (has_tuple_first_arg) { + int arg_idx = 0; + auto grid_size_result = PyArgHelpers::parseGridSize(args, kwds, &arg_idx); + + // If grid size parsing failed with an error, report it + if (!grid_size_result.valid) { + if (grid_size_result.error) { + PyErr_SetString(PyExc_TypeError, grid_size_result.error); + } else { + PyErr_SetString(PyExc_TypeError, "Invalid grid size tuple"); + } + return -1; + } + + // We got a valid grid size grid_x = grid_size_result.grid_w; grid_y = grid_size_result.grid_h; - // Set position if we got it + // Try to parse position and size + auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); if (pos_result.valid) { x = pos_result.x; y = pos_result.y; } - // Set size if we got it, otherwise calculate default + auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx); if (size_result.valid) { w = size_result.w; h = size_result.h; } else { - // Default size based on grid dimensions and texture - w = grid_x * 16.0f; // Will be recalculated if texture provided + // Default size based on grid dimensions + w = grid_x * 16.0f; h = grid_y * 16.0f; } @@ -380,10 +578,8 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { &textureObj); Py_DECREF(remaining_args); } - // Case 2: Traditional format + // Traditional format parsing else { - PyErr_Clear(); // Clear any errors from helpers - static const char* keywords[] = { "grid_x", "grid_y", "texture", "pos", "size", "grid_size", nullptr }; @@ -406,7 +602,13 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { grid_x = PyLong_AsLong(x_obj); grid_y = PyLong_AsLong(y_obj); + } else { + PyErr_SetString(PyExc_TypeError, "grid_size must contain integers"); + return -1; } + } else { + PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple of two integers"); + return -1; } } @@ -419,7 +621,13 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { (PyFloat_Check(y_val) || PyLong_Check(y_val))) { x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); + } else { + PyErr_SetString(PyExc_TypeError, "pos must contain numbers"); + return -1; } + } else { + PyErr_SetString(PyExc_TypeError, "pos must be a tuple of two numbers"); + return -1; } } @@ -432,7 +640,13 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { (PyFloat_Check(h_val) || PyLong_Check(h_val))) { w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val); h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val); + } else { + PyErr_SetString(PyExc_TypeError, "size must contain numbers"); + return -1; } + } else { + PyErr_SetString(PyExc_TypeError, "size must be a tuple of two numbers"); + return -1; } } else { // Default size based on grid @@ -440,17 +654,20 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { h = grid_y * 16.0f; } } + + // Validate grid dimensions + if (grid_x <= 0 || grid_y <= 0) { + PyErr_SetString(PyExc_ValueError, "Grid dimensions must be positive integers"); + return -1; + } // At this point we have x, y, w, h values from either parsing method - // Convert PyObject texture to IndexTexture* - // This requires the texture object to have been initialized similar to UISprite's texture handling - + // Convert PyObject texture to shared_ptr std::shared_ptr texture_ptr = nullptr; - // Allow None for texture - use default texture in that case - if (textureObj != Py_None) { - //if (!PyObject_IsInstance(textureObj, (PyObject*)&PyTextureType)) { + // Allow None or NULL for texture - use default texture in that case + if (textureObj && textureObj != Py_None) { if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) { PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); return -1; @@ -458,16 +675,12 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { PyTextureObject* pyTexture = reinterpret_cast(textureObj); texture_ptr = pyTexture->data; } else { - // Use default texture when None is provided + // Use default texture when None is provided or texture not specified texture_ptr = McRFPy_API::default_texture; } - // Initialize UIGrid - texture_ptr will be nullptr if texture was None - //self->data = new UIGrid(grid_x, grid_y, texture, sf::Vector2f(box_x, box_y), sf::Vector2f(box_w, box_h)); - //self->data = std::make_shared(grid_x, grid_y, pyTexture->data, - // sf::Vector2f(box_x, box_y), sf::Vector2f(box_w, box_h)); // Adjust size based on texture if available and size not explicitly set - if (!size_result.valid && texture_ptr) { + if (texture_ptr && w == grid_x * 16.0f && h == grid_y * 16.0f) { w = grid_x * texture_ptr->sprite_width; h = grid_y * texture_ptr->sprite_height; } @@ -719,8 +932,183 @@ 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) +{ + static char* kwlist[] = {"x", "y", "radius", "light_walls", "algorithm", NULL}; + int x, y, radius = 0; + int light_walls = 1; + int algorithm = FOV_BASIC; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|ipi", kwlist, + &x, &y, &radius, &light_walls, &algorithm)) { + return NULL; + } + + self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm); + Py_RETURN_NONE; +} + +PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args) +{ + int x, y; + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + return NULL; + } + + bool in_fov = self->data->isInFOV(x, y); + return PyBool_FromLong(in_fov); +} + +PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds) +{ + static char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL}; + int x1, y1, x2, y2; + float diagonal_cost = 1.41f; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", kwlist, + &x1, &y1, &x2, &y2, &diagonal_cost)) { + return NULL; + } + + std::vector> path = self->data->findPath(x1, y1, x2, y2, diagonal_cost); + + PyObject* path_list = PyList_New(path.size()); + if (!path_list) return NULL; + + for (size_t i = 0; i < path.size(); i++) { + PyObject* coord = Py_BuildValue("(ii)", path[i].first, path[i].second); + if (!coord) { + Py_DECREF(path_list); + return NULL; + } + PyList_SET_ITEM(path_list, i, coord); + } + + return path_list; +} + +PyObject* UIGrid::py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds) +{ + static char* kwlist[] = {"root_x", "root_y", "diagonal_cost", NULL}; + int root_x, root_y; + float diagonal_cost = 1.41f; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|f", kwlist, + &root_x, &root_y, &diagonal_cost)) { + return NULL; + } + + self->data->computeDijkstra(root_x, root_y, diagonal_cost); + Py_RETURN_NONE; +} + +PyObject* UIGrid::py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args) +{ + int x, y; + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + return NULL; + } + + float distance = self->data->getDijkstraDistance(x, y); + if (distance < 0) { + Py_RETURN_NONE; // Invalid position + } + + return PyFloat_FromDouble(distance); +} + +PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args) +{ + int x, y; + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + return NULL; + } + + std::vector> path = self->data->getDijkstraPath(x, y); + + 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; +} + +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, + "Compute field of view from a position. Args: x, y, radius=0, light_walls=True, algorithm=FOV_BASIC"}, + {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS, + "Check if a cell is in the field of view. Args: x, y"}, + {"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS, + "Find A* path between two points. Args: x1, y1, x2, y2, diagonal_cost=1.41"}, + {"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS, + "Compute Dijkstra map from root position. Args: root_x, root_y, diagonal_cost=1.41"}, + {"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS, + "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} }; @@ -731,6 +1119,20 @@ typedef PyUIGridObject PyObjectType; PyMethodDef UIGrid_all_methods[] = { UIDRAWABLE_METHODS, {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, + {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, + "Compute field of view from a position. Args: x, y, radius=0, light_walls=True, algorithm=FOV_BASIC"}, + {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS, + "Check if a cell is in the field of view. Args: x, y"}, + {"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS, + "Find A* path between two points. Args: x1, y1, x2, y2, diagonal_cost=1.41"}, + {"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS, + "Compute Dijkstra map from root position. Args: root_x, root_y, diagonal_cost=1.41"}, + {"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS, + "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 }; @@ -759,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, @@ -1101,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 ddbed75..96f41ed 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -5,6 +5,7 @@ #include "IndexTexture.h" #include "Resources.h" #include +#include #include "PyCallable.h" #include "PyTexture.h" @@ -25,10 +26,15 @@ private: // Default cell dimensions when no texture is provided static constexpr int DEFAULT_CELL_WIDTH = 16; 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(); //UIGrid(int, int, IndexTexture*, float, float, float, float); UIGrid(int, int, std::shared_ptr, sf::Vector2f, sf::Vector2f); + ~UIGrid(); // Destructor to clean up TCOD map void update(); void render(sf::Vector2f, sf::RenderTarget&) override final; UIGridPoint& at(int, int); @@ -36,6 +42,21 @@ public: //void setSprite(int); virtual UIDrawable* click_at(sf::Vector2f point) override final; + // TCOD integration methods + void syncTCODMap(); // Sync entire map with current grid state + void syncTCODMapCell(int x, int y); // Sync a single cell to TCOD map + void computeFOV(int x, int y, int radius, bool light_walls = true, TCOD_fov_algorithm_t algo = FOV_BASIC); + bool isInFOV(int x, int y) const; + + // Pathfinding methods + std::vector> findPath(int x1, int y1, int x2, int y2, float diagonalCost = 1.41f); + void computeDijkstra(int rootX, int rootY, float diagonalCost = 1.41f); + 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; @@ -56,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; @@ -77,7 +101,16 @@ 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); + static PyObject* py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds); + 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/UIGridPoint.cpp b/src/UIGridPoint.cpp index e255c3a..201fb27 100644 --- a/src/UIGridPoint.cpp +++ b/src/UIGridPoint.cpp @@ -1,19 +1,51 @@ #include "UIGridPoint.h" +#include "UIGrid.h" UIGridPoint::UIGridPoint() : color(1.0f, 1.0f, 1.0f), color_overlay(0.0f, 0.0f, 0.0f), walkable(false), transparent(false), - tilesprite(-1), tile_overlay(-1), uisprite(-1) + tilesprite(-1), tile_overlay(-1), uisprite(-1), grid_x(-1), grid_y(-1), parent_grid(nullptr) {} // Utility function to convert sf::Color to PyObject* PyObject* sfColor_to_PyObject(sf::Color color) { + // For now, keep returning tuples to avoid breaking existing code return Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a); } // Utility function to convert PyObject* to sf::Color sf::Color PyObject_to_sfColor(PyObject* obj) { + // Get the mcrfpy module and Color type + PyObject* module = PyImport_ImportModule("mcrfpy"); + if (!module) { + PyErr_SetString(PyExc_RuntimeError, "Failed to import mcrfpy module"); + return sf::Color(); + } + + PyObject* color_type = PyObject_GetAttrString(module, "Color"); + Py_DECREF(module); + + if (!color_type) { + PyErr_SetString(PyExc_RuntimeError, "Failed to get Color type from mcrfpy module"); + return sf::Color(); + } + + // Check if it's a mcrfpy.Color object + int is_color = PyObject_IsInstance(obj, color_type); + Py_DECREF(color_type); + + if (is_color == 1) { + PyColorObject* color_obj = (PyColorObject*)obj; + return color_obj->data; + } else if (is_color == -1) { + // Error occurred in PyObject_IsInstance + return sf::Color(); + } + + // Otherwise try to parse as tuple int r, g, b, a = 255; // Default alpha to fully opaque if not specified if (!PyArg_ParseTuple(obj, "iii|i", &r, &g, &b, &a)) { + PyErr_Clear(); // Clear the error from failed tuple parsing + PyErr_SetString(PyExc_TypeError, "color must be a Color object or a tuple of (r, g, b[, a])"); return sf::Color(); // Return default color on parse error } return sf::Color(r, g, b, a); @@ -29,6 +61,11 @@ PyObject* UIGridPoint::get_color(PyUIGridPointObject* self, void* closure) { int UIGridPoint::set_color(PyUIGridPointObject* self, PyObject* value, void* closure) { sf::Color color = PyObject_to_sfColor(value); + // Check if an error occurred during conversion + if (PyErr_Occurred()) { + return -1; + } + if (reinterpret_cast(closure) == 0) { // color self->data->color = color; } else { // color_overlay @@ -62,6 +99,12 @@ int UIGridPoint::set_bool_member(PyUIGridPointObject* self, PyObject* value, voi PyErr_SetString(PyExc_ValueError, "Expected a boolean value"); return -1; } + + // Sync with TCOD map if parent grid exists + if (self->data->parent_grid && self->data->grid_x >= 0 && self->data->grid_y >= 0) { + self->data->parent_grid->syncTCODMapCell(self->data->grid_x, self->data->grid_y); + } + return 0; } diff --git a/src/UIGridPoint.h b/src/UIGridPoint.h index 888c387..d02ad31 100644 --- a/src/UIGridPoint.h +++ b/src/UIGridPoint.h @@ -40,6 +40,8 @@ public: sf::Color color, color_overlay; bool walkable, transparent; int tilesprite, tile_overlay, uisprite; + int grid_x, grid_y; // Position in parent grid + UIGrid* parent_grid; // Parent grid reference for TCOD sync UIGridPoint(); static int set_int_member(PyUIGridPointObject* self, PyObject* value, 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/check_entity_attrs.py b/tests/check_entity_attrs.py new file mode 100644 index 0000000..d0a44b8 --- /dev/null +++ b/tests/check_entity_attrs.py @@ -0,0 +1,4 @@ +import mcrfpy +e = mcrfpy.Entity(0, 0) +print("Entity attributes:", dir(e)) +print("\nEntity repr:", repr(e)) \ No newline at end of file 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 new file mode 100644 index 0000000..fdf2176 --- /dev/null +++ b/tests/dijkstra_interactive.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +""" +Dijkstra Pathfinding Interactive Demo +===================================== + +Interactive visualization showing Dijkstra pathfinding between entities. + +Controls: +- Press 1/2/3 to select the first entity +- Press A/B/C to select the second entity +- Space to clear selection +- Q or ESC to quit + +The path between selected entities is automatically highlighted. +""" + +import mcrfpy +import sys + +# Colors - using more distinct values +WALL_COLOR = mcrfpy.Color(60, 30, 30) +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 + mcrfpy.Color(100, 100, 255), # Entity 3 - Blue +] + +# Global state +grid = None +entities = [] +first_point = None +second_point = None + +def create_map(): + """Create the interactive map with the layout specified by the user""" + global grid, entities + + mcrfpy.createScene("dijkstra_interactive") + + # Create grid - 14x10 as specified + grid = mcrfpy.Grid(grid_x=14, grid_y=10) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Define the map layout from user's specification + # . = floor, W = wall, E = entity position + 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': + # Wall + cell.walkable = False + cell.transparent = False + cell.color = WALL_COLOR + else: + # Floor + cell.walkable = True + cell.transparent = True + cell.color = FLOOR_COLOR + + if char == 'E': + # Entity position + entity_positions.append((x, y)) + + # Create entities at marked positions + 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) + + return grid + +def clear_path_highlight(): + """Clear any existing path highlighting""" + # Reset all floor tiles to original color + 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 + +def highlight_path(): + """Highlight the path between selected entities""" + if first_point is None or second_point is None: + return + + # Clear previous highlighting + clear_path_highlight() + + # Get entities + entity1 = entities[first_point] + entity2 = entities[second_point] + + # Compute Dijkstra from first entity + grid.compute_dijkstra(int(entity1.x), int(entity1.y)) + + # Get path to second entity + path = grid.get_dijkstra_path(int(entity2.x), int(entity2.y)) + + if path: + # Highlight the path + for x, y in path: + cell = grid.at(x, y) + if cell.walkable: + cell.color = PATH_COLOR + + # Also highlight start and end with entity colors + grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point] + grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point] + + # Update info + distance = grid.get_dijkstra_distance(int(entity2.x), int(entity2.y)) + info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps, {distance:.1f} units" + else: + info_text.text = f"No path between Entity {first_point+1} and Entity {second_point+1}" + +def handle_keypress(scene_name, keycode): + """Handle keyboard input""" + global first_point, second_point + + # Number keys for first entity + if keycode == 49: # '1' + first_point = 0 + status_text.text = f"First: Entity 1 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + elif keycode == 50: # '2' + first_point = 1 + status_text.text = f"First: Entity 2 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + elif keycode == 51: # '3' + first_point = 2 + status_text.text = f"First: Entity 3 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + + # Letter keys for second entity + elif keycode == 65 or keycode == 97: # 'A' or 'a' + second_point = 0 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 1" + highlight_path() + elif keycode == 66 or keycode == 98: # 'B' or 'b' + second_point = 1 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 2" + highlight_path() + elif keycode == 67 or keycode == 99: # 'C' or 'c' + second_point = 2 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 3" + highlight_path() + + # Clear selection + elif keycode == 32: # Space + first_point = None + second_point = None + clear_path_highlight() + status_text.text = "Press 1/2/3 for first entity, A/B/C for second" + info_text.text = "Space to clear, Q to quit" + + # Quit + elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC + print("\nExiting Dijkstra interactive demo...") + sys.exit(0) + +# Create the visualization +print("Dijkstra Pathfinding Interactive Demo") +print("=====================================") +print("Controls:") +print(" 1/2/3 - Select first entity") +print(" A/B/C - Select second entity") +print(" Space - Clear selection") +print(" Q/ESC - Quit") + +# Create map +grid = create_map() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_interactive") +ui.append(grid) + +# Scale and position grid for better visibility +grid.size = (560, 400) # 14*40, 10*40 +grid.position = (120, 60) + +# Add title +title = mcrfpy.Caption("Dijkstra Pathfinding Interactive", 250, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add status text +status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480) +status_text.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(status_text) + +# Add info text +info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500) +info_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(info_text) + +# Add legend +legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 540) +legend1.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend1) + +legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 560) +legend2.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend2) + +# Mark entity positions with colored indicators +for i, entity in enumerate(entities): + marker = mcrfpy.Caption(str(i+1), + 120 + int(entity.x) * 40 + 15, + 60 + int(entity.y) * 40 + 10) + marker.fill_color = ENTITY_COLORS[i] + marker.outline = 1 + marker.outline_color = mcrfpy.Color(0, 0, 0) + ui.append(marker) + +# Set up input handling +mcrfpy.keypressScene(handle_keypress) + +# Show the scene +mcrfpy.setScene("dijkstra_interactive") + +print("\nVisualization ready!") +print("Entities are at:") +for i, entity in enumerate(entities): + print(f" Entity {i+1}: ({int(entity.x)}, {int(entity.y)})") \ No newline at end of file diff --git a/tests/dijkstra_interactive_enhanced.py b/tests/dijkstra_interactive_enhanced.py new file mode 100644 index 0000000..34da805 --- /dev/null +++ b/tests/dijkstra_interactive_enhanced.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +""" +Enhanced Dijkstra Pathfinding Interactive Demo +============================================== + +Interactive visualization with entity pathfinding animations. + +Controls: +- Press 1/2/3 to select the first entity +- Press A/B/C to select the second entity +- Space to clear selection +- M to make selected entity move along path +- P to pause/resume animation +- R to reset entity positions +- Q or ESC to quit +""" + +import mcrfpy +import sys +import math + +# Colors +WALL_COLOR = mcrfpy.Color(60, 30, 30) +FLOOR_COLOR = mcrfpy.Color(200, 200, 220) +PATH_COLOR = mcrfpy.Color(200, 250, 220) +VISITED_COLOR = mcrfpy.Color(180, 230, 200) +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 +current_path = [] +animating = False +animation_progress = 0.0 +animation_speed = 2.0 # cells per second +original_positions = [] # Store original entity positions + +def create_map(): + """Create the interactive map with the layout specified by the user""" + global grid, entities, original_positions + + mcrfpy.createScene("dijkstra_enhanced") + + # Create grid - 14x10 as specified + grid = mcrfpy.Grid(grid_x=14, grid_y=10) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Define the map layout from user's specification + # . = floor, W = wall, E = entity position + 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': + # Wall + cell.walkable = False + cell.transparent = False + cell.color = WALL_COLOR + else: + # Floor + cell.walkable = True + cell.transparent = True + cell.color = FLOOR_COLOR + + if char == 'E': + # Entity position + entity_positions.append((x, y)) + + # Create entities at marked positions + entities = [] + original_positions = [] + 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) + original_positions.append((x, y)) + + return grid + +def clear_path_highlight(): + """Clear any existing path highlighting""" + global current_path + + # Reset all floor tiles to original color + 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 highlight_path(): + """Highlight the path between selected entities using entity.path_to()""" + global current_path + + if first_point is None or second_point is None: + return + + # Clear previous highlighting + clear_path_highlight() + + # Get entities + entity1 = entities[first_point] + entity2 = entities[second_point] + + # Use the new path_to method! + path = entity1.path_to(int(entity2.x), int(entity2.y)) + + if path: + current_path = path + + # Highlight the path + for i, (x, y) in enumerate(path): + cell = grid.at(x, y) + if cell.walkable: + # Use gradient for path visualization + if i < len(path) - 1: + cell.color = PATH_COLOR + else: + cell.color = VISITED_COLOR + + # Highlight start and end with entity colors + grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point] + grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point] + + # Update info + info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps" + else: + info_text.text = f"No path between Entity {first_point+1} and Entity {second_point+1}" + current_path = [] + +def animate_movement(dt): + """Animate entity movement along path""" + global animation_progress, animating, current_path + + if not animating or not current_path or first_point is None: + return + + entity = entities[first_point] + + # Update animation progress + animation_progress += animation_speed * dt + + # Calculate current position along path + path_index = int(animation_progress) + + if path_index >= len(current_path): + # Animation complete + animating = False + animation_progress = 0.0 + # Snap to final position + if current_path: + final_x, final_y = current_path[-1] + entity.x = float(final_x) + entity.y = float(final_y) + return + + # Interpolate between path points + if path_index < len(current_path) - 1: + curr_x, curr_y = current_path[path_index] + next_x, next_y = current_path[path_index + 1] + + # Calculate interpolation factor + t = animation_progress - path_index + + # Smooth interpolation + entity.x = curr_x + (next_x - curr_x) * t + entity.y = curr_y + (next_y - curr_y) * t + else: + # At last point + entity.x, entity.y = current_path[path_index] + +def handle_keypress(scene_name, keycode): + """Handle keyboard input""" + global first_point, second_point, animating, animation_progress + + # Number keys for first entity + if keycode == 49: # '1' + first_point = 0 + status_text.text = f"First: Entity 1 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + elif keycode == 50: # '2' + first_point = 1 + status_text.text = f"First: Entity 2 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + elif keycode == 51: # '3' + first_point = 2 + status_text.text = f"First: Entity 3 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}" + highlight_path() + + # Letter keys for second entity + elif keycode == 65 or keycode == 97: # 'A' or 'a' + second_point = 0 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 1" + highlight_path() + elif keycode == 66 or keycode == 98: # 'B' or 'b' + second_point = 1 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 2" + highlight_path() + elif keycode == 67 or keycode == 99: # 'C' or 'c' + second_point = 2 + status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 3" + highlight_path() + + # Movement control + elif keycode == 77 or keycode == 109: # 'M' or 'm' + if current_path and first_point is not None: + animating = True + animation_progress = 0.0 + control_text.text = "Animation: MOVING (press P to pause)" + + # Pause/Resume + elif keycode == 80 or keycode == 112: # 'P' or 'p' + animating = not animating + control_text.text = f"Animation: {'MOVING' if animating else 'PAUSED'} (press P to {'pause' if animating else 'resume'})" + + # Reset positions + elif keycode == 82 or keycode == 114: # 'R' or 'r' + animating = False + animation_progress = 0.0 + for i, entity in enumerate(entities): + entity.x, entity.y = original_positions[i] + control_text.text = "Entities reset to original positions" + highlight_path() # Re-highlight path after reset + + # Clear selection + elif keycode == 32: # Space + first_point = None + second_point = None + animating = False + animation_progress = 0.0 + clear_path_highlight() + status_text.text = "Press 1/2/3 for first entity, A/B/C for second" + info_text.text = "Space to clear, Q to quit" + control_text.text = "Press M to move, P to pause, R to reset" + + # Quit + elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC + print("\nExiting enhanced Dijkstra demo...") + sys.exit(0) + +# Timer callback for animation +def update_animation(dt): + """Update animation state""" + animate_movement(dt / 1000.0) # Convert ms to seconds + +# Create the visualization +print("Enhanced Dijkstra Pathfinding Demo") +print("==================================") +print("Controls:") +print(" 1/2/3 - Select first entity") +print(" A/B/C - Select second entity") +print(" M - Move first entity along path") +print(" P - Pause/Resume animation") +print(" R - Reset entity positions") +print(" Space - Clear selection") +print(" Q/ESC - Quit") + +# Create map +grid = create_map() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_enhanced") +ui.append(grid) + +# Scale and position grid for better visibility +grid.size = (560, 400) # 14*40, 10*40 +grid.position = (120, 60) + +# Add title +title = mcrfpy.Caption("Enhanced Dijkstra Pathfinding", 250, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add status text +status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480) +status_text.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(status_text) + +# Add info text +info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500) +info_text.fill_color = mcrfpy.Color(200, 200, 200) +ui.append(info_text) + +# Add control text +control_text = mcrfpy.Caption("Press M to move, P to pause, R to reset", 120, 520) +control_text.fill_color = mcrfpy.Color(150, 200, 150) +ui.append(control_text) + +# Add legend +legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 560) +legend1.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend1) + +legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 580) +legend2.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend2) + +# Mark entity positions with colored indicators +for i, entity in enumerate(entities): + marker = mcrfpy.Caption(str(i+1), + 120 + int(entity.x) * 40 + 15, + 60 + int(entity.y) * 40 + 10) + marker.fill_color = ENTITY_COLORS[i] + marker.outline = 1 + marker.outline_color = mcrfpy.Color(0, 0, 0) + ui.append(marker) + +# Set up input handling +mcrfpy.keypressScene(handle_keypress) + +# Set up animation timer (60 FPS) +mcrfpy.setTimer("animation", update_animation, 16) + +# Show the scene +mcrfpy.setScene("dijkstra_enhanced") + +print("\nVisualization ready!") +print("Entities are at:") +for i, entity in enumerate(entities): + print(f" Entity {i+1}: ({int(entity.x)}, {int(entity.y)})") \ No newline at end of file diff --git a/tests/dijkstra_test.py b/tests/dijkstra_test.py new file mode 100644 index 0000000..9f99eeb --- /dev/null +++ b/tests/dijkstra_test.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +Dijkstra Pathfinding Test - Headless +==================================== + +Tests all Dijkstra functionality and generates a screenshot. +""" + +import mcrfpy +from mcrfpy import automation +import sys + +def create_test_map(): + """Create a test map with obstacles""" + mcrfpy.createScene("dijkstra_test") + + # Create grid + grid = mcrfpy.Grid(grid_x=20, grid_y=12) + grid.fill_color = mcrfpy.Color(0, 0, 0) + + # Initialize all cells as walkable floor + for y in range(12): + for x in range(20): + grid.at(x, y).walkable = True + grid.at(x, y).transparent = True + grid.at(x, y).color = mcrfpy.Color(200, 200, 220) + + # Add walls to create interesting paths + walls = [ + # Vertical wall in the middle + (10, 1), (10, 2), (10, 3), (10, 4), (10, 5), (10, 6), (10, 7), (10, 8), + # Horizontal walls + (2, 6), (3, 6), (4, 6), (5, 6), (6, 6), + (14, 6), (15, 6), (16, 6), (17, 6), + # Some scattered obstacles + (5, 2), (15, 2), (5, 9), (15, 9) + ] + + for x, y in walls: + grid.at(x, y).walkable = False + grid.at(x, y).color = mcrfpy.Color(60, 30, 30) + + # Place test entities + entities = [] + positions = [(2, 2), (17, 2), (9, 10)] + colors = [ + mcrfpy.Color(255, 100, 100), # Red + mcrfpy.Color(100, 255, 100), # Green + mcrfpy.Color(100, 100, 255) # Blue + ] + + for i, (x, y) in enumerate(positions): + entity = mcrfpy.Entity(x, y) + entity.sprite_index = 49 + i # '1', '2', '3' + grid.entities.append(entity) + entities.append(entity) + # Mark entity positions + grid.at(x, y).color = colors[i] + + return grid, entities + +def test_dijkstra(grid, entities): + """Test Dijkstra pathfinding between all entity pairs""" + results = [] + + for i in range(len(entities)): + for j in range(len(entities)): + if i != j: + # Compute Dijkstra from entity i + e1 = entities[i] + e2 = entities[j] + grid.compute_dijkstra(int(e1.x), int(e1.y)) + + # Get distance and path to entity j + distance = grid.get_dijkstra_distance(int(e2.x), int(e2.y)) + path = grid.get_dijkstra_path(int(e2.x), int(e2.y)) + + if path: + results.append(f"Path {i+1}→{j+1}: {len(path)} steps, {distance:.1f} units") + + # Color one interesting path + if i == 0 and j == 2: # Path from 1 to 3 + for x, y in path[1:-1]: # Skip endpoints + if grid.at(x, y).walkable: + grid.at(x, y).color = mcrfpy.Color(200, 250, 220) + else: + results.append(f"Path {i+1}→{j+1}: No path found!") + + return results + +def run_test(runtime): + """Timer callback to run tests and take screenshot""" + # Run pathfinding tests + results = test_dijkstra(grid, entities) + + # Update display with results + y_pos = 380 + for result in results: + caption = mcrfpy.Caption(result, 50, y_pos) + caption.fill_color = mcrfpy.Color(200, 200, 200) + ui.append(caption) + y_pos += 20 + + # Take screenshot + mcrfpy.setTimer("screenshot", lambda rt: take_screenshot(), 500) + +def take_screenshot(): + """Take screenshot and exit""" + try: + automation.screenshot("dijkstra_test.png") + print("Screenshot saved: dijkstra_test.png") + except Exception as e: + print(f"Screenshot failed: {e}") + + # Exit + sys.exit(0) + +# Create test map +print("Creating Dijkstra pathfinding test...") +grid, entities = create_test_map() + +# Set up UI +ui = mcrfpy.sceneUI("dijkstra_test") +ui.append(grid) + +# Position and scale grid +grid.position = (50, 50) +grid.size = (500, 300) + +# Add title +title = mcrfpy.Caption("Dijkstra Pathfinding Test", 200, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add legend +legend = mcrfpy.Caption("Red=Entity1 Green=Entity2 Blue=Entity3 Cyan=Path 1→3", 50, 360) +legend.fill_color = mcrfpy.Color(180, 180, 180) +ui.append(legend) + +# Set scene +mcrfpy.setScene("dijkstra_test") + +# Run test after scene loads +mcrfpy.setTimer("test", run_test, 100) + +print("Running Dijkstra tests...") \ No newline at end of file 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/pathfinding_showcase.py b/tests/pathfinding_showcase.py new file mode 100644 index 0000000..d4e082f --- /dev/null +++ b/tests/pathfinding_showcase.py @@ -0,0 +1,373 @@ +#!/usr/bin/env python3 +""" +Pathfinding Showcase Demo +========================= + +Demonstrates various pathfinding scenarios with multiple entities. + +Features: +- Multiple entities pathfinding simultaneously +- Chase mode: entities pursue targets +- Flee mode: entities avoid threats +- Patrol mode: entities follow waypoints +- Visual debugging: show Dijkstra distance field +""" + +import mcrfpy +import sys +import random + +# Colors +WALL_COLOR = mcrfpy.Color(40, 40, 40) +FLOOR_COLOR = mcrfpy.Color(220, 220, 240) +PATH_COLOR = mcrfpy.Color(180, 250, 180) +THREAT_COLOR = mcrfpy.Color(255, 100, 100) +GOAL_COLOR = mcrfpy.Color(100, 255, 100) +DIJKSTRA_COLORS = [ + mcrfpy.Color(50, 50, 100), # Far + mcrfpy.Color(70, 70, 150), + mcrfpy.Color(90, 90, 200), + mcrfpy.Color(110, 110, 250), + mcrfpy.Color(150, 150, 255), + mcrfpy.Color(200, 200, 255), # Near +] + +# Entity types +PLAYER = 64 # @ +ENEMY = 69 # E +TREASURE = 36 # $ +PATROL = 80 # P + +# Global state +grid = None +player = None +enemies = [] +treasures = [] +patrol_entities = [] +mode = "CHASE" +show_dijkstra = False +animation_speed = 3.0 + +def create_dungeon(): + """Create a dungeon-like map""" + global grid + + mcrfpy.createScene("pathfinding_showcase") + + # Create larger grid for showcase + 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).transparent = True + grid.at(x, y).color = FLOOR_COLOR + + # Create rooms and corridors + rooms = [ + (2, 2, 8, 6), # Top-left room + (20, 2, 8, 6), # Top-right room + (11, 8, 8, 6), # Center room + (2, 14, 8, 5), # Bottom-left room + (20, 14, 8, 5), # Bottom-right room + ] + + # Create room walls + for rx, ry, rw, rh in rooms: + # Top and bottom walls + for x in range(rx, rx + rw): + if 0 <= x < 30: + grid.at(x, ry).walkable = False + grid.at(x, ry).color = WALL_COLOR + grid.at(x, ry + rh - 1).walkable = False + grid.at(x, ry + rh - 1).color = WALL_COLOR + + # Left and right walls + for y in range(ry, ry + rh): + if 0 <= y < 20: + grid.at(rx, y).walkable = False + grid.at(rx, y).color = WALL_COLOR + grid.at(rx + rw - 1, y).walkable = False + grid.at(rx + rw - 1, y).color = WALL_COLOR + + # Create doorways + doorways = [ + (6, 2), (24, 2), # Top room doors + (6, 7), (24, 7), # Top room doors bottom + (15, 8), (15, 13), # Center room doors + (6, 14), (24, 14), # Bottom room doors + (11, 11), (18, 11), # Center room side doors + ] + + for x, y in doorways: + if 0 <= x < 30 and 0 <= y < 20: + grid.at(x, y).walkable = True + grid.at(x, y).color = FLOOR_COLOR + + # Add some corridors + # Horizontal corridors + for x in range(10, 20): + grid.at(x, 5).walkable = True + grid.at(x, 5).color = FLOOR_COLOR + grid.at(x, 16).walkable = True + grid.at(x, 16).color = FLOOR_COLOR + + # Vertical corridors + for y in range(5, 17): + grid.at(10, y).walkable = True + grid.at(10, y).color = FLOOR_COLOR + grid.at(19, y).walkable = True + grid.at(19, y).color = FLOOR_COLOR + +def spawn_entities(): + """Spawn various entity types""" + global player, enemies, treasures, patrol_entities + + # Clear existing entities + grid.entities.clear() + enemies = [] + treasures = [] + patrol_entities = [] + + # Spawn player in center room + player = mcrfpy.Entity(15, 11) + player.sprite_index = PLAYER + grid.entities.append(player) + + # Spawn enemies in corners + enemy_positions = [(4, 4), (24, 4), (4, 16), (24, 16)] + for x, y in enemy_positions: + enemy = mcrfpy.Entity(x, y) + enemy.sprite_index = ENEMY + grid.entities.append(enemy) + enemies.append(enemy) + + # Spawn treasures + treasure_positions = [(6, 5), (24, 5), (15, 10)] + for x, y in treasure_positions: + treasure = mcrfpy.Entity(x, y) + treasure.sprite_index = TREASURE + grid.entities.append(treasure) + treasures.append(treasure) + + # Spawn patrol entities + patrol = mcrfpy.Entity(10, 10) + patrol.sprite_index = PATROL + patrol.waypoints = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol + patrol.waypoint_index = 0 + grid.entities.append(patrol) + patrol_entities.append(patrol) + +def visualize_dijkstra(target_x, target_y): + """Visualize Dijkstra distance field""" + if not show_dijkstra: + return + + # Compute Dijkstra from target + grid.compute_dijkstra(target_x, target_y) + + # Color tiles based on distance + max_dist = 30.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: + # Map distance to color index + color_idx = int((dist / max_dist) * len(DIJKSTRA_COLORS)) + color_idx = min(color_idx, len(DIJKSTRA_COLORS) - 1) + grid.at(x, y).color = DIJKSTRA_COLORS[color_idx] + +def move_enemies(dt): + """Move enemies based on current mode""" + if mode == "CHASE": + # Enemies chase player + for enemy in enemies: + path = enemy.path_to(int(player.x), int(player.y)) + if path and len(path) > 1: # Don't move onto player + # Move towards player + next_x, next_y = path[1] + # Smooth movement + dx = next_x - enemy.x + dy = next_y - enemy.y + enemy.x += dx * dt * animation_speed + enemy.y += dy * dt * animation_speed + + elif mode == "FLEE": + # Enemies flee from player + for enemy in enemies: + # Compute opposite direction + dx = enemy.x - player.x + dy = enemy.y - player.y + + # Find safe spot in that direction + target_x = int(enemy.x + dx * 2) + target_y = int(enemy.y + dy * 2) + + # Clamp to grid + target_x = max(0, min(29, target_x)) + target_y = max(0, min(19, target_y)) + + path = enemy.path_to(target_x, target_y) + if path and len(path) > 0: + next_x, next_y = path[0] + # Move away from player + dx = next_x - enemy.x + dy = next_y - enemy.y + enemy.x += dx * dt * animation_speed + enemy.y += dy * dt * animation_speed + +def move_patrols(dt): + """Move patrol entities along waypoints""" + for patrol in patrol_entities: + if not hasattr(patrol, 'waypoints'): + continue + + # Get current waypoint + target_x, target_y = patrol.waypoints[patrol.waypoint_index] + + # Check if reached waypoint + dist = abs(patrol.x - target_x) + abs(patrol.y - target_y) + if dist < 0.5: + # Move to next waypoint + patrol.waypoint_index = (patrol.waypoint_index + 1) % len(patrol.waypoints) + target_x, target_y = patrol.waypoints[patrol.waypoint_index] + + # Path to waypoint + path = patrol.path_to(target_x, target_y) + if path and len(path) > 0: + next_x, next_y = path[0] + dx = next_x - patrol.x + dy = next_y - patrol.y + patrol.x += dx * dt * animation_speed * 0.5 # Slower patrol speed + patrol.y += dy * dt * animation_speed * 0.5 + +def update_entities(dt): + """Update all entity movements""" + move_enemies(dt / 1000.0) # Convert to seconds + move_patrols(dt / 1000.0) + + # Update Dijkstra visualization + if show_dijkstra and player: + visualize_dijkstra(int(player.x), int(player.y)) + +def handle_keypress(scene_name, keycode): + """Handle keyboard input""" + global mode, show_dijkstra, player + + # Mode switching + if keycode == 49: # '1' + mode = "CHASE" + mode_text.text = "Mode: CHASE - Enemies pursue player" + clear_colors() + elif keycode == 50: # '2' + mode = "FLEE" + mode_text.text = "Mode: FLEE - Enemies avoid player" + clear_colors() + elif keycode == 51: # '3' + mode = "PATROL" + mode_text.text = "Mode: PATROL - Entities follow waypoints" + clear_colors() + + # Toggle Dijkstra visualization + elif keycode == 68 or keycode == 100: # 'D' or 'd' + show_dijkstra = not show_dijkstra + debug_text.text = f"Dijkstra Debug: {'ON' if show_dijkstra else 'OFF'}" + if not show_dijkstra: + clear_colors() + + # Move player with arrow keys or WASD + elif keycode in [87, 119]: # W/w - Up + if player.y > 0: + path = player.path_to(int(player.x), int(player.y) - 1) + if path: + player.y -= 1 + elif keycode in [83, 115]: # S/s - Down + if player.y < 19: + path = player.path_to(int(player.x), int(player.y) + 1) + if path: + player.y += 1 + elif keycode in [65, 97]: # A/a - Left + if player.x > 0: + path = player.path_to(int(player.x) - 1, int(player.y)) + if path: + player.x -= 1 + elif keycode in [68, 100]: # D/d - Right + if player.x < 29: + path = player.path_to(int(player.x) + 1, int(player.y)) + if path: + player.x += 1 + + # Reset + elif keycode == 82 or keycode == 114: # 'R' or 'r' + spawn_entities() + clear_colors() + + # Quit + elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC + print("\nExiting pathfinding showcase...") + sys.exit(0) + +def clear_colors(): + """Reset floor colors""" + for y in range(20): + for x in range(30): + if grid.at(x, y).walkable: + grid.at(x, y).color = FLOOR_COLOR + +# Create the showcase +print("Pathfinding Showcase Demo") +print("=========================") +print("Controls:") +print(" WASD - Move player") +print(" 1 - Chase mode (enemies pursue)") +print(" 2 - Flee mode (enemies avoid)") +print(" 3 - Patrol mode") +print(" D - Toggle Dijkstra visualization") +print(" R - Reset entities") +print(" Q/ESC - Quit") + +# Create dungeon +create_dungeon() +spawn_entities() + +# Set up UI +ui = mcrfpy.sceneUI("pathfinding_showcase") +ui.append(grid) + +# Scale and position +grid.size = (750, 500) # 30*25, 20*25 +grid.position = (25, 60) + +# Add title +title = mcrfpy.Caption("Pathfinding Showcase", 300, 10) +title.fill_color = mcrfpy.Color(255, 255, 255) +ui.append(title) + +# Add mode text +mode_text = mcrfpy.Caption("Mode: CHASE - Enemies pursue player", 25, 580) +mode_text.fill_color = mcrfpy.Color(255, 255, 200) +ui.append(mode_text) + +# Add debug text +debug_text = mcrfpy.Caption("Dijkstra Debug: OFF", 25, 600) +debug_text.fill_color = mcrfpy.Color(200, 200, 255) +ui.append(debug_text) + +# Add legend +legend = mcrfpy.Caption("@ Player E Enemy $ Treasure P Patrol", 25, 620) +legend.fill_color = mcrfpy.Color(150, 150, 150) +ui.append(legend) + +# Set up input handling +mcrfpy.keypressScene(handle_keypress) + +# Set up animation timer +mcrfpy.setTimer("entities", update_entities, 16) # 60 FPS + +# Show scene +mcrfpy.setScene("pathfinding_showcase") + +print("\nShowcase ready! Move with WASD and watch entities react.") \ No newline at end of file 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 diff --git a/tests/test_pathfinding_integration.py b/tests/test_pathfinding_integration.py new file mode 100644 index 0000000..8f779f6 --- /dev/null +++ b/tests/test_pathfinding_integration.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Test pathfinding integration with demos""" + +import mcrfpy +import sys + +print("Testing pathfinding integration...") +print("=" * 50) + +# Create scene and grid +mcrfpy.createScene("test") +grid = mcrfpy.Grid(grid_x=10, grid_y=10) + +# Initialize grid +for y in range(10): + for x in range(10): + grid.at(x, y).walkable = True + +# Add some walls +for i in range(5): + grid.at(5, i + 2).walkable = False + +# Create entities +e1 = mcrfpy.Entity(2, 5) +e2 = mcrfpy.Entity(8, 5) +grid.entities.append(e1) +grid.entities.append(e2) + +# Test pathfinding between entities +print(f"Entity 1 at ({e1.x}, {e1.y})") +print(f"Entity 2 at ({e2.x}, {e2.y})") + +# Entity 1 finds path to Entity 2 +path = e1.path_to(int(e2.x), int(e2.y)) +print(f"\nPath from E1 to E2: {path}") +print(f"Path length: {len(path)} steps") + +# Test movement simulation +if path and len(path) > 1: + print("\nSimulating movement along path:") + for i, (x, y) in enumerate(path[:5]): # Show first 5 steps + print(f" Step {i}: Move to ({x}, {y})") + +# Test path in reverse +path_reverse = e2.path_to(int(e1.x), int(e1.y)) +print(f"\nPath from E2 to E1: {path_reverse}") +print(f"Reverse path length: {len(path_reverse)} steps") + +print("\n✓ Pathfinding integration working correctly!") +print("Enhanced demos are ready for interactive use.") + +# Quick animation test +def test_timer(dt): + print(f"Timer callback received: dt={dt}ms") + sys.exit(0) + +# Set a quick timer to test animation system +mcrfpy.setTimer("test", test_timer, 100) + +print("\nTesting timer system for animations...") \ No newline at end of file