diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0dea84c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,458 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Gitea-First Workflow + +**IMPORTANT**: This project uses Gitea for issue tracking, documentation, and project management. Always consult and update Gitea resources before and during development work. + +**Gitea Instance**: https://gamedev.ffwf.net/gitea/john/McRogueFace + +### Core Principles + +1. **Gitea is the Single Source of Truth** + - Issue tracker contains current tasks, bugs, and feature requests + - Wiki contains living documentation and architecture decisions + - Use Gitea MCP tools to query and update issues programmatically + +2. **Always Check Gitea First** + - Before starting work: Check open issues for related tasks or blockers + - When using `/roadmap` command: Query Gitea for up-to-date issue status + - When researching a feature: Search Gitea wiki and issues before grepping codebase + - When encountering a bug: Check if an issue already exists + +3. **Create Granular Issues** + - Break large features into separate, focused issues + - Each issue should address one specific problem or enhancement + - Tag issues appropriately: `[Bugfix]`, `[Major Feature]`, `[Minor Feature]`, etc. + - Link related issues using dependencies or blocking relationships + +4. **Document as You Go** + - When work on one issue interacts with another system: Add notes to related issues + - When discovering undocumented behavior: Create task to document it + - When documentation misleads you: Create task to correct or expand it + - When implementing a feature: Update the Gitea wiki if appropriate + +5. **Cross-Reference Everything** + - Commit messages should reference issue numbers (e.g., "Fixes #104", "Addresses #125") + - Issue comments should link to commits when work is done + - Wiki pages should reference relevant issues for implementation details + - Issues should link to each other when dependencies exist + +### Workflow Pattern + +``` +┌─────────────────────────────────────────────────────┐ +│ 1. Check Gitea Issues & Wiki │ +│ - Is there an existing issue for this? │ +│ - What's the current status? │ +│ - Are there related issues or blockers? │ +└─────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 2. Create Issues (if needed) │ +│ - Break work into granular tasks │ +│ - Tag appropriately │ +│ - Link dependencies │ +└─────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 3. Do the Work │ +│ - Implement/fix/document │ +│ - Write tests first (TDD) │ +│ - Add inline documentation │ +└─────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 4. Update Gitea │ +│ - Add notes to affected issues │ +│ - Create follow-up issues for discovered work │ +│ - Update wiki if architecture/APIs changed │ +│ - Add documentation correction tasks │ +└─────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 5. Commit & Reference │ +│ - Commit messages reference issue numbers │ +│ - Close issues or update status │ +│ - Add commit links to issue comments │ +└─────────────────────────────────────────────────────┘ +``` + +### Benefits of Gitea-First Approach + +- **Reduced Context Switching**: Check brief issue descriptions instead of re-reading entire codebase +- **Better Planning**: Issues provide roadmap; avoid duplicate or contradictory work +- **Living Documentation**: Wiki and issues stay current as work progresses +- **Historical Context**: Issue comments capture why decisions were made +- **Efficiency**: MCP tools allow programmatic access to project state + +### MCP Tools Available + +Claude Code has access to Gitea MCP tools for: +- `list_repo_issues` - Query current issues with filtering +- `get_issue` - Get detailed issue information +- `create_issue` - Create new issues programmatically +- `create_issue_comment` - Add comments to issues +- `edit_issue` - Update issue status, title, body +- `add_issue_labels` - Tag issues appropriately +- `add_issue_dependency` / `add_issue_blocking` - Link related issues +- Plus wiki, milestone, and label management tools + +Use these tools liberally to keep the project organized! + +## Build Commands + +```bash +# Build the project (compiles to ./build directory) +make + +# Or use the build script directly +./build.sh + +# Run the game +make run + +# Clean build artifacts +make clean + +# The executable and all assets are in ./build/ +cd build +./mcrogueface +``` + +## Project Architecture + +McRogueFace is a C++ game engine with Python scripting support, designed for creating roguelike games. The architecture consists of: + +### Core Engine (C++) +- **Entry Point**: `src/main.cpp` initializes the game engine +- **Scene System**: `Scene.h/cpp` manages game states +- **Entity System**: `UIEntity.h/cpp` provides game objects +- **Python Integration**: `McRFPy_API.h/cpp` exposes engine functionality to Python +- **UI Components**: `UIFrame`, `UICaption`, `UISprite`, `UIGrid` for rendering + +### Game Logic (Python) +- **Main Script**: `src/scripts/game.py` contains game initialization and scene setup +- **Entity System**: `src/scripts/cos_entities.py` implements game entities (Player, Enemy, Boulder, etc.) +- **Level Generation**: `src/scripts/cos_level.py` uses BSP for procedural dungeon generation +- **Tile System**: `src/scripts/cos_tiles.py` implements Wave Function Collapse for tile placement + +### Key Python API (`mcrfpy` module) +The C++ engine exposes these primary functions to Python: +- Scene Management: `createScene()`, `setScene()`, `sceneUI()` +- Entity Creation: `Entity()` with position and sprite properties +- Grid Management: `Grid()` for tilemap rendering +- Input Handling: `keypressScene()` for keyboard events +- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()` +- Timers: `setTimer()`, `delTimer()` for event scheduling + +## Development Workflow + +### Running the Game +After building, the executable expects: +- `assets/` directory with sprites, fonts, and audio +- `scripts/` directory with Python game files +- Python 3.12 shared libraries in `./lib/` + +### Modifying Game Logic +- Game scripts are in `src/scripts/` +- Main game entry is `game.py` +- Entity behavior in `cos_entities.py` +- Level generation in `cos_level.py` + +### Adding New Features +1. C++ API additions go in `src/McRFPy_API.cpp` +2. Expose to Python using the existing binding pattern +3. Update Python scripts to use new functionality + +## Testing Game Changes + +Currently no automated test suite. Manual testing workflow: +1. Build with `make` +2. Run `make run` or `cd build && ./mcrogueface` +3. Test specific features through gameplay +4. Check console output for Python errors + +### Quick Testing Commands +```bash +# Test basic functionality +make test + +# Run in Python interactive mode +make python + +# Test headless mode +cd build +./mcrogueface --headless -c "import mcrfpy; print('Headless test')" +``` + +## Common Development Tasks + +### Compiling McRogueFace +```bash +# Standard build (to ./build directory) +make + +# Full rebuild +make clean && make + +# Manual CMake build +mkdir build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +make -j$(nproc) + +# The library path issue: if linking fails, check that libraries are in __lib/ +# CMakeLists.txt expects: link_directories(${CMAKE_SOURCE_DIR}/__lib) +``` + +### Running and Capturing Output +```bash +# Run with timeout and capture output +cd build +timeout 5 ./mcrogueface 2>&1 | tee output.log + +# Run in background and kill after delay +./mcrogueface > output.txt 2>&1 & PID=$!; sleep 3; kill $PID 2>/dev/null + +# Just capture first N lines (useful for crashes) +./mcrogueface 2>&1 | head -50 +``` + +### Debugging with GDB +```bash +# Interactive debugging +gdb ./mcrogueface +(gdb) run +(gdb) bt # backtrace after crash + +# Batch mode debugging (non-interactive) +gdb -batch -ex run -ex where -ex quit ./mcrogueface 2>&1 + +# Get just the backtrace after a crash +gdb -batch -ex "run" -ex "bt" ./mcrogueface 2>&1 | head -50 + +# Debug with specific commands +echo -e "run\nbt 5\nquit\ny" | gdb ./mcrogueface 2>&1 +``` + +### Testing Different Python Scripts +```bash +# The game automatically runs build/scripts/game.py on startup +# To test different behavior: + +# Option 1: Replace game.py temporarily +cd build +cp scripts/my_test_script.py scripts/game.py +./mcrogueface + +# Option 2: Backup original and test +mv scripts/game.py scripts/game.py.bak +cp my_test.py scripts/game.py +./mcrogueface +mv scripts/game.py.bak scripts/game.py + +# Option 3: For quick tests, create minimal game.py +echo 'import mcrfpy; print("Test"); mcrfpy.createScene("test")' > scripts/game.py +``` + +### Understanding Key Macros and Patterns + +#### RET_PY_INSTANCE Macro (UIDrawable.h) +This macro handles converting C++ UI objects to their Python equivalents: +```cpp +RET_PY_INSTANCE(target); +// Expands to a switch on target->derived_type() that: +// 1. Allocates the correct Python object type (Frame, Caption, Sprite, Grid) +// 2. Sets the shared_ptr data member +// 3. Returns the PyObject* +``` + +#### Collection Patterns +- `UICollection` wraps `std::vector>` +- `UIEntityCollection` wraps `std::list>` +- Different containers require different iteration code (vector vs list) + +#### Python Object Creation Patterns +```cpp +// Pattern 1: Using tp_alloc (most common) +auto o = (PyUIFrameObject*)type->tp_alloc(type, 0); +o->data = std::make_shared(); + +// Pattern 2: Getting type from module +auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); +auto o = (PyUIEntityObject*)type->tp_alloc(type, 0); + +// Pattern 3: Direct shared_ptr assignment +iterObj->data = self->data; // Shares the C++ object +``` + +### Working Directory Structure +``` +build/ +├── mcrogueface # The executable +├── scripts/ +│ └── game.py # Auto-loaded Python script +├── assets/ # Copied from source during build +└── lib/ # Python libraries (copied from __lib/) +``` + +### Quick Iteration Tips +- Keep a test script ready for quick experiments +- Use `timeout` to auto-kill hanging processes +- The game expects a window manager; use Xvfb for headless testing +- Python errors go to stderr, game output to stdout +- Segfaults usually mean Python type initialization issues + +## Important Notes + +- The project uses SFML for graphics/audio and libtcod for roguelike utilities +- Python scripts are loaded at runtime from the `scripts/` directory +- Asset loading expects specific paths relative to the executable +- The game was created for 7DRL 2025 as "Crypt of Sokoban" +- Iterator implementations require careful handling of C++/Python boundaries + +## Testing Guidelines + +### Test-Driven Development +- **Always write tests first**: Create automation tests in `./tests/` for all bugs and new features +- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix is applied +- **Close the loop**: Reproduce issue → change code → recompile → verify behavior change + +### Two Types of Tests + +#### 1. Direct Execution Tests (No Game Loop) +For tests that only need class initialization or direct code execution: +```python +# These tests can treat McRogueFace like a Python interpreter +import mcrfpy + +# Test code here +result = mcrfpy.some_function() +assert result == expected_value +print("PASS" if condition else "FAIL") +``` + +#### 2. Game Loop Tests (Timer-Based) +For tests requiring rendering, game state, or elapsed time: +```python +import mcrfpy +from mcrfpy import automation +import sys + +def run_test(runtime): + """Timer callback - runs after game loop starts""" + # Now rendering is active, screenshots will work + automation.screenshot("test_result.png") + + # Run your tests here + automation.click(100, 100) + + # Always exit at the end + print("PASS" if success else "FAIL") + sys.exit(0) + +# Set up the test scene +mcrfpy.createScene("test") +# ... add UI elements ... + +# Schedule test to run after game loop starts +mcrfpy.setTimer("test", run_test, 100) # 0.1 seconds +``` + +### Key Testing Principles +- **Timer callbacks are essential**: Screenshots and UI interactions only work after the render loop starts +- **Use automation API**: Always create and examine screenshots when visual feedback is required +- **Exit properly**: Call `sys.exit()` at the end of timer-based tests to prevent hanging +- **Headless mode**: Use `--exec` flag for automated testing: `./mcrogueface --headless --exec tests/my_test.py` + +### Example Test Pattern +```bash +# Run a test that requires game loop +./build/mcrogueface --headless --exec tests/issue_78_middle_click_test.py + +# The test will: +# 1. Set up the scene during script execution +# 2. Register a timer callback +# 3. Game loop starts +# 4. Timer fires after 100ms +# 5. Test runs with full rendering available +# 6. Test takes screenshots and validates behavior +# 7. Test calls sys.exit() to terminate +``` + +## Development Best Practices + +### Testing and Deployment +- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, and tests shouldn't be included + +## Documentation Guidelines + +### Inline C++ Documentation Format + +When adding new methods or modifying existing ones in C++ source files, use this documentation format in PyMethodDef arrays: + +```cpp +{"method_name", (PyCFunction)Class::method, METH_VARARGS | METH_KEYWORDS, + "method_name(arg1: type, arg2: type = default) -> return_type\n\n" + "Brief description of what the method does.\n\n" + "Args:\n" + " arg1: Description of first argument\n" + " arg2: Description of second argument (default: value)\n\n" + "Returns:\n" + " Description of return value\n\n" + "Example:\n" + " result = obj.method_name(value1, value2)\n\n" + "Note:\n" + " Any important notes or caveats"}, +``` + +For properties in PyGetSetDef arrays: +```cpp +{"property_name", (getter)getter_func, (setter)setter_func, + "Brief description of the property. " + "Additional details about valid values, side effects, etc.", NULL}, +``` + +### Regenerating Documentation + +After modifying C++ inline documentation: + +1. **Rebuild the project**: `make -j$(nproc)` + +2. **Generate stub files** (for IDE support): + ```bash + ./build/mcrogueface --exec generate_stubs.py + ``` + +3. **Generate dynamic documentation** (recommended): + ```bash + ./build/mcrogueface --exec generate_dynamic_docs.py + ``` + This creates: + - `docs/api_reference_dynamic.html` + - `docs/API_REFERENCE_DYNAMIC.md` + +4. **Update hardcoded documentation** (if still using old system): + - `generate_complete_api_docs.py` - Update method dictionaries + - `generate_complete_markdown_docs.py` - Update method dictionaries + +### Important Notes + +- **McRogueFace as Python interpreter**: Documentation scripts MUST be run using McRogueFace itself, not system Python +- **Use --exec flag**: `./build/mcrogueface --exec script.py` or `--headless --exec` for CI/automation +- **Dynamic is better**: The new `generate_dynamic_docs.py` extracts documentation directly from compiled module +- **Keep docstrings consistent**: Follow the format above for automatic parsing + +### Documentation Pipeline Architecture + +1. **C++ Source** → PyMethodDef/PyGetSetDef arrays with docstrings +2. **Compilation** → Docstrings embedded in compiled module +3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract +4. **Generation** → HTML/Markdown/Stub files created + +The documentation is only as good as the C++ inline docstrings! \ No newline at end of file diff --git a/README.md b/README.md index 6210792..9d74e5a 100644 --- a/README.md +++ b/README.md @@ -57,18 +57,28 @@ mcrfpy.setScene("intro") ## Documentation -### 📚 Full Documentation Site +### 📚 Developer Documentation -For comprehensive documentation, tutorials, and API reference, visit: -**[https://mcrogueface.github.io](https://mcrogueface.github.io)** +For comprehensive documentation about systems, architecture, and development workflows: -The documentation site includes: +**[Project Wiki](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki)** -- **[Quickstart Guide](https://mcrogueface.github.io/quickstart)** - Get running in 5 minutes -- **[McRogueFace Does The Entire Roguelike Tutorial](https://mcrogueface.github.io/tutorials)** - Step-by-step game building -- **[Complete API Reference](https://mcrogueface.github.io/api)** - Every function documented -- **[Cookbook](https://mcrogueface.github.io/cookbook)** - Ready-to-use code recipes -- **[C++ Extension Guide](https://mcrogueface.github.io/extending-cpp)** - For C++ developers: Add engine features +Key wiki pages: + +- **[Home](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Home)** - Documentation hub with multiple entry points +- **[Grid System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Grid-System)** - Three-layer grid architecture +- **[Python Binding System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Python-Binding-System)** - C++/Python integration +- **[Performance and Profiling](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Performance-and-Profiling)** - Optimization tools +- **[Adding Python Bindings](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Adding-Python-Bindings)** - Step-by-step binding guide +- **[Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap)** - All 46 open issues organized by system + +### 📖 Development Guides + +In the repository root: + +- **[CLAUDE.md](CLAUDE.md)** - Build instructions, testing guidelines, common tasks +- **[ROADMAP.md](ROADMAP.md)** - Strategic vision and development phases +- **[roguelike_tutorial/](roguelike_tutorial/)** - Complete roguelike tutorial implementations ## Build Requirements @@ -114,7 +124,15 @@ If you are writing a game in Python using McRogueFace, you only need to rename a PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request. -The project has a private roadmap and issue list. Reach out via email or social media if you have bugs or feature requests. +### Issue Tracking + +The project uses [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for task tracking and bug reports. Issues are organized with labels: + +- **System labels** (grid, animation, python-binding, etc.) - identify which codebase area +- **Priority labels** (tier1-active, tier2-foundation, tier3-future) - development timeline +- **Type labels** (Major Feature, Minor Feature, Bugfix, etc.) - effort and scope + +See the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap) on the wiki for organized view of all open tasks. ## License diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..8d02b12 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,222 @@ +# McRogueFace - Development Roadmap + +## Project Status + +**Current State**: Active development - C++ game engine with Python scripting +**Latest Release**: Alpha 0.1 +**Issue Tracking**: See [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current tasks and bugs + +--- + +## 🎯 Strategic Vision + +### Engine Philosophy + +- **C++ First**: Performance-critical code stays in C++ +- **Python Close Behind**: Rich scripting without frame-rate impact +- **Game-Ready**: Each improvement should benefit actual game development + +### Architecture Goals + +1. **Clean Inheritance**: Drawable → UI components, proper type preservation +2. **Collection Consistency**: Uniform iteration, indexing, and search patterns +3. **Resource Management**: RAII everywhere, proper lifecycle handling +4. **Multi-Platform**: Windows/Linux feature parity maintained + +--- + +## 🏗️ Architecture Decisions + +### Three-Layer Grid Architecture +Following successful roguelike patterns (Caves of Qud, Cogmind, DCSS): + +1. **Visual Layer** (UIGridPoint) - Sprites, colors, animations +2. **World State Layer** (TCODMap) - Walkability, transparency, physics +3. **Entity Perspective Layer** (UIGridPointState) - Per-entity FOV, knowledge + +### Performance Architecture +Critical for large maps (1000x1000): + +- **Spatial Hashing** for entity queries (not quadtrees!) +- **Batch Operations** with context managers (10-100x speedup) +- **Memory Pooling** for entities and components +- **Dirty Flag System** to avoid unnecessary updates +- **Zero-Copy NumPy Integration** via buffer protocol + +### Key Insight from Research +"Minimizing Python/C++ boundary crossings matters more than individual function complexity" +- Batch everything possible +- Use context managers for logical operations +- Expose arrays, not individual cells +- Profile and optimize hot paths only + +--- + +## 🚀 Development Phases + +For detailed task tracking and current priorities, see the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues). + +### Phase 1: Foundation Stabilization ✅ +**Status**: Complete +**Key Issues**: #7 (Safe Constructors), #71 (Base Class), #87 (Visibility), #88 (Opacity) + +### Phase 2: Constructor & API Polish ✅ +**Status**: Complete +**Key Features**: Pythonic API, tuple support, standardized defaults + +### Phase 3: Entity Lifecycle Management ✅ +**Status**: Complete +**Key Issues**: #30 (Entity.die()), #93 (Vector methods), #94 (Color helpers), #103 (Timer objects) + +### Phase 4: Visibility & Performance ✅ +**Status**: Complete +**Key Features**: AABB culling, name system, profiling tools + +### Phase 5: Window/Scene Architecture ✅ +**Status**: Complete +**Key Issues**: #34 (Window object), #61 (Scene object), #1 (Resize events), #105 (Scene transitions) + +### Phase 6: Rendering Revolution ✅ +**Status**: Complete +**Key Issues**: #50 (Grid backgrounds), #6 (RenderTexture), #8 (Viewport rendering) + +### Phase 7: Documentation & Distribution +**Status**: In Progress +**Key Issues**: #85 (Docstrings), #86 (Parameter docs), #108 (Type stubs), #97 (API docs) + +See [current open issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues?state=open) for active work. + +--- + +## 🔮 Future Vision: Pure Python Extension Architecture + +### Concept: McRogueFace as a Traditional Python Package +**Status**: Long-term vision +**Complexity**: Major architectural overhaul + +Instead of being a C++ application that embeds Python, McRogueFace could be redesigned as a pure Python extension module that can be installed via `pip install mcrogueface`. + +### Technical Approach + +1. **Separate Core Engine from Python Embedding** + - Extract SFML rendering, audio, and input into C++ extension modules + - Remove embedded CPython interpreter + - Use Python's C API to expose functionality + +2. **Module Structure** + ``` + mcrfpy/ + ├── __init__.py # Pure Python coordinator + ├── _core.so # C++ rendering/game loop extension + ├── _sfml.so # SFML bindings + ├── _audio.so # Audio system bindings + └── engine.py # Python game engine logic + ``` + +3. **Inverted Control Flow** + - Python drives the main loop instead of C++ + - C++ extensions handle performance-critical operations + - Python manages game logic, scenes, and entity systems + +### Benefits + +- **Standard Python Packaging**: `pip install mcrogueface` +- **Virtual Environment Support**: Works with venv, conda, poetry +- **Better IDE Integration**: Standard Python development workflow +- **Easier Testing**: Use pytest, standard Python testing tools +- **Cross-Python Compatibility**: Support multiple Python versions +- **Modular Architecture**: Users can import only what they need + +### Challenges + +- **Major Refactoring**: Complete restructure of codebase +- **Performance Considerations**: Python-driven main loop overhead +- **Build Complexity**: Multiple extension modules to compile +- **Platform Support**: Need wheels for many platform/Python combinations +- **API Stability**: Would need careful design to maintain compatibility + +### Example Usage (Future Vision) + +```python +import mcrfpy +from mcrfpy import Scene, Frame, Sprite, Grid + +# Create game directly in Python +game = mcrfpy.Game(width=1024, height=768) + +# Define scenes using Python classes +class MainMenu(Scene): + def on_enter(self): + self.ui.append(Frame(100, 100, 200, 50)) + self.ui.append(Sprite("logo.png", x=400, y=100)) + + def on_keypress(self, key, pressed): + if key == "ENTER" and pressed: + self.game.set_scene("game") + +# Run the game +game.add_scene("menu", MainMenu()) +game.run() +``` + +This architecture would make McRogueFace a first-class Python citizen, following standard Python packaging conventions while maintaining high performance through C++ extensions. + +--- + +## 📋 Major Feature Areas + +For current status and detailed tasks, see the corresponding Gitea issue labels: + +### Core Systems +- **UI/Rendering System**: Issues tagged `[Major Feature]` related to rendering +- **Grid/Entity System**: Pathfinding, FOV, entity management +- **Animation System**: Property animation, easing functions, callbacks +- **Scene/Window Management**: Scene lifecycle, transitions, viewport + +### Performance Optimization +- **#115**: SpatialHash for 10,000+ entities +- **#116**: Dirty flag system +- **#113**: Batch operations for NumPy-style access +- **#117**: Memory pool for entities + +### Advanced Features +- **#118**: Scene as Drawable (scenes can be drawn/animated) +- **#122**: Parent-Child UI System +- **#123**: Grid Subgrid System (256x256 chunks) +- **#124**: Grid Point Animation +- **#106**: Shader support +- **#107**: Particle system + +### Documentation +- **#92**: Inline C++ documentation system +- **#91**: Python type stub files (.pyi) +- **#97**: Automated API documentation extraction +- **#126**: Generate perfectly consistent Python interface + +--- + +## 📚 Resources + +- **Issue Tracker**: [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) +- **Source Code**: [Gitea Repository](https://gamedev.ffwf.net/gitea/john/McRogueFace) +- **Documentation**: See `CLAUDE.md` for build instructions and development guide +- **Tutorial**: See `roguelike_tutorial/` for implementation examples +- **Workflow**: See "Gitea-First Workflow" section in `CLAUDE.md` for issue management best practices + +--- + +## 🔄 Development Workflow + +**Gitea is the Single Source of Truth** for this project. Before starting any work: + +1. **Check Gitea Issues** for existing tasks, bugs, or related work +2. **Create granular issues** for new features or problems +3. **Update issues** when work affects other systems +4. **Document discoveries** - if something is undocumented or misleading, create a task to fix it +5. **Cross-reference commits** with issue numbers (e.g., "Fixes #104") + +See the "Gitea-First Workflow" section in `CLAUDE.md` for detailed guidelines on efficient development practices using the Gitea MCP tools. + +--- + +*For current priorities, task tracking, and bug reports, please use the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).* diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 43b9c03..e4e9035 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -42,8 +42,11 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) updateViewport(); scene = "uitest"; scenes["uitest"] = new UITestScene(this); - + McRFPy_API::game = this; + + // Initialize profiler overlay + profilerOverlay = new ProfilerOverlay(Resources::font); // Only load game.py if no custom script/command/module/exec is specified bool should_load_game = config.script_path.empty() && @@ -85,6 +88,7 @@ GameEngine::~GameEngine() for (auto& [name, scene] : scenes) { delete scene; } + delete profilerOverlay; } void GameEngine::cleanup() @@ -199,10 +203,14 @@ void GameEngine::run() testTimers(); // Update Python scenes - McRFPy_API::updatePythonScenes(frameTime); + { + ScopedTimer pyTimer(metrics.pythonScriptTime); + McRFPy_API::updatePythonScenes(frameTime); + } // Update animations (only if frameTime is valid) if (frameTime > 0.0f && frameTime < 1.0f) { + ScopedTimer animTimer(metrics.animationTime); AnimationManager::getInstance().update(frameTime); } @@ -240,6 +248,12 @@ void GameEngine::run() currentScene()->render(); } + // Update and render profiler overlay (if enabled) + if (profilerOverlay && !headless) { + profilerOverlay->update(metrics); + profilerOverlay->render(*render_target); + } + // Display the frame if (headless) { headless_renderer->display(); @@ -330,6 +344,14 @@ void GameEngine::processEvent(const sf::Event& event) int actionCode = 0; if (event.type == sf::Event::Closed) { running = false; return; } + + // Handle F3 for profiler overlay toggle + if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F3) { + if (profilerOverlay) { + profilerOverlay->toggle(); + } + return; + } // Handle window resize events else if (event.type == sf::Event::Resized) { // Update the viewport to handle the new window size diff --git a/src/GameEngine.h b/src/GameEngine.h index 30ed619..4721bb8 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -9,11 +9,16 @@ #include "McRogueFaceConfig.h" #include "HeadlessRenderer.h" #include "SceneTransition.h" +#include "Profiler.h" #include +#include class GameEngine { public: + // Forward declare nested class so private section can use it + class ProfilerOverlay; + // Viewport modes (moved here so private section can use it) enum class ViewportMode { Center, // 1:1 pixels, viewport centered in window @@ -51,7 +56,12 @@ private: sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution sf::View gameView; // View for the game content ViewportMode viewportMode = ViewportMode::Fit; - + + // Profiling overlay + bool showProfilerOverlay = false; // F3 key toggles this + int overlayUpdateCounter = 0; // Only update overlay every N frames + ProfilerOverlay* profilerOverlay = nullptr; // The actual overlay renderer + void updateViewport(); void testTimers(); @@ -69,17 +79,29 @@ public: int drawCalls = 0; // Draw calls per frame int uiElements = 0; // Number of UI elements rendered int visibleElements = 0; // Number of visible elements - + + // Detailed timing breakdowns (added for profiling system) + float gridRenderTime = 0.0f; // Time spent rendering grids (ms) + float entityRenderTime = 0.0f; // Time spent rendering entities (ms) + float fovOverlayTime = 0.0f; // Time spent rendering FOV overlays (ms) + float pythonScriptTime = 0.0f; // Time spent in Python callbacks (ms) + float animationTime = 0.0f; // Time spent updating animations (ms) + + // Grid-specific metrics + int gridCellsRendered = 0; // Number of grid cells drawn this frame + int entitiesRendered = 0; // Number of entities drawn this frame + int totalEntities = 0; // Total entities in scene + // Frame time history for averaging static constexpr int HISTORY_SIZE = 60; float frameTimeHistory[HISTORY_SIZE] = {0}; int historyIndex = 0; - + void updateFrameTime(float deltaMs) { frameTime = deltaMs; frameTimeHistory[historyIndex] = deltaMs; historyIndex = (historyIndex + 1) % HISTORY_SIZE; - + // Calculate average float sum = 0.0f; for (int i = 0; i < HISTORY_SIZE; ++i) { @@ -88,13 +110,26 @@ public: avgFrameTime = sum / HISTORY_SIZE; fps = avgFrameTime > 0 ? static_cast(1000.0f / avgFrameTime) : 0; } - + void resetPerFrame() { drawCalls = 0; uiElements = 0; visibleElements = 0; + + // Reset per-frame timing metrics + gridRenderTime = 0.0f; + entityRenderTime = 0.0f; + fovOverlayTime = 0.0f; + pythonScriptTime = 0.0f; + animationTime = 0.0f; + + // Reset per-frame counters + gridCellsRendered = 0; + entitiesRendered = 0; + totalEntities = 0; } } metrics; + GameEngine(); GameEngine(const McRogueFaceConfig& cfg); ~GameEngine(); @@ -144,5 +179,30 @@ public: sf::Music music; sf::Sound sfx; std::shared_ptr>> scene_ui(std::string scene); - + +}; + +/** + * @brief Visual overlay that displays real-time profiling metrics + */ +class GameEngine::ProfilerOverlay { +private: + sf::Font& font; + sf::Text text; + sf::RectangleShape background; + bool visible; + int updateInterval; + int frameCounter; + + sf::Color getPerformanceColor(float frameTimeMs); + std::string formatFloat(float value, int precision = 1); + std::string formatPercentage(float part, float total); + +public: + ProfilerOverlay(sf::Font& fontRef); + void toggle(); + void setVisible(bool vis); + bool isVisible() const; + void update(const ProfilingMetrics& metrics); + void render(sf::RenderTarget& target); }; diff --git a/src/Profiler.cpp b/src/Profiler.cpp new file mode 100644 index 0000000..e19e886 --- /dev/null +++ b/src/Profiler.cpp @@ -0,0 +1,61 @@ +#include "Profiler.h" +#include + +ProfilingLogger::ProfilingLogger() + : headers_written(false) +{ +} + +ProfilingLogger::~ProfilingLogger() { + close(); +} + +bool ProfilingLogger::open(const std::string& filename, const std::vector& columns) { + column_names = columns; + file.open(filename); + + if (!file.is_open()) { + std::cerr << "Failed to open profiling log file: " << filename << std::endl; + return false; + } + + // Write CSV header + for (size_t i = 0; i < columns.size(); ++i) { + file << columns[i]; + if (i < columns.size() - 1) { + file << ","; + } + } + file << "\n"; + file.flush(); + + headers_written = true; + return true; +} + +void ProfilingLogger::writeRow(const std::vector& values) { + if (!file.is_open()) { + return; + } + + if (values.size() != column_names.size()) { + std::cerr << "ProfilingLogger: value count (" << values.size() + << ") doesn't match column count (" << column_names.size() << ")" << std::endl; + return; + } + + for (size_t i = 0; i < values.size(); ++i) { + file << values[i]; + if (i < values.size() - 1) { + file << ","; + } + } + file << "\n"; +} + +void ProfilingLogger::close() { + if (file.is_open()) { + file.flush(); + file.close(); + } +} diff --git a/src/Profiler.h b/src/Profiler.h new file mode 100644 index 0000000..fefcc08 --- /dev/null +++ b/src/Profiler.h @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include + +/** + * @brief Simple RAII-based profiling timer for measuring code execution time + * + * Usage: + * float timing = 0.0f; + * { + * ScopedTimer timer(timing); + * // ... code to profile ... + * } // timing now contains elapsed milliseconds + */ +class ScopedTimer { +private: + std::chrono::high_resolution_clock::time_point start; + float& target_ms; + +public: + /** + * @brief Construct a new Scoped Timer and start timing + * @param target Reference to float that will receive elapsed time in milliseconds + */ + explicit ScopedTimer(float& target) + : target_ms(target) + { + start = std::chrono::high_resolution_clock::now(); + } + + /** + * @brief Destructor automatically records elapsed time + */ + ~ScopedTimer() { + auto end = std::chrono::high_resolution_clock::now(); + target_ms = std::chrono::duration(end - start).count(); + } + + // Prevent copying + ScopedTimer(const ScopedTimer&) = delete; + ScopedTimer& operator=(const ScopedTimer&) = delete; +}; + +/** + * @brief Accumulating timer that adds elapsed time to existing value + * + * Useful for measuring total time across multiple calls in a single frame + */ +class AccumulatingTimer { +private: + std::chrono::high_resolution_clock::time_point start; + float& target_ms; + +public: + explicit AccumulatingTimer(float& target) + : target_ms(target) + { + start = std::chrono::high_resolution_clock::now(); + } + + ~AccumulatingTimer() { + auto end = std::chrono::high_resolution_clock::now(); + target_ms += std::chrono::duration(end - start).count(); + } + + AccumulatingTimer(const AccumulatingTimer&) = delete; + AccumulatingTimer& operator=(const AccumulatingTimer&) = delete; +}; + +/** + * @brief CSV profiling data logger for batch analysis + * + * Writes profiling data to CSV file for later analysis with Python/pandas/Excel + */ +class ProfilingLogger { +private: + std::ofstream file; + bool headers_written; + std::vector column_names; + +public: + ProfilingLogger(); + ~ProfilingLogger(); + + /** + * @brief Open a CSV file for writing profiling data + * @param filename Path to CSV file + * @param columns Column names for the CSV header + * @return true if file opened successfully + */ + bool open(const std::string& filename, const std::vector& columns); + + /** + * @brief Write a row of profiling data + * @param values Data values (must match column count) + */ + void writeRow(const std::vector& values); + + /** + * @brief Close the file and flush data + */ + void close(); + + /** + * @brief Check if logger is ready to write + */ + bool isOpen() const { return file.is_open(); } +}; diff --git a/src/ProfilerOverlay.cpp b/src/ProfilerOverlay.cpp new file mode 100644 index 0000000..7860b49 --- /dev/null +++ b/src/ProfilerOverlay.cpp @@ -0,0 +1,135 @@ +#include "GameEngine.h" +#include +#include + +GameEngine::ProfilerOverlay::ProfilerOverlay(sf::Font& fontRef) + : font(fontRef), visible(false), updateInterval(10), frameCounter(0) +{ + text.setFont(font); + text.setCharacterSize(14); + text.setFillColor(sf::Color::White); + text.setPosition(10.0f, 10.0f); + + // Semi-transparent dark background + background.setFillColor(sf::Color(0, 0, 0, 180)); + background.setPosition(5.0f, 5.0f); +} + +void GameEngine::ProfilerOverlay::toggle() { + visible = !visible; +} + +void GameEngine::ProfilerOverlay::setVisible(bool vis) { + visible = vis; +} + +bool GameEngine::ProfilerOverlay::isVisible() const { + return visible; +} + +sf::Color GameEngine::ProfilerOverlay::getPerformanceColor(float frameTimeMs) { + if (frameTimeMs < 16.6f) { + return sf::Color::Green; // 60+ FPS + } else if (frameTimeMs < 33.3f) { + return sf::Color::Yellow; // 30-60 FPS + } else { + return sf::Color::Red; // <30 FPS + } +} + +std::string GameEngine::ProfilerOverlay::formatFloat(float value, int precision) { + std::stringstream ss; + ss << std::fixed << std::setprecision(precision) << value; + return ss.str(); +} + +std::string GameEngine::ProfilerOverlay::formatPercentage(float part, float total) { + if (total <= 0.0f) return "0%"; + float pct = (part / total) * 100.0f; + return formatFloat(pct, 0) + "%"; +} + +void GameEngine::ProfilerOverlay::update(const ProfilingMetrics& metrics) { + if (!visible) return; + + // Only update text every N frames to reduce overhead + frameCounter++; + if (frameCounter < updateInterval) { + return; + } + frameCounter = 0; + + std::stringstream ss; + ss << "McRogueFace Performance Monitor\n"; + ss << "================================\n"; + + // Frame time and FPS + float frameMs = metrics.avgFrameTime; + ss << "FPS: " << metrics.fps << " (" << formatFloat(frameMs, 1) << "ms/frame)\n"; + + // Performance warning + if (frameMs > 33.3f) { + ss << "WARNING: Frame time exceeds 30 FPS target!\n"; + } + + ss << "\n"; + + // Timing breakdown + ss << "Frame Time Breakdown:\n"; + ss << " Grid Render: " << formatFloat(metrics.gridRenderTime, 1) << "ms (" + << formatPercentage(metrics.gridRenderTime, frameMs) << ")\n"; + ss << " Cells: " << metrics.gridCellsRendered << " rendered\n"; + ss << " Entities: " << metrics.entitiesRendered << " / " << metrics.totalEntities << " drawn\n"; + + if (metrics.fovOverlayTime > 0.01f) { + ss << " FOV Overlay: " << formatFloat(metrics.fovOverlayTime, 1) << "ms\n"; + } + + if (metrics.entityRenderTime > 0.01f) { + ss << " Entity Render: " << formatFloat(metrics.entityRenderTime, 1) << "ms (" + << formatPercentage(metrics.entityRenderTime, frameMs) << ")\n"; + } + + if (metrics.pythonScriptTime > 0.01f) { + ss << " Python: " << formatFloat(metrics.pythonScriptTime, 1) << "ms (" + << formatPercentage(metrics.pythonScriptTime, frameMs) << ")\n"; + } + + if (metrics.animationTime > 0.01f) { + ss << " Animations: " << formatFloat(metrics.animationTime, 1) << "ms (" + << formatPercentage(metrics.animationTime, frameMs) << ")\n"; + } + + ss << "\n"; + + // Other metrics + ss << "Draw Calls: " << metrics.drawCalls << "\n"; + ss << "UI Elements: " << metrics.uiElements << " (" << metrics.visibleElements << " visible)\n"; + + // Calculate unaccounted time + float accountedTime = metrics.gridRenderTime + metrics.entityRenderTime + + metrics.pythonScriptTime + metrics.animationTime; + float unaccountedTime = frameMs - accountedTime; + + if (unaccountedTime > 1.0f) { + ss << "\n"; + ss << "Other: " << formatFloat(unaccountedTime, 1) << "ms (" + << formatPercentage(unaccountedTime, frameMs) << ")\n"; + } + + ss << "\n"; + ss << "Press F3 to hide this overlay"; + + text.setString(ss.str()); + + // Update background size to fit text + sf::FloatRect textBounds = text.getLocalBounds(); + background.setSize(sf::Vector2f(textBounds.width + 20.0f, textBounds.height + 20.0f)); +} + +void GameEngine::ProfilerOverlay::render(sf::RenderTarget& target) { + if (!visible) return; + + target.draw(background); + target.draw(text); +} diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index dafc6f8..b07e596 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -3,6 +3,7 @@ #include "McRFPy_API.h" #include "PythonObjectCache.h" #include "UIEntity.h" +#include "Profiler.h" #include // UIDrawable methods now in UIBase.h @@ -95,11 +96,14 @@ void UIGrid::update() {} void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) { + // Profile total grid rendering time + ScopedTimer gridTimer(Resources::game->metrics.gridRenderTime); + // Check visibility if (!visible) return; - + // TODO: Apply opacity to output sprite - + output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing // output size can change; update size when drawing output.setTextureRect( @@ -135,11 +139,12 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) if (y_limit > grid_y) y_limit = grid_y; // base layer - bottom color, tile sprite ("ground") + int cellsRendered = 0; for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); - x < x_limit; //x < view_width; + x < x_limit; //x < view_width; x+=1) { - //for (float y = (top_edge >= 0 ? top_edge : 0); + //for (float y = (top_edge >= 0 ? top_edge : 0); for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); y < y_limit; //y < view_height; y+=1) @@ -163,35 +168,53 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, sf::Vector2f(zoom, zoom)); //setSprite(gridpoint.tilesprite);; renderTexture.draw(sprite); } + + cellsRendered++; } } + // Record how many cells were rendered + Resources::game->metrics.gridCellsRendered += cellsRendered; + // middle layer - entities // disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window) - for (auto e : *entities) { - // Skip out-of-bounds entities for performance - // Check if entity is within visible bounds (with 1 cell margin for partially visible entities) - if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 || - e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) { - continue; // Skip this entity as it's not visible + { + ScopedTimer entityTimer(Resources::game->metrics.entityRenderTime); + int entitiesRendered = 0; + int totalEntities = entities->size(); + + for (auto e : *entities) { + // Skip out-of-bounds entities for performance + // Check if entity is within visible bounds (with 1 cell margin for partially visible entities) + if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 || + e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) { + continue; // Skip this entity as it's not visible + } + + //auto drawent = e->cGrid->indexsprite.drawable(); + auto& drawent = e->sprite; + //drawent.setScale(zoom, zoom); + drawent.setScale(sf::Vector2f(zoom, zoom)); + auto pixel_pos = sf::Vector2f( + (e->position.x*cell_width - left_spritepixels) * zoom, + (e->position.y*cell_height - top_spritepixels) * zoom ); + //drawent.setPosition(pixel_pos); + //renderTexture.draw(drawent); + drawent.render(pixel_pos, renderTexture); + + entitiesRendered++; } - - //auto drawent = e->cGrid->indexsprite.drawable(); - auto& drawent = e->sprite; - //drawent.setScale(zoom, zoom); - drawent.setScale(sf::Vector2f(zoom, zoom)); - auto pixel_pos = sf::Vector2f( - (e->position.x*cell_width - left_spritepixels) * zoom, - (e->position.y*cell_height - top_spritepixels) * zoom ); - //drawent.setPosition(pixel_pos); - //renderTexture.draw(drawent); - drawent.render(pixel_pos, renderTexture); + + // Record entity rendering stats + Resources::game->metrics.entitiesRendered += entitiesRendered; + Resources::game->metrics.totalEntities += totalEntities; } // top layer - opacity for discovered / visible status based on perspective // Only render visibility overlay if perspective is enabled if (perspective_enabled) { + ScopedTimer fovTimer(Resources::game->metrics.fovOverlayTime); auto entity = perspective_entity.lock(); // Create rectangle for overlays diff --git a/tests/benchmark_moving_entities.py b/tests/benchmark_moving_entities.py new file mode 100644 index 0000000..6c0fb76 --- /dev/null +++ b/tests/benchmark_moving_entities.py @@ -0,0 +1,152 @@ +""" +Benchmark: Moving Entities Performance Test + +This benchmark measures McRogueFace's performance with 50 randomly moving +entities on a 100x100 grid. + +Expected results: +- Should maintain 60 FPS +- Entity render time should be <3ms +- Grid render time will be higher due to constant updates (no dirty flag benefit) + +Usage: + ./build/mcrogueface --exec tests/benchmark_moving_entities.py + +Press F3 to toggle performance overlay +Press ESC to exit +""" + +import mcrfpy +import sys +import random + +# Create the benchmark scene +mcrfpy.createScene("benchmark") +mcrfpy.setScene("benchmark") + +# Get scene UI +ui = mcrfpy.sceneUI("benchmark") + +# Create a 100x100 grid +grid = mcrfpy.Grid( + grid_size=(100, 100), + pos=(0, 0), + size=(1024, 768) +) + +# Simple floor pattern +for x in range(100): + for y in range(100): + cell = grid.at((x, y)) + cell.tilesprite = 0 + cell.color = (40, 40, 40, 255) + +# Create 50 entities with random positions and velocities +entities = [] +ENTITY_COUNT = 50 + +for i in range(ENTITY_COUNT): + entity = mcrfpy.Entity( + grid_pos=(random.randint(0, 99), random.randint(0, 99)), + sprite_index=random.randint(10, 20) # Use varied sprites + ) + + # Give each entity a random velocity + entity.velocity_x = random.uniform(-0.5, 0.5) + entity.velocity_y = random.uniform(-0.5, 0.5) + + grid.entities.append(entity) + entities.append(entity) + +ui.append(grid) + +# Instructions caption +instructions = mcrfpy.Caption( + text=f"Moving Entities Benchmark ({ENTITY_COUNT} entities)\n" + "Press F3 for performance overlay\n" + "Press ESC to exit\n" + "Goal: 60 FPS with entities moving", + pos=(10, 10), + fill_color=(255, 255, 0, 255) +) +ui.append(instructions) + +# Benchmark info +print("=" * 60) +print("MOVING ENTITIES BENCHMARK") +print("=" * 60) +print(f"Entity count: {ENTITY_COUNT}") +print("Grid size: 100x100 cells") +print("Expected FPS: 60") +print("") +print("Entities move randomly and bounce off walls.") +print("This tests entity rendering performance and position updates.") +print("") +print("Press F3 in-game to see real-time performance metrics.") +print("=" * 60) + +# Exit handler +def handle_key(key, state): + if key == "Escape" and state: + print("\nBenchmark ended by user") + sys.exit(0) + +mcrfpy.keypressScene(handle_key) + +# Update entity positions +def update_entities(ms): + dt = ms / 1000.0 # Convert to seconds + + for entity in entities: + # Update position + new_x = entity.x + entity.velocity_x + new_y = entity.y + entity.velocity_y + + # Bounce off walls + if new_x < 0 or new_x >= 100: + entity.velocity_x = -entity.velocity_x + new_x = max(0, min(99, new_x)) + + if new_y < 0 or new_y >= 100: + entity.velocity_y = -entity.velocity_y + new_y = max(0, min(99, new_y)) + + # Update entity position + entity.x = new_x + entity.y = new_y + +# Run movement update every frame (16ms) +mcrfpy.setTimer("movement", update_entities, 16) + +# Benchmark statistics +frame_count = 0 +start_time = None + +def benchmark_timer(ms): + global frame_count, start_time + + if start_time is None: + import time + start_time = time.time() + + frame_count += 1 + + # After 10 seconds, print summary + import time + elapsed = time.time() - start_time + + if elapsed >= 10.0: + print("\n" + "=" * 60) + print("BENCHMARK COMPLETE") + print("=" * 60) + print(f"Frames rendered: {frame_count}") + print(f"Time elapsed: {elapsed:.2f}s") + print(f"Average FPS: {frame_count / elapsed:.1f}") + print(f"Entities: {ENTITY_COUNT}") + print("") + print("Check profiler overlay (F3) for detailed timing breakdown.") + print("Entity render time and total frame time are key metrics.") + print("=" * 60) + # Don't exit - let user review + +mcrfpy.setTimer("benchmark", benchmark_timer, 100) diff --git a/tests/benchmark_static_grid.py b/tests/benchmark_static_grid.py new file mode 100644 index 0000000..5307232 --- /dev/null +++ b/tests/benchmark_static_grid.py @@ -0,0 +1,122 @@ +""" +Benchmark: Static Grid Performance Test + +This benchmark measures McRogueFace's grid rendering performance with a static +100x100 grid. The goal is 60 FPS with minimal CPU usage. + +Expected results: +- 60 FPS (16.6ms per frame) +- Grid render time should be <2ms after dirty flag optimization +- Currently will be higher (likely 8-12ms) - this establishes baseline + +Usage: + ./build/mcrogueface --exec tests/benchmark_static_grid.py + +Press F3 to toggle performance overlay +Press ESC to exit +""" + +import mcrfpy +import sys + +# Create the benchmark scene +mcrfpy.createScene("benchmark") +mcrfpy.setScene("benchmark") + +# Get scene UI +ui = mcrfpy.sceneUI("benchmark") + +# Create a 100x100 grid with default texture +grid = mcrfpy.Grid( + grid_size=(100, 100), + pos=(0, 0), + size=(1024, 768) +) + +# Fill grid with varied tile patterns to ensure realistic rendering +for x in range(100): + for y in range(100): + cell = grid.at((x, y)) + # Checkerboard pattern with different sprites + if (x + y) % 2 == 0: + cell.tilesprite = 0 + cell.color = (50, 50, 50, 255) + else: + cell.tilesprite = 1 + cell.color = (70, 70, 70, 255) + + # Add some variation + if x % 10 == 0 or y % 10 == 0: + cell.tilesprite = 2 + cell.color = (100, 100, 100, 255) + +# Add grid to scene +ui.append(grid) + +# Instructions caption +instructions = mcrfpy.Caption( + text="Static Grid Benchmark (100x100)\n" + "Press F3 for performance overlay\n" + "Press ESC to exit\n" + "Goal: 60 FPS with low grid render time", + pos=(10, 10), + fill_color=(255, 255, 0, 255) +) +ui.append(instructions) + +# Benchmark info +print("=" * 60) +print("STATIC GRID BENCHMARK") +print("=" * 60) +print("Grid size: 100x100 cells") +print("Expected FPS: 60") +print("Tiles rendered: ~1024 visible cells per frame") +print("") +print("This benchmark establishes baseline grid rendering performance.") +print("After dirty flag optimization, grid render time should drop") +print("significantly for static content.") +print("") +print("Press F3 in-game to see real-time performance metrics.") +print("=" * 60) + +# Exit handler +def handle_key(key, state): + if key == "Escape" and state: + print("\nBenchmark ended by user") + sys.exit(0) + +mcrfpy.keypressScene(handle_key) + +# Run for 10 seconds then provide summary +frame_count = 0 +start_time = None + +def benchmark_timer(ms): + global frame_count, start_time + + if start_time is None: + import time + start_time = time.time() + + frame_count += 1 + + # After 10 seconds, print summary and exit + import time + elapsed = time.time() - start_time + + if elapsed >= 10.0: + print("\n" + "=" * 60) + print("BENCHMARK COMPLETE") + print("=" * 60) + print(f"Frames rendered: {frame_count}") + print(f"Time elapsed: {elapsed:.2f}s") + print(f"Average FPS: {frame_count / elapsed:.1f}") + print("") + print("Check profiler overlay (F3) for detailed timing breakdown.") + print("Grid render time is the key metric for optimization.") + print("=" * 60) + # Don't exit automatically - let user review with F3 + # sys.exit(0) + +# Update every 100ms +mcrfpy.setTimer("benchmark", benchmark_timer, 100)