Compare commits

...

7 Commits

Author SHA1 Message Date
John McCardle 64ffe1f699 Draft tutorials 2025-07-09 22:34:43 -04:00
John McCardle 9be3161a24 docs: update ROADMAP with FOV, A* pathfinding, and GUI text widget completions
- Mark TCOD Integration Sprint as complete
- Document FOV system with perspective rendering implementation
- Update UIEntity pathfinding status to complete with A* and caching
- Add comprehensive achievement entry for July 10 work
- Reflect current engine capabilities accurately

The engine now has all core roguelike mechanics:
- Field of View with per-entity visibility
- Pathfinding (both Dijkstra and A*)
- Text input for in-game consoles
- Performance optimizations throughout

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-09 22:19:38 -04:00
John McCardle d13153ddb4 feat(engine): implement perspective FOV, pathfinding, and GUI text widgets
Major Engine Enhancements:
- Complete FOV (Field of View) system with perspective rendering
  - UIGrid.perspective property for entity-based visibility
  - Three-layer overlay colors (unexplored, explored, visible)
  - Per-entity visibility state tracking
  - Perfect knowledge updates only for explored areas

- Advanced Pathfinding Integration
  - A* pathfinding implementation in UIGrid
  - Entity.path_to() method for direct pathfinding
  - Dijkstra maps for multi-target pathfinding
  - Path caching for performance optimization

- GUI Text Input Widgets
  - TextInputWidget class with cursor, selection, scrolling
  - Improved widget with proper text rendering and input handling
  - Example showcase of multiple text input fields
  - Foundation for in-game console and chat systems

- Performance & Architecture Improvements
  - PyTexture copy operations optimized
  - GameEngine update cycle refined
  - UIEntity property handling enhanced
  - UITestScene modernized

Test Suite:
- Interactive visibility demos showing FOV in action
- Pathfinding comparison (A* vs Dijkstra)
- Debug utilities for visibility and empty path handling
- Sizzle reel demo combining pathfinding and vision
- Multiple text input test scenarios

This commit brings McRogueFace closer to a complete roguelike engine
with essential features like line-of-sight, intelligent pathfinding,
and interactive text input capabilities.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-09 22:18:29 -04:00
John McCardle 051a2ca951 feat(demos): enhance interactive pathfinding demos with entity.path_to()
- dijkstra_interactive_enhanced.py: Animation along paths with smooth movement
  - M key to start movement animation
  - P to pause/resume
  - R to reset positions
  - Visual path gradient for better clarity

- pathfinding_showcase.py: Advanced multi-entity behaviors
  - Chase mode: enemies pursue player
  - Flee mode: enemies avoid player
  - Patrol mode: entities follow waypoints
  - WASD player movement
  - Dijkstra distance field visualization (D key)
  - Larger dungeon map with multiple rooms

- Both demos use new entity.path_to() method
- Smooth interpolated movement animations
- Real-time pathfinding recalculation
- Comprehensive test coverage

These demos showcase the power of integrated pathfinding for game AI.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-09 13:07:33 -04:00
John McCardle 7ee0a08662 feat(entity): implement path_to() method for entity pathfinding
- Add path_to(target_x, target_y) method to UIEntity class
- Uses existing Dijkstra pathfinding implementation from UIGrid
- Returns list of (x, y) coordinate tuples for complete path
- Supports both positional and keyword argument formats
- Proper error handling for out-of-bounds and no-grid scenarios
- Comprehensive test suite covering normal and edge cases

Part of TCOD integration sprint - gives entities immediate pathfinding capabilities.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-09 13:01:03 -04:00
John McCardle 1a3e308c77 docs: update roadmap with Dijkstra pathfinding progress
- Mark UIGrid TCOD Integration as completed
- Document critical PyArg bug fix achievement
- Update UIEntity Pathfinding to 50% complete
- Add detailed progress notes for July 9 sprint work

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-09 12:11:43 -04:00
John McCardle c1e02070f4 feat(tcod): complete Dijkstra pathfinding implementation with critical PyArg fix
- Add complete Dijkstra pathfinding to UIGrid class
  - compute_dijkstra(), get_dijkstra_distance(), get_dijkstra_path()
  - Full TCODMap and TCODDijkstra integration
  - Proper memory management in constructors/destructors

- Create mcrfpy.libtcod submodule with Python bindings
  - dijkstra_compute(), dijkstra_get_distance(), dijkstra_get_path()
  - line() function for drawing corridors
  - Foundation for future FOV and pathfinding algorithms

- Fix critical PyArg bug in UIGridPoint color setter
  - PyObject_to_sfColor() now handles both Color objects and tuples
  - Prevents "SystemError: new style getargs format but argument is not a tuple"
  - Proper error handling and exception propagation

- Add comprehensive test suite
  - test_dijkstra_simple.py validates all pathfinding operations
  - dijkstra_test.py for headless testing with screenshots
  - dijkstra_interactive.py for full user interaction demos

- Consolidate and clean up test files
  - Removed 6 duplicate/broken demo attempts
  - Two clean versions: headless test + interactive demo

Part of TCOD integration sprint for RoguelikeDev Tutorial Event.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-09 12:10:48 -04:00
47 changed files with 7769 additions and 260 deletions

3
.gitignore vendored
View File

@ -9,6 +9,7 @@ obj
build build
lib lib
obj obj
__pycache__
.cache/ .cache/
7DRL2025 Release/ 7DRL2025 Release/
@ -27,3 +28,5 @@ forest_fire_CA.py
mcrogueface.github.io mcrogueface.github.io
scripts/ scripts/
test_* test_*
tcod_reference

View File

@ -22,16 +22,19 @@
- Mass entity choreography (100+ entities) - Mass entity choreography (100+ entities)
- Performance stress test with 1000+ entities - Performance stress test with 1000+ entities
#### 2. TCOD Integration Sprint #### 2. TCOD Integration Sprint ✅ COMPLETE!
- [ ] **UIGrid TCOD Integration** (8 hours) - [x] **UIGrid TCOD Integration** (8 hours) ✅ COMPLETED!
- Add TCODMap* to UIGrid constructor - ✅ Add TCODMap* to UIGrid constructor with proper lifecycle
- Implement mcrfpy.libtcod.compute_fov() - ✅ Implement complete Dijkstra pathfinding system
- Add batch operations for NumPy-style access - ✅ Create mcrfpy.libtcod submodule with Python bindings
- Create CellView for ergonomic .at((x,y)) access - ✅ Fix critical PyArg bug preventing Color object assignments
- [ ] **UIEntity Pathfinding** (4 hours) - ✅ Implement FOV with perspective rendering
- Add path_to(target) method using A* - [ ] Add batch operations for NumPy-style access (deferred)
- Implement Dijkstra maps for multiple targets - [ ] Create CellView for ergonomic .at((x,y)) access (deferred)
- Cache paths in UIEntity for performance - [x] **UIEntity Pathfinding** (4 hours) ✅ COMPLETED!
- ✅ Implement Dijkstra maps for multiple targets in UIGrid
- ✅ Add path_to(target) method using A* to UIEntity
- ✅ Cache paths in UIEntity for performance
#### 3. Performance Critical Path #### 3. Performance Critical Path
- [ ] **Implement SpatialHash** for 10,000+ entities (2 hours) - [ ] **Implement SpatialHash** for 10,000+ entities (2 hours)
@ -121,6 +124,44 @@ entity.can_see(other_entity) # FOV check
## Recent Achievements ## 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! 🔧 ### 2025-07-08: PyArgHelpers Infrastructure Complete! 🔧
**Standardized Python API Argument Parsing** **Standardized Python API Argument Parsing**
- Unified position handling: (x, y) tuples or separate x, y args - Unified position handling: (x, y) tuples or separate x, y args

