McRogueFace/tests/demos/path_vision_sizzle_reel.py

391 lines
13 KiB
Python
Raw Normal View History

Squashed commit of the following: [alpha_presentable] Author: John McCardle <mccardle.john@gmail.com> Co-Authored-By: Claude <noreply@anthropic.com> commit dc47f2474c7b2642d368f9772894aed857527807 the UIEntity rant commit 673ca8e1b089ea670257fc04ae1a676ed95a40ed I forget when these tests were written, but I want them in the squash merge commit 70c71565c684fa96e222179271ecb13a156d80ad Fix UI object segfault by switching from managed to manual weakref management The UI types (Frame, Caption, Sprite, Grid, Entity) were using Py_TPFLAGS_MANAGED_WEAKREF while also trying to manually create weakrefs for the PythonObjectCache. This is fundamentally incompatible - when Python manages weakrefs internally, PyWeakref_NewRef() cannot access the weakref list properly, causing segfaults. Changed all UI types to use manual weakref management (like PyTimer): - Restored weakreflist field in all UI type structures - Removed Py_TPFLAGS_MANAGED_WEAKREF from all UI type flags - Added tp_weaklistoffset for all UI types in module initialization - Initialize weakreflist=NULL in tp_new and init methods - Call PyObject_ClearWeakRefs() in dealloc functions This allows the PythonObjectCache to continue working correctly, maintaining Python object identity for C++ objects across the boundary. Fixes segfault when creating UI objects (e.g., Caption, Grid) that was preventing tutorial scripts from running. This is the bulk of the required behavior for Issue #126. that issure isn't ready for closure yet; several other sub-issues left. closes #110 mention issue #109 - resolves some __init__ related nuisances commit 3dce3ec539ae99e32d869007bf3f49d03e4e2f89 Refactor timer system for cleaner architecture and enhanced functionality Major improvements to the timer system: - Unified all timer logic in the Timer class (C++) - Removed PyTimerCallable subclass, now using PyCallable directly - Timer objects are now passed to callbacks as first argument - Added 'once' parameter for one-shot timers that auto-stop - Implemented proper PythonObjectCache integration with weakref support API enhancements: - New callback signature: callback(timer, runtime) instead of just (runtime) - Timer objects expose: name, interval, remaining, paused, active, once properties - Methods: pause(), resume(), cancel(), restart() - Comprehensive documentation with examples - Enhanced repr showing timer state (active/paused/once/remaining time) This cleanup follows the UIEntity/PyUIEntity pattern and makes the timer system more Pythonic while maintaining backward compatibility through the legacy setTimer/delTimer API. closes #121 commit 145834cfc31b8dabc4cb3591b9cb4ed99fc8b964 Implement Python object cache to preserve derived types in collections Add a global cache system that maintains weak references to Python objects, ensuring that derived Python classes maintain their identity when stored in and retrieved from C++ collections. Key changes: - Add PythonObjectCache singleton with serial number system - Each cacheable object (UIDrawable, UIEntity, Timer, Animation) gets unique ID - Cache stores weak references to prevent circular reference memory leaks - Update all UI type definitions to support weak references (Py_TPFLAGS_MANAGED_WEAKREF) - Enable subclassing for all UI types (Py_TPFLAGS_BASETYPE) - Collections check cache before creating new Python wrappers - Register objects in cache during __init__ methods - Clean up cache entries in C++ destructors This ensures that Python code like: ```python class MyFrame(mcrfpy.Frame): def __init__(self): super().__init__() self.custom_data = "preserved" frame = MyFrame() scene.ui.append(frame) retrieved = scene.ui[0] # Same MyFrame instance with custom_data intact ``` Works correctly, with retrieved maintaining the derived type and custom attributes. Closes #112 commit c5e7e8e29835a69f4c50f3c99fd3123012635a9a Update test demos for new Python API and entity system - Update all text input demos to use new Entity constructor signature - Fix pathfinding showcase to work with new entity position handling - Remove entity_waypoints tracking in favor of simplified movement - Delete obsolete exhaustive_api_demo.py (superseded by newer demos) - Adjust entity creation calls to match Entity((x, y), texture, sprite_index) pattern commit 6d29652ae7418745dc24066532454167d447df89 Update animation demo suite with crash fixes and improvements - Add warnings about AnimationManager segfault bug in sizzle_reel_final.py - Create sizzle_reel_final_fixed.py that works around the crash by hiding objects instead of removing them - Increase font sizes for better visibility in demos - Extend demo durations for better showcase of animations - Remove debug prints from animation_sizzle_reel_working.py - Minor cleanup and improvements to all animation demos commit a010e5fa968feaba620dcf2eda44fb9514512151 Update game scripts for new Python API - Convert entity position access from tuple to x/y properties - Update caption size property to font_size - Fix grid boundary checks to use grid_size instead of exceptions - Clean up demo timer on menu exit to prevent callbacks These changes adapt the game scripts to work with the new standardized Python API constructors and property names. commit 9c8d6c459109be883cb8070b8ef83c60bfc1a970 Fix click event z-order handling in PyScene Changed click detection to properly respect z-index by: - Sorting ui_elements in-place when needed (same as render order) - Using reverse iterators to check highest z-index elements first - This ensures top-most elements receive clicks before lower ones commit dcd1b0ca33d46639023221f4d7d52000b947dbdf Add roguelike tutorial implementation files Implement Parts 0-2 of the classic roguelike tutorial adapted for McRogueFace: - Part 0: Basic grid setup and tile rendering - Part 1: Drawing '@' symbol and basic movement - Part 1b: Variant with sprite-based player - Part 2: Entity system and NPC implementation with three movement variants: - part_2.py: Standard implementation - part_2-naive.py: Naive movement approach - part_2-onemovequeued.py: Queued movement system Includes tutorial assets: - tutorial2.png: Tileset for dungeon tiles - tutorial_hero.png: Player sprite sheet commit 6813fb5129738cca2d79c80304834523561ba7fb Standardize Python API constructors and remove PyArgHelpers - Remove PyArgHelpers.h and all macro-based argument parsing - Convert all UI class constructors to use PyArg_ParseTupleAndKeywords - Standardize constructor signatures across UICaption, UIEntity, UIFrame, UIGrid, and UISprite - Replace PYARGHELPER_SINGLE/MULTI macros with explicit argument parsing - Improve error messages and argument validation - Maintain backward compatibility with existing Python code This change improves code maintainability and consistency across the Python API. commit 6f67fbb51efaf70e52fba8c939298dcdff50450a Fix animation callback crashes from iterator invalidation (#119) Resolved segfaults caused by creating new animations from within animation callbacks. The issue was iterator invalidation in AnimationManager::update() when callbacks modified the active animations vector. Changes: - Add deferred animation queue to AnimationManager - New animations created during update are queued and added after - Set isUpdating flag to track when in update loop - Properly handle Animation destructor during callback execution - Add clearCallback() method for safe cleanup scenarios This fixes the "free(): invalid pointer" and "malloc(): unaligned fastbin chunk detected" errors that occurred with rapid animation creation in callbacks. commit eb88c7b3aab3da519db7569106c34f3510b6e963 Add animation completion callbacks (#119) Implement callbacks that fire when animations complete, enabling direct causality between animation end and game state changes. This eliminates race conditions from parallel timer workarounds. - Add optional callback parameter to Animation constructor - Callbacks execute synchronously when animation completes - Proper Python reference counting with GIL safety - Callbacks receive (anim, target) parameters (currently None) - Exception handling prevents crashes from Python errors Example usage: ```python def on_complete(anim, target): player_moving = False anim = mcrfpy.Animation("x", 300.0, 1.0, "easeOut", callback=on_complete) anim.start(player) ``` closes #119 commit 9fb428dd0176a4d7cfad09deb7509d8aa5562868 Update ROADMAP with GitHub issue numbers (#111-#125) Added issue numbers from GitHub tracker to roadmap items: - #111: Grid Click Events Broken in Headless - #112: Object Splitting Bug (Python type preservation) - #113: Batch Operations for Grid - #114: CellView API - #115: SpatialHash Implementation - #116: Dirty Flag System - #117: Memory Pool for Entities - #118: Scene as Drawable - #119: Animation Completion Callbacks - #120: Animation Property Locking - #121: Timer Object System - #122: Parent-Child UI System - #123: Grid Subgrid System - #124: Grid Point Animation - #125: GitHub Issues Automation Also updated existing references: - #101/#110: Constructor standardization - #109: Vector class indexing Note: Tutorial-specific items and Python-implementable features (input queue, collision reservation) are not tracked as engine issues. commit 062e4dadc42833bf5a3559e5d7c4ceb4abb7e9c0 Fix animation segfaults with RAII weak_ptr implementation Resolved two critical segmentation faults in AnimationManager: 1. Race condition when creating multiple animations in timer callbacks 2. Exit crash when animations outlive their target objects Changes: - Replace raw pointers with std::weak_ptr for automatic target invalidation - Add Animation::complete() to jump animations to final value - Add Animation::hasValidTarget() to check if target still exists - Update AnimationManager to auto-remove invalid animations - Add AnimationManager::clear() call to GameEngine::cleanup() - Update Python bindings to pass shared_ptr instead of raw pointers This ensures animations can never reference destroyed objects, following proper RAII principles. Tested with sizzle_reel_final.py and stress tests creating/destroying hundreds of animated objects. commit 98fc49a978ec792ee6096f40fd4e19841b8ec6a3 Directory structure cleanup and organization overhaul
2025-07-15 21:30:49 -04:00
#!/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")