diff --git a/.gitignore b/.gitignore index 174f159..a00ca39 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ obj build lib obj -__pycache__ .cache/ 7DRL2025 Release/ @@ -28,5 +27,3 @@ forest_fire_CA.py mcrogueface.github.io scripts/ test_* - -tcod_reference diff --git a/ROADMAP.md b/ROADMAP.md index 4d00996..d5040b6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -22,19 +22,16 @@ - Mass entity choreography (100+ entities) - Performance stress test with 1000+ entities -#### 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 +#### 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 #### 3. Performance Critical Path - [ ] **Implement SpatialHash** for 10,000+ entities (2 hours) @@ -124,44 +121,6 @@ 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 deleted file mode 100644 index d33326e..0000000 Binary files a/dijkstra_working.png and /dev/null differ diff --git a/docs/visibility_tracking_example.cpp b/docs/visibility_tracking_example.cpp deleted file mode 100644 index aefe50b..0000000 --- a/docs/visibility_tracking_example.cpp +++ /dev/null @@ -1,342 +0,0 @@ -/** - * 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 deleted file mode 100644 index 00c9de2..0000000 --- a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/game.py +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 0b39a49..0000000 --- a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_0/code/setup_test.py +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 2f0c157..0000000 --- a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_1/code/game.py +++ /dev/null @@ -1,162 +0,0 @@ -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 deleted file mode 100644 index 38eef78..0000000 --- a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_2/code/game.py +++ /dev/null @@ -1,217 +0,0 @@ -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 deleted file mode 100644 index 1256ef9..0000000 --- a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_3/code/game.py +++ /dev/null @@ -1,312 +0,0 @@ -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 deleted file mode 100644 index e5c23da..0000000 --- a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_4/code/game.py +++ /dev/null @@ -1,334 +0,0 @@ -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 deleted file mode 100644 index 3e5947f..0000000 --- a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_5/code/game.py +++ /dev/null @@ -1,388 +0,0 @@ -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 deleted file mode 100644 index b738dcc..0000000 --- a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py +++ /dev/null @@ -1,568 +0,0 @@ -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 5b35d79..0199b37 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -5,7 +5,6 @@ #include "UITestScene.h" #include "Resources.h" #include "Animation.h" -#include GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{}) { @@ -36,8 +35,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) // Initialize the game view gameView.setSize(static_cast(gameResolution.x), static_cast(gameResolution.y)); - // Use integer center coordinates for pixel-perfect rendering - gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f)); + gameView.setCenter(gameResolution.x / 2.0f, gameResolution.y / 2.0f); updateViewport(); scene = "uitest"; scenes["uitest"] = new UITestScene(this); @@ -419,8 +417,7 @@ 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)); - // Use integer center coordinates for pixel-perfect rendering - gameView.setCenter(std::floor(width / 2.0f), std::floor(height / 2.0f)); + gameView.setCenter(width / 2.0f, height / 2.0f); updateViewport(); } @@ -449,9 +446,8 @@ 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)); - // 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); + float offsetX = (windowSize.x - viewportWidth) / 2.0f; + float offsetY = (windowSize.y - viewportHeight) / 2.0f; gameView.setViewport(sf::FloatRect( offsetX / windowSize.x, @@ -478,21 +474,13 @@ 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 = pixelWidth / windowSize.x; + viewportWidth = gameAspect / windowAspect; 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 = pixelHeight / windowSize.y; + viewportHeight = windowAspect / gameAspect; offsetY = (1.0f - viewportHeight) / 2.0f; } diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 2aa7905..f759b0a 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -1,6 +1,5 @@ #include "McRFPy_API.h" #include "McRFPy_Automation.h" -#include "McRFPy_Libtcod.h" #include "platform.h" #include "PyAnimation.h" #include "PyDrawable.h" @@ -13,7 +12,6 @@ #include "PyScene.h" #include #include -#include std::vector* McRFPy_API::soundbuffers = nullptr; sf::Music* McRFPy_API::music = nullptr; @@ -289,21 +287,6 @@ 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) { @@ -314,16 +297,6 @@ 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 deleted file mode 100644 index bb5de49..0000000 --- a/src/McRFPy_Libtcod.cpp +++ /dev/null @@ -1,324 +0,0 @@ -#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 deleted file mode 100644 index 8aad75c..0000000 --- a/src/McRFPy_Libtcod.h +++ /dev/null @@ -1,27 +0,0 @@ -#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 631d8af..d4ea3f3 100644 --- a/src/PyTexture.cpp +++ b/src/PyTexture.cpp @@ -2,15 +2,10 @@ #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), sheet_width(0), sheet_height(0) +: source(filename), sprite_width(sprite_w), sprite_height(sprite_h) { texture = sf::Texture(); - 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 + texture.loadFromFile(source); auto size = texture.getSize(); sheet_width = (size.x / sprite_width); sheet_height = (size.y / sprite_height); @@ -23,12 +18,6 @@ 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); @@ -82,16 +71,7 @@ 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 c8a053b..e001db7 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -9,52 +9,16 @@ #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 - will be lazily initialized when needed + // gridstate vector starts empty since we don't know grid dimensions } -// Removed UIEntity(UIGrid&) constructor - using lazy initialization instead - -void UIEntity::updateVisibility() +UIEntity::UIEntity(UIGrid& grid) +: gridstate(grid.grid_x * grid.grid_y) { - 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) { @@ -68,29 +32,17 @@ 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; } - - // 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; - } - + /* + PyUIGridPointStateObject* obj = (PyUIGridPointStateObject*)((&mcrfpydef::PyUIGridPointStateType)->tp_alloc(&mcrfpydef::PyUIGridPointStateType, 0)); + */ auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState"); auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0); - obj->data = &(self->data->gridstate[y * self->data->grid->grid_x + x]); + //auto target = std::static_pointer_cast(target); + 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)) { @@ -214,8 +166,10 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { return -1; } - // Always use default constructor for lazy initialization - self->data = std::make_shared(); + if (grid_obj == NULL) + self->data = std::make_shared(); + else + self->data = std::make_shared(*((PyUIGridObject*)grid_obj)->data); // Store reference to Python object self->data->self = (PyObject*)self; @@ -237,9 +191,6 @@ 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; } @@ -286,26 +237,11 @@ 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) { - // 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; + // This function is incomplete - it creates an empty object without setting state data + // Should use PyObjectUtils::createGridPointState() instead + return PyObjectUtils::createPyObjectGeneric("GridPointState"); } PyObject* UIGridPointStateVector_to_PyList(const std::vector& vec) { @@ -441,75 +377,10 @@ 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} }; @@ -522,8 +393,6 @@ 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 }; @@ -557,12 +426,15 @@ PyObject* UIEntity::repr(PyUIEntityObject* self) { bool UIEntity::setProperty(const std::string& name, float value) { if (name == "x") { position.x = value; - // Don't update sprite position here - UIGrid::render() handles the pixel positioning + // 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)); return true; } else if (name == "y") { position.y = value; - // Don't update sprite position here - UIGrid::render() handles the pixel positioning + // Update sprite position based on grid position + sprite.setPosition(sf::Vector2f(position.x, position.y)); return true; } else if (name == "sprite_scale") { diff --git a/src/UIEntity.h b/src/UIEntity.h index dfd155e..86b7e92 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -27,10 +27,10 @@ class UIGrid; //} PyUIEntityObject; // helper methods with no namespace requirement -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); +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); // TODO: make UIEntity a drawable class UIEntity//: public UIDrawable @@ -44,9 +44,7 @@ public: //void render(sf::Vector2f); //override final; UIEntity(); - - // Visibility methods - void updateVisibility(); // Update gridstate from current FOV + UIEntity(UIGrid&); // Property system for animations bool setProperty(const std::string& name, float value); @@ -61,8 +59,6 @@ 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 e65901e..fe6eec7 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -7,8 +7,7 @@ UIGrid::UIGrid() : grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr), - fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr), - perspective(-1) // Default to omniscient view + fill_color(8, 8, 8, 255) // Default dark gray background { // Initialize entities list entities = std::make_shared>>(); @@ -28,15 +27,13 @@ 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), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr), - perspective(-1) // Default to omniscient view + fill_color(8, 8, 8, 255) // Default dark gray background { // Use texture dimensions if available, otherwise use defaults int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH; @@ -66,27 +63,6 @@ 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() {} @@ -188,55 +164,43 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) } - // 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) + // 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) { - 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 ); - // 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) - } + 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); } + + // overlay + + // uisprite } } + */ // grid lines for testing & validation /* @@ -270,155 +234,11 @@ 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 { @@ -518,53 +338,35 @@ 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; - // 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 + // Case 1: Got grid size and position from helpers (tuple format) + if (grid_size_result.valid) { grid_x = grid_size_result.grid_w; grid_y = grid_size_result.grid_h; - // Try to parse position and size - auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); + // Set position if we got it if (pos_result.valid) { x = pos_result.x; y = pos_result.y; } - auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx); + // Set size if we got it, otherwise calculate default if (size_result.valid) { w = size_result.w; h = size_result.h; } else { - // Default size based on grid dimensions - w = grid_x * 16.0f; + // Default size based on grid dimensions and texture + w = grid_x * 16.0f; // Will be recalculated if texture provided h = grid_y * 16.0f; } @@ -578,8 +380,10 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { &textureObj); Py_DECREF(remaining_args); } - // Traditional format parsing + // Case 2: Traditional format else { + PyErr_Clear(); // Clear any errors from helpers + static const char* keywords[] = { "grid_x", "grid_y", "texture", "pos", "size", "grid_size", nullptr }; @@ -602,13 +406,7 @@ 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; } } @@ -621,13 +419,7 @@ 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; } } @@ -640,13 +432,7 @@ 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 @@ -654,20 +440,17 @@ 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 shared_ptr + // Convert PyObject texture to IndexTexture* + // This requires the texture object to have been initialized similar to UISprite's texture handling + std::shared_ptr texture_ptr = nullptr; - // Allow None or NULL for texture - use default texture in that case - if (textureObj && textureObj != Py_None) { + // Allow None for texture - use default texture in that case + if (textureObj != Py_None) { + //if (!PyObject_IsInstance(textureObj, (PyObject*)&PyTextureType)) { 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; @@ -675,12 +458,16 @@ 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 or texture not specified + // Use default texture when None is provided 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 (texture_ptr && w == grid_x * 16.0f && h == grid_y * 16.0f) { + if (!size_result.valid && texture_ptr) { w = grid_x * texture_ptr->sprite_width; h = grid_y * texture_ptr->sprite_height; } @@ -932,183 +719,8 @@ 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} }; @@ -1119,20 +731,6 @@ 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 }; @@ -1161,7 +759,6 @@ 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, @@ -1504,16 +1101,6 @@ 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 96f41ed..ddbed75 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -5,7 +5,6 @@ #include "IndexTexture.h" #include "Resources.h" #include -#include #include "PyCallable.h" #include "PyTexture.h" @@ -26,15 +25,10 @@ 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); @@ -42,21 +36,6 @@ 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; @@ -77,9 +56,6 @@ 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; @@ -101,16 +77,7 @@ 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 201fb27..e255c3a 100644 --- a/src/UIGridPoint.cpp +++ b/src/UIGridPoint.cpp @@ -1,51 +1,19 @@ #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), grid_x(-1), grid_y(-1), parent_grid(nullptr) + tilesprite(-1), tile_overlay(-1), uisprite(-1) {} // 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); @@ -61,11 +29,6 @@ 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 @@ -99,12 +62,6 @@ 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 d02ad31..888c387 100644 --- a/src/UIGridPoint.h +++ b/src/UIGridPoint.h @@ -40,8 +40,6 @@ 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 f505b75..d3d5ff9 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(); // Default constructor - lazy initialization + auto e5a = std::make_shared(*e5); // this basic constructor sucks: sprite position + zoom are irrelevant for UIEntity. 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 deleted file mode 100644 index 913e913..0000000 --- a/src/scripts/example_text_widgets.py +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 396d82c..0000000 --- a/src/scripts/text_input_widget.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -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 deleted file mode 100644 index 7f7f7b6..0000000 --- a/src/scripts/text_input_widget_improved.py +++ /dev/null @@ -1,265 +0,0 @@ -""" -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 716cded..f12fc70 100644 --- a/tests/animation_demo.py +++ b/tests/animation_demo.py @@ -1,208 +1,165 @@ #!/usr/bin/env python3 -""" -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 -""" +"""Animation System Demo - Shows all animation capabilities""" import mcrfpy -import sys +import math -# Setup scene -mcrfpy.createScene("anim_demo") +# Create main scene +mcrfpy.createScene("animation_demo") +ui = mcrfpy.sceneUI("animation_demo") +mcrfpy.setScene("animation_demo") -# 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) +# 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 ui.append(title) -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) +# 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) -info = mcrfpy.Caption("Perspective: Player", 500, 70) -info.fill_color = mcrfpy.Color(100, 255, 100) +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 ui.append(info) -# 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) +# Schedule animations +mcrfpy.setTimer("start", start_animations, 500) +mcrfpy.setTimer("reverse", reverse_animations, 4000) +mcrfpy.setTimer("cycle", cycle_colors, 2500) -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) +# Exit handler +def on_key(key): + if key == "Escape": + mcrfpy.exit() -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) +mcrfpy.keypressScene(on_key) -# 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 +print("Animation demo started! Press Escape to exit.") \ No newline at end of file diff --git a/tests/astar_vs_dijkstra.py b/tests/astar_vs_dijkstra.py deleted file mode 100644 index 5b93c99..0000000 --- a/tests/astar_vs_dijkstra.py +++ /dev/null @@ -1,235 +0,0 @@ -#!/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 deleted file mode 100644 index d0a44b8..0000000 --- a/tests/check_entity_attrs.py +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 3c26d3c..0000000 --- a/tests/debug_astar_demo.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/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 deleted file mode 100644 index 1485177..0000000 --- a/tests/debug_empty_paths.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/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 deleted file mode 100644 index da0bd60..0000000 --- a/tests/debug_visibility.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/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 deleted file mode 100644 index e205f08..0000000 --- a/tests/dijkstra_all_paths.py +++ /dev/null @@ -1,234 +0,0 @@ -#!/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 deleted file mode 100644 index 201219c..0000000 --- a/tests/dijkstra_cycle_paths.py +++ /dev/null @@ -1,236 +0,0 @@ -#!/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 deleted file mode 100644 index fd182b8..0000000 --- a/tests/dijkstra_debug.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/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 deleted file mode 100644 index 91efc51..0000000 --- a/tests/dijkstra_demo_working.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/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 deleted file mode 100644 index fdf2176..0000000 --- a/tests/dijkstra_interactive.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/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 deleted file mode 100644 index 34da805..0000000 --- a/tests/dijkstra_interactive_enhanced.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/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 deleted file mode 100644 index 9f99eeb..0000000 --- a/tests/dijkstra_test.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/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 deleted file mode 100644 index 3d7aef8..0000000 --- a/tests/interactive_visibility.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/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 deleted file mode 100644 index ee4c804..0000000 --- a/tests/path_vision_fixed.py +++ /dev/null @@ -1,375 +0,0 @@ -#!/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 deleted file mode 100644 index b067b6c..0000000 --- a/tests/path_vision_sizzle_reel.py +++ /dev/null @@ -1,391 +0,0 @@ -#!/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 deleted file mode 100644 index d4e082f..0000000 --- a/tests/pathfinding_showcase.py +++ /dev/null @@ -1,373 +0,0 @@ -#!/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 deleted file mode 100644 index fd95d5a..0000000 --- a/tests/simple_interactive_visibility.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/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 deleted file mode 100644 index 5c20758..0000000 --- a/tests/simple_visibility_test.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/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 deleted file mode 100644 index 8f779f6..0000000 --- a/tests/test_pathfinding_integration.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/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