BIN
dijkstra_working.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -0,0 +1,342 @@
/**
* Example implementation demonstrating the proposed visibility tracking system
* This shows how UIGridPoint, UIGridPointState, and libtcod maps work together
*/
#include <memory>
#include <unordered_map>
#include <vector>
#include <string>
// 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<std::string, std::vector<UIGridPointState>> grid_knowledge;
public:
// Get or create knowledge vector for a specific grid
std::vector<UIGridPointState>& 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<EntityGridKnowledge&>(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<EntityGridKnowledge&>(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<std::pair<int, int>> 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<UIEntity>();
auto dungeon_level1 = std::make_shared<UIGrid>("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<UIEntity>();
guardian->setOmniscient(true); // Knows everything about any grid it enters
auto temple = std::make_shared<UIGrid>("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<UIEntity>();
auto forest = std::make_shared<UIGrid>("forest", 40, 40);
auto cave = std::make_shared<UIGrid>("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<UIEntity>();
auto dungeon = std::make_shared<UIGrid>("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)
*/

View File

@ -0,0 +1,33 @@
import mcrfpy
# Create a new scene called "hello"
mcrfpy.createScene("hello")
# Switch to our new scene
mcrfpy.setScene("hello")
# Get the UI container for our scene
ui = mcrfpy.sceneUI("hello")
# Create a text caption
caption = mcrfpy.Caption("Hello Roguelike!", 400, 300)
caption.font_size = 32
caption.fill_color = mcrfpy.Color(255, 255, 255) # White text
# Add the caption to our scene
ui.append(caption)
# Create a smaller instruction caption
instruction = mcrfpy.Caption("Press ESC to exit", 400, 350)
instruction.font_size = 16
instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
ui.append(instruction)
# Set up a simple key handler
def handle_keys(key, state):
if state == "start" and key == "Escape":
mcrfpy.setScene(None) # This exits the game
mcrfpy.keypressScene(handle_keys)
print("Hello Roguelike is running!")

View File

@ -0,0 +1,55 @@
import mcrfpy
# Create our test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test")
# Create a background frame
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray
ui.append(background)
# Title text
title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100)
title.font_size = 36
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
ui.append(title)
# Status text that will update
status_text = mcrfpy.Caption("Press any key to test input...", 512, 300)
status_text.font_size = 20
status_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status_text)
# Instructions
instructions = [
"Arrow Keys: Test movement input",
"Space: Test action input",
"Mouse Click: Test mouse input",
"ESC: Exit"
]
y_offset = 400
for instruction in instructions:
inst_caption = mcrfpy.Caption(instruction, 512, y_offset)
inst_caption.font_size = 16
inst_caption.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(inst_caption)
y_offset += 30
# Input handler
def handle_input(key, state):
if state != "start":
return
if key == "Escape":
mcrfpy.setScene(None)
else:
status_text.text = f"You pressed: {key}"
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green
# Set up input handling
mcrfpy.keypressScene(handle_input)
print("Setup test is running! Try pressing different keys.")

View File

@ -0,0 +1,162 @@
import mcrfpy
# Window configuration
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 1"
# Get the UI container for our scene
ui = mcrfpy.sceneUI("game")
# Create a dark background
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
ui.append(background)
# Load the ASCII tileset
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
# Create the game grid
GRID_WIDTH = 50
GRID_HEIGHT = 30
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
grid.position = (100, 100)
grid.size = (800, 480)
ui.append(grid)
def create_room():
"""Create a room with walls around the edges"""
# Fill everything with floor tiles first
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.sprite_index = 46 # '.' character
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
# Create walls around the edges
for x in range(GRID_WIDTH):
# Top wall
cell = grid.at(x, 0)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
# Bottom wall
cell = grid.at(x, GRID_HEIGHT - 1)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
for y in range(GRID_HEIGHT):
# Left wall
cell = grid.at(0, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Right wall
cell = grid.at(GRID_WIDTH - 1, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Create the room
create_room()
# Create the player entity
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
player.sprite_index = 64 # '@' character
player.color = mcrfpy.Color(255, 255, 255) # White
def move_player(dx, dy):
"""Move the player if the destination is walkable"""
# Calculate new position
new_x = player.x + dx
new_y = player.y + dy
# Check bounds
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
return
# Check if the destination is walkable
destination = grid.at(new_x, new_y)
if destination.walkable:
# Move the player
player.x = new_x
player.y = new_y
def handle_input(key, state):
"""Handle keyboard input for player movement"""
# Only process key presses, not releases
if state != "start":
return
# Movement deltas
dx, dy = 0, 0
# Arrow keys
if key == "Up":
dy = -1
elif key == "Down":
dy = 1
elif key == "Left":
dx = -1
elif key == "Right":
dx = 1
# Numpad movement (for true roguelike feel!)
elif key == "Num7": # Northwest
dx, dy = -1, -1
elif key == "Num8": # North
dy = -1
elif key == "Num9": # Northeast
dx, dy = 1, -1
elif key == "Num4": # West
dx = -1
elif key == "Num6": # East
dx = 1
elif key == "Num1": # Southwest
dx, dy = -1, 1
elif key == "Num2": # South
dy = 1
elif key == "Num3": # Southeast
dx, dy = 1, 1
# Escape to quit
elif key == "Escape":
mcrfpy.setScene(None)
return
# If there's movement, try to move the player
if dx != 0 or dy != 0:
move_player(dx, dy)
# Register the input handler
mcrfpy.keypressScene(handle_input)
# Add UI elements
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(title)
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(instructions)
status = mcrfpy.Caption("@ You", 100, 600)
status.font_size = 18
status.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status)
print("Part 1: The @ symbol moves!")

View File

@ -0,0 +1,217 @@
import mcrfpy
class GameObject:
"""Base class for all game objects (player, monsters, items)"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount if possible"""
if not self.grid:
return
new_x = self.x + dx
new_y = self.y + dy
self.x = new_x
self.y = new_y
if self._entity:
self._entity.x = new_x
self._entity.y = new_y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
self.fill_with_walls()
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def create_room(self, x1, y1, x2, y2):
"""Carve out a room in the map"""
x1, x2 = min(x1, x2), max(x1, x2)
y1, y2 = min(y1, y2), max(y1, y2)
for y in range(y1, y2 + 1):
for x in range(x1, x2 + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_h(self, x1, x2, y):
"""Create a horizontal tunnel"""
for x in range(min(x1, x2), max(x1, x2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_v(self, y1, y2, x):
"""Create a vertical tunnel"""
for y in range(min(y1, y2), max(y1, y2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
class Engine:
"""Main game engine that manages game state"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 2"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(50, 30)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
self.game_map.create_room(10, 10, 20, 20)
self.game_map.create_room(30, 15, 40, 25)
self.game_map.create_room(15, 22, 25, 28)
self.game_map.create_tunnel_h(20, 30, 15)
self.game_map.create_tunnel_v(20, 22, 20)
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
self.game_map.add_entity(self.player)
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
self.game_map.add_entity(npc)
self.entities.append(npc)
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
self.game_map.add_entity(potion)
self.entities.append(potion)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
else:
target = self.game_map.get_blocking_entity_at(new_x, new_y)
if target:
print(f"You bump into the {target.name}!")
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Create and run the game
engine = Engine()
print("Part 2: Entities and Maps!")

View File

@ -0,0 +1,312 @@
import mcrfpy
import random
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
"""Return the center coordinates of the room"""
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
"""Return the inner area of the room"""
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
"""Return True if this room overlaps with another"""
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
# Generate the coordinates
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
self.carve_tunnel(self.rooms[-1].center, new_room.center)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(30, 30, 40))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 3"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player (before dungeon generation)
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Add player to map
self.game_map.add_entity(self.player)
# Add some monsters in random rooms
for i in range(5):
if i < len(self.game_map.rooms) - 1: # Don't spawn in first room
room = self.game_map.rooms[i + 1]
x, y = room.center
# Create an orc
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
elif key == "Space":
# Regenerate the dungeon
self.regenerate_dungeon()
mcrfpy.keypressScene(handle_keys)
def regenerate_dungeon(self):
"""Generate a new dungeon"""
# Clear existing entities
self.game_map.entities.clear()
self.game_map.rooms.clear()
self.entities.clear()
# Clear the entity list in the grid
if self.game_map.grid:
self.game_map.grid.entities.clear()
# Regenerate
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Re-add player
self.game_map.add_entity(self.player)
# Add new monsters
for i in range(5):
if i < len(self.game_map.rooms) - 1:
room = self.game_map.rooms[i + 1]
x, y = room.center
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Create and run the game
engine = Engine()
print("Part 3: Procedural Dungeon Generation!")
print("Press SPACE to generate a new dungeon")

View File

@ -0,0 +1,334 @@
import mcrfpy
import random
# Color configurations for visibility
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering (0 = first entity = player)
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
self.carve_tunnel(self.rooms[-1].center, new_room.center)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
self.fov_radius = 8
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 4"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Add player to map
self.game_map.add_entity(self.player)
# Add monsters in random rooms
for i in range(10):
if i < len(self.game_map.rooms) - 1:
room = self.game_map.rooms[i + 1]
x, y = room.center
# Randomly offset from center
x += random.randint(-2, 2)
y += random.randint(-2, 2)
# Make sure position is walkable
if self.game_map.grid.at(x, y).walkable:
if i % 2 == 0:
# Create an orc
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
else:
# Create a troll
troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
self.game_map.add_entity(troll)
self.entities.append(troll)
# Initial FOV calculation
self.player.update_fov()
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
elif key == "v":
# Toggle FOV on/off
if self.game_map.grid.perspective == 0:
self.game_map.grid.perspective = -1 # Omniscient
print("FOV disabled - omniscient view")
else:
self.game_map.grid.perspective = 0 # Player perspective
print("FOV enabled - player perspective")
elif key == "Plus" or key == "Equals":
# Increase FOV radius
self.fov_radius = min(self.fov_radius + 1, 20)
self.player._entity.update_fov(radius=self.fov_radius)
print(f"FOV radius: {self.fov_radius}")
elif key == "Minus":
# Decrease FOV radius
self.fov_radius = max(self.fov_radius - 1, 3)
self.player._entity.update_fov(radius=self.fov_radius)
print(f"FOV radius: {self.fov_radius}")
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Field of View", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# FOV indicator
self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100)
self.fov_text.font_size = 14
self.fov_text.fill_color = mcrfpy.Color(150, 200, 255)
self.ui.append(self.fov_text)
# Create and run the game
engine = Engine()
print("Part 4: Field of View!")
print("Press V to toggle FOV on/off")
print("Press +/- to adjust FOV radius")

View File

@ -0,0 +1,388 @@
import mcrfpy
import random
# Color configurations
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
# Actions
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
number_of_enemies = random.randint(0, max_enemies)
enemies_spawned = []
for i in range(number_of_enemies):
# Try to find a valid position
attempts = 10
while attempts > 0:
# Random position within room bounds
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
# Check if position is valid
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
else:
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
game_map.add_entity(enemy)
enemies_spawned.append(enemy)
break
attempts -= 1
return enemies_spawned
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
# First room - place player
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All other rooms - add tunnel and enemies
self.carve_tunnel(self.rooms[-1].center, new_room.center)
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
if self.get_blocking_entity_at(x, y):
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 5"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player,
max_enemies_per_room=2
)
# Add player to map
self.game_map.add_entity(self.player)
# Store reference to all entities
self.entities = [e for e in self.game_map.entities if e != self.player]
# Initial FOV calculation
self.player.update_fov()
def handle_player_turn(self, action):
"""Process the player's action"""
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# We bumped into something!
print(f"You kick the {target.name} in the shins, much to its annoyance!")
self.status_text.text = f"You kick the {target.name}!"
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
self.status_text.text = ""
else:
# Bumped into a wall
self.status_text.text = "Blocked!"
elif isinstance(action, WaitAction):
self.status_text.text = "You wait..."
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
action = None
# Movement keys
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
if dx == 0 and dy == 0:
action = WaitAction()
else:
action = MovementAction(dx, dy)
elif key == "Period":
action = WaitAction()
elif key == "Escape":
mcrfpy.setScene(None)
return
# Process the action
if action:
self.handle_player_turn(action)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Placing Enemies", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Status text
self.status_text = mcrfpy.Caption("", 512, 600)
self.status_text.font_size = 18
self.status_text.fill_color = mcrfpy.Color(255, 200, 200)
self.ui.append(self.status_text)
# Entity count
entity_count = len(self.entities)
count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100)
count_text.font_size = 14
count_text.fill_color = mcrfpy.Color(150, 150, 255)
self.ui.append(count_text)
# Create and run the game
engine = Engine()
print("Part 5: Placing Enemies!")
print("Try bumping into enemies - combat coming in Part 6!")

View File

@ -0,0 +1,568 @@
import mcrfpy
import random
# Color configurations
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
# Message colors
COLOR_PLAYER_ATK = (230, 230, 230)
COLOR_ENEMY_ATK = (255, 200, 200)
COLOR_PLAYER_DIE = (255, 100, 100)
COLOR_ENEMY_DIE = (255, 165, 0)
# Actions
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class MeleeAction(Action):
"""Action for melee attacks"""
def __init__(self, attacker, target):
self.attacker = attacker
self.target = target
def perform(self):
"""Execute the attack"""
if not self.target.is_alive:
return None
damage = self.attacker.power - self.target.defense
if damage > 0:
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
self.target.take_damage(damage)
# Choose color based on attacker
if self.attacker.name == "Player":
color = COLOR_PLAYER_ATK
else:
color = COLOR_ENEMY_ATK
return attack_desc, color
else:
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
return attack_desc, (150, 150, 150)
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name,
blocks=False, hp=0, defense=0, power=0):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
# Combat stats
self.max_hp = hp
self.hp = hp
self.defense = defense
self.power = power
@property
def is_alive(self):
"""Returns True if this entity can act"""
return self.hp > 0
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
def take_damage(self, amount):
"""Apply damage to this entity"""
self.hp -= amount
# Check for death
if self.hp <= 0:
self.die()
def die(self):
"""Handle entity death"""
if self.name == "Player":
# Player death
self.sprite_index = 64 # Stay as @
self.color = (127, 0, 0) # Dark red
if self._entity:
self._entity.color = mcrfpy.Color(127, 0, 0)
else:
# Enemy death
self.sprite_index = 37 # % character for corpse
self.color = (127, 0, 0) # Dark red
self.blocks = False # Corpses don't block
self.name = f"remains of {self.name}"
if self._entity:
self._entity.sprite_index = 37
self._entity.color = mcrfpy.Color(127, 0, 0)
# Entity factories
def create_player(x, y):
"""Create the player entity"""
return GameObject(
x=x, y=y,
sprite_index=64, # @
color=(255, 255, 255),
name="Player",
blocks=True,
hp=30,
defense=2,
power=5
)
def create_orc(x, y):
"""Create an orc enemy"""
return GameObject(
x=x, y=y,
sprite_index=111, # o
color=(63, 127, 63),
name="Orc",
blocks=True,
hp=10,
defense=0,
power=3
)
def create_troll(x, y):
"""Create a troll enemy"""
return GameObject(
x=x, y=y,
sprite_index=84, # T
color=(0, 127, 0),
name="Troll",
blocks=True,
hp=16,
defense=1,
power=4
)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
number_of_enemies = random.randint(0, max_enemies)
enemies_spawned = []
for i in range(number_of_enemies):
attempts = 10
while attempts > 0:
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = create_orc(x, y)
else:
enemy = create_troll(x, y)
game_map.add_entity(enemy)
enemies_spawned.append(enemy)
break
attempts -= 1
return enemies_spawned
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
# First room - place player
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All other rooms - add tunnel and enemies
self.carve_tunnel(self.rooms[-1].center, new_room.center)
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
if self.get_blocking_entity_at(x, y):
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
self.messages = [] # Simple message log
self.max_messages = 5
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 6"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def add_message(self, text, color=(255, 255, 255)):
"""Add a message to the log"""
self.messages.append((text, color))
if len(self.messages) > self.max_messages:
self.messages.pop(0)
self.update_message_display()
def update_message_display(self):
"""Update the message display"""
# Clear old messages
for caption in self.message_captions:
# Remove from UI (McRogueFace doesn't have remove, so we hide it)
caption.text = ""
# Display current messages
for i, (text, color) in enumerate(self.messages):
if i < len(self.message_captions):
self.message_captions[i].text = text
self.message_captions[i].fill_color = mcrfpy.Color(*color)
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = create_player(0, 0)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player,
max_enemies_per_room=2
)
# Add player to map
self.game_map.add_entity(self.player)
# Store reference to all entities
self.entities = [e for e in self.game_map.entities if e != self.player]
# Initial FOV calculation
self.player.update_fov()
# Welcome message
self.add_message("Welcome to the dungeon!", (100, 100, 255))
def handle_player_turn(self, action):
"""Process the player's action"""
if not self.player.is_alive:
return
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# Attack!
attack = MeleeAction(self.player, target)
result = attack.perform()
if result:
text, color = result
self.add_message(text, color)
# Check if target died
if not target.is_alive:
death_msg = f"The {target.name.replace('remains of ', '')} is dead!"
self.add_message(death_msg, COLOR_ENEMY_DIE)
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
elif isinstance(action, WaitAction):
pass # Do nothing
# Enemy turns
self.handle_enemy_turns()
def handle_enemy_turns(self):
"""Let all enemies take their turn"""
for entity in self.entities:
if entity.is_alive:
# Simple AI: if player is adjacent, attack. Otherwise, do nothing.
dx = entity.x - self.player.x
dy = entity.y - self.player.y
distance = abs(dx) + abs(dy)
if distance == 1: # Adjacent to player
attack = MeleeAction(entity, self.player)
result = attack.perform()
if result:
text, color = result
self.add_message(text, color)
# Check if player died
if not self.player.is_alive:
self.add_message("You have died!", COLOR_PLAYER_DIE)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
action = None
# Movement keys
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
if dx == 0 and dy == 0:
action = WaitAction()
else:
action = MovementAction(dx, dy)
elif key == "Period":
action = WaitAction()
elif key == "Escape":
mcrfpy.setScene(None)
return
# Process the action
if action:
self.handle_player_turn(action)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Combat System", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Player stats
self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100)
self.hp_text.font_size = 18
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
self.ui.append(self.hp_text)
# Message log
self.message_captions = []
for i in range(self.max_messages):
caption = mcrfpy.Caption("", 50, 620 + i * 20)
caption.font_size = 14
caption.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(caption)
self.message_captions.append(caption)
# Timer to update HP display
def update_stats(dt):
self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}"
if self.player.hp <= 0:
self.hp_text.fill_color = mcrfpy.Color(127, 0, 0)
elif self.player.hp < self.player.max_hp // 3:
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
else:
self.hp_text.fill_color = mcrfpy.Color(0, 255, 0)
mcrfpy.setTimer("update_stats", update_stats, 100)
# Create and run the game
engine = Engine()
print("Part 6: Combat System!")
print("Attack enemies to defeat them, but watch your HP!")

View File

@ -5,6 +5,7 @@
#include "UITestScene.h" #include "UITestScene.h"
#include "Resources.h" #include "Resources.h"
#include "Animation.h" #include "Animation.h"
#include <cmath>
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{}) GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
{ {
@ -35,7 +36,8 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
// Initialize the game view // Initialize the game view
gameView.setSize(static_cast<float>(gameResolution.x), static_cast<float>(gameResolution.y)); gameView.setSize(static_cast<float>(gameResolution.x), static_cast<float>(gameResolution.y));
gameView.setCenter(gameResolution.x / 2.0f, gameResolution.y / 2.0f); // Use integer center coordinates for pixel-perfect rendering
gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f));
updateViewport(); updateViewport();
scene = "uitest"; scene = "uitest";
scenes["uitest"] = new UITestScene(this); scenes["uitest"] = new UITestScene(this);
@ -417,7 +419,8 @@ void GameEngine::setFramerateLimit(unsigned int limit)
void GameEngine::setGameResolution(unsigned int width, unsigned int height) { void GameEngine::setGameResolution(unsigned int width, unsigned int height) {
gameResolution = sf::Vector2u(width, height); gameResolution = sf::Vector2u(width, height);
gameView.setSize(static_cast<float>(width), static_cast<float>(height)); gameView.setSize(static_cast<float>(width), static_cast<float>(height));
gameView.setCenter(width / 2.0f, height / 2.0f); // Use integer center coordinates for pixel-perfect rendering
gameView.setCenter(std::floor(width / 2.0f), std::floor(height / 2.0f));
updateViewport(); updateViewport();
} }
@ -446,8 +449,9 @@ void GameEngine::updateViewport() {
float viewportWidth = std::min(static_cast<float>(gameResolution.x), static_cast<float>(windowSize.x)); float viewportWidth = std::min(static_cast<float>(gameResolution.x), static_cast<float>(windowSize.x));
float viewportHeight = std::min(static_cast<float>(gameResolution.y), static_cast<float>(windowSize.y)); float viewportHeight = std::min(static_cast<float>(gameResolution.y), static_cast<float>(windowSize.y));
float offsetX = (windowSize.x - viewportWidth) / 2.0f; // Floor offsets to ensure integer pixel alignment
float offsetY = (windowSize.y - viewportHeight) / 2.0f; float offsetX = std::floor((windowSize.x - viewportWidth) / 2.0f);
float offsetY = std::floor((windowSize.y - viewportHeight) / 2.0f);
gameView.setViewport(sf::FloatRect( gameView.setViewport(sf::FloatRect(
offsetX / windowSize.x, offsetX / windowSize.x,
@ -474,13 +478,21 @@ void GameEngine::updateViewport() {
if (windowAspect > gameAspect) { if (windowAspect > gameAspect) {
// Window is wider - black bars on sides // Window is wider - black bars on sides
// Calculate viewport size in pixels and floor for pixel-perfect scaling
float pixelHeight = static_cast<float>(windowSize.y);
float pixelWidth = std::floor(pixelHeight * gameAspect);
viewportHeight = 1.0f; viewportHeight = 1.0f;
viewportWidth = gameAspect / windowAspect; viewportWidth = pixelWidth / windowSize.x;
offsetX = (1.0f - viewportWidth) / 2.0f; offsetX = (1.0f - viewportWidth) / 2.0f;
} else { } else {
// Window is taller - black bars on top/bottom // Window is taller - black bars on top/bottom
// Calculate viewport size in pixels and floor for pixel-perfect scaling
float pixelWidth = static_cast<float>(windowSize.x);
float pixelHeight = std::floor(pixelWidth / gameAspect);
viewportWidth = 1.0f; viewportWidth = 1.0f;
viewportHeight = windowAspect / gameAspect; viewportHeight = pixelHeight / windowSize.y;
offsetY = (1.0f - viewportHeight) / 2.0f; offsetY = (1.0f - viewportHeight) / 2.0f;
} }

View File

@ -1,5 +1,6 @@
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "McRFPy_Automation.h" #include "McRFPy_Automation.h"
#include "McRFPy_Libtcod.h"
#include "platform.h" #include "platform.h"
#include "PyAnimation.h" #include "PyAnimation.h"
#include "PyDrawable.h" #include "PyDrawable.h"
@ -12,6 +13,7 @@
#include "PyScene.h" #include "PyScene.h"
#include <filesystem> #include <filesystem>
#include <cstring> #include <cstring>
#include <libtcod.h>
std::vector<sf::SoundBuffer>* McRFPy_API::soundbuffers = nullptr; std::vector<sf::SoundBuffer>* McRFPy_API::soundbuffers = nullptr;
sf::Music* McRFPy_API::music = nullptr; sf::Music* McRFPy_API::music = nullptr;
@ -287,6 +289,21 @@ PyObject* PyInit_mcrfpy()
PyModule_AddObject(m, "default_font", Py_None); PyModule_AddObject(m, "default_font", Py_None);
PyModule_AddObject(m, "default_texture", 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 // Add automation submodule
PyObject* automation_module = McRFPy_Automation::init_automation_module(); PyObject* automation_module = McRFPy_Automation::init_automation_module();
if (automation_module != NULL) { if (automation_module != NULL) {
@ -297,6 +314,16 @@ PyObject* PyInit_mcrfpy()
PyDict_SetItemString(sys_modules, "mcrfpy.automation", automation_module); PyDict_SetItemString(sys_modules, "mcrfpy.automation", automation_module);
} }
// Add libtcod submodule
PyObject* libtcod_module = McRFPy_Libtcod::init_libtcod_module();
if (libtcod_module != NULL) {
PyModule_AddObject(m, "libtcod", libtcod_module);
// Also add to sys.modules for proper import behavior
PyObject* sys_modules = PyImport_GetModuleDict();
PyDict_SetItemString(sys_modules, "mcrfpy.libtcod", libtcod_module);
}
//McRFPy_API::mcrf_module = m; //McRFPy_API::mcrf_module = m;
return m; return m;
} }

324
src/McRFPy_Libtcod.cpp Normal file
View File

@ -0,0 +1,324 @@
#include "McRFPy_Libtcod.h"
#include "McRFPy_API.h"
#include "UIGrid.h"
#include <vector>
// Helper function to get UIGrid from Python object
static UIGrid* get_grid_from_pyobject(PyObject* obj) {
auto grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
if (!grid_type) {
PyErr_SetString(PyExc_RuntimeError, "Could not find Grid type");
return nullptr;
}
if (!PyObject_IsInstance(obj, (PyObject*)grid_type)) {
Py_DECREF(grid_type);
PyErr_SetString(PyExc_TypeError, "First argument must be a Grid object");
return nullptr;
}
Py_DECREF(grid_type);
PyUIGridObject* pygrid = (PyUIGridObject*)obj;
return pygrid->data.get();
}
// Field of View computation
static PyObject* McRFPy_Libtcod::compute_fov(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y, radius;
int light_walls = 1;
int algorithm = FOV_BASIC;
if (!PyArg_ParseTuple(args, "Oiii|ii", &grid_obj, &x, &y, &radius,
&light_walls, &algorithm)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// Compute FOV using grid's method
grid->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
// Return list of visible cells
PyObject* visible_list = PyList_New(0);
for (int gy = 0; gy < grid->grid_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<std::pair<int, int>> 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<std::pair<int, int>> 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;
}

27
src/McRFPy_Libtcod.h Normal file
View File

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

View File

@ -2,10 +2,15 @@
#include "McRFPy_API.h" #include "McRFPy_API.h"
PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h) PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h) : source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0)
{ {
texture = sf::Texture(); texture = sf::Texture();
texture.loadFromFile(source); if (!texture.loadFromFile(source)) {
// Failed to load texture - leave sheet dimensions as 0
// This will be checked in init()
return;
}
texture.setSmooth(false); // Disable smoothing for pixel art
auto size = texture.getSize(); auto size = texture.getSize();
sheet_width = (size.x / sprite_width); sheet_width = (size.x / sprite_width);
sheet_height = (size.y / sprite_height); sheet_height = (size.y / sprite_height);
@ -18,6 +23,12 @@ PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s) 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; int tx = index % sheet_width, ty = index / sheet_width;
auto ir = sf::IntRect(tx * sprite_width, ty * sprite_height, sprite_width, sprite_height); auto ir = sf::IntRect(tx * sprite_width, ty * sprite_height, sprite_width, sprite_height);
auto sprite = sf::Sprite(texture, ir); auto sprite = sf::Sprite(texture, ir);
@ -71,7 +82,16 @@ int PyTexture::init(PyTextureObject* self, PyObject* args, PyObject* kwds)
int sprite_width, sprite_height; int sprite_width, sprite_height;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii", const_cast<char**>(keywords), &filename, &sprite_width, &sprite_height)) if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii", const_cast<char**>(keywords), &filename, &sprite_width, &sprite_height))
return -1; return -1;
// Create the texture object
self->data = std::make_shared<PyTexture>(filename, sprite_width, sprite_height); self->data = std::make_shared<PyTexture>(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; return 0;
} }

View File

@ -9,16 +9,52 @@
#include "UIEntityPyMethods.h" #include "UIEntityPyMethods.h"
UIEntity::UIEntity() UIEntity::UIEntity()
: self(nullptr), grid(nullptr), position(0.0f, 0.0f) : self(nullptr), grid(nullptr), position(0.0f, 0.0f)
{ {
// Initialize sprite with safe defaults (sprite has its own safe constructor now) // Initialize sprite with safe defaults (sprite has its own safe constructor now)
// gridstate vector starts empty since we don't know grid dimensions // gridstate vector starts empty - will be lazily initialized when needed
} }
UIEntity::UIEntity(UIGrid& grid) // Removed UIEntity(UIGrid&) constructor - using lazy initialization instead
: gridstate(grid.grid_x * grid.grid_y)
void UIEntity::updateVisibility()
{ {
if (!grid) return;
// Lazy initialize gridstate if needed
if (gridstate.size() == 0) {
gridstate.resize(grid->grid_x * grid->grid_y);
// Initialize all cells as not visible/discovered
for (auto& state : gridstate) {
state.visible = false;
state.discovered = false;
}
}
// First, mark all cells as not visible
for (auto& state : gridstate) {
state.visible = false;
}
// Compute FOV from entity's position
int x = static_cast<int>(position.x);
int y = static_cast<int>(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) { PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
@ -32,17 +68,29 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
PyErr_SetString(PyExc_ValueError, "Entity cannot access surroundings because it is not associated with a grid"); PyErr_SetString(PyExc_ValueError, "Entity cannot access surroundings because it is not associated with a grid");
return NULL; return NULL;
} }
/*
PyUIGridPointStateObject* obj = (PyUIGridPointStateObject*)((&mcrfpydef::PyUIGridPointStateType)->tp_alloc(&mcrfpydef::PyUIGridPointStateType, 0)); // Lazy initialize gridstate if needed
*/ if (self->data->gridstate.size() == 0) {
self->data->gridstate.resize(self->data->grid->grid_x * self->data->grid->grid_y);
// Initialize all cells as not visible/discovered
for (auto& state : self->data->gridstate) {
state.visible = false;
state.discovered = false;
}
}
// Bounds check
if (x < 0 || x >= self->data->grid->grid_x || y < 0 || y >= self->data->grid->grid_y) {
PyErr_Format(PyExc_IndexError, "Grid coordinates (%d, %d) out of bounds", x, y);
return NULL;
}
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState"); auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState");
auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0); auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0);
//auto target = std::static_pointer_cast<UIEntity>(target); obj->data = &(self->data->gridstate[y * self->data->grid->grid_x + x]);
obj->data = &(self->data->gridstate[y + self->data->grid->grid_x * x]);
obj->grid = self->data->grid; obj->grid = self->data->grid;
obj->entity = self->data; obj->entity = self->data;
return (PyObject*)obj; return (PyObject*)obj;
} }
PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) { PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) {
@ -166,10 +214,8 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
return -1; return -1;
} }
if (grid_obj == NULL) // Always use default constructor for lazy initialization
self->data = std::make_shared<UIEntity>(); self->data = std::make_shared<UIEntity>();
else
self->data = std::make_shared<UIEntity>(*((PyUIGridObject*)grid_obj)->data);
// Store reference to Python object // Store reference to Python object
self->data->self = (PyObject*)self; self->data->self = (PyObject*)self;
@ -191,6 +237,9 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
self->data->grid = pygrid->data; self->data->grid = pygrid->data;
// todone - on creation of Entity with Grid assignment, also append it to the entity list // todone - on creation of Entity with Grid assignment, also append it to the entity list
pygrid->data->entities->push_back(self->data); 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; return 0;
} }
@ -237,11 +286,26 @@ sf::Vector2i PyObject_to_sfVector2i(PyObject* obj) {
return sf::Vector2i(static_cast<int>(vec->data.x), static_cast<int>(vec->data.y)); return sf::Vector2i(static_cast<int>(vec->data.x), static_cast<int>(vec->data.y));
} }
// TODO - deprecate / remove this helper
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state) { PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state) {
// This function is incomplete - it creates an empty object without setting state data // Create a new GridPointState Python object
// Should use PyObjectUtils::createGridPointState() instead auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState");
return PyObjectUtils::createPyObjectGeneric("GridPointState"); if (!type) {
return NULL;
}
auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0);
if (!obj) {
Py_DECREF(type);
return NULL;
}
// Allocate new data and copy values
obj->data = new UIGridPointState();
obj->data->visible = state.visible;
obj->data->discovered = state.discovered;
Py_DECREF(type);
return (PyObject*)obj;
} }
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec) { PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec) {
@ -377,10 +441,75 @@ PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
Py_RETURN_NONE; 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<char**>(keywords),
&target_x, &target_y)) {
PyErr_Clear();
// Try alternative parameter names
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiii", const_cast<char**>(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<int>(self->data->position.x);
int current_y = static_cast<int>(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[] = { PyMethodDef UIEntity::methods[] = {
{"at", (PyCFunction)UIEntity::at, METH_O}, {"at", (PyCFunction)UIEntity::at, METH_O},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"}, {"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"}, {"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} {NULL, NULL, 0, NULL}
}; };
@ -393,6 +522,8 @@ PyMethodDef UIEntity_all_methods[] = {
{"at", (PyCFunction)UIEntity::at, METH_O}, {"at", (PyCFunction)UIEntity::at, METH_O},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"}, {"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"}, {"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 {NULL} // Sentinel
}; };
@ -426,15 +557,12 @@ PyObject* UIEntity::repr(PyUIEntityObject* self) {
bool UIEntity::setProperty(const std::string& name, float value) { bool UIEntity::setProperty(const std::string& name, float value) {
if (name == "x") { if (name == "x") {
position.x = value; position.x = value;
// Update sprite position based on grid position // Don't update sprite position here - UIGrid::render() handles the pixel positioning
// 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; return true;
} }
else if (name == "y") { else if (name == "y") {
position.y = value; position.y = value;
// Update sprite position based on grid position // Don't update sprite position here - UIGrid::render() handles the pixel positioning
sprite.setPosition(sf::Vector2f(position.x, position.y));
return true; return true;
} }
else if (name == "sprite_scale") { else if (name == "sprite_scale") {

View File

@ -27,10 +27,10 @@ class UIGrid;
//} PyUIEntityObject; //} PyUIEntityObject;
// helper methods with no namespace requirement // helper methods with no namespace requirement
static PyObject* sfVector2f_to_PyObject(sf::Vector2f vector); PyObject* sfVector2f_to_PyObject(sf::Vector2f vector);
static sf::Vector2f PyObject_to_sfVector2f(PyObject* obj); sf::Vector2f PyObject_to_sfVector2f(PyObject* obj);
static PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state); PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state);
static PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec); PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec);
// TODO: make UIEntity a drawable // TODO: make UIEntity a drawable
class UIEntity//: public UIDrawable class UIEntity//: public UIDrawable
@ -44,7 +44,9 @@ public:
//void render(sf::Vector2f); //override final; //void render(sf::Vector2f); //override final;
UIEntity(); UIEntity();
UIEntity(UIGrid&);
// Visibility methods
void updateVisibility(); // Update gridstate from current FOV
// Property system for animations // Property system for animations
bool setProperty(const std::string& name, float value); bool setProperty(const std::string& name, float value);
@ -59,6 +61,8 @@ public:
static PyObject* at(PyUIEntityObject* self, PyObject* o); static PyObject* at(PyUIEntityObject* self, PyObject* o);
static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* die(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 int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static PyObject* get_position(PyUIEntityObject* self, void* closure); static PyObject* get_position(PyUIEntityObject* self, void* closure);

View File

@ -7,7 +7,8 @@
UIGrid::UIGrid() UIGrid::UIGrid()
: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr), : grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
fill_color(8, 8, 8, 255) // Default dark gray background fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
perspective(-1) // Default to omniscient view
{ {
// Initialize entities list // Initialize entities list
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>(); entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
@ -27,13 +28,15 @@ UIGrid::UIGrid()
output.setTexture(renderTexture.getTexture()); output.setTexture(renderTexture.getTexture());
// Points vector starts empty (grid_x * grid_y = 0) // 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<PyTexture> _ptex, sf::Vector2f _xy, sf::Vector2f _wh) UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _xy, sf::Vector2f _wh)
: grid_x(gx), grid_y(gy), : grid_x(gx), grid_y(gy),
zoom(1.0f), zoom(1.0f),
ptex(_ptex), points(gx * gy), ptex(_ptex), points(gx * gy),
fill_color(8, 8, 8, 255) // Default dark gray background fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
perspective(-1) // Default to omniscient view
{ {
// Use texture dimensions if available, otherwise use defaults // Use texture dimensions if available, otherwise use defaults
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH; int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
@ -63,6 +66,27 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
// textures are upside-down inside renderTexture // textures are upside-down inside renderTexture
output.setTexture(renderTexture.getTexture()); 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() {} void UIGrid::update() {}
@ -164,43 +188,55 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
} }
// top layer - opacity for discovered / visible status (debug, basically) // top layer - opacity for discovered / visible status based on perspective
/* // Disabled until I attach a "perspective" // Only render visibility overlay if perspective is set (not omniscient)
if (perspective >= 0 && perspective < static_cast<int>(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); for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
x < x_limit; //x < view_width; x < x_limit;
x+=1) x+=1)
{ {
//for (float y = (top_edge >= 0 ? top_edge : 0);
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
y < y_limit; //y < view_height; y < y_limit;
y+=1) y+=1)
{ {
// Skip out-of-bounds cells
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
auto pixel_pos = sf::Vector2f( auto pixel_pos = sf::Vector2f(
(x*itex->grid_size - left_spritepixels) * zoom, (x*cell_width - left_spritepixels) * zoom,
(y*itex->grid_size - top_spritepixels) * zoom ); (y*cell_height - top_spritepixels) * zoom );
auto gridpoint = at(std::floor(x), std::floor(y)); // Get visibility state from entity's perspective
int idx = y * grid_x + x;
if (idx >= 0 && idx < static_cast<int>(entity->gridstate.size())) {
const auto& state = entity->gridstate[idx];
sprite.setPosition(pixel_pos); overlay.setPosition(pixel_pos);
r.setPosition(pixel_pos); // Three overlay colors as specified:
if (!state.discovered) {
// visible & discovered layers for testing purposes // Never seen - black
if (!gridpoint.discovered) { overlay.setFillColor(sf::Color(0, 0, 0, 255));
r.setFillColor(sf::Color(16, 16, 20, 192)); // 255 opacity for actual blackout renderTexture.draw(overlay);
renderTexture.draw(r); } else if (!state.visible) {
} else if (!gridpoint.visible) { // Discovered but not currently visible - dark gray
r.setFillColor(sf::Color(32, 32, 40, 128)); overlay.setFillColor(sf::Color(32, 32, 40, 192));
renderTexture.draw(r); renderTexture.draw(overlay);
}
// If visible and discovered, no overlay (fully visible)
}
} }
// overlay
// uisprite
} }
} }
*/
// grid lines for testing & validation // grid lines for testing & validation
/* /*
@ -234,11 +270,155 @@ UIGridPoint& UIGrid::at(int x, int y)
return points[y * grid_x + x]; 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() PyObjectsEnum UIGrid::derived_type()
{ {
return PyObjectsEnum::UIGRID; 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<std::pair<int, int>> UIGrid::findPath(int x1, int y1, int x2, int y2, float diagonalCost)
{
std::vector<std::pair<int, int>> 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<std::pair<int, int>> UIGrid::getDijkstraPath(int x, int y) const
{
std::vector<std::pair<int, int>> 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<std::pair<int, int>> UIGrid::computeAStarPath(int x1, int y1, int x2, int y2, float diagonalCost)
{
std::vector<std::pair<int, int>> 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 // Phase 1 implementations
sf::FloatRect UIGrid::get_bounds() const sf::FloatRect UIGrid::get_bounds() const
{ {
@ -338,35 +518,53 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { 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 // Default values
int grid_x = 0, grid_y = 0; int grid_x = 0, grid_y = 0;
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
PyObject* textureObj = nullptr; PyObject* textureObj = nullptr;
// Case 1: Got grid size and position from helpers (tuple format) // Check if first argument is a tuple (for tuple-based initialization)
if (grid_size_result.valid) { bool has_tuple_first_arg = false;
if (args && PyTuple_Size(args) > 0) {
PyObject* first_arg = PyTuple_GetItem(args, 0);
if (PyTuple_Check(first_arg)) {
has_tuple_first_arg = true;
}
}
// Try tuple-based parsing if we have a tuple as first argument
if (has_tuple_first_arg) {
int arg_idx = 0;
auto grid_size_result = PyArgHelpers::parseGridSize(args, kwds, &arg_idx);
// If grid size parsing failed with an error, report it
if (!grid_size_result.valid) {
if (grid_size_result.error) {
PyErr_SetString(PyExc_TypeError, grid_size_result.error);
} else {
PyErr_SetString(PyExc_TypeError, "Invalid grid size tuple");
}
return -1;
}
// We got a valid grid size
grid_x = grid_size_result.grid_w; grid_x = grid_size_result.grid_w;
grid_y = grid_size_result.grid_h; grid_y = grid_size_result.grid_h;
// Set position if we got it // Try to parse position and size
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
if (pos_result.valid) { if (pos_result.valid) {
x = pos_result.x; x = pos_result.x;
y = pos_result.y; y = pos_result.y;
} }
// Set size if we got it, otherwise calculate default auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx);
if (size_result.valid) { if (size_result.valid) {
w = size_result.w; w = size_result.w;
h = size_result.h; h = size_result.h;
} else { } else {
// Default size based on grid dimensions and texture // Default size based on grid dimensions
w = grid_x * 16.0f; // Will be recalculated if texture provided w = grid_x * 16.0f;
h = grid_y * 16.0f; h = grid_y * 16.0f;
} }
@ -380,10 +578,8 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
&textureObj); &textureObj);
Py_DECREF(remaining_args); Py_DECREF(remaining_args);
} }
// Case 2: Traditional format // Traditional format parsing
else { else {
PyErr_Clear(); // Clear any errors from helpers
static const char* keywords[] = { static const char* keywords[] = {
"grid_x", "grid_y", "texture", "pos", "size", "grid_size", nullptr "grid_x", "grid_y", "texture", "pos", "size", "grid_size", nullptr
}; };
@ -406,7 +602,13 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
grid_x = PyLong_AsLong(x_obj); grid_x = PyLong_AsLong(x_obj);
grid_y = PyLong_AsLong(y_obj); grid_y = PyLong_AsLong(y_obj);
} else {
PyErr_SetString(PyExc_TypeError, "grid_size must contain integers");
return -1;
} }
} else {
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple of two integers");
return -1;
} }
} }
@ -419,7 +621,13 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
(PyFloat_Check(y_val) || PyLong_Check(y_val))) { (PyFloat_Check(y_val) || PyLong_Check(y_val))) {
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_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); y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
} else {
PyErr_SetString(PyExc_TypeError, "pos must contain numbers");
return -1;
} }
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of two numbers");
return -1;
} }
} }
@ -432,7 +640,13 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
(PyFloat_Check(h_val) || PyLong_Check(h_val))) { (PyFloat_Check(h_val) || PyLong_Check(h_val))) {
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_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); 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 { } else {
// Default size based on grid // Default size based on grid
@ -441,16 +655,19 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
} }
} }
// 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 // At this point we have x, y, w, h values from either parsing method
// Convert PyObject texture to IndexTexture* // Convert PyObject texture to shared_ptr<PyTexture>
// This requires the texture object to have been initialized similar to UISprite's texture handling
std::shared_ptr<PyTexture> texture_ptr = nullptr; std::shared_ptr<PyTexture> texture_ptr = nullptr;
// Allow None for texture - use default texture in that case // Allow None or NULL for texture - use default texture in that case
if (textureObj != Py_None) { if (textureObj && textureObj != Py_None) {
//if (!PyObject_IsInstance(textureObj, (PyObject*)&PyTextureType)) {
if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) { if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
return -1; return -1;
@ -458,16 +675,12 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
PyTextureObject* pyTexture = reinterpret_cast<PyTextureObject*>(textureObj); PyTextureObject* pyTexture = reinterpret_cast<PyTextureObject*>(textureObj);
texture_ptr = pyTexture->data; texture_ptr = pyTexture->data;
} else { } else {
// Use default texture when None is provided // Use default texture when None is provided or texture not specified
texture_ptr = McRFPy_API::default_texture; 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<UIGrid>(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 // Adjust size based on texture if available and size not explicitly set
if (!size_result.valid && texture_ptr) { if (texture_ptr && w == grid_x * 16.0f && h == grid_y * 16.0f) {
w = grid_x * texture_ptr->sprite_width; w = grid_x * texture_ptr->sprite_width;
h = grid_y * texture_ptr->sprite_height; h = grid_y * texture_ptr->sprite_height;
} }
@ -719,8 +932,183 @@ int UIGrid::set_fill_color(PyUIGridObject* self, PyObject* value, void* closure)
return 0; 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<std::pair<int, int>> 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<std::pair<int, int>> 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<std::pair<int, int>> 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[] = { PyMethodDef UIGrid::methods[] = {
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, {"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} {NULL, NULL, 0, NULL}
}; };
@ -731,6 +1119,20 @@ typedef PyUIGridObject PyObjectType;
PyMethodDef UIGrid_all_methods[] = { PyMethodDef UIGrid_all_methods[] = {
UIDRAWABLE_METHODS, UIDRAWABLE_METHODS,
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, {"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 {NULL} // Sentinel
}; };
@ -759,6 +1161,7 @@ PyGetSetDef UIGrid::getsetters[] = {
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5 {"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}, {"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}, {"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}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
UIDRAWABLE_GETSETTERS, UIDRAWABLE_GETSETTERS,
@ -1102,6 +1505,16 @@ PyObject* UIEntityCollection::append(PyUIEntityCollectionObject* self, PyObject*
self->data->push_back(entity->data); self->data->push_back(entity->data);
entity->data->grid = self->grid; 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); Py_INCREF(Py_None);
return Py_None; return Py_None;
} }

View File

@ -5,6 +5,7 @@
#include "IndexTexture.h" #include "IndexTexture.h"
#include "Resources.h" #include "Resources.h"
#include <list> #include <list>
#include <libtcod.h>
#include "PyCallable.h" #include "PyCallable.h"
#include "PyTexture.h" #include "PyTexture.h"
@ -25,10 +26,15 @@ private:
// Default cell dimensions when no texture is provided // Default cell dimensions when no texture is provided
static constexpr int DEFAULT_CELL_WIDTH = 16; static constexpr int DEFAULT_CELL_WIDTH = 16;
static constexpr int DEFAULT_CELL_HEIGHT = 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: public:
UIGrid(); UIGrid();
//UIGrid(int, int, IndexTexture*, float, float, float, float); //UIGrid(int, int, IndexTexture*, float, float, float, float);
UIGrid(int, int, std::shared_ptr<PyTexture>, sf::Vector2f, sf::Vector2f); UIGrid(int, int, std::shared_ptr<PyTexture>, sf::Vector2f, sf::Vector2f);
~UIGrid(); // Destructor to clean up TCOD map
void update(); void update();
void render(sf::Vector2f, sf::RenderTarget&) override final; void render(sf::Vector2f, sf::RenderTarget&) override final;
UIGridPoint& at(int, int); UIGridPoint& at(int, int);
@ -36,6 +42,21 @@ public:
//void setSprite(int); //void setSprite(int);
virtual UIDrawable* click_at(sf::Vector2f point) override final; 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<std::pair<int, int>> 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<std::pair<int, int>> getDijkstraPath(int x, int y) const;
// A* pathfinding methods
std::vector<std::pair<int, int>> computeAStarPath(int x1, int y1, int x2, int y2, float diagonalCost = 1.41f);
// Phase 1 virtual method implementations // Phase 1 virtual method implementations
sf::FloatRect get_bounds() const override; sf::FloatRect get_bounds() const override;
void move(float dx, float dy) override; void move(float dx, float dy) override;
@ -56,6 +77,9 @@ public:
// Background rendering // Background rendering
sf::Color fill_color; sf::Color fill_color;
// Perspective system - which entity's view to render (-1 = omniscient/default)
int perspective;
// Property system for animations // Property system for animations
bool setProperty(const std::string& name, float value) override; bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Vector2f& value) override; bool setProperty(const std::string& name, const sf::Vector2f& value) override;
@ -77,7 +101,16 @@ public:
static PyObject* get_texture(PyUIGridObject* self, void* closure); static PyObject* get_texture(PyUIGridObject* self, void* closure);
static PyObject* get_fill_color(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 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_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 PyMethodDef methods[];
static PyGetSetDef getsetters[]; static PyGetSetDef getsetters[];
static PyObject* get_children(PyUIGridObject* self, void* closure); static PyObject* get_children(PyUIGridObject* self, void* closure);

View File

@ -1,19 +1,51 @@
#include "UIGridPoint.h" #include "UIGridPoint.h"
#include "UIGrid.h"
UIGridPoint::UIGridPoint() UIGridPoint::UIGridPoint()
: color(1.0f, 1.0f, 1.0f), color_overlay(0.0f, 0.0f, 0.0f), walkable(false), transparent(false), : color(1.0f, 1.0f, 1.0f), color_overlay(0.0f, 0.0f, 0.0f), walkable(false), transparent(false),
tilesprite(-1), tile_overlay(-1), uisprite(-1) tilesprite(-1), tile_overlay(-1), uisprite(-1), grid_x(-1), grid_y(-1), parent_grid(nullptr)
{} {}
// Utility function to convert sf::Color to PyObject* // Utility function to convert sf::Color to PyObject*
PyObject* sfColor_to_PyObject(sf::Color color) { 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); return Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a);
} }
// Utility function to convert PyObject* to sf::Color // Utility function to convert PyObject* to sf::Color
sf::Color PyObject_to_sfColor(PyObject* obj) { 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 int r, g, b, a = 255; // Default alpha to fully opaque if not specified
if (!PyArg_ParseTuple(obj, "iii|i", &r, &g, &b, &a)) { 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(); // Return default color on parse error
} }
return sf::Color(r, g, b, a); return sf::Color(r, g, b, a);
@ -29,6 +61,11 @@ PyObject* UIGridPoint::get_color(PyUIGridPointObject* self, void* closure) {
int UIGridPoint::set_color(PyUIGridPointObject* self, PyObject* value, void* closure) { int UIGridPoint::set_color(PyUIGridPointObject* self, PyObject* value, void* closure) {
sf::Color color = PyObject_to_sfColor(value); sf::Color color = PyObject_to_sfColor(value);
// Check if an error occurred during conversion
if (PyErr_Occurred()) {
return -1;
}
if (reinterpret_cast<long>(closure) == 0) { // color if (reinterpret_cast<long>(closure) == 0) { // color
self->data->color = color; self->data->color = color;
} else { // color_overlay } else { // color_overlay
@ -62,6 +99,12 @@ int UIGridPoint::set_bool_member(PyUIGridPointObject* self, PyObject* value, voi
PyErr_SetString(PyExc_ValueError, "Expected a boolean value"); PyErr_SetString(PyExc_ValueError, "Expected a boolean value");
return -1; 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; return 0;
} }

View File

@ -40,6 +40,8 @@ public:
sf::Color color, color_overlay; sf::Color color, color_overlay;
bool walkable, transparent; bool walkable, transparent;
int tilesprite, tile_overlay, uisprite; int tilesprite, tile_overlay, uisprite;
int grid_x, grid_y; // Position in parent grid
UIGrid* parent_grid; // Parent grid reference for TCOD sync
UIGridPoint(); UIGridPoint();
static int set_int_member(PyUIGridPointObject* self, PyObject* value, void* closure); static int set_int_member(PyUIGridPointObject* self, PyObject* value, void* closure);

View File

@ -121,7 +121,7 @@ UITestScene::UITestScene(GameEngine* g) : Scene(g)
//UIEntity test: //UIEntity test:
// asdf // 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. // 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<UIEntity>(*e5); // this basic constructor sucks: sprite position + zoom are irrelevant for UIEntity. auto e5a = std::make_shared<UIEntity>(); // Default constructor - lazy initialization
e5a->grid = e5; e5a->grid = e5;
//auto e5as = UISprite(indextex, 85, sf::Vector2f(0, 0), 1.0); //auto e5as = UISprite(indextex, 85, sf::Vector2f(0, 0), 1.0);
//e5a->sprite = e5as; // will copy constructor even exist for UISprite...? //e5a->sprite = e5as; // will copy constructor even exist for UISprite...?

View File

@ -0,0 +1,48 @@
from text_input_widget_improved import FocusManager, TextInput
# Create focus manager
focus_mgr = FocusManager()
# Create input field
name_input = TextInput(
x=50, y=100,
width=300,
label="Name:",
placeholder="Enter your name",
on_change=lambda text: print(f"Name changed to: {text}")
)
tags_input = TextInput(
x=50, y=160,
width=300,
label="Tags:",
placeholder="door,chest,floor,wall",
on_change=lambda text: print(f"Text: {text}")
)
# Register with focus manager
name_input._focus_manager = focus_mgr
focus_mgr.register(name_input)
# Create demo scene
import mcrfpy
mcrfpy.createScene("text_example")
mcrfpy.setScene("text_example")
ui = mcrfpy.sceneUI("text_example")
# Add to scene
#ui.append(name_input) # don't do this, only the internal Frame class can go into the UI; have to manage derived objects "carefully" (McRogueFace alpha anti-feature)
name_input.add_to_scene(ui)
tags_input.add_to_scene(ui)
# Handle keyboard events
def handle_keys(key, state):
if not focus_mgr.handle_key(key, state):
if key == "Tab" and state == "start":
focus_mgr.focus_next()
# McRogueFace alpha anti-feature: only the active scene can be given a keypress callback
mcrfpy.keypressScene(handle_keys)

View File

@ -0,0 +1,201 @@
"""
Text Input Widget System for McRogueFace
A reusable module for text input fields with focus management
"""
import mcrfpy
class FocusManager:
"""Manages focus across multiple widgets"""
def __init__(self):
self.widgets = []
self.focused_widget = None
self.focus_index = -1
def register(self, widget):
"""Register a widget"""
self.widgets.append(widget)
if self.focused_widget is None:
self.focus(widget)
def focus(self, widget):
"""Set focus to widget"""
if self.focused_widget:
self.focused_widget.on_blur()
self.focused_widget = widget
self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1
if widget:
widget.on_focus()
def focus_next(self):
"""Focus next widget"""
if not self.widgets:
return
self.focus_index = (self.focus_index + 1) % len(self.widgets)
self.focus(self.widgets[self.focus_index])
def focus_prev(self):
"""Focus previous widget"""
if not self.widgets:
return
self.focus_index = (self.focus_index - 1) % len(self.widgets)
self.focus(self.widgets[self.focus_index])
def handle_key(self, key):
"""Send key to focused widget"""
if self.focused_widget:
return self.focused_widget.handle_key(key)
return False
class TextInput:
"""Text input field widget"""
def __init__(self, x, y, width, height=24, label="", placeholder="", on_change=None):
self.x = x
self.y = y
self.width = width
self.height = height
self.label = label
self.placeholder = placeholder
self.on_change = on_change
# Text state
self.text = ""
self.cursor_pos = 0
self.focused = False
# Visual elements
self._create_ui()
def _create_ui(self):
"""Create UI components"""
# Background frame
self.frame = mcrfpy.Frame(self.x, self.y, self.width, self.height)
self.frame.fill_color = (255, 255, 255, 255)
self.frame.outline_color = (128, 128, 128, 255)
self.frame.outline = 2
# Label (above input)
if self.label:
self.label_text = mcrfpy.Caption(self.label, self.x, self.y - 20)
self.label_text.fill_color = (255, 255, 255, 255)
# Text content
self.text_display = mcrfpy.Caption("", self.x + 4, self.y + 4)
self.text_display.fill_color = (0, 0, 0, 255)
# Placeholder text
if self.placeholder:
self.placeholder_text = mcrfpy.Caption(self.placeholder, self.x + 4, self.y + 4)
self.placeholder_text.fill_color = (180, 180, 180, 255)
# Cursor
self.cursor = mcrfpy.Frame(self.x + 4, self.y + 4, 2, self.height - 8)
self.cursor.fill_color = (0, 0, 0, 255)
self.cursor.visible = False
# Click handler
self.frame.click = self._on_click
def _on_click(self, x, y, button, state):
"""Handle mouse clicks"""
print(self, x, y, button, state)
if button == "left" and hasattr(self, '_focus_manager'):
self._focus_manager.focus(self)
def on_focus(self):
"""Called when focused"""
self.focused = True
self.frame.outline_color = (0, 120, 255, 255)
self.frame.outline = 3
self.cursor.visible = True
self._update_display()
def on_blur(self):
"""Called when focus lost"""
self.focused = False
self.frame.outline_color = (128, 128, 128, 255)
self.frame.outline = 2
self.cursor.visible = False
self._update_display()
def handle_key(self, key):
"""Process keyboard input"""
if not self.focused:
return False
old_text = self.text
handled = True
# Navigation and editing keys
if key == "BackSpace":
if self.cursor_pos > 0:
self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
self.cursor_pos -= 1
elif key == "Delete":
if self.cursor_pos < len(self.text):
self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
elif key == "Left":
self.cursor_pos = max(0, self.cursor_pos - 1)
elif key == "Right":
self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
elif key == "Home":
self.cursor_pos = 0
elif key == "End":
self.cursor_pos = len(self.text)
elif key in ("Tab", "Return"):
handled = False # Let parent handle
elif len(key) == 1 and key.isprintable():
self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:]
self.cursor_pos += 1
else:
handled = False
# Update if changed
if old_text != self.text:
self._update_display()
if self.on_change:
self.on_change(self.text)
elif handled:
self._update_cursor()
return handled
def _update_display(self):
"""Update visual state"""
# Show/hide placeholder
if hasattr(self, 'placeholder_text'):
self.placeholder_text.visible = (self.text == "" and not self.focused)
# Update text
self.text_display.text = self.text
self._update_cursor()
def _update_cursor(self):
"""Update cursor position"""
if self.focused:
# Estimate position (10 pixels per character)
self.cursor.x = self.x + 4 + (self.cursor_pos * 10)
def set_text(self, text):
"""Set text programmatically"""
self.text = text
self.cursor_pos = len(text)
self._update_display()
def get_text(self):
"""Get current text"""
return self.text
def add_to_scene(self, scene):
"""Add all components to scene"""
scene.append(self.frame)
if hasattr(self, 'label_text'):
scene.append(self.label_text)
if hasattr(self, 'placeholder_text'):
scene.append(self.placeholder_text)
scene.append(self.text_display)
scene.append(self.cursor)

View File

@ -0,0 +1,265 @@
"""
Improved Text Input Widget System for McRogueFace
Uses proper parent-child frame structure and handles keyboard input correctly
"""
import mcrfpy
class FocusManager:
"""Manages focus across multiple widgets"""
def __init__(self):
self.widgets = []
self.focused_widget = None
self.focus_index = -1
# Global keyboard state
self.shift_pressed = False
self.caps_lock = False
def register(self, widget):
"""Register a widget"""
self.widgets.append(widget)
if self.focused_widget is None:
self.focus(widget)
def focus(self, widget):
"""Set focus to widget"""
if self.focused_widget:
self.focused_widget.on_blur()
self.focused_widget = widget
self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1
if widget:
widget.on_focus()
def focus_next(self):
"""Focus next widget"""
if not self.widgets:
return
self.focus_index = (self.focus_index + 1) % len(self.widgets)
self.focus(self.widgets[self.focus_index])
def focus_prev(self):
"""Focus previous widget"""
if not self.widgets:
return
self.focus_index = (self.focus_index - 1) % len(self.widgets)
self.focus(self.widgets[self.focus_index])
def handle_key(self, key, state):
"""Send key to focused widget"""
# Track shift state
if key == "LShift" or key == "RShift":
self.shift_pressed = True
return True
elif key == "start": # Key release for shift
self.shift_pressed = False
return True
elif key == "CapsLock":
self.caps_lock = not self.caps_lock
return True
if self.focused_widget:
return self.focused_widget.handle_key(key, self.shift_pressed, self.caps_lock)
return False
class TextInput:
"""Text input field widget with proper parent-child structure"""
def __init__(self, x, y, width, height=24, label="", placeholder="", on_change=None):
self.x = x
self.y = y
self.width = width
self.height = height
self.label = label
self.placeholder = placeholder
self.on_change = on_change
# Text state
self.text = ""
self.cursor_pos = 0
self.focused = False
# Create the widget structure
self._create_ui()
def _create_ui(self):
"""Create UI components with proper parent-child structure"""
# Parent frame that contains everything
self.parent_frame = mcrfpy.Frame(self.x, self.y - (20 if self.label else 0),
self.width, self.height + (20 if self.label else 0))
self.parent_frame.fill_color = (0, 0, 0, 0) # Transparent parent
# Input frame (relative to parent)
self.frame = mcrfpy.Frame(0, 20 if self.label else 0, self.width, self.height)
self.frame.fill_color = (255, 255, 255, 255)
self.frame.outline_color = (128, 128, 128, 255)
self.frame.outline = 2
# Label (relative to parent)
if self.label:
self.label_text = mcrfpy.Caption(self.label, 0, 0)
self.label_text.fill_color = (255, 255, 255, 255)
self.parent_frame.children.append(self.label_text)
# Text content (relative to input frame)
self.text_display = mcrfpy.Caption("", 4, 4)
self.text_display.fill_color = (0, 0, 0, 255)
# Placeholder text (relative to input frame)
if self.placeholder:
self.placeholder_text = mcrfpy.Caption(self.placeholder, 4, 4)
self.placeholder_text.fill_color = (180, 180, 180, 255)
self.frame.children.append(self.placeholder_text)
# Cursor (relative to input frame)
# Experiment: replacing cursor frame with an inline text character
#self.cursor = mcrfpy.Frame(4, 4, 2, self.height - 8)
#self.cursor.fill_color = (0, 0, 0, 255)
#self.cursor.visible = False
# Add children to input frame
self.frame.children.append(self.text_display)
#self.frame.children.append(self.cursor)
# Add input frame to parent
self.parent_frame.children.append(self.frame)
# Click handler on the input frame
self.frame.click = self._on_click
def _on_click(self, x, y, button, state):
"""Handle mouse clicks"""
print(f"{x=} {y=} {button=} {state=}")
if button == "left" and hasattr(self, '_focus_manager'):
self._focus_manager.focus(self)
def on_focus(self):
"""Called when focused"""
self.focused = True
self.frame.outline_color = (0, 120, 255, 255)
self.frame.outline = 3
#self.cursor.visible = True
self._update_display()
def on_blur(self):
"""Called when focus lost"""
self.focused = False
self.frame.outline_color = (128, 128, 128, 255)
self.frame.outline = 2
#self.cursor.visible = False
self._update_display()
def handle_key(self, key, shift_pressed, caps_lock):
"""Process keyboard input with shift state"""
if not self.focused:
return False
old_text = self.text
handled = True
# Special key mappings for shifted characters
shift_map = {
"1": "!", "2": "@", "3": "#", "4": "$", "5": "%",
"6": "^", "7": "&", "8": "*", "9": "(", "0": ")",
"-": "_", "=": "+", "[": "{", "]": "}", "\\": "|",
";": ":", "'": '"', ",": "<", ".": ">", "/": "?",
"`": "~"
}
# Navigation and editing keys
if key == "BackSpace":
if self.cursor_pos > 0:
self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
self.cursor_pos -= 1
elif key == "Delete":
if self.cursor_pos < len(self.text):
self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
elif key == "Left":
self.cursor_pos = max(0, self.cursor_pos - 1)
elif key == "Right":
self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
elif key == "Home":
self.cursor_pos = 0
elif key == "End":
self.cursor_pos = len(self.text)
elif key == "Space":
self._insert_at_cursor(" ")
elif key in ("Tab", "Return"):
handled = False # Let parent handle
# Handle number keys with "Num" prefix
elif key.startswith("Num") and len(key) == 4:
num = key[3] # Get the digit after "Num"
if shift_pressed and num in shift_map:
self._insert_at_cursor(shift_map[num])
else:
self._insert_at_cursor(num)
# Handle single character keys
elif len(key) == 1:
char = key
# Apply shift transformations
if shift_pressed:
if char in shift_map:
char = shift_map[char]
elif char.isalpha():
char = char.upper()
else:
# Apply caps lock for letters
if char.isalpha():
if caps_lock:
char = char.upper()
else:
char = char.lower()
self._insert_at_cursor(char)
else:
# Unhandled key - print for debugging
print(f"[TextInput] Unhandled key: '{key}' (shift={shift_pressed}, caps={caps_lock})")
handled = False
# Update if changed
if old_text != self.text:
self._update_display()
if self.on_change:
self.on_change(self.text)
elif handled:
self._update_cursor()
return handled
def _insert_at_cursor(self, char):
"""Insert a character at the cursor position"""
self.text = self.text[:self.cursor_pos] + char + self.text[self.cursor_pos:]
self.cursor_pos += 1
def _update_display(self):
"""Update visual state"""
# Show/hide placeholder
if hasattr(self, 'placeholder_text'):
self.placeholder_text.visible = (self.text == "" and not self.focused)
# Update text
self.text_display.text = self.text[:self.cursor_pos] + "|" + self.text[self.cursor_pos:]
self._update_cursor()
def _update_cursor(self):
"""Update cursor position"""
if self.focused:
# Estimate position (10 pixels per character)
#self.cursor.x = 4 + (self.cursor_pos * 10)
self.text_display.text = self.text[:self.cursor_pos] + "|" + self.text[self.cursor_pos:]
pass
def set_text(self, text):
"""Set text programmatically"""
self.text = text
self.cursor_pos = len(text)
self._update_display()
def get_text(self):
"""Get current text"""
return self.text
def add_to_scene(self, scene):
"""Add only the parent frame to scene"""
scene.append(self.parent_frame)

View File

@ -1,165 +1,208 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Animation System Demo - Shows all animation capabilities""" """
Animation Demo: Grid Center & Entity Movement
=============================================
Demonstrates:
- Animated grid centering following entity
- Smooth entity movement along paths
- Perspective shifts with zoom transitions
- Field of view updates
"""
import mcrfpy import mcrfpy
import math import sys
# Create main scene # Setup scene
mcrfpy.createScene("animation_demo") mcrfpy.createScene("anim_demo")
ui = mcrfpy.sceneUI("animation_demo")
mcrfpy.setScene("animation_demo")
# Title # Create grid
title = mcrfpy.Caption((400, 30), "McRogueFace Animation System Demo", mcrfpy.default_font) grid = mcrfpy.Grid(grid_x=30, grid_y=20)
title.size = 24 grid.fill_color = mcrfpy.Color(20, 20, 30)
title.fill_color = (255, 255, 255)
# Note: centered property doesn't exist for Caption # Simple map
for y in range(20):
for x in range(30):
cell = grid.at(x, y)
# Create walls around edges and some obstacles
if x == 0 or x == 29 or y == 0 or y == 19:
cell.walkable = False
cell.transparent = False
cell.color = mcrfpy.Color(40, 30, 30)
elif (x == 10 and 5 <= y <= 15) or (y == 10 and 5 <= x <= 25):
cell.walkable = False
cell.transparent = False
cell.color = mcrfpy.Color(60, 40, 40)
else:
cell.walkable = True
cell.transparent = True
cell.color = mcrfpy.Color(80, 80, 100)
# Create entities
player = mcrfpy.Entity(5, 5, grid=grid)
player.sprite_index = 64 # @
enemy = mcrfpy.Entity(25, 15, grid=grid)
enemy.sprite_index = 69 # E
# Update visibility
player.update_visibility()
enemy.update_visibility()
# UI setup
ui = mcrfpy.sceneUI("anim_demo")
ui.append(grid)
grid.position = (100, 100)
grid.size = (600, 400)
title = mcrfpy.Caption("Animation Demo - Grid Center & Entity Movement", 200, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# 1. Position Animation Demo status = mcrfpy.Caption("Press 1: Move Player | 2: Move Enemy | 3: Perspective Shift | Q: Quit", 100, 50)
pos_frame = mcrfpy.Frame(50, 100, 80, 80) status.fill_color = mcrfpy.Color(200, 200, 200)
pos_frame.fill_color = (255, 100, 100) ui.append(status)
pos_frame.outline = 2
ui.append(pos_frame)
pos_label = mcrfpy.Caption((50, 80), "Position Animation", mcrfpy.default_font) info = mcrfpy.Caption("Perspective: Player", 500, 70)
pos_label.fill_color = (200, 200, 200) info.fill_color = mcrfpy.Color(100, 255, 100)
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) ui.append(info)
# Schedule animations # Movement functions
mcrfpy.setTimer("start", start_animations, 500) def move_player_demo():
mcrfpy.setTimer("reverse", reverse_animations, 4000) """Demo player movement with camera follow"""
mcrfpy.setTimer("cycle", cycle_colors, 2500) # Calculate path to a destination
path = player.path_to(20, 10)
if not path:
status.text = "No path available!"
return
# Exit handler status.text = f"Moving player along {len(path)} steps..."
def on_key(key):
if key == "Escape":
mcrfpy.exit()
mcrfpy.keypressScene(on_key) # Animate along path
for i, (x, y) in enumerate(path[:5]): # First 5 steps
delay = i * 500 # 500ms between steps
print("Animation demo started! Press Escape to exit.") # 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)
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)
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)
# 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!")

235
tests/astar_vs_dijkstra.py Normal file
View File

@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""
A* vs Dijkstra Visual Comparison
=================================
Shows the difference between A* (single target) and Dijkstra (multi-target).
"""
import mcrfpy
import sys
# Colors
WALL_COLOR = mcrfpy.Color(40, 20, 20)
FLOOR_COLOR = mcrfpy.Color(60, 60, 80)
ASTAR_COLOR = mcrfpy.Color(0, 255, 0) # Green for A*
DIJKSTRA_COLOR = mcrfpy.Color(0, 150, 255) # Blue for Dijkstra
START_COLOR = mcrfpy.Color(255, 100, 100) # Red for start
END_COLOR = mcrfpy.Color(255, 255, 100) # Yellow for end
# Global state
grid = None
mode = "ASTAR"
start_pos = (5, 10)
end_pos = (27, 10) # Changed from 25 to 27 to avoid the wall
def create_map():
"""Create a map with obstacles to show pathfinding differences"""
global grid
mcrfpy.createScene("pathfinding_comparison")
# Create grid
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
grid.fill_color = mcrfpy.Color(0, 0, 0)
# Initialize all as floor
for y in range(20):
for x in range(30):
grid.at(x, y).walkable = True
grid.at(x, y).color = FLOOR_COLOR
# Create obstacles that make A* and Dijkstra differ
obstacles = [
# Vertical wall with gaps
[(15, y) for y in range(3, 17) if y not in [8, 12]],
# Horizontal walls
[(x, 5) for x in range(10, 20)],
[(x, 15) for x in range(10, 20)],
# Maze-like structure
[(x, 10) for x in range(20, 25)],
[(25, y) for y in range(5, 15)],
]
for obstacle_group in obstacles:
for x, y in obstacle_group:
grid.at(x, y).walkable = False
grid.at(x, y).color = WALL_COLOR
# Mark start and end
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
def clear_paths():
"""Clear path highlighting"""
for y in range(20):
for x in range(30):
cell = grid.at(x, y)
if cell.walkable:
cell.color = FLOOR_COLOR
# Restore start and end colors
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
def show_astar():
"""Show A* path"""
clear_paths()
# Compute A* path
path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
# Color the path
for i, (x, y) in enumerate(path):
if (x, y) != start_pos and (x, y) != end_pos:
grid.at(x, y).color = ASTAR_COLOR
status_text.text = f"A* Path: {len(path)} steps (optimized for single target)"
status_text.fill_color = ASTAR_COLOR
def show_dijkstra():
"""Show Dijkstra exploration"""
clear_paths()
# Compute Dijkstra from start
grid.compute_dijkstra(start_pos[0], start_pos[1])
# Color cells by distance (showing exploration)
max_dist = 40.0
for y in range(20):
for x in range(30):
if grid.at(x, y).walkable:
dist = grid.get_dijkstra_distance(x, y)
if dist is not None and dist < max_dist:
# Color based on distance
intensity = int(255 * (1 - dist / max_dist))
grid.at(x, y).color = mcrfpy.Color(0, intensity // 2, intensity)
# Get the actual path
path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
# Highlight the actual path more brightly
for x, y in path:
if (x, y) != start_pos and (x, y) != end_pos:
grid.at(x, y).color = DIJKSTRA_COLOR
# Restore start and end
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
status_text.text = f"Dijkstra: {len(path)} steps (explores all directions)"
status_text.fill_color = DIJKSTRA_COLOR
def show_both():
"""Show both paths overlaid"""
clear_paths()
# Get both paths
astar_path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
grid.compute_dijkstra(start_pos[0], start_pos[1])
dijkstra_path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
print(astar_path, dijkstra_path)
# Color Dijkstra path first (blue)
for x, y in dijkstra_path:
if (x, y) != start_pos and (x, y) != end_pos:
grid.at(x, y).color = DIJKSTRA_COLOR
# Then A* path (green) - will overwrite shared cells
for x, y in astar_path:
if (x, y) != start_pos and (x, y) != end_pos:
grid.at(x, y).color = ASTAR_COLOR
# Mark differences
different_cells = []
for cell in dijkstra_path:
if cell not in astar_path:
different_cells.append(cell)
status_text.text = f"Both paths: A*={len(astar_path)} steps, Dijkstra={len(dijkstra_path)} steps"
if different_cells:
info_text.text = f"Paths differ at {len(different_cells)} cells"
else:
info_text.text = "Paths are identical"
def handle_keypress(key_str, state):
"""Handle keyboard input"""
global mode
if state == "end": return
print(key_str)
if key_str == "Esc" or key_str == "Q":
print("\nExiting...")
sys.exit(0)
elif key_str == "A" or key_str == "1":
mode = "ASTAR"
show_astar()
elif key_str == "D" or key_str == "2":
mode = "DIJKSTRA"
show_dijkstra()
elif key_str == "B" or key_str == "3":
mode = "BOTH"
show_both()
elif key_str == "Space":
# Refresh current mode
if mode == "ASTAR":
show_astar()
elif mode == "DIJKSTRA":
show_dijkstra()
else:
show_both()
# Create the demo
print("A* vs Dijkstra Pathfinding Comparison")
print("=====================================")
print("Controls:")
print(" A or 1 - Show A* path (green)")
print(" D or 2 - Show Dijkstra (blue gradient)")
print(" B or 3 - Show both paths")
print(" Q/ESC - Quit")
print()
print("A* is optimized for single-target pathfinding")
print("Dijkstra explores in all directions (good for multiple targets)")
create_map()
# Set up UI
ui = mcrfpy.sceneUI("pathfinding_comparison")
ui.append(grid)
# Scale and position
grid.size = (600, 400) # 30*20, 20*20
grid.position = (100, 100)
# Add title
title = mcrfpy.Caption("A* vs Dijkstra Pathfinding", 250, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add status
status_text = mcrfpy.Caption("Press A for A*, D for Dijkstra, B for Both", 100, 60)
status_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status_text)
# Add info
info_text = mcrfpy.Caption("", 100, 520)
info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text)
# Add legend
legend1 = mcrfpy.Caption("Red=Start, Yellow=End, Green=A*, Blue=Dijkstra", 100, 540)
legend1.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend1)
legend2 = mcrfpy.Caption("Dark=Walls, Light=Floor", 100, 560)
legend2.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend2)
# Set scene and input
mcrfpy.setScene("pathfinding_comparison")
mcrfpy.keypressScene(handle_keypress)
# Show initial A* path
show_astar()
print("\nDemo ready!")

View File

@ -0,0 +1,4 @@
import mcrfpy
e = mcrfpy.Entity(0, 0)
print("Entity attributes:", dir(e))
print("\nEntity repr:", repr(e))

99
tests/debug_astar_demo.py Normal file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Debug the astar_vs_dijkstra demo issue"""
import mcrfpy
import sys
# Same setup as the demo
start_pos = (5, 10)
end_pos = (25, 10)
print("Debugging A* vs Dijkstra demo...")
print(f"Start: {start_pos}, End: {end_pos}")
# Create scene and grid
mcrfpy.createScene("debug")
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
# Initialize all as floor
print("\nInitializing 30x20 grid...")
for y in range(20):
for x in range(30):
grid.at(x, y).walkable = True
# Test path before obstacles
print("\nTest 1: Path with no obstacles")
path1 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
print(f" Path: {path1[:5]}...{path1[-3:] if len(path1) > 5 else ''}")
print(f" Length: {len(path1)}")
# Add obstacles from the demo
obstacles = [
# Vertical wall with gaps
[(15, y) for y in range(3, 17) if y not in [8, 12]],
# Horizontal walls
[(x, 5) for x in range(10, 20)],
[(x, 15) for x in range(10, 20)],
# Maze-like structure
[(x, 10) for x in range(20, 25)],
[(25, y) for y in range(5, 15)],
]
print("\nAdding obstacles...")
wall_count = 0
for obstacle_group in obstacles:
for x, y in obstacle_group:
grid.at(x, y).walkable = False
wall_count += 1
if wall_count <= 5:
print(f" Wall at ({x}, {y})")
print(f" Total walls added: {wall_count}")
# Check specific cells
print(f"\nChecking key positions:")
print(f" Start ({start_pos[0]}, {start_pos[1]}): walkable={grid.at(start_pos[0], start_pos[1]).walkable}")
print(f" End ({end_pos[0]}, {end_pos[1]}): walkable={grid.at(end_pos[0], end_pos[1]).walkable}")
# Check if path is blocked
print(f"\nChecking horizontal line at y=10:")
blocked_x = []
for x in range(30):
if not grid.at(x, 10).walkable:
blocked_x.append(x)
print(f" Blocked x positions: {blocked_x}")
# Test path with obstacles
print("\nTest 2: Path with obstacles")
path2 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
print(f" Path: {path2}")
print(f" Length: {len(path2)}")
# Check if there's any path at all
if not path2:
print("\n No path found! Checking why...")
# Check if we can reach the vertical wall gap
print("\n Testing path to wall gap at (15, 8):")
path_to_gap = grid.compute_astar_path(start_pos[0], start_pos[1], 15, 8)
print(f" Path to gap: {path_to_gap}")
# Check from gap to end
print("\n Testing path from gap (15, 8) to end:")
path_from_gap = grid.compute_astar_path(15, 8, end_pos[0], end_pos[1])
print(f" Path from gap: {path_from_gap}")
# Check walls more carefully
print("\nDetailed wall analysis:")
print(" Walls at x=25 (blocking end?):")
for y in range(5, 15):
print(f" ({25}, {y}): walkable={grid.at(25, y).walkable}")
def timer_cb(dt):
sys.exit(0)
ui = mcrfpy.sceneUI("debug")
ui.append(grid)
mcrfpy.setScene("debug")
mcrfpy.setTimer("exit", timer_cb, 100)

View File

@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Debug empty paths issue"""
import mcrfpy
import sys
print("Debugging empty paths...")
# Create scene and grid
mcrfpy.createScene("debug")
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
# Initialize grid - all walkable
print("\nInitializing grid...")
for y in range(10):
for x in range(10):
grid.at(x, y).walkable = True
# Test simple path
print("\nTest 1: Simple path from (0,0) to (5,5)")
path = grid.compute_astar_path(0, 0, 5, 5)
print(f" A* path: {path}")
print(f" Path length: {len(path)}")
# Test with Dijkstra
print("\nTest 2: Same path with Dijkstra")
grid.compute_dijkstra(0, 0)
dpath = grid.get_dijkstra_path(5, 5)
print(f" Dijkstra path: {dpath}")
print(f" Path length: {len(dpath)}")
# Check if grid is properly initialized
print("\nTest 3: Checking grid cells")
for y in range(3):
for x in range(3):
cell = grid.at(x, y)
print(f" Cell ({x},{y}): walkable={cell.walkable}")
# Test with walls
print("\nTest 4: Path with wall")
grid.at(2, 2).walkable = False
grid.at(3, 2).walkable = False
grid.at(4, 2).walkable = False
print(" Added wall at y=2, x=2,3,4")
path2 = grid.compute_astar_path(0, 0, 5, 5)
print(f" A* path with wall: {path2}")
print(f" Path length: {len(path2)}")
# Test invalid paths
print("\nTest 5: Path to blocked cell")
grid.at(9, 9).walkable = False
path3 = grid.compute_astar_path(0, 0, 9, 9)
print(f" Path to blocked cell: {path3}")
# Check TCOD map sync
print("\nTest 6: Verify TCOD map is synced")
# Try to force a sync
print(" Checking if syncTCODMap exists...")
if hasattr(grid, 'sync_tcod_map'):
print(" Calling sync_tcod_map()")
grid.sync_tcod_map()
else:
print(" No sync_tcod_map method found")
# Try path again
print("\nTest 7: Path after potential sync")
path4 = grid.compute_astar_path(0, 0, 5, 5)
print(f" A* path: {path4}")
def timer_cb(dt):
sys.exit(0)
# Quick UI setup
ui = mcrfpy.sceneUI("debug")
ui.append(grid)
mcrfpy.setScene("debug")
mcrfpy.setTimer("exit", timer_cb, 100)
print("\nStarting timer...")

59
tests/debug_visibility.py Normal file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Debug visibility crash"""
import mcrfpy
import sys
print("Debug visibility...")
# Create scene and grid
mcrfpy.createScene("debug")
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
# Initialize grid
print("Initializing grid...")
for y in range(5):
for x in range(5):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
# Create entity
print("Creating entity...")
entity = mcrfpy.Entity(2, 2)
entity.sprite_index = 64
grid.entities.append(entity)
print(f"Entity at ({entity.x}, {entity.y})")
# Check gridstate
print(f"\nGridstate length: {len(entity.gridstate)}")
print(f"Expected: {5 * 5}")
# Try to access gridstate
print("\nChecking gridstate access...")
try:
if len(entity.gridstate) > 0:
state = entity.gridstate[0]
print(f"First state: visible={state.visible}, discovered={state.discovered}")
except Exception as e:
print(f"Error accessing gridstate: {e}")
# Try update_visibility
print("\nTrying update_visibility...")
try:
entity.update_visibility()
print("update_visibility succeeded")
except Exception as e:
print(f"Error in update_visibility: {e}")
# Try perspective
print("\nTesting perspective...")
print(f"Initial perspective: {grid.perspective}")
try:
grid.perspective = 0
print(f"Set perspective to 0: {grid.perspective}")
except Exception as e:
print(f"Error setting perspective: {e}")
print("\nTest complete")
sys.exit(0)

234
tests/dijkstra_all_paths.py Normal file
View File

@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""
Dijkstra Demo - Shows ALL Path Combinations (Including Invalid)
===============================================================
Cycles through every possible entity pair to demonstrate both
valid paths and properly handled invalid paths (empty lists).
"""
import mcrfpy
import sys
# High contrast colors
WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown
FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray
PATH_COLOR = mcrfpy.Color(0, 255, 0) # Bright green
START_COLOR = mcrfpy.Color(255, 100, 100) # Light red
END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue
NO_PATH_COLOR = mcrfpy.Color(255, 0, 0) # Pure red for unreachable
# Global state
grid = None
entities = []
current_combo_index = 0
all_combinations = [] # All possible pairs
current_path = []
def create_map():
"""Create the map with entities"""
global grid, entities, all_combinations
mcrfpy.createScene("dijkstra_all")
# Create grid
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0)
# Map layout - Entity 1 is intentionally trapped!
map_layout = [
"..............", # Row 0
"..W.....WWWW..", # Row 1
"..W.W...W.EW..", # Row 2 - Entity 1 TRAPPED at (10,2)
"..W.....W..W..", # Row 3
"..W...E.WWWW..", # Row 4 - Entity 2 at (6,4)
"E.W...........", # Row 5 - Entity 3 at (0,5)
"..W...........", # Row 6
"..W...........", # Row 7
"..W.WWW.......", # Row 8
"..............", # Row 9
]
# Create the map
entity_positions = []
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
if char == 'W':
cell.walkable = False
cell.color = WALL_COLOR
else:
cell.walkable = True
cell.color = FLOOR_COLOR
if char == 'E':
entity_positions.append((x, y))
# Create entities
entities = []
for i, (x, y) in enumerate(entity_positions):
entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity)
print("Map Analysis:")
print("=============")
for i, (x, y) in enumerate(entity_positions):
print(f"Entity {i+1} at ({x}, {y})")
# Generate ALL combinations (including invalid ones)
all_combinations = []
for i in range(len(entities)):
for j in range(len(entities)):
if i != j: # Skip self-paths
all_combinations.append((i, j))
print(f"\nTotal path combinations to test: {len(all_combinations)}")
def clear_path_colors():
"""Reset all floor tiles to original color"""
global current_path
for y in range(grid.grid_y):
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
cell.color = FLOOR_COLOR
current_path = []
def show_combination(index):
"""Show a specific path combination (valid or invalid)"""
global current_combo_index, current_path
current_combo_index = index % len(all_combinations)
from_idx, to_idx = all_combinations[current_combo_index]
# Clear previous path
clear_path_colors()
# Get entities
e_from = entities[from_idx]
e_to = entities[to_idx]
# Calculate path
path = e_from.path_to(int(e_to.x), int(e_to.y))
current_path = path if path else []
# Always color start and end positions
grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR
grid.at(int(e_to.x), int(e_to.y)).color = NO_PATH_COLOR if not path else END_COLOR
# Color the path if it exists
if path:
# Color intermediate steps
for i, (x, y) in enumerate(path):
if i > 0 and i < len(path) - 1:
grid.at(x, y).color = PATH_COLOR
status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = {len(path)} steps"
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green for valid
# Show path steps
path_display = []
for i, (x, y) in enumerate(path[:5]):
path_display.append(f"({x},{y})")
if len(path) > 5:
path_display.append("...")
path_text.text = "Path: " + "".join(path_display)
else:
status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = NO PATH!"
status_text.fill_color = mcrfpy.Color(255, 100, 100) # Red for invalid
path_text.text = "Path: [] (No valid path exists)"
# Update info
info_text.text = f"From: Entity {from_idx+1} at ({int(e_from.x)}, {int(e_from.y)}) | To: Entity {to_idx+1} at ({int(e_to.x)}, {int(e_to.y)})"
def handle_keypress(key_str, state):
"""Handle keyboard input"""
global current_combo_index
if state == "end": return
if key_str == "Esc" or key_str == "Q":
print("\nExiting...")
sys.exit(0)
elif key_str == "Space" or key_str == "N":
show_combination(current_combo_index + 1)
elif key_str == "P":
show_combination(current_combo_index - 1)
elif key_str == "R":
show_combination(current_combo_index)
elif key_str in "123456":
combo_num = int(key_str) - 1 # 0-based index
if combo_num < len(all_combinations):
show_combination(combo_num)
# Create the demo
print("Dijkstra All Paths Demo")
print("=======================")
print("Shows ALL path combinations including invalid ones")
print("Entity 1 is trapped - paths to/from it will be empty!")
print()
create_map()
# Set up UI
ui = mcrfpy.sceneUI("dijkstra_all")
ui.append(grid)
# Scale and position
grid.size = (560, 400)
grid.position = (120, 100)
# Add title
title = mcrfpy.Caption("Dijkstra - All Paths (Valid & Invalid)", 200, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add status (will change color based on validity)
status_text = mcrfpy.Caption("Ready", 120, 60)
status_text.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(status_text)
# Add info
info_text = mcrfpy.Caption("", 120, 80)
info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text)
# Add path display
path_text = mcrfpy.Caption("Path: None", 120, 520)
path_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(path_text)
# Add controls
controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, 1-6=Jump to path, Q=Quit", 120, 540)
controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls)
# Add legend
legend = mcrfpy.Caption("Red Start→Blue End (valid) | Red Start→Red End (invalid)", 120, 560)
legend.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend)
# Expected results info
expected = mcrfpy.Caption("Entity 1 is trapped: paths 1→2, 1→3, 2→1, 3→1 will fail", 120, 580)
expected.fill_color = mcrfpy.Color(255, 150, 150)
ui.append(expected)
# Set scene first, then set up input handler
mcrfpy.setScene("dijkstra_all")
mcrfpy.keypressScene(handle_keypress)
# Show first combination
show_combination(0)
print("\nDemo ready!")
print("Expected results:")
print(" Path 1: Entity 1→2 = NO PATH (Entity 1 is trapped)")
print(" Path 2: Entity 1→3 = NO PATH (Entity 1 is trapped)")
print(" Path 3: Entity 2→1 = NO PATH (Entity 1 is trapped)")
print(" Path 4: Entity 2→3 = Valid path")
print(" Path 5: Entity 3→1 = NO PATH (Entity 1 is trapped)")
print(" Path 6: Entity 3→2 = Valid path")

View File

@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""
Dijkstra Demo - Cycles Through Different Path Combinations
==========================================================
Shows paths between different entity pairs, skipping impossible paths.
"""
import mcrfpy
import sys
# High contrast colors
WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown
FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray
PATH_COLOR = mcrfpy.Color(0, 255, 0) # Bright green
START_COLOR = mcrfpy.Color(255, 100, 100) # Light red
END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue
# Global state
grid = None
entities = []
current_path_index = 0
path_combinations = []
current_path = []
def create_map():
"""Create the map with entities"""
global grid, entities
mcrfpy.createScene("dijkstra_cycle")
# Create grid
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0)
# Map layout
map_layout = [
"..............", # Row 0
"..W.....WWWW..", # Row 1
"..W.W...W.EW..", # Row 2 - Entity 1 at (10,2) is TRAPPED!
"..W.....W..W..", # Row 3
"..W...E.WWWW..", # Row 4 - Entity 2 at (6,4)
"E.W...........", # Row 5 - Entity 3 at (0,5)
"..W...........", # Row 6
"..W...........", # Row 7
"..W.WWW.......", # Row 8
"..............", # Row 9
]
# Create the map
entity_positions = []
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
if char == 'W':
cell.walkable = False
cell.color = WALL_COLOR
else:
cell.walkable = True
cell.color = FLOOR_COLOR
if char == 'E':
entity_positions.append((x, y))
# Create entities
entities = []
for i, (x, y) in enumerate(entity_positions):
entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity)
print("Entities created:")
for i, (x, y) in enumerate(entity_positions):
print(f" Entity {i+1} at ({x}, {y})")
# Check which entity is trapped
print("\nChecking accessibility:")
for i, e in enumerate(entities):
# Try to path to each other entity
can_reach = []
for j, other in enumerate(entities):
if i != j:
path = e.path_to(int(other.x), int(other.y))
if path:
can_reach.append(j+1)
if not can_reach:
print(f" Entity {i+1} at ({int(e.x)}, {int(e.y)}) is TRAPPED!")
else:
print(f" Entity {i+1} can reach entities: {can_reach}")
# Generate valid path combinations (excluding trapped entity)
global path_combinations
path_combinations = []
# Only paths between entities 2 and 3 (indices 1 and 2) will work
# since entity 1 (index 0) is trapped
if len(entities) >= 3:
# Entity 2 to Entity 3
path = entities[1].path_to(int(entities[2].x), int(entities[2].y))
if path:
path_combinations.append((1, 2, path))
# Entity 3 to Entity 2
path = entities[2].path_to(int(entities[1].x), int(entities[1].y))
if path:
path_combinations.append((2, 1, path))
print(f"\nFound {len(path_combinations)} valid paths")
def clear_path_colors():
"""Reset all floor tiles to original color"""
global current_path
for y in range(grid.grid_y):
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
cell.color = FLOOR_COLOR
current_path = []
def show_path(index):
"""Show a specific path combination"""
global current_path_index, current_path
if not path_combinations:
status_text.text = "No valid paths available (Entity 1 is trapped!)"
return
current_path_index = index % len(path_combinations)
from_idx, to_idx, path = path_combinations[current_path_index]
# Clear previous path
clear_path_colors()
# Get entities
e_from = entities[from_idx]
e_to = entities[to_idx]
# Color the path
current_path = path
if path:
# Color start and end
grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR
grid.at(int(e_to.x), int(e_to.y)).color = END_COLOR
# Color intermediate steps
for i, (x, y) in enumerate(path):
if i > 0 and i < len(path) - 1:
grid.at(x, y).color = PATH_COLOR
# Update status
status_text.text = f"Path {current_path_index + 1}/{len(path_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} ({len(path)} steps)"
# Update path display
path_display = []
for i, (x, y) in enumerate(path[:5]): # Show first 5 steps
path_display.append(f"({x},{y})")
if len(path) > 5:
path_display.append("...")
path_text.text = "Path: " + "".join(path_display) if path_display else "Path: None"
def handle_keypress(key_str, state):
"""Handle keyboard input"""
global current_path_index
if state == "end": return
if key_str == "Esc":
print("\nExiting...")
sys.exit(0)
elif key_str == "N" or key_str == "Space":
show_path(current_path_index + 1)
elif key_str == "P":
show_path(current_path_index - 1)
elif key_str == "R":
show_path(current_path_index)
# Create the demo
print("Dijkstra Path Cycling Demo")
print("==========================")
print("Note: Entity 1 is trapped by walls!")
print()
create_map()
# Set up UI
ui = mcrfpy.sceneUI("dijkstra_cycle")
ui.append(grid)
# Scale and position
grid.size = (560, 400)
grid.position = (120, 100)
# Add title
title = mcrfpy.Caption("Dijkstra Pathfinding - Cycle Paths", 200, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add status
status_text = mcrfpy.Caption("Press SPACE to cycle paths", 120, 60)
status_text.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(status_text)
# Add path display
path_text = mcrfpy.Caption("Path: None", 120, 520)
path_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(path_text)
# Add controls
controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, R=Refresh, Q=Quit", 120, 540)
controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls)
# Add legend
legend = mcrfpy.Caption("Red=Start, Blue=End, Green=Path, Dark=Wall", 120, 560)
legend.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend)
# Show first valid path
mcrfpy.setScene("dijkstra_cycle")
mcrfpy.keypressScene(handle_keypress)
# Display initial path
if path_combinations:
show_path(0)
else:
status_text.text = "No valid paths! Entity 1 is trapped!"
print("\nDemo ready!")
print("Controls:")
print(" SPACE or N - Next path")
print(" P - Previous path")
print(" R - Refresh current path")
print(" Q - Quit")

161
tests/dijkstra_debug.py Normal file
View File

@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
Debug version of Dijkstra pathfinding to diagnose visualization issues
"""
import mcrfpy
import sys
# Colors
WALL_COLOR = mcrfpy.Color(60, 30, 30)
FLOOR_COLOR = mcrfpy.Color(200, 200, 220)
PATH_COLOR = mcrfpy.Color(200, 250, 220)
ENTITY_COLORS = [
mcrfpy.Color(255, 100, 100), # Entity 1 - Red
mcrfpy.Color(100, 255, 100), # Entity 2 - Green
mcrfpy.Color(100, 100, 255), # Entity 3 - Blue
]
# Global state
grid = None
entities = []
first_point = None
second_point = None
def create_simple_map():
"""Create a simple test map"""
global grid, entities
mcrfpy.createScene("dijkstra_debug")
# Small grid for easy debugging
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0)
print("Initializing 10x10 grid...")
# Initialize all as floor
for y in range(10):
for x in range(10):
grid.at(x, y).walkable = True
grid.at(x, y).transparent = True
grid.at(x, y).color = FLOOR_COLOR
# Add a simple wall
print("Adding walls at:")
walls = [(5, 2), (5, 3), (5, 4), (5, 5), (5, 6)]
for x, y in walls:
print(f" Wall at ({x}, {y})")
grid.at(x, y).walkable = False
grid.at(x, y).color = WALL_COLOR
# Create 3 entities
entity_positions = [(2, 5), (8, 5), (5, 8)]
entities = []
print("\nCreating entities at:")
for i, (x, y) in enumerate(entity_positions):
print(f" Entity {i+1} at ({x}, {y})")
entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity)
return grid
def test_path_highlighting():
"""Test path highlighting with debug output"""
print("\n" + "="*50)
print("Testing path highlighting...")
# Select first two entities
e1 = entities[0]
e2 = entities[1]
print(f"\nEntity 1 position: ({e1.x}, {e1.y})")
print(f"Entity 2 position: ({e2.x}, {e2.y})")
# Use entity.path_to()
print("\nCalling entity.path_to()...")
path = e1.path_to(int(e2.x), int(e2.y))
print(f"Path returned: {path}")
print(f"Path length: {len(path)} steps")
if path:
print("\nHighlighting path cells:")
for i, (x, y) in enumerate(path):
print(f" Step {i}: ({x}, {y})")
# Get current color for debugging
cell = grid.at(x, y)
old_color = (cell.color.r, cell.color.g, cell.color.b)
# Set new color
cell.color = PATH_COLOR
new_color = (cell.color.r, cell.color.g, cell.color.b)
print(f" Color changed from {old_color} to {new_color}")
print(f" Walkable: {cell.walkable}")
# Also test grid's Dijkstra methods
print("\n" + "-"*30)
print("Testing grid Dijkstra methods...")
grid.compute_dijkstra(int(e1.x), int(e1.y))
grid_path = grid.get_dijkstra_path(int(e2.x), int(e2.y))
distance = grid.get_dijkstra_distance(int(e2.x), int(e2.y))
print(f"Grid path: {grid_path}")
print(f"Grid distance: {distance}")
# Verify colors were set
print("\nVerifying cell colors after highlighting:")
for x, y in path[:3]: # Check first 3 cells
cell = grid.at(x, y)
color = (cell.color.r, cell.color.g, cell.color.b)
expected = (PATH_COLOR.r, PATH_COLOR.g, PATH_COLOR.b)
match = color == expected
print(f" Cell ({x}, {y}): color={color}, expected={expected}, match={match}")
def handle_keypress(scene_name, keycode):
"""Simple keypress handler"""
if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
print("\nExiting debug...")
sys.exit(0)
elif keycode == 32: # Space
print("\nSpace pressed - retesting path highlighting...")
test_path_highlighting()
# Create the map
print("Dijkstra Debug Test")
print("===================")
grid = create_simple_map()
# Initial path test
test_path_highlighting()
# Set up UI
ui = mcrfpy.sceneUI("dijkstra_debug")
ui.append(grid)
# Position and scale
grid.position = (50, 50)
grid.size = (400, 400) # 10*40
# Add title
title = mcrfpy.Caption("Dijkstra Debug - Press SPACE to retest, Q to quit", 50, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add debug info
info = mcrfpy.Caption("Check console for debug output", 50, 470)
info.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info)
# Set up scene
mcrfpy.keypressScene(handle_keypress)
mcrfpy.setScene("dijkstra_debug")
print("\nScene ready. The path should be highlighted in cyan.")
print("If you don't see the path, there may be a rendering issue.")
print("Press SPACE to retest, Q to quit.")

View File

@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
Working Dijkstra Demo with Clear Visual Feedback
================================================
This demo shows pathfinding with high-contrast colors.
"""
import mcrfpy
import sys
# High contrast colors
WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown for walls
FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray for floors
PATH_COLOR = mcrfpy.Color(0, 255, 0) # Pure green for paths
START_COLOR = mcrfpy.Color(255, 0, 0) # Red for start
END_COLOR = mcrfpy.Color(0, 0, 255) # Blue for end
print("Dijkstra Demo - High Contrast")
print("==============================")
# Create scene
mcrfpy.createScene("dijkstra_demo")
# Create grid with exact layout from user
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0)
# Map layout
map_layout = [
"..............", # Row 0
"..W.....WWWW..", # Row 1
"..W.W...W.EW..", # Row 2
"..W.....W..W..", # Row 3
"..W...E.WWWW..", # Row 4
"E.W...........", # Row 5
"..W...........", # Row 6
"..W...........", # Row 7
"..W.WWW.......", # Row 8
"..............", # Row 9
]
# Create the map
entity_positions = []
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
if char == 'W':
cell.walkable = False
cell.color = WALL_COLOR
else:
cell.walkable = True
cell.color = FLOOR_COLOR
if char == 'E':
entity_positions.append((x, y))
print(f"Map created: {grid.grid_x}x{grid.grid_y}")
print(f"Entity positions: {entity_positions}")
# Create entities
entities = []
for i, (x, y) in enumerate(entity_positions):
entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity)
print(f"Entity {i+1} at ({x}, {y})")
# Highlight a path immediately
if len(entities) >= 2:
e1, e2 = entities[0], entities[1]
print(f"\nCalculating path from Entity 1 ({e1.x}, {e1.y}) to Entity 2 ({e2.x}, {e2.y})...")
path = e1.path_to(int(e2.x), int(e2.y))
print(f"Path found: {path}")
print(f"Path length: {len(path)} steps")
if path:
print("\nHighlighting path in bright green...")
# Color start and end specially
grid.at(int(e1.x), int(e1.y)).color = START_COLOR
grid.at(int(e2.x), int(e2.y)).color = END_COLOR
# Color the path
for i, (x, y) in enumerate(path):
if i > 0 and i < len(path) - 1: # Skip start and end
grid.at(x, y).color = PATH_COLOR
print(f" Colored ({x}, {y}) green")
# Keypress handler
def handle_keypress(scene_name, keycode):
if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
print("\nExiting...")
sys.exit(0)
elif keycode == 32: # Space
print("\nRefreshing path colors...")
# Re-color the path to ensure it's visible
if len(entities) >= 2 and path:
for x, y in path[1:-1]:
grid.at(x, y).color = PATH_COLOR
# Set up UI
ui = mcrfpy.sceneUI("dijkstra_demo")
ui.append(grid)
# Scale grid
grid.size = (560, 400) # 14*40, 10*40
grid.position = (120, 100)
# Add title
title = mcrfpy.Caption("Dijkstra Pathfinding - High Contrast", 200, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add legend
legend1 = mcrfpy.Caption("Red=Start, Blue=End, Green=Path", 120, 520)
legend1.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(legend1)
legend2 = mcrfpy.Caption("Press Q to quit, SPACE to refresh", 120, 540)
legend2.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend2)
# Entity info
info = mcrfpy.Caption(f"Path: Entity 1 to 2 = {len(path) if 'path' in locals() else 0} steps", 120, 60)
info.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(info)
# Set up input
mcrfpy.keypressScene(handle_keypress)
mcrfpy.setScene("dijkstra_demo")
print("\nDemo ready! The path should be clearly visible in bright green.")
print("Red = Start, Blue = End, Green = Path")
print("Press SPACE to refresh colors if needed.")

View File

@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""
Dijkstra Pathfinding Interactive Demo
=====================================
Interactive visualization showing Dijkstra pathfinding between entities.
Controls:
- Press 1/2/3 to select the first entity
- Press A/B/C to select the second entity
- Space to clear selection
- Q or ESC to quit
The path between selected entities is automatically highlighted.
"""
import mcrfpy
import sys
# Colors - using more distinct values
WALL_COLOR = mcrfpy.Color(60, 30, 30)
FLOOR_COLOR = mcrfpy.Color(100, 100, 120) # Darker floor for better contrast
PATH_COLOR = mcrfpy.Color(50, 255, 50) # Bright green for path
ENTITY_COLORS = [
mcrfpy.Color(255, 100, 100), # Entity 1 - Red
mcrfpy.Color(100, 255, 100), # Entity 2 - Green
mcrfpy.Color(100, 100, 255), # Entity 3 - Blue
]
# Global state
grid = None
entities = []
first_point = None
second_point = None
def create_map():
"""Create the interactive map with the layout specified by the user"""
global grid, entities
mcrfpy.createScene("dijkstra_interactive")
# Create grid - 14x10 as specified
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0)
# Define the map layout from user's specification
# . = floor, W = wall, E = entity position
map_layout = [
"..............", # Row 0
"..W.....WWWW..", # Row 1
"..W.W...W.EW..", # Row 2
"..W.....W..W..", # Row 3
"..W...E.WWWW..", # Row 4
"E.W...........", # Row 5
"..W...........", # Row 6
"..W...........", # Row 7
"..W.WWW.......", # Row 8
"..............", # Row 9
]
# Create the map
entity_positions = []
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
if char == 'W':
# Wall
cell.walkable = False
cell.transparent = False
cell.color = WALL_COLOR
else:
# Floor
cell.walkable = True
cell.transparent = True
cell.color = FLOOR_COLOR
if char == 'E':
# Entity position
entity_positions.append((x, y))
# Create entities at marked positions
entities = []
for i, (x, y) in enumerate(entity_positions):
entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity)
return grid
def clear_path_highlight():
"""Clear any existing path highlighting"""
# Reset all floor tiles to original color
for y in range(grid.grid_y):
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
cell.color = FLOOR_COLOR
def highlight_path():
"""Highlight the path between selected entities"""
if first_point is None or second_point is None:
return
# Clear previous highlighting
clear_path_highlight()
# Get entities
entity1 = entities[first_point]
entity2 = entities[second_point]
# Compute Dijkstra from first entity
grid.compute_dijkstra(int(entity1.x), int(entity1.y))
# Get path to second entity
path = grid.get_dijkstra_path(int(entity2.x), int(entity2.y))
if path:
# Highlight the path
for x, y in path:
cell = grid.at(x, y)
if cell.walkable:
cell.color = PATH_COLOR
# Also highlight start and end with entity colors
grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point]
grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point]
# Update info
distance = grid.get_dijkstra_distance(int(entity2.x), int(entity2.y))
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps, {distance:.1f} units"
else:
info_text.text = f"No path between Entity {first_point+1} and Entity {second_point+1}"
def handle_keypress(scene_name, keycode):
"""Handle keyboard input"""
global first_point, second_point
# Number keys for first entity
if keycode == 49: # '1'
first_point = 0
status_text.text = f"First: Entity 1 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
highlight_path()
elif keycode == 50: # '2'
first_point = 1
status_text.text = f"First: Entity 2 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
highlight_path()
elif keycode == 51: # '3'
first_point = 2
status_text.text = f"First: Entity 3 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
highlight_path()
# Letter keys for second entity
elif keycode == 65 or keycode == 97: # 'A' or 'a'
second_point = 0
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 1"
highlight_path()
elif keycode == 66 or keycode == 98: # 'B' or 'b'
second_point = 1
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 2"
highlight_path()
elif keycode == 67 or keycode == 99: # 'C' or 'c'
second_point = 2
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 3"
highlight_path()
# Clear selection
elif keycode == 32: # Space
first_point = None
second_point = None
clear_path_highlight()
status_text.text = "Press 1/2/3 for first entity, A/B/C for second"
info_text.text = "Space to clear, Q to quit"
# Quit
elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
print("\nExiting Dijkstra interactive demo...")
sys.exit(0)
# Create the visualization
print("Dijkstra Pathfinding Interactive Demo")
print("=====================================")
print("Controls:")
print(" 1/2/3 - Select first entity")
print(" A/B/C - Select second entity")
print(" Space - Clear selection")
print(" Q/ESC - Quit")
# Create map
grid = create_map()
# Set up UI
ui = mcrfpy.sceneUI("dijkstra_interactive")
ui.append(grid)
# Scale and position grid for better visibility
grid.size = (560, 400) # 14*40, 10*40
grid.position = (120, 60)
# Add title
title = mcrfpy.Caption("Dijkstra Pathfinding Interactive", 250, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add status text
status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480)
status_text.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status_text)
# Add info text
info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500)
info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text)
# Add legend
legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 540)
legend1.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend1)
legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 560)
legend2.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend2)
# Mark entity positions with colored indicators
for i, entity in enumerate(entities):
marker = mcrfpy.Caption(str(i+1),
120 + int(entity.x) * 40 + 15,
60 + int(entity.y) * 40 + 10)
marker.fill_color = ENTITY_COLORS[i]
marker.outline = 1
marker.outline_color = mcrfpy.Color(0, 0, 0)
ui.append(marker)
# Set up input handling
mcrfpy.keypressScene(handle_keypress)
# Show the scene
mcrfpy.setScene("dijkstra_interactive")
print("\nVisualization ready!")
print("Entities are at:")
for i, entity in enumerate(entities):
print(f" Entity {i+1}: ({int(entity.x)}, {int(entity.y)})")

View File

@ -0,0 +1,344 @@
#!/usr/bin/env python3
"""
Enhanced Dijkstra Pathfinding Interactive Demo
==============================================
Interactive visualization with entity pathfinding animations.
Controls:
- Press 1/2/3 to select the first entity
- Press A/B/C to select the second entity
- Space to clear selection
- M to make selected entity move along path
- P to pause/resume animation
- R to reset entity positions
- Q or ESC to quit
"""
import mcrfpy
import sys
import math
# Colors
WALL_COLOR = mcrfpy.Color(60, 30, 30)
FLOOR_COLOR = mcrfpy.Color(200, 200, 220)
PATH_COLOR = mcrfpy.Color(200, 250, 220)
VISITED_COLOR = mcrfpy.Color(180, 230, 200)
ENTITY_COLORS = [
mcrfpy.Color(255, 100, 100), # Entity 1 - Red
mcrfpy.Color(100, 255, 100), # Entity 2 - Green
mcrfpy.Color(100, 100, 255), # Entity 3 - Blue
]
# Global state
grid = None
entities = []
first_point = None
second_point = None
current_path = []
animating = False
animation_progress = 0.0
animation_speed = 2.0 # cells per second
original_positions = [] # Store original entity positions
def create_map():
"""Create the interactive map with the layout specified by the user"""
global grid, entities, original_positions
mcrfpy.createScene("dijkstra_enhanced")
# Create grid - 14x10 as specified
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0)
# Define the map layout from user's specification
# . = floor, W = wall, E = entity position
map_layout = [
"..............", # Row 0
"..W.....WWWW..", # Row 1
"..W.W...W.EW..", # Row 2
"..W.....W..W..", # Row 3
"..W...E.WWWW..", # Row 4
"E.W...........", # Row 5
"..W...........", # Row 6
"..W...........", # Row 7
"..W.WWW.......", # Row 8
"..............", # Row 9
]
# Create the map
entity_positions = []
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
if char == 'W':
# Wall
cell.walkable = False
cell.transparent = False
cell.color = WALL_COLOR
else:
# Floor
cell.walkable = True
cell.transparent = True
cell.color = FLOOR_COLOR
if char == 'E':
# Entity position
entity_positions.append((x, y))
# Create entities at marked positions
entities = []
original_positions = []
for i, (x, y) in enumerate(entity_positions):
entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity)
original_positions.append((x, y))
return grid
def clear_path_highlight():
"""Clear any existing path highlighting"""
global current_path
# Reset all floor tiles to original color
for y in range(grid.grid_y):
for x in range(grid.grid_x):
cell = grid.at(x, y)
if cell.walkable:
cell.color = FLOOR_COLOR
current_path = []
def highlight_path():
"""Highlight the path between selected entities using entity.path_to()"""
global current_path
if first_point is None or second_point is None:
return
# Clear previous highlighting
clear_path_highlight()
# Get entities
entity1 = entities[first_point]
entity2 = entities[second_point]
# Use the new path_to method!
path = entity1.path_to(int(entity2.x), int(entity2.y))
if path:
current_path = path
# Highlight the path
for i, (x, y) in enumerate(path):
cell = grid.at(x, y)
if cell.walkable:
# Use gradient for path visualization
if i < len(path) - 1:
cell.color = PATH_COLOR
else:
cell.color = VISITED_COLOR
# Highlight start and end with entity colors
grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point]
grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point]
# Update info
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps"
else:
info_text.text = f"No path between Entity {first_point+1} and Entity {second_point+1}"
current_path = []
def animate_movement(dt):
"""Animate entity movement along path"""
global animation_progress, animating, current_path
if not animating or not current_path or first_point is None:
return
entity = entities[first_point]
# Update animation progress
animation_progress += animation_speed * dt
# Calculate current position along path
path_index = int(animation_progress)
if path_index >= len(current_path):
# Animation complete
animating = False
animation_progress = 0.0
# Snap to final position
if current_path:
final_x, final_y = current_path[-1]
entity.x = float(final_x)
entity.y = float(final_y)
return
# Interpolate between path points
if path_index < len(current_path) - 1:
curr_x, curr_y = current_path[path_index]
next_x, next_y = current_path[path_index + 1]
# Calculate interpolation factor
t = animation_progress - path_index
# Smooth interpolation
entity.x = curr_x + (next_x - curr_x) * t
entity.y = curr_y + (next_y - curr_y) * t
else:
# At last point
entity.x, entity.y = current_path[path_index]
def handle_keypress(scene_name, keycode):
"""Handle keyboard input"""
global first_point, second_point, animating, animation_progress
# Number keys for first entity
if keycode == 49: # '1'
first_point = 0
status_text.text = f"First: Entity 1 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
highlight_path()
elif keycode == 50: # '2'
first_point = 1
status_text.text = f"First: Entity 2 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
highlight_path()
elif keycode == 51: # '3'
first_point = 2
status_text.text = f"First: Entity 3 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
highlight_path()
# Letter keys for second entity
elif keycode == 65 or keycode == 97: # 'A' or 'a'
second_point = 0
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 1"
highlight_path()
elif keycode == 66 or keycode == 98: # 'B' or 'b'
second_point = 1
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 2"
highlight_path()
elif keycode == 67 or keycode == 99: # 'C' or 'c'
second_point = 2
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 3"
highlight_path()
# Movement control
elif keycode == 77 or keycode == 109: # 'M' or 'm'
if current_path and first_point is not None:
animating = True
animation_progress = 0.0
control_text.text = "Animation: MOVING (press P to pause)"
# Pause/Resume
elif keycode == 80 or keycode == 112: # 'P' or 'p'
animating = not animating
control_text.text = f"Animation: {'MOVING' if animating else 'PAUSED'} (press P to {'pause' if animating else 'resume'})"
# Reset positions
elif keycode == 82 or keycode == 114: # 'R' or 'r'
animating = False
animation_progress = 0.0
for i, entity in enumerate(entities):
entity.x, entity.y = original_positions[i]
control_text.text = "Entities reset to original positions"
highlight_path() # Re-highlight path after reset
# Clear selection
elif keycode == 32: # Space
first_point = None
second_point = None
animating = False
animation_progress = 0.0
clear_path_highlight()
status_text.text = "Press 1/2/3 for first entity, A/B/C for second"
info_text.text = "Space to clear, Q to quit"
control_text.text = "Press M to move, P to pause, R to reset"
# Quit
elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
print("\nExiting enhanced Dijkstra demo...")
sys.exit(0)
# Timer callback for animation
def update_animation(dt):
"""Update animation state"""
animate_movement(dt / 1000.0) # Convert ms to seconds
# Create the visualization
print("Enhanced Dijkstra Pathfinding Demo")
print("==================================")
print("Controls:")
print(" 1/2/3 - Select first entity")
print(" A/B/C - Select second entity")
print(" M - Move first entity along path")
print(" P - Pause/Resume animation")
print(" R - Reset entity positions")
print(" Space - Clear selection")
print(" Q/ESC - Quit")
# Create map
grid = create_map()
# Set up UI
ui = mcrfpy.sceneUI("dijkstra_enhanced")
ui.append(grid)
# Scale and position grid for better visibility
grid.size = (560, 400) # 14*40, 10*40
grid.position = (120, 60)
# Add title
title = mcrfpy.Caption("Enhanced Dijkstra Pathfinding", 250, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add status text
status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480)
status_text.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status_text)
# Add info text
info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500)
info_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(info_text)
# Add control text
control_text = mcrfpy.Caption("Press M to move, P to pause, R to reset", 120, 520)
control_text.fill_color = mcrfpy.Color(150, 200, 150)
ui.append(control_text)
# Add legend
legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 560)
legend1.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend1)
legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 580)
legend2.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend2)
# Mark entity positions with colored indicators
for i, entity in enumerate(entities):
marker = mcrfpy.Caption(str(i+1),
120 + int(entity.x) * 40 + 15,
60 + int(entity.y) * 40 + 10)
marker.fill_color = ENTITY_COLORS[i]
marker.outline = 1
marker.outline_color = mcrfpy.Color(0, 0, 0)
ui.append(marker)
# Set up input handling
mcrfpy.keypressScene(handle_keypress)
# Set up animation timer (60 FPS)
mcrfpy.setTimer("animation", update_animation, 16)
# Show the scene
mcrfpy.setScene("dijkstra_enhanced")
print("\nVisualization ready!")
print("Entities are at:")
for i, entity in enumerate(entities):
print(f" Entity {i+1}: ({int(entity.x)}, {int(entity.y)})")

146
tests/dijkstra_test.py Normal file
View File

@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""
Dijkstra Pathfinding Test - Headless
====================================
Tests all Dijkstra functionality and generates a screenshot.
"""
import mcrfpy
from mcrfpy import automation
import sys
def create_test_map():
"""Create a test map with obstacles"""
mcrfpy.createScene("dijkstra_test")
# Create grid
grid = mcrfpy.Grid(grid_x=20, grid_y=12)
grid.fill_color = mcrfpy.Color(0, 0, 0)
# Initialize all cells as walkable floor
for y in range(12):
for x in range(20):
grid.at(x, y).walkable = True
grid.at(x, y).transparent = True
grid.at(x, y).color = mcrfpy.Color(200, 200, 220)
# Add walls to create interesting paths
walls = [
# Vertical wall in the middle
(10, 1), (10, 2), (10, 3), (10, 4), (10, 5), (10, 6), (10, 7), (10, 8),
# Horizontal walls
(2, 6), (3, 6), (4, 6), (5, 6), (6, 6),
(14, 6), (15, 6), (16, 6), (17, 6),
# Some scattered obstacles
(5, 2), (15, 2), (5, 9), (15, 9)
]
for x, y in walls:
grid.at(x, y).walkable = False
grid.at(x, y).color = mcrfpy.Color(60, 30, 30)
# Place test entities
entities = []
positions = [(2, 2), (17, 2), (9, 10)]
colors = [
mcrfpy.Color(255, 100, 100), # Red
mcrfpy.Color(100, 255, 100), # Green
mcrfpy.Color(100, 100, 255) # Blue
]
for i, (x, y) in enumerate(positions):
entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity)
# Mark entity positions
grid.at(x, y).color = colors[i]
return grid, entities
def test_dijkstra(grid, entities):
"""Test Dijkstra pathfinding between all entity pairs"""
results = []
for i in range(len(entities)):
for j in range(len(entities)):
if i != j:
# Compute Dijkstra from entity i
e1 = entities[i]
e2 = entities[j]
grid.compute_dijkstra(int(e1.x), int(e1.y))
# Get distance and path to entity j
distance = grid.get_dijkstra_distance(int(e2.x), int(e2.y))
path = grid.get_dijkstra_path(int(e2.x), int(e2.y))
if path:
results.append(f"Path {i+1}{j+1}: {len(path)} steps, {distance:.1f} units")
# Color one interesting path
if i == 0 and j == 2: # Path from 1 to 3
for x, y in path[1:-1]: # Skip endpoints
if grid.at(x, y).walkable:
grid.at(x, y).color = mcrfpy.Color(200, 250, 220)
else:
results.append(f"Path {i+1}{j+1}: No path found!")
return results
def run_test(runtime):
"""Timer callback to run tests and take screenshot"""
# Run pathfinding tests
results = test_dijkstra(grid, entities)
# Update display with results
y_pos = 380
for result in results:
caption = mcrfpy.Caption(result, 50, y_pos)
caption.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(caption)
y_pos += 20
# Take screenshot
mcrfpy.setTimer("screenshot", lambda rt: take_screenshot(), 500)
def take_screenshot():
"""Take screenshot and exit"""
try:
automation.screenshot("dijkstra_test.png")
print("Screenshot saved: dijkstra_test.png")
except Exception as e:
print(f"Screenshot failed: {e}")
# Exit
sys.exit(0)
# Create test map
print("Creating Dijkstra pathfinding test...")
grid, entities = create_test_map()
# Set up UI
ui = mcrfpy.sceneUI("dijkstra_test")
ui.append(grid)
# Position and scale grid
grid.position = (50, 50)
grid.size = (500, 300)
# Add title
title = mcrfpy.Caption("Dijkstra Pathfinding Test", 200, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add legend
legend = mcrfpy.Caption("Red=Entity1 Green=Entity2 Blue=Entity3 Cyan=Path 1→3", 50, 360)
legend.fill_color = mcrfpy.Color(180, 180, 180)
ui.append(legend)
# Set scene
mcrfpy.setScene("dijkstra_test")
# Run test after scene loads
mcrfpy.setTimer("test", run_test, 100)
print("Running Dijkstra tests...")

View File

@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""
Interactive Visibility Demo
==========================
Controls:
- WASD: Move the player (green @)
- Arrow keys: Move enemy (red E)
- Tab: Cycle perspective (Omniscient Player Enemy Omniscient)
- Space: Update visibility for current entity
- R: Reset positions
"""
import mcrfpy
import sys
# Create scene and grid
mcrfpy.createScene("visibility_demo")
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background
# Initialize grid - all walkable and transparent
for y in range(20):
for x in range(30):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.color = mcrfpy.Color(100, 100, 120) # Floor color
# Create walls
walls = [
# Central cross
[(15, y) for y in range(8, 12)],
[(x, 10) for x in range(13, 18)],
# Rooms
# Top-left room
[(x, 5) for x in range(2, 8)] + [(8, y) for y in range(2, 6)],
[(2, y) for y in range(2, 6)] + [(x, 2) for x in range(2, 8)],
# Top-right room
[(x, 5) for x in range(22, 28)] + [(22, y) for y in range(2, 6)],
[(28, y) for y in range(2, 6)] + [(x, 2) for x in range(22, 28)],
# Bottom-left room
[(x, 15) for x in range(2, 8)] + [(8, y) for y in range(15, 18)],
[(2, y) for y in range(15, 18)] + [(x, 18) for x in range(2, 8)],
# Bottom-right room
[(x, 15) for x in range(22, 28)] + [(22, y) for y in range(15, 18)],
[(28, y) for y in range(15, 18)] + [(x, 18) for x in range(22, 28)],
]
for wall_group in walls:
for x, y in wall_group:
if 0 <= x < 30 and 0 <= y < 20:
cell = grid.at(x, y)
cell.walkable = False
cell.transparent = False
cell.color = mcrfpy.Color(40, 20, 20) # Wall color
# Create entities
player = mcrfpy.Entity(5, 10, grid=grid)
player.sprite_index = 64 # @
enemy = mcrfpy.Entity(25, 10, grid=grid)
enemy.sprite_index = 69 # E
# Update initial visibility
player.update_visibility()
enemy.update_visibility()
# Global state
current_perspective = -1
perspective_names = ["Omniscient", "Player", "Enemy"]
# UI Setup
ui = mcrfpy.sceneUI("visibility_demo")
ui.append(grid)
grid.position = (50, 100)
grid.size = (900, 600) # 30*30, 20*30
# Title
title = mcrfpy.Caption("Interactive Visibility Demo", 350, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Info displays
perspective_label = mcrfpy.Caption("Perspective: Omniscient", 50, 50)
perspective_label.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(perspective_label)
controls = mcrfpy.Caption("WASD: Move player | Arrows: Move enemy | Tab: Cycle perspective | Space: Update visibility | R: Reset", 50, 730)
controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls)
player_info = mcrfpy.Caption("Player: (5, 10)", 700, 50)
player_info.fill_color = mcrfpy.Color(100, 255, 100)
ui.append(player_info)
enemy_info = mcrfpy.Caption("Enemy: (25, 10)", 700, 70)
enemy_info.fill_color = mcrfpy.Color(255, 100, 100)
ui.append(enemy_info)
# Helper functions
def move_entity(entity, dx, dy):
"""Move entity if target is walkable"""
new_x = int(entity.x + dx)
new_y = int(entity.y + dy)
if 0 <= new_x < 30 and 0 <= new_y < 20:
cell = grid.at(new_x, new_y)
if cell.walkable:
entity.x = new_x
entity.y = new_y
entity.update_visibility()
return True
return False
def update_info():
"""Update info displays"""
player_info.text = f"Player: ({int(player.x)}, {int(player.y)})"
enemy_info.text = f"Enemy: ({int(enemy.x)}, {int(enemy.y)})"
def cycle_perspective():
"""Cycle through perspectives"""
global current_perspective
# Cycle: -1 → 0 → 1 → -1
current_perspective = (current_perspective + 2) % 3 - 1
grid.perspective = current_perspective
name = perspective_names[current_perspective + 1]
perspective_label.text = f"Perspective: {name}"
# Key handlers
def handle_keys(key, state):
"""Handle keyboard input"""
if state == "end": return
key = key.lower()
# Player movement (WASD)
if key == "w":
move_entity(player, 0, -1)
elif key == "s":
move_entity(player, 0, 1)
elif key == "a":
move_entity(player, -1, 0)
elif key == "d":
move_entity(player, 1, 0)
# Enemy movement (Arrows)
elif key == "up":
move_entity(enemy, 0, -1)
elif key == "down":
move_entity(enemy, 0, 1)
elif key == "left":
move_entity(enemy, -1, 0)
elif key == "right":
move_entity(enemy, 1, 0)
# Tab to cycle perspective
elif key == "tab":
cycle_perspective()
# Space to update visibility
elif key == "space":
player.update_visibility()
enemy.update_visibility()
print("Updated visibility for both entities")
# R to reset
elif key == "r":
player.x, player.y = 5, 10
enemy.x, enemy.y = 25, 10
player.update_visibility()
enemy.update_visibility()
update_info()
print("Reset positions")
# Q to quit
elif key == "q":
print("Exiting...")
sys.exit(0)
update_info()
# Set scene first
mcrfpy.setScene("visibility_demo")
# Register key handler (operates on current scene)
mcrfpy.keypressScene(handle_keys)
print("Interactive Visibility Demo")
print("===========================")
print("WASD: Move player (green @)")
print("Arrows: Move enemy (red E)")
print("Tab: Cycle perspective")
print("Space: Update visibility")
print("R: Reset positions")
print("Q: Quit")
print("\nCurrent perspective: Omniscient (shows all)")
print("Try moving entities and switching perspectives!")

375
tests/path_vision_fixed.py Normal file
View File

@ -0,0 +1,375 @@
#!/usr/bin/env python3
"""
Path & Vision Sizzle Reel (Fixed)
=================================
Fixed version with proper animation chaining to prevent glitches.
"""
import mcrfpy
import sys
class PathAnimator:
"""Handles step-by-step animation with proper completion tracking"""
def __init__(self, entity, name="animator"):
self.entity = entity
self.name = name
self.path = []
self.current_index = 0
self.step_duration = 0.4
self.animating = False
self.on_step = None
self.on_complete = None
def set_path(self, path):
"""Set the path to animate along"""
self.path = path
self.current_index = 0
def start(self):
"""Start animating"""
if not self.path:
return
self.animating = True
self.current_index = 0
self._move_to_next()
def stop(self):
"""Stop animating"""
self.animating = False
mcrfpy.delTimer(f"{self.name}_check")
def _move_to_next(self):
"""Move to next position in path"""
if not self.animating or self.current_index >= len(self.path):
self.animating = False
if self.on_complete:
self.on_complete()
return
# Get next position
x, y = self.path[self.current_index]
# Create animations
anim_x = mcrfpy.Animation("x", float(x), self.step_duration, "easeInOut")
anim_y = mcrfpy.Animation("y", float(y), self.step_duration, "easeInOut")
anim_x.start(self.entity)
anim_y.start(self.entity)
# Update visibility
self.entity.update_visibility()
# Callback for each step
if self.on_step:
self.on_step(self.current_index, x, y)
# Schedule next move
delay = int(self.step_duration * 1000) + 50 # Add small buffer
mcrfpy.setTimer(f"{self.name}_next", self._handle_next, delay)
def _handle_next(self, dt):
"""Timer callback to move to next position"""
self.current_index += 1
mcrfpy.delTimer(f"{self.name}_next")
self._move_to_next()
# Global state
grid = None
player = None
enemy = None
player_animator = None
enemy_animator = None
demo_phase = 0
def create_scene():
"""Create the demo environment"""
global grid, player, enemy
mcrfpy.createScene("fixed_demo")
# Create grid
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
grid.fill_color = mcrfpy.Color(20, 20, 30)
# Simple dungeon layout
map_layout = [
"##############################",
"#......#########.....#########",
"#......#########.....#########",
"#......#.........#...#########",
"#......#.........#...#########",
"####.###.........#.###########",
"####.............#.###########",
"####.............#.###########",
"####.###.........#.###########",
"#......#.........#...#########",
"#......#.........#...#########",
"#......#########.#...........#",
"#......#########.#...........#",
"#......#########.#...........#",
"#......#########.#############",
"####.###########.............#",
"####.........................#",
"####.###########.............#",
"#......#########.............#",
"##############################",
]
# Build map
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
if char == '#':
cell.walkable = False
cell.transparent = False
cell.color = mcrfpy.Color(40, 30, 30)
else:
cell.walkable = True
cell.transparent = True
cell.color = mcrfpy.Color(80, 80, 100)
# Create entities
player = mcrfpy.Entity(3, 3, grid=grid)
player.sprite_index = 64 # @
enemy = mcrfpy.Entity(26, 16, grid=grid)
enemy.sprite_index = 69 # E
# Initial visibility
player.update_visibility()
enemy.update_visibility()
# Set initial perspective
grid.perspective = 0
def setup_ui():
"""Create UI elements"""
ui = mcrfpy.sceneUI("fixed_demo")
ui.append(grid)
grid.position = (50, 80)
grid.size = (700, 500)
title = mcrfpy.Caption("Path & Vision Demo (Fixed)", 300, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
global status_text, perspective_text
status_text = mcrfpy.Caption("Initializing...", 50, 50)
status_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status_text)
perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50)
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
ui.append(perspective_text)
controls = mcrfpy.Caption("Space: Start/Pause | R: Restart | Q: Quit", 250, 600)
controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls)
def update_camera_smooth(target, duration=0.3):
"""Smoothly move camera to entity"""
center_x = target.x * 23 # Approximate pixel size
center_y = target.y * 23
cam_anim = mcrfpy.Animation("center", (center_x, center_y), duration, "easeOut")
cam_anim.start(grid)
def start_demo():
"""Start the demo sequence"""
global demo_phase, player_animator, enemy_animator
demo_phase = 1
status_text.text = "Phase 1: Player movement with camera follow"
# Player path
player_path = [
(3, 3), (3, 6), (4, 6), (7, 6), (7, 8),
(10, 8), (13, 8), (16, 8), (16, 10),
(16, 13), (16, 16), (20, 16), (24, 16)
]
# Setup player animator
player_animator = PathAnimator(player, "player")
player_animator.set_path(player_path)
player_animator.step_duration = 0.5
def on_player_step(index, x, y):
"""Called for each player step"""
status_text.text = f"Player step {index+1}/{len(player_path)}"
if grid.perspective == 0:
update_camera_smooth(player, 0.4)
def on_player_complete():
"""Called when player path is complete"""
start_phase_2()
player_animator.on_step = on_player_step
player_animator.on_complete = on_player_complete
player_animator.start()
def start_phase_2():
"""Start enemy movement phase"""
global demo_phase
demo_phase = 2
status_text.text = "Phase 2: Enemy movement (may enter player's view)"
# Enemy path
enemy_path = [
(26, 16), (22, 16), (18, 16), (16, 16),
(16, 13), (16, 10), (16, 8), (13, 8),
(10, 8), (7, 8), (7, 6), (4, 6)
]
# Setup enemy animator
enemy_animator.set_path(enemy_path)
enemy_animator.step_duration = 0.4
def on_enemy_step(index, x, y):
"""Check if enemy is visible to player"""
if grid.perspective == 0:
# Check if enemy is in player's view
enemy_idx = int(y) * grid.grid_x + int(x)
if enemy_idx < len(player.gridstate) and player.gridstate[enemy_idx].visible:
status_text.text = "Enemy spotted in player's view!"
def on_enemy_complete():
"""Start perspective transition"""
start_phase_3()
enemy_animator.on_step = on_enemy_step
enemy_animator.on_complete = on_enemy_complete
enemy_animator.start()
def start_phase_3():
"""Dramatic perspective shift"""
global demo_phase
demo_phase = 3
status_text.text = "Phase 3: Perspective shift..."
# Stop any ongoing animations
player_animator.stop()
enemy_animator.stop()
# Zoom out
zoom_out = mcrfpy.Animation("zoom", 0.6, 2.0, "easeInExpo")
zoom_out.start(grid)
# Schedule perspective switch
mcrfpy.setTimer("switch_persp", switch_perspective, 2100)
def switch_perspective(dt):
"""Switch to enemy perspective"""
grid.perspective = 1
perspective_text.text = "Perspective: Enemy"
perspective_text.fill_color = mcrfpy.Color(255, 100, 100)
# Update camera
update_camera_smooth(enemy, 0.5)
# Zoom back in
zoom_in = mcrfpy.Animation("zoom", 1.0, 2.0, "easeOutExpo")
zoom_in.start(grid)
status_text.text = "Now following enemy perspective"
# Clean up timer
mcrfpy.delTimer("switch_persp")
# Continue enemy movement after transition
mcrfpy.setTimer("continue_enemy", continue_enemy_movement, 2500)
def continue_enemy_movement(dt):
"""Continue enemy movement after perspective shift"""
mcrfpy.delTimer("continue_enemy")
# Continue path
enemy_path_2 = [
(4, 6), (3, 6), (3, 3), (3, 2), (3, 1)
]
enemy_animator.set_path(enemy_path_2)
def on_step(index, x, y):
update_camera_smooth(enemy, 0.4)
status_text.text = f"Following enemy: step {index+1}"
def on_complete():
status_text.text = "Demo complete! Press R to restart"
enemy_animator.on_step = on_step
enemy_animator.on_complete = on_complete
enemy_animator.start()
# Control state
running = False
def handle_keys(key, state):
"""Handle keyboard input"""
global running
if state != "start":
return
key = key.lower()
if key == "q":
sys.exit(0)
elif key == "space":
if not running:
running = True
start_demo()
else:
running = False
player_animator.stop()
enemy_animator.stop()
status_text.text = "Paused"
elif key == "r":
# Reset everything
player.x, player.y = 3, 3
enemy.x, enemy.y = 26, 16
grid.perspective = 0
perspective_text.text = "Perspective: Player"
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
grid.zoom = 1.0
update_camera_smooth(player, 0.5)
if running:
player_animator.stop()
enemy_animator.stop()
running = False
status_text.text = "Reset - Press SPACE to start"
# Initialize
create_scene()
setup_ui()
# Setup animators
player_animator = PathAnimator(player, "player")
enemy_animator = PathAnimator(enemy, "enemy")
# Set scene
mcrfpy.setScene("fixed_demo")
mcrfpy.keypressScene(handle_keys)
# Initial camera
grid.zoom = 1.0
update_camera_smooth(player, 0.5)
print("Path & Vision Demo (Fixed)")
print("==========================")
print("This version properly chains animations to prevent glitches.")
print()
print("The demo will:")
print("1. Move player with camera following")
print("2. Move enemy (may enter player's view)")
print("3. Dramatic perspective shift to enemy")
print("4. Continue following enemy")
print()
print("Press SPACE to start, Q to quit")

View File

@ -0,0 +1,391 @@
#!/usr/bin/env python3
"""
Path & Vision Sizzle Reel
=========================
A choreographed demo showing:
- Smooth entity movement along paths
- Camera following with grid center animation
- Field of view updates as entities move
- Dramatic perspective transitions with zoom effects
"""
import mcrfpy
import sys
# Colors
WALL_COLOR = mcrfpy.Color(40, 30, 30)
FLOOR_COLOR = mcrfpy.Color(80, 80, 100)
PATH_COLOR = mcrfpy.Color(120, 120, 180)
DARK_FLOOR = mcrfpy.Color(40, 40, 50)
# Global state
grid = None
player = None
enemy = None
sequence_step = 0
player_path = []
enemy_path = []
player_path_index = 0
enemy_path_index = 0
def create_scene():
"""Create the demo environment"""
global grid, player, enemy
mcrfpy.createScene("path_vision_demo")
# Create larger grid for more dramatic movement
grid = mcrfpy.Grid(grid_x=40, grid_y=25)
grid.fill_color = mcrfpy.Color(20, 20, 30)
# Map layout - interconnected rooms with corridors
map_layout = [
"########################################", # 0
"#......##########......################", # 1
"#......##########......################", # 2
"#......##########......################", # 3
"#......#.........#.....################", # 4
"#......#.........#.....################", # 5
"####.###.........####.#################", # 6
"####.....................##############", # 7
"####.....................##############", # 8
"####.###.........####.#################", # 9
"#......#.........#.....################", # 10
"#......#.........#.....################", # 11
"#......#.........#.....################", # 12
"#......###.....###.....################", # 13
"#......###.....###.....################", # 14
"#......###.....###.....#########......#", # 15
"#......###.....###.....#########......#", # 16
"#......###.....###.....#########......#", # 17
"#####.############.#############......#", # 18
"#####...........................#.....#", # 19
"#####...........................#.....#", # 20
"#####.############.#############......#", # 21
"#......###########.##########.........#", # 22
"#......###########.##########.........#", # 23
"########################################", # 24
]
# Build the map
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
if char == '#':
cell.walkable = False
cell.transparent = False
cell.color = WALL_COLOR
else:
cell.walkable = True
cell.transparent = True
cell.color = FLOOR_COLOR
# Create player in top-left room
player = mcrfpy.Entity(3, 3, grid=grid)
player.sprite_index = 64 # @
# Create enemy in bottom-right area
enemy = mcrfpy.Entity(35, 20, grid=grid)
enemy.sprite_index = 69 # E
# Initial visibility
player.update_visibility()
enemy.update_visibility()
# Set initial perspective to player
grid.perspective = 0
def setup_paths():
"""Define the paths for entities"""
global player_path, enemy_path
# Player path: Top-left room → corridor → middle room
player_waypoints = [
(3, 3), # Start
(3, 8), # Move down
(7, 8), # Enter corridor
(16, 8), # Through corridor
(16, 12), # Enter middle room
(12, 12), # Move in room
(12, 16), # Move down
(16, 16), # Move right
(16, 19), # Exit room
(25, 19), # Move right
(30, 19), # Continue
(35, 19), # Near enemy start
]
# Enemy path: Bottom-right → around → approach player area
enemy_waypoints = [
(35, 20), # Start
(30, 20), # Move left
(25, 20), # Continue
(20, 20), # Continue
(16, 20), # Corridor junction
(16, 16), # Move up (might see player)
(16, 12), # Continue up
(16, 8), # Top corridor
(10, 8), # Move left
(7, 8), # Continue
(3, 8), # Player's area
(3, 12), # Move down
]
# Calculate full paths using pathfinding
player_path = []
for i in range(len(player_waypoints) - 1):
x1, y1 = player_waypoints[i]
x2, y2 = player_waypoints[i + 1]
# Use grid's A* pathfinding
segment = grid.compute_astar_path(x1, y1, x2, y2)
if segment:
# Add segment (avoiding duplicates)
if not player_path or segment[0] != player_path[-1]:
player_path.extend(segment)
else:
player_path.extend(segment[1:])
enemy_path = []
for i in range(len(enemy_waypoints) - 1):
x1, y1 = enemy_waypoints[i]
x2, y2 = enemy_waypoints[i + 1]
segment = grid.compute_astar_path(x1, y1, x2, y2)
if segment:
if not enemy_path or segment[0] != enemy_path[-1]:
enemy_path.extend(segment)
else:
enemy_path.extend(segment[1:])
print(f"Player path: {len(player_path)} steps")
print(f"Enemy path: {len(enemy_path)} steps")
def setup_ui():
"""Create UI elements"""
ui = mcrfpy.sceneUI("path_vision_demo")
ui.append(grid)
# Position and size grid
grid.position = (50, 80)
grid.size = (700, 500) # Adjust based on zoom
# Title
title = mcrfpy.Caption("Path & Vision Sizzle Reel", 300, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Status
global status_text, perspective_text
status_text = mcrfpy.Caption("Starting demo...", 50, 50)
status_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status_text)
perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50)
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
ui.append(perspective_text)
# Controls
controls = mcrfpy.Caption("Space: Pause/Resume | R: Restart | Q: Quit", 250, 600)
controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls)
# Animation control
paused = False
move_timer = 0
zoom_transition = False
def move_entity_smooth(entity, target_x, target_y, duration=0.3):
"""Smoothly animate entity to position"""
# Create position animation
anim_x = mcrfpy.Animation("x", float(target_x), duration, "easeInOut")
anim_y = mcrfpy.Animation("y", float(target_y), duration, "easeInOut")
anim_x.start(entity)
anim_y.start(entity)
def update_camera_smooth(center_x, center_y, duration=0.3):
"""Smoothly move camera center"""
# Convert grid coords to pixel coords (assuming 16x16 tiles)
pixel_x = center_x * 16
pixel_y = center_y * 16
anim = mcrfpy.Animation("center", (pixel_x, pixel_y), duration, "easeOut")
anim.start(grid)
def start_perspective_transition():
"""Begin the dramatic perspective shift"""
global zoom_transition, sequence_step
zoom_transition = True
sequence_step = 100 # Special sequence number
status_text.text = "Perspective shift: Zooming out..."
# Zoom out with elastic easing
zoom_out = mcrfpy.Animation("zoom", 0.5, 2.0, "easeInExpo")
zoom_out.start(grid)
# Schedule the perspective switch
mcrfpy.setTimer("switch_perspective", switch_perspective, 2100)
def switch_perspective(dt):
"""Switch perspective at the peak of zoom"""
global sequence_step
# Switch to enemy perspective
grid.perspective = 1
perspective_text.text = "Perspective: Enemy"
perspective_text.fill_color = mcrfpy.Color(255, 100, 100)
status_text.text = "Perspective shift: Following enemy..."
# Update camera to enemy position
update_camera_smooth(enemy.x, enemy.y, 0.1)
# Zoom back in
zoom_in = mcrfpy.Animation("zoom", 1.2, 2.0, "easeOutExpo")
zoom_in.start(grid)
# Resume sequence
mcrfpy.setTimer("resume_enemy", resume_enemy_sequence, 2100)
# Cancel this timer
mcrfpy.delTimer("switch_perspective")
def resume_enemy_sequence(dt):
"""Resume following enemy after perspective shift"""
global sequence_step, zoom_transition
zoom_transition = False
sequence_step = 101 # Continue with enemy movement
mcrfpy.delTimer("resume_enemy")
def sequence_tick(dt):
"""Main sequence controller"""
global sequence_step, player_path_index, enemy_path_index, move_timer
if paused or zoom_transition:
return
move_timer += dt
if move_timer < 400: # Move every 400ms
return
move_timer = 0
if sequence_step < 50:
# Phase 1: Follow player movement
if player_path_index < len(player_path):
x, y = player_path[player_path_index]
move_entity_smooth(player, x, y)
player.update_visibility()
# Camera follows player
if grid.perspective == 0:
update_camera_smooth(player.x, player.y)
player_path_index += 1
status_text.text = f"Player moving... Step {player_path_index}/{len(player_path)}"
# Start enemy movement after player has moved a bit
if player_path_index == 10:
sequence_step = 1 # Enable enemy movement
else:
# Player reached destination, start perspective transition
start_perspective_transition()
if sequence_step >= 1 and sequence_step < 50:
# Phase 2: Enemy movement (concurrent with player)
if enemy_path_index < len(enemy_path):
x, y = enemy_path[enemy_path_index]
move_entity_smooth(enemy, x, y)
enemy.update_visibility()
# Check if enemy is visible to player
if grid.perspective == 0:
enemy_cell_idx = int(enemy.y) * grid.grid_x + int(enemy.x)
if enemy_cell_idx < len(player.gridstate) and player.gridstate[enemy_cell_idx].visible:
status_text.text = "Enemy spotted!"
enemy_path_index += 1
elif sequence_step == 101:
# Phase 3: Continue following enemy after perspective shift
if enemy_path_index < len(enemy_path):
x, y = enemy_path[enemy_path_index]
move_entity_smooth(enemy, x, y)
enemy.update_visibility()
# Camera follows enemy
update_camera_smooth(enemy.x, enemy.y)
enemy_path_index += 1
status_text.text = f"Following enemy... Step {enemy_path_index}/{len(enemy_path)}"
else:
status_text.text = "Demo complete! Press R to restart"
sequence_step = 200 # Done
def handle_keys(key, state):
"""Handle keyboard input"""
global paused, sequence_step, player_path_index, enemy_path_index, move_timer
key = key.lower()
if state != "start":
return
if key == "q":
print("Exiting sizzle reel...")
sys.exit(0)
elif key == "space":
paused = not paused
status_text.text = "PAUSED" if paused else "Running..."
elif key == "r":
# Reset everything
player.x, player.y = 3, 3
enemy.x, enemy.y = 35, 20
player.update_visibility()
enemy.update_visibility()
grid.perspective = 0
perspective_text.text = "Perspective: Player"
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
sequence_step = 0
player_path_index = 0
enemy_path_index = 0
move_timer = 0
update_camera_smooth(player.x, player.y, 0.5)
# Reset zoom
zoom_reset = mcrfpy.Animation("zoom", 1.2, 0.5, "easeOut")
zoom_reset.start(grid)
status_text.text = "Demo restarted!"
# Initialize everything
print("Path & Vision Sizzle Reel")
print("=========================")
print("Demonstrating:")
print("- Smooth entity movement along calculated paths")
print("- Camera following with animated grid centering")
print("- Field of view updates as entities move")
print("- Dramatic perspective transitions with zoom effects")
print()
create_scene()
setup_paths()
setup_ui()
# Set scene and input
mcrfpy.setScene("path_vision_demo")
mcrfpy.keypressScene(handle_keys)
# Initial camera setup
grid.zoom = 1.2
update_camera_smooth(player.x, player.y, 0.1)
# Start the sequence
mcrfpy.setTimer("sequence", sequence_tick, 50) # Tick every 50ms
print("Demo started!")
print("- Player (@) will navigate through rooms")
print("- Enemy (E) will move on a different path")
print("- Watch for the dramatic perspective shift!")
print()
print("Controls: Space=Pause, R=Restart, Q=Quit")

View File

@ -0,0 +1,373 @@
#!/usr/bin/env python3
"""
Pathfinding Showcase Demo
=========================
Demonstrates various pathfinding scenarios with multiple entities.
Features:
- Multiple entities pathfinding simultaneously
- Chase mode: entities pursue targets
- Flee mode: entities avoid threats
- Patrol mode: entities follow waypoints
- Visual debugging: show Dijkstra distance field
"""
import mcrfpy
import sys
import random
# Colors
WALL_COLOR = mcrfpy.Color(40, 40, 40)
FLOOR_COLOR = mcrfpy.Color(220, 220, 240)
PATH_COLOR = mcrfpy.Color(180, 250, 180)
THREAT_COLOR = mcrfpy.Color(255, 100, 100)
GOAL_COLOR = mcrfpy.Color(100, 255, 100)
DIJKSTRA_COLORS = [
mcrfpy.Color(50, 50, 100), # Far
mcrfpy.Color(70, 70, 150),
mcrfpy.Color(90, 90, 200),
mcrfpy.Color(110, 110, 250),
mcrfpy.Color(150, 150, 255),
mcrfpy.Color(200, 200, 255), # Near
]
# Entity types
PLAYER = 64 # @
ENEMY = 69 # E
TREASURE = 36 # $
PATROL = 80 # P
# Global state
grid = None
player = None
enemies = []
treasures = []
patrol_entities = []
mode = "CHASE"
show_dijkstra = False
animation_speed = 3.0
def create_dungeon():
"""Create a dungeon-like map"""
global grid
mcrfpy.createScene("pathfinding_showcase")
# Create larger grid for showcase
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
grid.fill_color = mcrfpy.Color(0, 0, 0)
# Initialize all as floor
for y in range(20):
for x in range(30):
grid.at(x, y).walkable = True
grid.at(x, y).transparent = True
grid.at(x, y).color = FLOOR_COLOR
# Create rooms and corridors
rooms = [
(2, 2, 8, 6), # Top-left room
(20, 2, 8, 6), # Top-right room
(11, 8, 8, 6), # Center room
(2, 14, 8, 5), # Bottom-left room
(20, 14, 8, 5), # Bottom-right room
]
# Create room walls
for rx, ry, rw, rh in rooms:
# Top and bottom walls
for x in range(rx, rx + rw):
if 0 <= x < 30:
grid.at(x, ry).walkable = False
grid.at(x, ry).color = WALL_COLOR
grid.at(x, ry + rh - 1).walkable = False
grid.at(x, ry + rh - 1).color = WALL_COLOR
# Left and right walls
for y in range(ry, ry + rh):
if 0 <= y < 20:
grid.at(rx, y).walkable = False
grid.at(rx, y).color = WALL_COLOR
grid.at(rx + rw - 1, y).walkable = False
grid.at(rx + rw - 1, y).color = WALL_COLOR
# Create doorways
doorways = [
(6, 2), (24, 2), # Top room doors
(6, 7), (24, 7), # Top room doors bottom
(15, 8), (15, 13), # Center room doors
(6, 14), (24, 14), # Bottom room doors
(11, 11), (18, 11), # Center room side doors
]
for x, y in doorways:
if 0 <= x < 30 and 0 <= y < 20:
grid.at(x, y).walkable = True
grid.at(x, y).color = FLOOR_COLOR
# Add some corridors
# Horizontal corridors
for x in range(10, 20):
grid.at(x, 5).walkable = True
grid.at(x, 5).color = FLOOR_COLOR
grid.at(x, 16).walkable = True
grid.at(x, 16).color = FLOOR_COLOR
# Vertical corridors
for y in range(5, 17):
grid.at(10, y).walkable = True
grid.at(10, y).color = FLOOR_COLOR
grid.at(19, y).walkable = True
grid.at(19, y).color = FLOOR_COLOR
def spawn_entities():
"""Spawn various entity types"""
global player, enemies, treasures, patrol_entities
# Clear existing entities
grid.entities.clear()
enemies = []
treasures = []
patrol_entities = []
# Spawn player in center room
player = mcrfpy.Entity(15, 11)
player.sprite_index = PLAYER
grid.entities.append(player)
# Spawn enemies in corners
enemy_positions = [(4, 4), (24, 4), (4, 16), (24, 16)]
for x, y in enemy_positions:
enemy = mcrfpy.Entity(x, y)
enemy.sprite_index = ENEMY
grid.entities.append(enemy)
enemies.append(enemy)
# Spawn treasures
treasure_positions = [(6, 5), (24, 5), (15, 10)]
for x, y in treasure_positions:
treasure = mcrfpy.Entity(x, y)
treasure.sprite_index = TREASURE
grid.entities.append(treasure)
treasures.append(treasure)
# Spawn patrol entities
patrol = mcrfpy.Entity(10, 10)
patrol.sprite_index = PATROL
patrol.waypoints = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol
patrol.waypoint_index = 0
grid.entities.append(patrol)
patrol_entities.append(patrol)
def visualize_dijkstra(target_x, target_y):
"""Visualize Dijkstra distance field"""
if not show_dijkstra:
return
# Compute Dijkstra from target
grid.compute_dijkstra(target_x, target_y)
# Color tiles based on distance
max_dist = 30.0
for y in range(20):
for x in range(30):
if grid.at(x, y).walkable:
dist = grid.get_dijkstra_distance(x, y)
if dist is not None and dist < max_dist:
# Map distance to color index
color_idx = int((dist / max_dist) * len(DIJKSTRA_COLORS))
color_idx = min(color_idx, len(DIJKSTRA_COLORS) - 1)
grid.at(x, y).color = DIJKSTRA_COLORS[color_idx]
def move_enemies(dt):
"""Move enemies based on current mode"""
if mode == "CHASE":
# Enemies chase player
for enemy in enemies:
path = enemy.path_to(int(player.x), int(player.y))
if path and len(path) > 1: # Don't move onto player
# Move towards player
next_x, next_y = path[1]
# Smooth movement
dx = next_x - enemy.x
dy = next_y - enemy.y
enemy.x += dx * dt * animation_speed
enemy.y += dy * dt * animation_speed
elif mode == "FLEE":
# Enemies flee from player
for enemy in enemies:
# Compute opposite direction
dx = enemy.x - player.x
dy = enemy.y - player.y
# Find safe spot in that direction
target_x = int(enemy.x + dx * 2)
target_y = int(enemy.y + dy * 2)
# Clamp to grid
target_x = max(0, min(29, target_x))
target_y = max(0, min(19, target_y))
path = enemy.path_to(target_x, target_y)
if path and len(path) > 0:
next_x, next_y = path[0]
# Move away from player
dx = next_x - enemy.x
dy = next_y - enemy.y
enemy.x += dx * dt * animation_speed
enemy.y += dy * dt * animation_speed
def move_patrols(dt):
"""Move patrol entities along waypoints"""
for patrol in patrol_entities:
if not hasattr(patrol, 'waypoints'):
continue
# Get current waypoint
target_x, target_y = patrol.waypoints[patrol.waypoint_index]
# Check if reached waypoint
dist = abs(patrol.x - target_x) + abs(patrol.y - target_y)
if dist < 0.5:
# Move to next waypoint
patrol.waypoint_index = (patrol.waypoint_index + 1) % len(patrol.waypoints)
target_x, target_y = patrol.waypoints[patrol.waypoint_index]
# Path to waypoint
path = patrol.path_to(target_x, target_y)
if path and len(path) > 0:
next_x, next_y = path[0]
dx = next_x - patrol.x
dy = next_y - patrol.y
patrol.x += dx * dt * animation_speed * 0.5 # Slower patrol speed
patrol.y += dy * dt * animation_speed * 0.5
def update_entities(dt):
"""Update all entity movements"""
move_enemies(dt / 1000.0) # Convert to seconds
move_patrols(dt / 1000.0)
# Update Dijkstra visualization
if show_dijkstra and player:
visualize_dijkstra(int(player.x), int(player.y))
def handle_keypress(scene_name, keycode):
"""Handle keyboard input"""
global mode, show_dijkstra, player
# Mode switching
if keycode == 49: # '1'
mode = "CHASE"
mode_text.text = "Mode: CHASE - Enemies pursue player"
clear_colors()
elif keycode == 50: # '2'
mode = "FLEE"
mode_text.text = "Mode: FLEE - Enemies avoid player"
clear_colors()
elif keycode == 51: # '3'
mode = "PATROL"
mode_text.text = "Mode: PATROL - Entities follow waypoints"
clear_colors()
# Toggle Dijkstra visualization
elif keycode == 68 or keycode == 100: # 'D' or 'd'
show_dijkstra = not show_dijkstra
debug_text.text = f"Dijkstra Debug: {'ON' if show_dijkstra else 'OFF'}"
if not show_dijkstra:
clear_colors()
# Move player with arrow keys or WASD
elif keycode in [87, 119]: # W/w - Up
if player.y > 0:
path = player.path_to(int(player.x), int(player.y) - 1)
if path:
player.y -= 1
elif keycode in [83, 115]: # S/s - Down
if player.y < 19:
path = player.path_to(int(player.x), int(player.y) + 1)
if path:
player.y += 1
elif keycode in [65, 97]: # A/a - Left
if player.x > 0:
path = player.path_to(int(player.x) - 1, int(player.y))
if path:
player.x -= 1
elif keycode in [68, 100]: # D/d - Right
if player.x < 29:
path = player.path_to(int(player.x) + 1, int(player.y))
if path:
player.x += 1
# Reset
elif keycode == 82 or keycode == 114: # 'R' or 'r'
spawn_entities()
clear_colors()
# Quit
elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
print("\nExiting pathfinding showcase...")
sys.exit(0)
def clear_colors():
"""Reset floor colors"""
for y in range(20):
for x in range(30):
if grid.at(x, y).walkable:
grid.at(x, y).color = FLOOR_COLOR
# Create the showcase
print("Pathfinding Showcase Demo")
print("=========================")
print("Controls:")
print(" WASD - Move player")
print(" 1 - Chase mode (enemies pursue)")
print(" 2 - Flee mode (enemies avoid)")
print(" 3 - Patrol mode")
print(" D - Toggle Dijkstra visualization")
print(" R - Reset entities")
print(" Q/ESC - Quit")
# Create dungeon
create_dungeon()
spawn_entities()
# Set up UI
ui = mcrfpy.sceneUI("pathfinding_showcase")
ui.append(grid)
# Scale and position
grid.size = (750, 500) # 30*25, 20*25
grid.position = (25, 60)
# Add title
title = mcrfpy.Caption("Pathfinding Showcase", 300, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add mode text
mode_text = mcrfpy.Caption("Mode: CHASE - Enemies pursue player", 25, 580)
mode_text.fill_color = mcrfpy.Color(255, 255, 200)
ui.append(mode_text)
# Add debug text
debug_text = mcrfpy.Caption("Dijkstra Debug: OFF", 25, 600)
debug_text.fill_color = mcrfpy.Color(200, 200, 255)
ui.append(debug_text)
# Add legend
legend = mcrfpy.Caption("@ Player E Enemy $ Treasure P Patrol", 25, 620)
legend.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend)
# Set up input handling
mcrfpy.keypressScene(handle_keypress)
# Set up animation timer
mcrfpy.setTimer("entities", update_entities, 16) # 60 FPS
# Show scene
mcrfpy.setScene("pathfinding_showcase")
print("\nShowcase ready! Move with WASD and watch entities react.")

View File

@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Simple interactive visibility test"""
import mcrfpy
import sys
# Create scene and grid
print("Creating scene...")
mcrfpy.createScene("vis_test")
print("Creating grid...")
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
# Initialize grid
print("Initializing grid...")
for y in range(10):
for x in range(10):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.color = mcrfpy.Color(100, 100, 120)
# Create entity
print("Creating entity...")
entity = mcrfpy.Entity(5, 5, grid=grid)
entity.sprite_index = 64
print("Updating visibility...")
entity.update_visibility()
# Set up UI
print("Setting up UI...")
ui = mcrfpy.sceneUI("vis_test")
ui.append(grid)
grid.position = (50, 50)
grid.size = (300, 300)
# Test perspective
print("Testing perspective...")
grid.perspective = -1 # Omniscient
print(f"Perspective set to: {grid.perspective}")
print("Setting scene...")
mcrfpy.setScene("vis_test")
print("Ready!")

View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Simple visibility test without entity append"""
import mcrfpy
import sys
print("Simple visibility test...")
# Create scene and grid
mcrfpy.createScene("simple")
print("Scene created")
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
print("Grid created")
# Create entity without appending
entity = mcrfpy.Entity(2, 2, grid=grid)
print(f"Entity created at ({entity.x}, {entity.y})")
# Check if gridstate is initialized
print(f"Gridstate length: {len(entity.gridstate)}")
# Try to access at method
try:
state = entity.at(0, 0)
print(f"at(0,0) returned: {state}")
print(f"visible: {state.visible}, discovered: {state.discovered}")
except Exception as e:
print(f"Error in at(): {e}")
# Try update_visibility
try:
entity.update_visibility()
print("update_visibility() succeeded")
except Exception as e:
print(f"Error in update_visibility(): {e}")
print("Test complete")
sys.exit(0)

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Test pathfinding integration with demos"""
import mcrfpy
import sys
print("Testing pathfinding integration...")
print("=" * 50)
# Create scene and grid
mcrfpy.createScene("test")
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
# Initialize grid
for y in range(10):
for x in range(10):
grid.at(x, y).walkable = True
# Add some walls
for i in range(5):
grid.at(5, i + 2).walkable = False
# Create entities
e1 = mcrfpy.Entity(2, 5)
e2 = mcrfpy.Entity(8, 5)
grid.entities.append(e1)
grid.entities.append(e2)
# Test pathfinding between entities
print(f"Entity 1 at ({e1.x}, {e1.y})")
print(f"Entity 2 at ({e2.x}, {e2.y})")
# Entity 1 finds path to Entity 2
path = e1.path_to(int(e2.x), int(e2.y))
print(f"\nPath from E1 to E2: {path}")
print(f"Path length: {len(path)} steps")
# Test movement simulation
if path and len(path) > 1:
print("\nSimulating movement along path:")
for i, (x, y) in enumerate(path[:5]): # Show first 5 steps
print(f" Step {i}: Move to ({x}, {y})")
# Test path in reverse
path_reverse = e2.path_to(int(e1.x), int(e1.y))
print(f"\nPath from E2 to E1: {path_reverse}")
print(f"Reverse path length: {len(path_reverse)} steps")
print("\n✓ Pathfinding integration working correctly!")
print("Enhanced demos are ready for interactive use.")
# Quick animation test
def test_timer(dt):
print(f"Timer callback received: dt={dt}ms")
sys.exit(0)
# Set a quick timer to test animation system
mcrfpy.setTimer("test", test_timer, 100)
print("\nTesting timer system for animations...")