Compare commits

..

3 commits

1085 changed files with 5590 additions and 2060718 deletions

42
.gitignore vendored
View file

@ -7,44 +7,6 @@ PCbuild
.vs
obj
build
/lib
__pycache__
lib
obj
# unimportant files that won't pass clean dir check
build*
docs
.claude
my_games
# images are produced by many tests
*.png
# WASM stdlib for Emscripten build
!wasm_stdlib/
.cache/
7DRL2025 Release/
CMakeFiles/
Makefile
*.zip
__lib/
__lib_windows/
build-windows/
build_windows/
_oldscripts/
assets/
cellular_automata_fire/
deps/
fetch_issues_txt.py
forest_fire_CA.py
mcrogueface.github.io
scripts/
tcod_reference
.archive
.mcp.json
dist/
# Keep important documentation and tests
!CLAUDE.md
!README.md
!tests/

13
.gitmodules vendored
View file

@ -10,13 +10,6 @@
[submodule "modules/SFML"]
path = modules/SFML
url = git@github.com:SFML/SFML.git
[submodule "modules/libtcod-headless"]
path = modules/libtcod-headless
url = git@github.com:jmccardle/libtcod-headless.git
branch = 2.2.1-headless
[submodule "modules/RapidXML"]
path = modules/RapidXML
url = https://github.com/Fe-Bell/RapidXML
[submodule "modules/json"]
path = modules/json
url = git@github.com:nlohmann/json.git
[submodule "modules/libtcod"]
path = modules/libtcod
url = git@github.com:libtcod/libtcod.git

View file

@ -1,306 +0,0 @@
# Building McRogueFace from Source
This document describes how to build McRogueFace from a fresh clone.
## Build Options
There are two ways to build McRogueFace:
1. **Quick Build** (recommended): Use pre-built dependency libraries from a `build_deps` archive
2. **Full Build**: Compile all dependencies from submodules
## Prerequisites
### System Dependencies
Install these packages before building:
```bash
# Debian/Ubuntu
sudo apt install \
build-essential \
cmake \
git \
zlib1g-dev \
libx11-dev \
libxrandr-dev \
libxcursor-dev \
libfreetype-dev \
libudev-dev \
libvorbis-dev \
libflac-dev \
libgl-dev \
libopenal-dev
```
**Note:** SDL is NOT required - McRogueFace uses libtcod-headless which has no SDL dependency.
---
## Option 1: Quick Build (Using Pre-built Dependencies)
If you have a `build_deps.tar.gz` or `build_deps.zip` archive:
```bash
# Clone McRogueFace (no submodules needed)
git clone <repository-url> McRogueFace
cd McRogueFace
# Extract pre-built dependencies
tar -xzf /path/to/build_deps.tar.gz
# Or for zip: unzip /path/to/build_deps.zip
# Build McRogueFace
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
# Run
./mcrogueface
```
The `build_deps` archive contains:
- `__lib/` - Pre-built shared libraries (Python, SFML, libtcod-headless)
- `deps/` - Header symlinks for compilation
**Total build time: ~30 seconds**
---
## Option 2: Full Build (Compiling All Dependencies)
### 1. Clone with Submodules
```bash
git clone --recursive <repository-url> McRogueFace
cd McRogueFace
```
If submodules weren't cloned:
```bash
git submodule update --init --recursive
```
**Note:** imgui/imgui-sfml submodules may fail - this is fine, they're not used.
### 2. Create Dependency Symlinks
```bash
cd deps
ln -sf ../modules/cpython cpython
ln -sf ../modules/libtcod-headless/src/libtcod libtcod
ln -sf ../modules/cpython/Include Python
ln -sf ../modules/SFML/include/SFML SFML
cd ..
```
### 3. Build libtcod-headless
libtcod-headless is our SDL-free fork with vendored dependencies:
```bash
cd modules/libtcod-headless
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=ON
make -j$(nproc)
cd ../../..
```
That's it! No special flags needed - libtcod-headless defaults to:
- `LIBTCOD_SDL3=disable` (no SDL dependency)
- Vendored lodepng, utf8proc, stb
### 4. Build Python 3.12
```bash
cd modules/cpython
./configure --enable-shared
make -j$(nproc)
cd ../..
```
### 5. Build SFML 2.6
```bash
cd modules/SFML
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=ON
make -j$(nproc)
cd ../../..
```
### 6. Copy Libraries
```bash
mkdir -p __lib
# Python
cp modules/cpython/libpython3.12.so* __lib/
# SFML
cp modules/SFML/build/lib/libsfml-*.so* __lib/
# libtcod-headless
cp modules/libtcod-headless/build/bin/libtcod.so* __lib/
# Python standard library
cp -r modules/cpython/Lib __lib/Python
```
### 7. Build McRogueFace
```bash
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
```
### 8. Run
```bash
./mcrogueface
```
---
## Submodule Versions
| Submodule | Version | Notes |
|-----------|---------|-------|
| SFML | 2.6.1 | Graphics, audio, windowing |
| cpython | 3.12.2 | Embedded Python interpreter |
| libtcod-headless | 2.2.1 | SDL-free fork for FOV, pathfinding |
---
## Creating a build_deps Archive
To create a `build_deps` archive for distribution:
```bash
cd McRogueFace
# Create archive directory
mkdir -p build_deps_staging
# Copy libraries
cp -r __lib build_deps_staging/
# Copy/create deps symlinks as actual directories with only needed headers
mkdir -p build_deps_staging/deps
cp -rL deps/libtcod build_deps_staging/deps/ # Follow symlink
cp -rL deps/Python build_deps_staging/deps/
cp -rL deps/SFML build_deps_staging/deps/
cp -r deps/platform build_deps_staging/deps/
# Create archives
cd build_deps_staging
tar -czf ../build_deps.tar.gz __lib deps
zip -r ../build_deps.zip __lib deps
cd ..
# Cleanup
rm -rf build_deps_staging
```
The resulting archive can be distributed alongside releases for users who want to build McRogueFace without compiling dependencies.
**Archive contents:**
```
build_deps.tar.gz
├── __lib/
│ ├── libpython3.12.so*
│ ├── libsfml-*.so*
│ ├── libtcod.so*
│ └── Python/ # Python standard library
└── deps/
├── libtcod/ # libtcod headers
├── Python/ # Python headers
├── SFML/ # SFML headers
└── platform/ # Platform-specific configs
```
---
## Verify the Build
```bash
cd build
# Check version
./mcrogueface --version
# Test headless mode
./mcrogueface --headless -c "import mcrfpy; print('Success')"
# Verify no SDL dependencies
ldd mcrogueface | grep -i sdl # Should output nothing
```
---
## Troubleshooting
### OpenAL not found
```bash
sudo apt install libopenal-dev
```
### FreeType not found
```bash
sudo apt install libfreetype-dev
```
### X11/Xrandr not found
```bash
sudo apt install libx11-dev libxrandr-dev
```
### Python standard library missing
Ensure `__lib/Python` contains the standard library:
```bash
ls __lib/Python/os.py # Should exist
```
### libtcod symbols not found
Ensure libtcod.so is in `__lib/` with correct version:
```bash
ls -la __lib/libtcod.so*
# Should show libtcod.so -> libtcod.so.2 -> libtcod.so.2.2.1
```
---
## Build Times (approximate)
On a typical 4-core system:
| Component | Time |
|-----------|------|
| libtcod-headless | ~30 seconds |
| Python 3.12 | ~3-5 minutes |
| SFML 2.6 | ~1 minute |
| McRogueFace | ~30 seconds |
| **Full build total** | **~5-7 minutes** |
| **Quick build (pre-built deps)** | **~30 seconds** |
---
## Runtime Dependencies
The built executable requires these system libraries:
- `libz.so.1` (zlib)
- `libopenal.so.1` (OpenAL)
- `libX11.so.6`, `libXrandr.so.2` (X11)
- `libfreetype.so.6` (FreeType)
- `libGL.so.1` (OpenGL)
All other dependencies (Python, SFML, libtcod) are bundled in `lib/`.

992
CLAUDE.md
View file

@ -1,992 +0,0 @@
# 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
- Before implementing: Read relevant wiki pages per the [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow) consultation table
- 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: Note it for wiki update
- When documentation misleads you: Note it for wiki correction
- After committing code changes: Update relevant wiki pages (with user permission)
- Follow the [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow) for wiki update procedures
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!
### Gitea Label System
**IMPORTANT**: Always apply appropriate labels when creating new issues!
The project uses a structured label system to organize issues:
**Label Categories:**
1. **System Labels** (identify affected codebase area):
- `system:rendering` - Rendering pipeline and visuals
- `system:ui-hierarchy` - UI component hierarchy and composition
- `system:grid` - Grid system and spatial containers
- `system:animation` - Animation and property interpolation
- `system:python-binding` - Python/C++ binding layer
- `system:input` - Input handling and events
- `system:performance` - Performance optimization and profiling
- `system:documentation` - Documentation infrastructure
2. **Priority Labels** (development timeline):
- `priority:tier1-active` - Current development focus - critical path to v1.0
- `priority:tier2-foundation` - Important foundation work - not blocking v1.0
- `priority:tier3-future` - Future features - deferred until after v1.0
3. **Type/Scope Labels** (effort and complexity):
- `Major Feature` - Significant time and effort required
- `Minor Feature` - Some effort required to create or overhaul functionality
- `Tiny Feature` - Quick and easy - a few lines or little interconnection
- `Bugfix` - Fixes incorrect behavior
- `Refactoring & Cleanup` - No new functionality, just improving codebase
- `Documentation` - Documentation work
- `Demo Target` - Functionality to demonstrate
4. **Workflow Labels** (current blockers/needs):
- `workflow:blocked` - Blocked by other work - waiting on dependencies
- `workflow:needs-documentation` - Needs documentation before or after implementation
- `workflow:needs-benchmark` - Needs performance testing and benchmarks
- `Alpha Release Requirement` - Blocker to 0.1 Alpha release
**When creating issues:**
- Apply at least one `system:*` label (what part of codebase)
- Apply one `priority:tier*` label (when to address it)
- Apply one type label (`Major Feature`, `Minor Feature`, `Tiny Feature`, or `Bugfix`)
- Apply `workflow:*` labels if applicable (blocked, needs docs, needs benchmarks)
**Example label combinations:**
- New rendering feature: `system:rendering`, `priority:tier2-foundation`, `Major Feature`
- Python API improvement: `system:python-binding`, `priority:tier1-active`, `Minor Feature`
- Performance work: `system:performance`, `priority:tier1-active`, `Major Feature`, `workflow:needs-benchmark`
**⚠️ CRITICAL BUG**: The Gitea MCP tool (v0.07) has a label application bug documented in `GITEA_MCP_LABEL_BUG_REPORT.md`:
- `add_issue_labels` and `replace_issue_labels` behave inconsistently
- Single ID arrays produce different results than multi-ID arrays for the SAME IDs
- Label IDs do not map reliably to actual labels
**Workaround Options:**
1. **Best**: Apply labels manually via web interface: `https://gamedev.ffwf.net/gitea/john/McRogueFace/issues/<number>`
2. **Automated**: Apply labels ONE AT A TIME using single-element arrays (slower but more reliable)
3. **Use single-ID mapping** (documented below)
**Label ID Reference** (for documentation purposes - see issue #131 for details):
```
1=Major Feature, 2=Alpha Release, 3=Bugfix, 4=Demo Target, 5=Documentation,
6=Minor Feature, 7=tier1-active, 8=tier2-foundation, 9=tier3-future,
10=Refactoring, 11=animation, 12=docs, 13=grid, 14=input, 15=performance,
16=python-binding, 17=rendering, 18=ui-hierarchy, 19=Tiny Feature,
20=blocked, 21=needs-benchmark, 22=needs-documentation
```
## Build System
McRogueFace uses a unified Makefile for both Linux native builds and Windows cross-compilation.
**IMPORTANT**: All `make` commands must be run from the **project root directory** (`/home/john/Development/McRogueFace/`), not from `build/` or any subdirectory.
### Quick Reference
```bash
# Linux builds
make # Build for Linux (default target)
make linux # Same as above
make run # Build and run
make clean # Remove Linux build artifacts
# Windows cross-compilation (requires MinGW-w64)
make windows # Release build for Windows
make windows-debug # Debug build with console output
make clean-windows # Remove Windows build artifacts
# Distribution packages
make package-linux-light # Linux with minimal stdlib (~25 MB)
make package-linux-full # Linux with full stdlib (~26 MB)
make package-windows-light # Windows with minimal stdlib
make package-windows-full # Windows with full stdlib
make package-all # All platform/preset combinations
# Cleanup
make clean-all # Remove all builds and packages
make clean-dist # Remove only distribution packages
```
### Build Outputs
| Command | Output Directory | Executable |
|---------|------------------|------------|
| `make` / `make linux` | `build/` | `build/mcrogueface` |
| `make windows` | `build-windows/` | `build-windows/mcrogueface.exe` |
| `make windows-debug` | `build-windows-debug/` | `build-windows-debug/mcrogueface.exe` |
| `make package-*` | `dist/` | `.tar.gz` or `.zip` archives |
### Prerequisites
**Linux build:**
- CMake 3.14+
- GCC/G++ with C++17 support
- SFML 2.6 development libraries
- Libraries in `__lib/` directory (libpython3.14, libtcod, etc.)
**Windows cross-compilation:**
- MinGW-w64 (`x86_64-w64-mingw32-g++-posix`)
- Libraries in `__lib_windows/` directory
- Toolchain file: `cmake/toolchains/mingw-w64-x86_64.cmake`
### Library Dependencies
The build expects pre-built libraries in:
- `__lib/` - Linux shared libraries (libpython3.14.so, libsfml-*.so, libtcod.so)
- `__lib/Python/Lib/` - Python standard library source
- `__lib/Python/lib.linux-x86_64-3.14/` - Python extension modules (.so)
- `__lib_windows/` - Windows DLLs and libraries
### Manual CMake Build
If you need more control over the build:
```bash
# Linux
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
# Windows cross-compile
mkdir build-windows && cd build-windows
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
-DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
# Windows debug with console
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
-DCMAKE_BUILD_TYPE=Debug \
-DMCRF_WINDOWS_CONSOLE=ON
```
### Distribution Packaging
The packaging system creates self-contained archives with:
- Executable
- Required shared libraries
- Assets (sprites, fonts, audio)
- Python scripts
- Filtered Python stdlib (light or full variant)
**Light variant** (~25 MB): Core + gamedev + utility modules only
**Full variant** (~26 MB): Includes networking, async, debugging modules
Packaging tools:
- `tools/package.sh` - Main packaging orchestrator
- `tools/package_stdlib.py` - Creates filtered stdlib archives
- `tools/stdlib_modules.yaml` - Module categorization config
### Troubleshooting
**"No rule to make target 'linux'"**: You're in the wrong directory. Run `make` from project root.
**Library linking errors**: Ensure `__lib/` contains all required .so files. Check `CMakeLists.txt` for `link_directories(${CMAKE_SOURCE_DIR}/__lib)`.
**Windows build fails**: Verify MinGW-w64 is installed with posix thread model: `x86_64-w64-mingw32-g++-posix --version`
### Legacy Build Scripts
The following are deprecated but kept for reference:
- `build.sh` - Original Linux build script (use `make` instead)
- `GNUmakefile.legacy` - Old wrapper makefile (renamed to avoid conflicts)
## Emscripten / WebAssembly Builds
McRogueFace supports WebGL deployment via Emscripten with an SDL2+OpenGL ES 2 backend.
### Quick Start
```bash
source ~/emsdk/emsdk_env.sh # Activate Emscripten SDK
make wasm # Full game web build
make playground # REPL-focused web build
make serve # Serve at http://localhost:8080
```
### Build Variants
| Target | Output Directory | Purpose |
|--------|------------------|---------|
| `make wasm` | `build-emscripten/` | Full game with all scripts/assets |
| `make playground` | `build-playground/` | Minimal REPL build for interactive testing |
### Rendering Backend Selection
The build system supports three backends via CMake defines:
```bash
cmake .. # SFML (default desktop)
cmake -DMCRF_SDL2=ON .. # SDL2 + OpenGL ES 2 (Emscripten)
cmake -DMCRF_HEADLESS=ON .. # No graphics (CI/testing)
```
Emscripten builds automatically select SDL2 mode. Backend selection happens in `Common.h`:
```cpp
#ifdef MCRF_HEADLESS
#include "platform/HeadlessTypes.h"
#elif defined(MCRF_SDL2)
#include "platform/SDL2Types.h"
#else
#include <SFML/Graphics.hpp>
#endif
```
### SDL2 Backend Architecture
Game code remains unchanged because `SDL2Types.h` provides SFML-compatible type stubs:
- `sf::Vector2f`, `sf::Color`, `sf::RectangleShape`, etc. all work identically
- Rendering uses OpenGL ES 2 shaders internally
- Text rendering uses FreeType 2 (with outline support)
### Web Build Constraints
When developing features that must work in WebGL:
| Feature | Desktop (SFML) | Web (SDL2) | Notes |
|---------|----------------|------------|-------|
| Audio | ✅ Full | ❌ Stubbed | SoundBuffer/Sound/Music do nothing |
| ImGui console | ✅ Full | ❌ Disabled | Debug overlay unavailable |
| Dynamic assets | ✅ Filesystem | ❌ Preloaded | All assets bundled at build time |
| Threading | ✅ Full | ⚠️ Limited | Single-threaded JS execution |
| Input | ✅ Full | ✅ Full | SDL events translated to SFML enums |
### Development Workflow
For iterative Emscripten development:
```bash
source ~/emsdk/emsdk_env.sh
# Initial configure (once)
emmake cmake -B build-emscripten -DMCRF_SDL2=ON
# Rebuild after code changes (fast)
emmake make -C build-emscripten -j$(nproc)
# Test in browser
make serve
```
### Adding Cross-Platform Features
When adding rendering features:
1. **Check backend at compile time** if needed:
```cpp
#ifdef MCRF_SDL2
// SDL2-specific implementation
#else
// SFML implementation
#endif
```
2. **Prefer SFML API abstractions** - SDL2Types.h mirrors the SFML interface
3. **For new SDL2 features**: Add to `SDL2Renderer.cpp` with GLSL ES 2.0 shaders
4. **Test headless mode** for CI compatibility:
```bash
./mcrogueface --headless --exec ../tests/unit/my_test.py
```
### Asset Paths
Emscripten uses a virtual filesystem with preloaded assets:
- Use absolute paths: `/assets/sprite.png`, `/scripts/game.py`
- Assets defined in CMakeLists.txt `--preload-file` flags
- No runtime file loading from disk
### Playground Mode
The playground build (`-DMCRF_PLAYGROUND=ON`) provides:
- Python REPL widget in browser
- Scene reset capability via `reset_python_environment()`
- Minimal `src/scripts_playground/game.py` with idempotent initialization
- JavaScript interop via `Module.ccall()` to C functions
### Key Files
- `src/platform/SDL2Types.h` - SFML-compatible type stubs
- `src/platform/SDL2Renderer.cpp` - OpenGL ES 2 rendering implementation
- `src/platform/HeadlessTypes.h` - No-op types for headless mode
- `src/EmscriptenStubs.cpp` - JavaScript interop functions
- `emscripten_pre.js` - Browser quirk fixes
- `shell.html` - HTML template with REPL widget
## 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: `Scene("name")` object with `scene.on_key` for keyboard events
- Entity Creation: `Entity()` with position and sprite properties
- Grid Management: `Grid()` for tilemap rendering with cell callbacks
- Input Handling: `scene.on_key = handler` receives `(Key, InputState)` enums
- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()`
- Timers: `Timer("name", callback, interval)` object 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.14 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
### Test Suite Structure
The `tests/` directory contains the comprehensive test suite:
```
tests/
├── run_tests.py # Test runner - executes all tests with timeout
├── unit/ # Unit tests for individual components (105+ tests)
├── integration/ # Integration tests for system interactions
├── regression/ # Bug regression tests (issue_XX_*.py)
├── benchmarks/ # Performance benchmarks
├── demo/ # Feature demonstration system
│ ├── demo_main.py # Interactive demo runner
│ ├── screens/ # Per-feature demo screens
│ └── screenshots/ # Generated demo screenshots
└── notes/ # Analysis files and documentation
```
### Running Tests
```bash
# Run the full test suite (from tests/ directory)
cd tests && python3 run_tests.py
# Run a specific test
cd build && ./mcrogueface --headless --exec ../tests/unit/some_test.py
# Run the demo system interactively
cd build && ./mcrogueface ../tests/demo/demo_main.py
# Generate demo screenshots (headless)
cd build && ./mcrogueface --headless --exec ../tests/demo/demo_main.py
```
### Reading Tests as Examples
**IMPORTANT**: Before implementing a feature or fixing a bug, check existing tests for API usage examples:
- `tests/unit/` - Shows correct usage of individual mcrfpy classes and functions
- `tests/demo/screens/` - Complete working examples of UI components
- `tests/regression/` - Documents edge cases and bug scenarios
Example: To understand Animation API:
```bash
grep -r "Animation" tests/unit/
cat tests/demo/screens/animation_demo.py
```
### Writing Tests
**Always write tests when adding features or fixing bugs:**
1. **For new features**: Create `tests/unit/feature_name_test.py`
2. **For bug fixes**: Create `tests/regression/issue_XX_description_test.py`
3. **For demos**: Add to `tests/demo/screens/` if it showcases a feature
### Quick Testing Commands
```bash
# Test headless mode with inline Python
cd build
./mcrogueface --headless -c "import mcrfpy; print('Headless test')"
# Run specific test with output
./mcrogueface --headless --exec ../tests/unit/my_test.py 2>&1
```
## Common Development Tasks
### Compiling McRogueFace
See the [Build System](#build-system) section above for comprehensive build instructions.
```bash
# Quick reference (run from project root!)
make # Linux build
make windows # Windows cross-compile
make clean && make # Full rebuild
```
### 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"); scene = mcrfpy.Scene("test"); scene.activate()' > 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<std::shared_ptr<UIDrawable>>`
- `UIEntityCollection` wraps `std::list<std::shared_ptr<UIEntity>>`
- 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<UIFrame>();
// 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 (or SDL2 when building for wasm) 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 2023
- Iterator implementations require careful handling of C++/Python boundaries
## Testing Guidelines
### Test-Driven Development
- **Always write tests first**: Create tests in `./tests/` for all bugs and new features
- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix
- **Read existing tests**: Check `tests/unit/` and `tests/demo/screens/` for API examples before writing code
- **Close the loop**: Reproduce issue → change code → recompile → run test → verify
### Two Types of Tests
#### 1. Direct Execution Tests (No Game Loop)
For tests that only need class initialization or direct code execution:
```python
# tests/unit/my_feature_test.py
import mcrfpy
import sys
# Test code - runs immediately
frame = mcrfpy.Frame(pos=(0,0), size=(100,100))
assert frame.x == 0
assert frame.w == 100
print("PASS")
sys.exit(0)
```
#### 2. Game Loop Tests (Timer-Based)
For tests requiring rendering, screenshots, or elapsed time:
```python
# tests/unit/my_visual_test.py
import mcrfpy
from mcrfpy import automation
import sys
def run_test(runtime):
"""Timer callback - runs after game loop starts"""
automation.screenshot("test_result.png")
# Validate results...
print("PASS")
sys.exit(0)
test_scene = mcrfpy.Scene("test")
ui = test_scene.children
ui.append(mcrfpy.Frame(pos=(50,50), size=(100,100)))
mcrfpy.current_scene = test_scene
timer = mcrfpy.Timer("test", run_test, 100)
```
### Key Testing Principles
- **Timer callbacks are essential**: Screenshots only work after the render loop starts
- **Use automation API**: `automation.screenshot()`, `automation.click()` for visual testing
- **Exit properly**: Always call `sys.exit(0)` for PASS or `sys.exit(1)` for FAIL
- **Headless mode**: Use `--headless --exec` for CI/automated testing
- **Check examples first**: Read `tests/demo/screens/*.py` for correct API usage
### API Quick Reference (from tests)
```python
# Scene: create and activate a scene, or create another scene
mcrfpy.current_scene = mcrfpy.Scene("test")
demo_scene = mcrfpy.Scene("demo")
# Animation: (property, target_value, duration, easing)
# direct use of Animation object: deprecated
#anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut")
#anim.start(frame)
# preferred: create animations directly against the targeted object; use Enum of easing functions
frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT)
# Animation callbacks (#229) receive (target, property, final_value):
def on_anim_complete(target, prop, value):
print(f"{type(target).__name__}.{prop} reached {value}")
frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT, callback=on_anim_complete)
# Caption: use keyword arguments to avoid positional conflicts
cap = mcrfpy.Caption(text="Hello", pos=(100, 100))
# Grid center: uses pixel coordinates, not cell coordinates
grid = mcrfpy.Grid(grid_size=(15, 10), pos=(50, 50), size=(400, 300))
grid.center = (120, 80) # pixels: (cells * cell_size / 2)
# grid center defaults to the position that puts (0, 0) in the top left corner of the grid's visible area.
# set grid.center to focus on that position. To position the camera in tile coordinates, use grid.center_camera():
grid.center_camera((14.5, 8.5)) # offset of 0.5 tiles to point at the middle of the tile
# Keyboard handler (#184): receives Key and InputState enums
def on_key(key, action):
if key == mcrfpy.Key.Num1 and action == mcrfpy.InputState.PRESSED:
demo_scene.activate()
scene.on_key = on_key
# Mouse callbacks (#230):
# on_click receives (pos: Vector, button: MouseButton, action: InputState)
def on_click(pos, button, action):
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
print(f"Clicked at {pos.x}, {pos.y}")
frame.on_click = on_click
# Hover callbacks (#230) receive only position:
def on_enter(pos):
print(f"Entered at {pos.x}, {pos.y}")
frame.on_enter = on_enter # Also: on_exit, on_move
# Grid cell callbacks (#230):
# on_cell_click receives (cell_pos: Vector, button: MouseButton, action: InputState)
# on_cell_enter/on_cell_exit receive only (cell_pos: Vector)
```
## Development Best Practices
### Testing and Deployment
- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, tests shouldn't be included
- **Run full suite before commits**: `cd tests && python3 run_tests.py`
## Documentation Guidelines
### Documentation Macro System
**As of 2025-10-30, McRogueFace uses a macro-based documentation system** (`src/McRFPy_Doc.h`) that ensures consistent, complete docstrings across all Python bindings.
#### Include the Header
```cpp
#include "McRFPy_Doc.h"
```
#### Documenting Methods
For methods in PyMethodDef arrays, use `MCRF_METHOD`:
```cpp
{"method_name", (PyCFunction)Class::method, METH_VARARGS,
MCRF_METHOD(ClassName, method_name,
MCRF_SIG("(arg1: type, arg2: type)", "return_type"),
MCRF_DESC("Brief description of what the method does."),
MCRF_ARGS_START
MCRF_ARG("arg1", "Description of first argument")
MCRF_ARG("arg2", "Description of second argument")
MCRF_RETURNS("Description of return value")
MCRF_RAISES("ValueError", "Condition that raises this exception")
MCRF_NOTE("Important notes or caveats")
MCRF_LINK("docs/guide.md", "Related Documentation")
)},
```
#### Documenting Properties
For properties in PyGetSetDef arrays, use `MCRF_PROPERTY`:
```cpp
{"property_name", (getter)getter_func, (setter)setter_func,
MCRF_PROPERTY(property_name,
"Brief description of the property. "
"Additional details about valid values, side effects, etc."
), NULL},
```
#### Available Macros
- `MCRF_SIG(params, ret)` - Method signature
- `MCRF_DESC(text)` - Description paragraph
- `MCRF_ARGS_START` - Begin arguments section
- `MCRF_ARG(name, desc)` - Individual argument
- `MCRF_RETURNS(text)` - Return value description
- `MCRF_RAISES(exception, condition)` - Exception documentation
- `MCRF_NOTE(text)` - Important notes
- `MCRF_LINK(path, text)` - Reference to external documentation
#### Documentation Prose Guidelines
**Keep C++ docstrings concise** (1-2 sentences per section). For complex topics, use `MCRF_LINK` to reference external guides:
```cpp
MCRF_LINK("docs/animation-guide.md", "Animation System Tutorial")
```
**External documentation** (in `docs/`) can be verbose with examples, tutorials, and design rationale.
### Regenerating Documentation
After modifying C++ inline documentation with MCRF_* macros:
1. **Rebuild the project**: `make -j$(nproc)`
2. **Generate all documentation** (recommended - single command):
```bash
./tools/generate_all_docs.sh
```
This creates:
- `docs/api_reference_dynamic.html` - HTML API reference
- `docs/API_REFERENCE_DYNAMIC.md` - Markdown API reference
- `docs/mcrfpy.3` - Unix man page (section 3)
- `stubs/mcrfpy.pyi` - Type stubs for IDE support
3. **Or generate individually**:
```bash
# API docs (HTML + Markdown)
./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py
# Type stubs (manually-maintained with @overload support)
./build/mcrogueface --headless --exec tools/generate_stubs_v2.py
# Man page (requires pandoc)
./tools/generate_man_page.sh
```
**System Requirements:**
- `pandoc` must be installed for man page generation: `sudo apt-get install pandoc`
### Important Notes
- **Single source of truth**: Documentation lives in C++ source files via MCRF_* macros
- **McRogueFace as Python interpreter**: Documentation scripts MUST be run using McRogueFace itself, not system Python
- **Use --headless --exec**: For non-interactive documentation generation
- **Link transformation**: `MCRF_LINK` references are transformed to appropriate format (HTML, Markdown, etc.)
- **No manual dictionaries**: The old hardcoded documentation system has been removed
### Documentation Pipeline Architecture
1. **C++ Source** → MCRF_* macros in PyMethodDef/PyGetSetDef arrays
2. **Compilation** → Macros expand to complete docstrings embedded in module
3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract
4. **Generation** → HTML/Markdown/Stub files created with transformed links
5. **No drift**: Impossible for docs and code to disagree - they're the same file!
The macro system ensures complete, consistent documentation across all Python bindings.
### Adding Documentation for New Python Types
When adding a new Python class/type to the engine, follow these steps to ensure it's properly documented:
#### 1. Class Docstring (tp_doc)
In the `PyTypeObject` definition (usually in the header file), set `tp_doc` with a comprehensive docstring:
```cpp
// In PyMyClass.h
.tp_doc = PyDoc_STR(
"MyClass(arg1: type, arg2: type)\n\n"
"Brief description of what this class does.\n\n"
"Args:\n"
" arg1: Description of first argument.\n"
" arg2: Description of second argument.\n\n"
"Properties:\n"
" prop1 (type, read-only): Description of property.\n"
" prop2 (type): Description of writable property.\n\n"
"Example:\n"
" obj = mcrfpy.MyClass('example', 42)\n"
" print(obj.prop1)\n"
),
```
#### 2. Method Documentation (PyMethodDef)
For each method in the `methods[]` array, use the MCRF_* macros:
```cpp
// In PyMyClass.cpp
PyMethodDef PyMyClass::methods[] = {
{"do_something", (PyCFunction)do_something, METH_VARARGS,
MCRF_METHOD(MyClass, do_something,
MCRF_SIG("(value: int)", "bool"),
MCRF_DESC("Does something with the value."),
MCRF_ARGS_START
MCRF_ARG("value", "The value to process")
MCRF_RETURNS("True if successful, False otherwise")
)},
{NULL} // Sentinel
};
```
#### 3. Property Documentation (PyGetSetDef)
For each property in the `getsetters[]` array, include a docstring:
```cpp
// In PyMyClass.cpp
PyGetSetDef PyMyClass::getsetters[] = {
{"property_name", (getter)get_property, (setter)set_property,
"Property description. Include (type, read-only) if not writable.",
NULL},
{NULL} // Sentinel
};
```
**Important for read-only properties:** Include "read-only" in the docstring so the doc generator detects it:
```cpp
{"name", (getter)get_name, NULL, // NULL setter = read-only
"Object name (str, read-only). Unique identifier.",
NULL},
```
#### 4. Register Type in Module
Ensure the type is properly registered in `McRFPy_API.cpp` and its methods/getsetters are assigned:
```cpp
// Set methods and getsetters before PyType_Ready
mcrfpydef::PyMyClassType.tp_methods = PyMyClass::methods;
mcrfpydef::PyMyClassType.tp_getset = PyMyClass::getsetters;
// Then call PyType_Ready and add to module
```
#### 5. Regenerate Documentation
After adding the new type, regenerate all docs:
```bash
make -j4 # Rebuild with new documentation
cd build
./mcrogueface --headless --exec ../tools/generate_dynamic_docs.py
cp docs/API_REFERENCE_DYNAMIC.md ../docs/
cp docs/api_reference_dynamic.html ../docs/
```
#### 6. Update Type Stubs (Optional)
For IDE support, update `stubs/mcrfpy.pyi` with the new class:
```python
class MyClass:
"""Brief description."""
def __init__(self, arg1: str, arg2: int) -> None: ...
@property
def prop1(self) -> str: ...
def do_something(self, value: int) -> bool: ...
```
### Documentation Extraction Details
The doc generator (`tools/generate_dynamic_docs.py`) uses Python introspection:
- **Classes**: Detected via `inspect.isclass()`, docstring from `cls.__doc__`
- **Methods**: Detected via `callable()` check on class attributes
- **Properties**: Detected via `types.GetSetDescriptorType` (C++ extension) or `property` (Python)
- **Read-only detection**: Checks if "read-only" appears in property docstring
If documentation isn't appearing, verify:
1. The type is exported to the `mcrfpy` module
2. Methods/getsetters arrays are properly assigned before `PyType_Ready()`
3. Docstrings don't contain null bytes or invalid UTF-8
---
- Close issues automatically in gitea by adding to the commit message "closes #X", where X is the issue number. This associates the issue closure with the specific commit, so granular commits are preferred. You should only use the MCP tool to close issues directly when discovering that the issue is already complete; when committing changes, always such "closes" (or the opposite, "reopens") references to related issues. If on a feature branch, the issue will be referenced by the commit, and when merged to master, the issue will be actually closed (or reopened).

View file

@ -8,361 +8,49 @@ project(McRogueFace)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# Headless build option (no SFML, no graphics - for server/testing/Emscripten prep)
option(MCRF_HEADLESS "Build without graphics dependencies (SFML, ImGui)" OFF)
# SDL2 backend option (SDL2 + OpenGL ES 2 - for Emscripten/WebGL, Android, cross-platform)
option(MCRF_SDL2 "Build with SDL2+OpenGL ES 2 backend instead of SFML" OFF)
# Playground mode - minimal scripts for web playground (REPL-focused)
option(MCRF_PLAYGROUND "Build with minimal playground scripts instead of full game" OFF)
# Game shell mode - fullscreen canvas, no REPL chrome (for itch.io / standalone web games)
option(MCRF_GAME_SHELL "Use minimal game-only HTML shell (no REPL)" OFF)
# Emscripten builds: use SDL2 if specified, otherwise fall back to headless
if(EMSCRIPTEN)
if(MCRF_SDL2)
message(STATUS "Emscripten detected - using SDL2 backend")
set(MCRF_HEADLESS OFF)
else()
set(MCRF_HEADLESS ON)
message(STATUS "Emscripten detected - forcing HEADLESS mode (use -DMCRF_SDL2=ON for graphics)")
endif()
endif()
if(MCRF_SDL2)
message(STATUS "Building with SDL2 backend - SDL2+OpenGL ES 2")
endif()
if(MCRF_PLAYGROUND)
message(STATUS "Building in PLAYGROUND mode - minimal scripts for web REPL")
endif()
if(MCRF_HEADLESS)
message(STATUS "Building in HEADLESS mode - no SFML/ImGui dependencies")
endif()
# Detect cross-compilation for Windows (MinGW)
if(CMAKE_CROSSCOMPILING AND WIN32)
set(MCRF_CROSS_WINDOWS TRUE)
message(STATUS "Cross-compiling for Windows using MinGW")
endif()
# Add include directories
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux)
include_directories(${CMAKE_SOURCE_DIR}/deps)
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/deps/libtcod)
include_directories(${CMAKE_SOURCE_DIR}/src)
include_directories(${CMAKE_SOURCE_DIR}/src/3d)
include_directories(${CMAKE_SOURCE_DIR}/src/platform)
include_directories(${CMAKE_SOURCE_DIR}/src/tiled)
include_directories(${CMAKE_SOURCE_DIR}/src/ldtk)
include_directories(${CMAKE_SOURCE_DIR}/src/audio)
include_directories(${CMAKE_SOURCE_DIR}/modules/RapidXML)
include_directories(${CMAKE_SOURCE_DIR}/modules/json/single_include)
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux/Python-3.11.1)
include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
# Python includes: use different paths for Windows vs Linux vs Emscripten
if(EMSCRIPTEN)
# Emscripten build: use Python headers compiled for wasm32-emscripten
# The pyconfig.h from cross-build has correct LONG_BIT and other settings
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
# Force-include wasm pyconfig.h BEFORE anything else to set correct platform defines
add_compile_options(-include ${PYTHON_WASM_BUILD}/pyconfig.h)
# Override LONG_BIT - Emscripten's limits.h incorrectly defines it as 64 for wasm32
add_compile_definitions(LONG_BIT=32)
# Include wasm build directory FIRST so its pyconfig.h is found by #include "pyconfig.h"
include_directories(BEFORE ${PYTHON_WASM_BUILD})
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/Include)
message(STATUS "Using Emscripten Python from: ${PYTHON_WASM_BUILD}")
elseif(MCRF_CROSS_WINDOWS)
# Windows cross-compilation: use cpython headers with PC/pyconfig.h
# Problem: Python.h uses #include "pyconfig.h" which finds Include/pyconfig.h (Linux) first
# Solution: Use -include to force Windows pyconfig.h to be included first
# This defines MS_WINDOWS before Python.h is processed, ensuring correct struct layouts
add_compile_options(-include ${CMAKE_SOURCE_DIR}/deps/cpython/PC/pyconfig.h)
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/Include)
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/PC) # For other Windows-specific headers
# Also include SFML and libtcod Windows headers
include_directories(${CMAKE_SOURCE_DIR}/__lib_windows/sfml/include)
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/include)
else()
# Native builds (Linux/Windows): use existing Python setup
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
endif()
# ImGui and ImGui-SFML include directories (not needed in headless or SDL2 mode)
# SDL2 builds will use ImGui with SDL2 backend later; for now, no ImGui
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
include_directories(${CMAKE_SOURCE_DIR}/modules/imgui)
include_directories(${CMAKE_SOURCE_DIR}/modules/imgui-sfml)
# ImGui source files
set(IMGUI_SOURCES
${CMAKE_SOURCE_DIR}/modules/imgui/imgui.cpp
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_draw.cpp
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_tables.cpp
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_widgets.cpp
${CMAKE_SOURCE_DIR}/modules/imgui-sfml/imgui-SFML.cpp
)
endif()
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
# Collect all the source files
file(GLOB_RECURSE SOURCES "src/*.cpp")
# Add ImGui sources to the build (only if using SFML)
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
list(APPEND SOURCES ${IMGUI_SOURCES})
# Add GLAD for OpenGL function loading (needed for 3D rendering on SFML)
list(APPEND SOURCES "${CMAKE_SOURCE_DIR}/src/3d/glad.c")
endif()
# Find OpenGL (required by ImGui-SFML) - not needed in headless mode
# SDL2 builds handle OpenGL ES 2 differently (via SDL2 or Emscripten)
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
if(MCRF_CROSS_WINDOWS)
# For cross-compilation, OpenGL is provided by MinGW
set(OPENGL_LIBRARIES opengl32)
else()
find_package(OpenGL REQUIRED)
set(OPENGL_LIBRARIES OpenGL::GL)
endif()
endif()
# Create a list of libraries to link against
if(EMSCRIPTEN)
# Emscripten build: link against WASM-compiled Python and libtcod
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
set(PYTHON_WASM_PREFIX "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/prefix")
set(LIBTCOD_WASM_BUILD "${CMAKE_SOURCE_DIR}/modules/libtcod-headless/build-emscripten")
# Collect HACL crypto object files (not included in libpython3.14.a)
file(GLOB PYTHON_HACL_OBJECTS "${PYTHON_WASM_BUILD}/Modules/_hacl/*.o")
set(LINK_LIBS
${PYTHON_WASM_BUILD}/libpython3.14.a
${PYTHON_HACL_OBJECTS}
${PYTHON_WASM_BUILD}/Modules/expat/libexpat.a
${PYTHON_WASM_PREFIX}/lib/libmpdec.a
${PYTHON_WASM_PREFIX}/lib/libffi.a
${LIBTCOD_WASM_BUILD}/libtcod.a
${LIBTCOD_WASM_BUILD}/_deps/lodepng-c-build/liblodepng-c.a
${LIBTCOD_WASM_BUILD}/_deps/utf8proc-build/libutf8proc.a)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux) # Use Linux platform stubs for now
# For SDL2 builds, add stb headers for image/font loading
if(MCRF_SDL2)
include_directories(${CMAKE_SOURCE_DIR}/deps/stb)
endif()
message(STATUS "Linking Emscripten Python: ${PYTHON_WASM_BUILD}/libpython3.14.a")
message(STATUS "Linking Emscripten libtcod: ${LIBTCOD_WASM_BUILD}/libtcod.a")
elseif(MCRF_SDL2)
# SDL2 build (non-Emscripten): link against SDL2 and system libraries
# Note: For desktop SDL2 builds in the future
find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED)
set(LINK_LIBS
SDL2::SDL2
OpenGL::GL
tcod
python3.14
m dl util pthread)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
include_directories(${CMAKE_SOURCE_DIR}/deps/stb) # stb_image.h, stb_truetype.h
link_directories(${CMAKE_SOURCE_DIR}/__lib)
message(STATUS "Building with SDL2 backend (desktop)")
elseif(MCRF_HEADLESS)
# Headless build: no SFML, no OpenGL
if(WIN32 OR MCRF_CROSS_WINDOWS)
set(LINK_LIBS
libtcod
python314)
if(MCRF_CROSS_WINDOWS)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/lib)
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows)
else()
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
link_directories(${CMAKE_SOURCE_DIR}/__lib)
endif()
else()
# Unix/Linux headless build
set(LINK_LIBS
tcod
python3.14
m dl util pthread)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
link_directories(${CMAKE_SOURCE_DIR}/__lib)
endif()
elseif(MCRF_CROSS_WINDOWS)
# MinGW cross-compilation: use full library names
set(LINK_LIBS
sfml-graphics
sfml-window
sfml-system
sfml-audio
libtcod
python314
${OPENGL_LIBRARIES})
# Add Windows system libraries needed by SFML and MinGW
list(APPEND LINK_LIBS
winmm # Windows multimedia (for audio)
gdi32 # Graphics Device Interface
ws2_32 # Winsock (networking, used by some deps)
ole32 # OLE support
oleaut32 # OLE automation
uuid # UUID library
comdlg32 # Common dialogs
imm32 # Input Method Manager
version # Version info
)
set(LINK_LIBS
m
dl
util
pthread
python3.12
sfml-graphics
sfml-window
sfml-system
sfml-audio
tcod)
# On Windows, add any additional libs and include directories
if(WIN32)
# Add the necessary Windows-specific libraries and include directories
# include_directories(path_to_additional_includes)
# link_directories(path_to_additional_libs)
# list(APPEND LINK_LIBS additional_windows_libs)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
# Link directories for cross-compiled Windows libs
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/sfml/lib)
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/lib)
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows)
elseif(WIN32)
# Native Windows build (MSVC)
set(LINK_LIBS
sfml-graphics
sfml-window
sfml-system
sfml-audio
tcod
python314
${OPENGL_LIBRARIES})
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
link_directories(${CMAKE_SOURCE_DIR}/__lib)
else()
# Unix/Linux build
set(LINK_LIBS
sfml-graphics
sfml-window
sfml-system
sfml-audio
tcod
python3.14
m dl util pthread
${OPENGL_LIBRARIES})
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
link_directories(${CMAKE_SOURCE_DIR}/__lib)
endif()
# Add the directory where the linker should look for the libraries
#link_directories(${CMAKE_SOURCE_DIR}/deps_linux)
link_directories(${CMAKE_SOURCE_DIR}/lib)
# Define the executable target before linking libraries
add_executable(mcrogueface ${SOURCES})
# Define NO_SDL for libtcod-headless headers (excludes SDL-dependent code)
# We ALWAYS need this because libtcod headers expect SDL3, not SDL2
# Our SDL2 backend is separate from libtcod's SDL3 renderer
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
# Define MCRF_HEADLESS for headless builds (excludes SFML/ImGui code)
if(MCRF_HEADLESS)
target_compile_definitions(mcrogueface PRIVATE MCRF_HEADLESS)
endif()
# Define MCRF_SDL2 for SDL2 builds (uses SDL2+OpenGL ES 2 instead of SFML)
if(MCRF_SDL2)
target_compile_definitions(mcrogueface PRIVATE MCRF_SDL2)
endif()
# Asset/script directories for WASM preloading (game projects override these)
set(MCRF_ASSETS_DIR "${CMAKE_SOURCE_DIR}/assets" CACHE PATH "Assets directory for WASM preloading")
set(MCRF_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/src/scripts" CACHE PATH "Scripts directory for WASM preloading")
set(MCRF_SCRIPTS_PLAYGROUND_DIR "${CMAKE_SOURCE_DIR}/src/scripts_playground" CACHE PATH "Playground scripts for WASM")
# Emscripten-specific link options (use ports for zlib, bzip2, sqlite3)
if(EMSCRIPTEN)
# Base Emscripten options
set(EMSCRIPTEN_LINK_OPTIONS
-sUSE_ZLIB=1
-sUSE_BZIP2=1
-sUSE_SQLITE3=1
-sALLOW_MEMORY_GROWTH=1
-sSTACK_SIZE=2097152
-sEXPORTED_RUNTIME_METHODS=ccall,cwrap,FS
-sEXPORTED_FUNCTIONS=_main,_run_python_string,_run_python_string_with_output,_reset_python_environment,_notify_canvas_resize,_sync_storage
-lidbfs.js
-sASSERTIONS=2
-sSTACK_OVERFLOW_CHECK=2
-fexceptions
-sNO_DISABLE_EXCEPTION_CATCHING
# Disable features that require dynamic linking support
-sERROR_ON_UNDEFINED_SYMBOLS=0
-sALLOW_UNIMPLEMENTED_SYSCALLS=1
# Preload Python stdlib into virtual filesystem at /lib/python3.14
--preload-file=${CMAKE_SOURCE_DIR}/wasm_stdlib/lib@/lib
# Preload game scripts into /scripts (use playground scripts if MCRF_PLAYGROUND is set)
--preload-file=$<IF:$<BOOL:${MCRF_PLAYGROUND}>,${MCRF_SCRIPTS_PLAYGROUND_DIR},${MCRF_SCRIPTS_DIR}>@/scripts
# Preload assets
--preload-file=${MCRF_ASSETS_DIR}@/assets
# Use custom HTML shell - game shell (fullscreen) or playground shell (REPL)
--shell-file=${CMAKE_SOURCE_DIR}/src/$<IF:$<BOOL:${MCRF_GAME_SHELL}>,shell_game.html,shell.html>
# Pre-JS to fix browser zoom causing undefined values in events
--pre-js=${CMAKE_SOURCE_DIR}/src/emscripten_pre.js
)
# Add SDL2 options if using SDL2 backend
if(MCRF_SDL2)
list(APPEND EMSCRIPTEN_LINK_OPTIONS
-sUSE_SDL=2
-sUSE_SDL_MIXER=2
-sFULL_ES2=1
-sMIN_WEBGL_VERSION=2
-sMAX_WEBGL_VERSION=2
-sUSE_FREETYPE=1
)
# SDL2, SDL2_mixer, and FreeType flags are also needed at compile time for headers
target_compile_options(mcrogueface PRIVATE
-sUSE_SDL=2
-sUSE_SDL_MIXER=2
-sUSE_FREETYPE=1
)
message(STATUS "Emscripten SDL2 options enabled: -sUSE_SDL=2 -sUSE_SDL_MIXER=2 -sFULL_ES2=1 -sUSE_FREETYPE=1")
endif()
target_link_options(mcrogueface PRIVATE ${EMSCRIPTEN_LINK_OPTIONS})
# Output as HTML to use the shell file
set_target_properties(mcrogueface PROPERTIES SUFFIX ".html")
# Set Python home for the embedded interpreter
target_compile_definitions(mcrogueface PRIVATE
MCRF_WASM_PYTHON_HOME="/lib/python3.14"
)
endif()
# On Windows, define Py_ENABLE_SHARED for proper Python DLL imports
# Py_PYCONFIG_H prevents Include/pyconfig.h (Linux config) from being included
# (PC/pyconfig.h already defines HAVE_DECLSPEC_DLL and MS_WINDOWS)
if(WIN32 OR MCRF_CROSS_WINDOWS)
target_compile_definitions(mcrogueface PRIVATE Py_ENABLE_SHARED Py_PYCONFIG_H)
endif()
# On Windows, set subsystem to WINDOWS to hide console (release builds only)
# Use -DMCRF_WINDOWS_CONSOLE=ON for debug builds with console output
option(MCRF_WINDOWS_CONSOLE "Keep console window visible for debugging" OFF)
if(WIN32 AND NOT MCRF_CROSS_WINDOWS)
# MSVC-specific flags
if(NOT MCRF_WINDOWS_CONSOLE)
set_target_properties(mcrogueface PROPERTIES
WIN32_EXECUTABLE TRUE
LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup")
endif()
elseif(MCRF_CROSS_WINDOWS)
# MinGW cross-compilation
if(NOT MCRF_WINDOWS_CONSOLE)
# Release: use -mwindows to hide console
set_target_properties(mcrogueface PROPERTIES
WIN32_EXECUTABLE TRUE
LINK_FLAGS "-mwindows")
else()
# Debug: keep console for stdout/stderr output
message(STATUS "Windows console enabled for debugging")
endif()
endif()
# Now the linker will find the libraries in the specified directory
target_link_libraries(mcrogueface ${LINK_LIBS})
@ -379,44 +67,9 @@ add_custom_command(TARGET mcrogueface POST_BUILD
# Copy Python standard library to build directory
add_custom_command(TARGET mcrogueface POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
${CMAKE_SOURCE_DIR}/lib $<TARGET_FILE_DIR:mcrogueface>/lib)
# On Windows, copy DLLs to executable directory
if(MCRF_CROSS_WINDOWS)
# Cross-compilation: copy DLLs from __lib_windows
add_custom_command(TARGET mcrogueface POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/__lib_windows/sfml/bin $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/bin $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/__lib_windows/python314.dll $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/__lib_windows/python3.dll $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140.dll $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140_1.dll $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E copy
/usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E echo "Copied Windows DLLs to executable directory")
# Copy Python standard library zip
add_custom_command(TARGET mcrogueface POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/__lib_windows/python314.zip $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E echo "Copied Python stdlib")
elseif(WIN32)
# Native Windows build: copy DLLs from __lib
add_custom_command(TARGET mcrogueface POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E echo "Copied DLLs to executable directory")
endif()
# rpath for including shared libraries (Linux/Unix only)
if(NOT WIN32)
set_target_properties(mcrogueface PROPERTIES
INSTALL_RPATH "$ORIGIN/./lib")
endif()
# rpath for including shared libraries
set_target_properties(mcrogueface PROPERTIES
INSTALL_RPATH "./lib")

242
README.md
View file

@ -1,226 +1,30 @@
# McRogueFace
# McRogueFace - 2D Game Engine
An experimental prototype game engine built for my own use in 7DRL 2023.
*Blame my wife for the name*
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
## Tenets:
* Core roguelike logic from libtcod: field of view, pathfinding
* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera
* Simple GUI element system allows keyboard and mouse input, composition
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship"
* C++ first, Python close behind.
* Entity-Component system based on David Churchill's Memorial University COMP4300 course lectures available on Youtube.
* Graphics, particles and shaders provided by SFML.
* Pathfinding, noise generation, and other Roguelike goodness provided by TCOD.
📖 **[Full Documentation & Tutorials](https://mcrogueface.github.io/)** - Quickstart guide, API reference, and cookbook
## Why?
## Quick Start
I did the r/RoguelikeDev TCOD tutorial in Python. I loved it, but I did not want to be limited to ASCII. I want to be able to draw pixels on top of my tiles (like lines or circles) and eventually incorporate even more polish.
**Download** the [latest release](https://github.com/jmccardle/McRogueFace/releases/latest):
- **Windows**: `McRogueFace-*-Win.zip`
- **Linux**: `McRogueFace-*-Linux.tar.bz2`
## To-do
Extract and run `mcrogueface` (or `mcrogueface.exe` on Windows) to see the demo game.
### Your First Game
Create `scripts/game.py` (or edit the existing one):
```python
import mcrfpy
# Create and activate a scene
scene = mcrfpy.Scene("game")
scene.activate()
# Load a sprite sheet
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create a tile grid
grid = mcrfpy.Grid(grid_size=(20, 15), texture=texture, pos=(50, 50), size=(640, 480))
grid.zoom = 2.0
scene.children.append(grid)
# Add a player entity
player = mcrfpy.Entity(pos=(10, 7), texture=texture, sprite_index=84)
grid.entities.append(player)
# Handle keyboard input
def on_key(key, state):
if state != "start":
return
x, y = int(player.x), int(player.y)
if key == "W": y -= 1
elif key == "S": y += 1
elif key == "A": x -= 1
elif key == "D": x += 1
player.x, player.y = x, y
scene.on_key = on_key
```
Run `mcrogueface` and you have a movable character!
### Visual Framework
- **Sprite**: Single image or sprite from a shared sheet
- **Caption**: Text rendering with fonts
- **Frame**: Container rectangle for composing UIs
- **Grid**: 2D tile array with zoom and camera control
- **Entity**: Grid-based game object with sprite and pathfinding
- **Animation**: Interpolate any property over time with easing
## Building from Source
For most users, pre-built releases are available. If you need to build from source:
### Quick Build (with pre-built dependencies)
Download `build_deps.tar.gz` from the releases page, then:
```bash
git clone <repository-url> McRogueFace
cd McRogueFace
tar -xzf /path/to/build_deps.tar.gz
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
```
### Full Build (compiling all dependencies)
```bash
git clone --recursive <repository-url> McRogueFace
cd McRogueFace
# See BUILD_FROM_SOURCE.md for complete instructions
```
**[BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md)** - Complete build guide including:
- System dependency installation
- Compiling SFML, Python, and libtcod-headless from source
- Creating `build_deps` archives for distribution
- Troubleshooting common build issues
### System Requirements
- **Linux**: Debian/Ubuntu tested; other distros should work
- **Windows**: Supported (see build guide for details)
- **macOS**: Untested
## Example: Main Menu with Buttons
```python
import mcrfpy
# Create a scene
scene = mcrfpy.Scene("menu")
# Add a background frame
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
fill_color=mcrfpy.Color(20, 20, 40))
scene.children.append(bg)
# Add a title
title = mcrfpy.Caption(pos=(312, 100), text="My Roguelike",
fill_color=mcrfpy.Color(255, 255, 100))
title.font_size = 48
scene.children.append(title)
# Create a button
button = mcrfpy.Frame(pos=(362, 300), size=(300, 80),
fill_color=mcrfpy.Color(50, 150, 50))
button_text = mcrfpy.Caption(pos=(90, 25), text="Start Game")
button.children.append(button_text)
def on_click(x, y, btn):
print("Game starting!")
button.on_click = on_click
scene.children.append(button)
scene.activate()
```
## Documentation
### 📚 Developer Documentation
For comprehensive documentation about systems, architecture, and development workflows:
**[Project Wiki](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki)**
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 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
- C++17 compiler (GCC 7+ or Clang 5+)
- CMake 3.14+
- Python 3.14 (embedded)
- SFML 2.6
- Linux or Windows (macOS untested)
See [BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md) for detailed compilation instructions.
## Project Structure
```
McRogueFace/
├── assets/ # Sprites, fonts, audio
├── build/ # Build output: this is what you distribute
│ ├── assets/ # (copied from assets/)
│ ├── scripts/ # (copied from src/scripts/)
│ └── lib/ # Python stdlib and extension modules
├── docs/ # Generated HTML, markdown API docs
├── src/ # C++ engine source
│ └── scripts/ # Python game scripts
├── stubs/ # .pyi type stubs for IDE integration
├── tests/ # Automated test suite
└── tools/ # Documentation generation scripts
```
If you are building McRogueFace to implement game logic or scene configuration in C++, you'll have to compile the project.
If you are writing a game in Python using McRogueFace, you only need to rename and zip/distribute the `build` directory.
## Philosophy
- **C++ every frame, Python every tick**: All rendering data is handled in C++. Structure your UI and program animations in Python, and they are rendered without Python. All game logic can be written in Python.
- **No Compiling Required; Zip And Ship**: Implement your game objects with Python, zip up McRogueFace with your "game.py" to ship
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
- **Hands-Off Testing**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
## Contributing
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.
### 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
This project is licensed under the MIT License - see LICENSE file for details.
## Acknowledgments
- Developed for 7-Day Roguelike 2023, 2024, 2025, 2026 - here's to many more
- Built with [SFML](https://www.sfml-dev.org/), [libtcod](https://github.com/libtcod/libtcod), and Python
- Inspired by David Churchill's COMP4300 game engine lectures
* ✅ Initial Commit
* ✅ Integrate scene, action, entity, component system from COMP4300 engine
* ✅ Windows / Visual Studio project
* ✅ Draw Sprites
* ✅ Play Sounds
* ✅ Draw UI, spawn entity from Python code
* ❌ Python AI for entities (NPCs on set paths, enemies towards player)
* ✅ Walking / Collision
* ❌ "Boards" (stairs / doors / walk off edge of screen)
* ❌ Cutscenes - interrupt normal controls, text scroll, character portraits
* ❌ Mouse integration - tooltips, zoom, click to select targets, cursors

View file

@ -1,120 +0,0 @@
# McRogueFace - Development Roadmap
**Version**: 0.2.6-prerelease | **Era**: McRogueFace (2D roguelikes)
For detailed architecture, philosophy, and decision framework, see the [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction) wiki page. For per-issue tracking, see the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap).
---
## What Has Shipped
**Alpha 0.1** (2024) -- First complete release. Milestone: all datatypes behaving.
**0.2 series** (Jan-Feb 2026) -- Weekly updates to GitHub. Key additions:
- 3D/Voxel pipeline (experimental): Viewport3D, Camera3D, Entity3D, VoxelGrid with greedy meshing and serialization
- Procedural generation: HeightMap, BSP, NoiseSource, DiscreteMap
- Tiled and LDtk import with Wang tile / AutoRule resolution
- Emscripten/SDL2 backend for WebAssembly deployment
- Animation callbacks, mouse event system, grid cell callbacks
- Multi-layer grid system with chunk-based rendering and dirty-flag caching
- Documentation macro system with auto-generated API docs, man pages, and type stubs
- Windows cross-compilation, mobile-ish WASM support, SDL2_mixer audio
**Proving grounds**: Crypt of Sokoban (7DRL 2025) was the first complete game. 7DRL 2026 is the current target.
---
## Current Focus: 7DRL 2026
**Dates**: February 28 -- March 8, 2026
Engine preparation is complete. All 2D systems are production-ready. The jam will expose remaining rough edges in the workflow of building a complete game on McRogueFace.
Open prep items:
- **#248** -- Crypt of Sokoban Remaster (game content for the jam)
---
## Post-7DRL: The Road to 1.0
After 7DRL, the priority shifts from feature development to **API stability**. 1.0 means the Python API is frozen: documented, stable, and not going to break.
### API Freeze Process
1. Catalog every public Python class, method, and property
2. Identify anything that should change before committing (naming, signatures, defaults)
3. Make breaking changes in a single coordinated pass
4. Document the stable API as the contract
5. Experimental modules (3D/Voxel) get an explicit `experimental` label and are exempt from the freeze
### Post-Jam Priorities
- Fix pain points discovered during actual 7DRL game development
- Progress on the r/roguelikedev tutorial series (#167)
- API consistency audit and freeze
- Better pip/virtualenv integration for adding packages to McRogueFace's embedded interpreter
---
## Engine Eras
One engine, accumulating capabilities. Nothing is thrown away.
| Era | Focus | Status |
|-----|-------|--------|
| **McRogueFace** | 2D tiles, roguelike systems, procgen | Active -- approaching 1.0 |
| **McVectorFace** | Sparse grids, vector graphics, physics | Planned |
| **McVoxelFace** | Voxel terrain, 3D gameplay | Proof-of-concept complete |
---
## 3D/Voxel Pipeline (Experimental)
The 3D pipeline is proof-of-concept scouting for the McVoxelFace era. It works and is tested but is explicitly **not** part of the 1.0 API freeze.
**What exists**: Viewport3D, Camera3D, Entity3D, MeshLayer, Model3D (glTF), Billboard, Shader3D, VoxelGrid with greedy meshing, face culling, RLE serialization, and navigation projection.
**Known gaps**: Some Entity3D collection methods, animation stubs, shader pipeline incomplete.
**Maturity track**: These modules will mature on their own timeline, driven by games that need 3D. They won't block 2D stability.
---
## Future Directions
These are ideas on the horizon -- not yet concrete enough for issues, but worth capturing.
### McRogueFace Lite
A spiritual port to MicroPython targeting the PicoCalc and other microcontrollers. Could provide a migration path to retro ROMs or compete in the Pico-8 space. The core idea: strip McRogueFace down to its essential tile/entity/scene model and run it on constrained hardware.
### McVectorFace Era
The next major capability expansion. Sparse grid layers, a polygon/shape rendering class, and eventually physics integration. This would support games that aren't purely tile-based -- top-down action, strategy maps with irregular regions, or hybrid tile+vector visuals. See the [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction) wiki for the full era model.
### McRogueFace Standard Library
A built-in collection of reusable GUI widgets and game UI patterns: menus, dialogs, inventory screens, stat bars, text input fields, scrollable lists. These would ship with the engine as importable Python modules, saving every game from reimplementing the same UI primitives. Think of it as `mcrfpy.widgets` -- batteries included.
### Pip/Virtualenv Integration
Rather than inverting the architecture to make McRogueFace a pip-installable package, the nearer-term goal is better integration in the other direction: making it easy to install and use third-party Python packages within McRogueFace's embedded interpreter. This could mean virtualenv awareness, a `mcrf install` command, or bundling pip itself.
---
## Open Issues by Area
30 open issues across the tracker. Key groupings:
- **Multi-tile entities** (#233-#237) -- Oversized sprites, composite entities, origin offsets
- **Grid enhancements** (#152, #149, #67) -- Sparse layers, refactoring, infinite worlds
- **Performance** (#117, #124, #145) -- Memory pools, grid point animation, texture reuse
- **LLM agent testbed** (#154, #156, #55) -- Multi-agent simulation, turn-based orchestration
- **Platform/distribution** (#70, #54, #62, #53) -- Packaging, Jupyter, multiple windows, input methods
- **WASM tooling** (#238-#240) -- Debug infrastructure, automated browser testing, troubleshooting docs
- **Rendering** (#107, #218) -- Particle system, Color/Vector animation targets
See the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current status.
---
## Resources
- **Issue Tracker**: [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues)
- **Wiki**: [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction), [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap), [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow)
- **Build Guide**: See `CLAUDE.md` for build instructions
- **Tutorial**: `roguelike_tutorial/` for implementation examples

BIN
assets/Sprite-0001.ase Normal file

Binary file not shown.

BIN
assets/Sprite-0001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
assets/alives_other.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
assets/boom.wav Normal file

Binary file not shown.

BIN
assets/custom_player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/gamescale_decor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/temp_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 MiB

BIN
assets/terrain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/terrain_alpha.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
assets/test_portraits.ase Normal file

Binary file not shown.

BIN
assets/test_portraits.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,54 +0,0 @@
#!/bin/bash
# Build script for McRogueFace - compiles everything into ./build directory
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}McRogueFace Build Script${NC}"
echo "========================="
# Create build directory if it doesn't exist
if [ ! -d "build" ]; then
echo -e "${YELLOW}Creating build directory...${NC}"
mkdir build
fi
# Change to build directory
cd build
# Run CMake to generate build files
echo -e "${YELLOW}Running CMake...${NC}"
cmake .. -DCMAKE_BUILD_TYPE=Release
# Check if CMake succeeded
if [ $? -ne 0 ]; then
echo -e "${RED}CMake configuration failed!${NC}"
exit 1
fi
# Run make with parallel jobs
echo -e "${YELLOW}Building with make...${NC}"
make -j$(nproc)
# Check if make succeeded
if [ $? -ne 0 ]; then
echo -e "${RED}Build failed!${NC}"
exit 1
fi
echo -e "${GREEN}Build completed successfully!${NC}"
echo ""
echo "The build directory contains:"
ls -la
echo ""
echo -e "${GREEN}To run McRogueFace:${NC}"
echo " cd build"
echo " ./mcrogueface"
echo ""
echo -e "${GREEN}To create a distribution archive:${NC}"
echo " cd build"
echo " zip -r ../McRogueFace-$(date +%Y%m%d).zip ."

View file

@ -1,36 +0,0 @@
@echo off
REM Windows build script for McRogueFace
REM Run this over SSH without Visual Studio GUI
echo Building McRogueFace for Windows...
REM Clean previous build
if exist build_win rmdir /s /q build_win
mkdir build_win
cd build_win
REM Generate Visual Studio project files with CMake
REM Use -G to specify generator, -A for architecture
REM Visual Studio 2022 = "Visual Studio 17 2022"
REM Visual Studio 2019 = "Visual Studio 16 2019"
cmake -G "Visual Studio 17 2022" -A x64 ..
if errorlevel 1 (
echo CMake configuration failed!
exit /b 1
)
REM Build using MSBuild (comes with Visual Studio)
REM You can also use cmake --build . --config Release
msbuild McRogueFace.sln /p:Configuration=Release /p:Platform=x64 /m
if errorlevel 1 (
echo Build failed!
exit /b 1
)
echo Build completed successfully!
echo Executable location: build_win\Release\mcrogueface.exe
REM Alternative: Using cmake to build (works with any generator)
REM cmake --build . --config Release --parallel
cd ..

View file

@ -1,42 +0,0 @@
@echo off
REM Windows build script using cmake --build (generator-agnostic)
REM This version works with any CMake generator
echo Building McRogueFace for Windows using CMake...
REM Set build directory
set BUILD_DIR=build_win
set CONFIG=Release
REM Clean previous build
if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR%
mkdir %BUILD_DIR%
cd %BUILD_DIR%
REM Configure with CMake
REM You can change the generator here if needed:
REM -G "Visual Studio 17 2022" (VS 2022)
REM -G "Visual Studio 16 2019" (VS 2019)
REM -G "MinGW Makefiles" (MinGW)
REM -G "Ninja" (Ninja build system)
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%CONFIG% ..
if errorlevel 1 (
echo CMake configuration failed!
cd ..
exit /b 1
)
REM Build using cmake (works with any generator)
cmake --build . --config %CONFIG% --parallel
if errorlevel 1 (
echo Build failed!
cd ..
exit /b 1
)
echo.
echo Build completed successfully!
echo Executable: %BUILD_DIR%\%CONFIG%\mcrogueface.exe
echo.
cd ..

View file

@ -1,34 +0,0 @@
# CMake toolchain file for cross-compiling to Windows using MinGW-w64
# Usage: cmake -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64-x86_64.cmake ..
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
# Specify the cross-compiler (use posix variant for std::mutex support)
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc-posix)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++-posix)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
# Target environment location
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
# Add MinGW system include directories for Windows headers
include_directories(SYSTEM /usr/x86_64-w64-mingw32/include)
# Adjust search behavior
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
# Static linking of libgcc and libstdc++ to avoid runtime dependency issues
# Enable auto-import for Python DLL data symbols
set(CMAKE_EXE_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++ -Wl,--enable-auto-import")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++ -Wl,--enable-auto-import")
# Windows-specific defines
add_definitions(-DWIN32 -D_WIN32 -D_WINDOWS)
add_definitions(-DMINGW_HAS_SECURE_API)
# Disable console window for GUI applications (optional, can be overridden)
# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -mwindows")

View file

@ -1,112 +0,0 @@
[
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/GameEngine.cpp.o -c /home/john/Development/McRogueFace/src/GameEngine.cpp",
"file": "/home/john/Development/McRogueFace/src/GameEngine.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/IndexTexture.cpp.o -c /home/john/Development/McRogueFace/src/IndexTexture.cpp",
"file": "/home/john/Development/McRogueFace/src/IndexTexture.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/McRFPy_API.cpp.o -c /home/john/Development/McRogueFace/src/McRFPy_API.cpp",
"file": "/home/john/Development/McRogueFace/src/McRFPy_API.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyCallable.cpp.o -c /home/john/Development/McRogueFace/src/PyCallable.cpp",
"file": "/home/john/Development/McRogueFace/src/PyCallable.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyColor.cpp.o -c /home/john/Development/McRogueFace/src/PyColor.cpp",
"file": "/home/john/Development/McRogueFace/src/PyColor.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyFont.cpp.o -c /home/john/Development/McRogueFace/src/PyFont.cpp",
"file": "/home/john/Development/McRogueFace/src/PyFont.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyScene.cpp.o -c /home/john/Development/McRogueFace/src/PyScene.cpp",
"file": "/home/john/Development/McRogueFace/src/PyScene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyTexture.cpp.o -c /home/john/Development/McRogueFace/src/PyTexture.cpp",
"file": "/home/john/Development/McRogueFace/src/PyTexture.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyVector.cpp.o -c /home/john/Development/McRogueFace/src/PyVector.cpp",
"file": "/home/john/Development/McRogueFace/src/PyVector.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Resources.cpp.o -c /home/john/Development/McRogueFace/src/Resources.cpp",
"file": "/home/john/Development/McRogueFace/src/Resources.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Scene.cpp.o -c /home/john/Development/McRogueFace/src/Scene.cpp",
"file": "/home/john/Development/McRogueFace/src/Scene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Timer.cpp.o -c /home/john/Development/McRogueFace/src/Timer.cpp",
"file": "/home/john/Development/McRogueFace/src/Timer.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICaption.cpp.o -c /home/john/Development/McRogueFace/src/UICaption.cpp",
"file": "/home/john/Development/McRogueFace/src/UICaption.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICollection.cpp.o -c /home/john/Development/McRogueFace/src/UICollection.cpp",
"file": "/home/john/Development/McRogueFace/src/UICollection.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIDrawable.cpp.o -c /home/john/Development/McRogueFace/src/UIDrawable.cpp",
"file": "/home/john/Development/McRogueFace/src/UIDrawable.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIEntity.cpp.o -c /home/john/Development/McRogueFace/src/UIEntity.cpp",
"file": "/home/john/Development/McRogueFace/src/UIEntity.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIFrame.cpp.o -c /home/john/Development/McRogueFace/src/UIFrame.cpp",
"file": "/home/john/Development/McRogueFace/src/UIFrame.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGrid.cpp.o -c /home/john/Development/McRogueFace/src/UIGrid.cpp",
"file": "/home/john/Development/McRogueFace/src/UIGrid.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGridPoint.cpp.o -c /home/john/Development/McRogueFace/src/UIGridPoint.cpp",
"file": "/home/john/Development/McRogueFace/src/UIGridPoint.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UISprite.cpp.o -c /home/john/Development/McRogueFace/src/UISprite.cpp",
"file": "/home/john/Development/McRogueFace/src/UISprite.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UITestScene.cpp.o -c /home/john/Development/McRogueFace/src/UITestScene.cpp",
"file": "/home/john/Development/McRogueFace/src/UITestScene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/main.cpp.o -c /home/john/Development/McRogueFace/src/main.cpp",
"file": "/home/john/Development/McRogueFace/src/main.cpp"
}
]

157
css_colors.txt Normal file
View file

@ -0,0 +1,157 @@
aqua #00FFFF
black #000000
blue #0000FF
fuchsia #FF00FF
gray #808080
green #008000
lime #00FF00
maroon #800000
navy #000080
olive #808000
purple #800080
red #FF0000
silver #C0C0C0
teal #008080
white #FFFFFF
yellow #FFFF00
aliceblue #F0F8FF
antiquewhite #FAEBD7
aqua #00FFFF
aquamarine #7FFFD4
azure #F0FFFF
beige #F5F5DC
bisque #FFE4C4
black #000000
blanchedalmond #FFEBCD
blue #0000FF
blueviolet #8A2BE2
brown #A52A2A
burlywood #DEB887
cadetblue #5F9EA0
chartreuse #7FFF00
chocolate #D2691E
coral #FF7F50
cornflowerblue #6495ED
cornsilk #FFF8DC
crimson #DC143C
cyan #00FFFF
darkblue #00008B
darkcyan #008B8B
darkgoldenrod #B8860B
darkgray #A9A9A9
darkgreen #006400
darkkhaki #BDB76B
darkmagenta #8B008B
darkolivegreen #556B2F
darkorange #FF8C00
darkorchid #9932CC
darkred #8B0000
darksalmon #E9967A
darkseagreen #8FBC8F
darkslateblue #483D8B
darkslategray #2F4F4F
darkturquoise #00CED1
darkviolet #9400D3
deeppink #FF1493
deepskyblue #00BFFF
dimgray #696969
dodgerblue #1E90FF
firebrick #B22222
floralwhite #FFFAF0
forestgreen #228B22
fuchsia #FF00FF
gainsboro #DCDCDC
ghostwhite #F8F8FF
gold #FFD700
goldenrod #DAA520
gray #7F7F7F
green #008000
greenyellow #ADFF2F
honeydew #F0FFF0
hotpink #FF69B4
indianred #CD5C5C
indigo #4B0082
ivory #FFFFF0
khaki #F0E68C
lavender #E6E6FA
lavenderblush #FFF0F5
lawngreen #7CFC00
lemonchiffon #FFFACD
lightblue #ADD8E6
lightcoral #F08080
lightcyan #E0FFFF
lightgoldenrodyellow #FAFAD2
lightgreen #90EE90
lightgrey #D3D3D3
lightpink #FFB6C1
lightsalmon #FFA07A
lightseagreen #20B2AA
lightskyblue #87CEFA
lightslategray #778899
lightsteelblue #B0C4DE
lightyellow #FFFFE0
lime #00FF00
limegreen #32CD32
linen #FAF0E6
magenta #FF00FF
maroon #800000
mediumaquamarine #66CDAA
mediumblue #0000CD
mediumorchid #BA55D3
mediumpurple #9370DB
mediumseagreen #3CB371
mediumslateblue #7B68EE
mediumspringgreen #00FA9A
mediumturquoise #48D1CC
mediumvioletred #C71585
midnightblue #191970
mintcream #F5FFFA
mistyrose #FFE4E1
moccasin #FFE4B5
navajowhite #FFDEAD
navy #000080
navyblue #9FAFDF
oldlace #FDF5E6
olive #808000
olivedrab #6B8E23
orange #FFA500
orangered #FF4500
orchid #DA70D6
palegoldenrod #EEE8AA
palegreen #98FB98
paleturquoise #AFEEEE
palevioletred #DB7093
papayawhip #FFEFD5
peachpuff #FFDAB9
peru #CD853F
pink #FFC0CB
plum #DDA0DD
powderblue #B0E0E6
purple #800080
red #FF0000
rosybrown #BC8F8F
royalblue #4169E1
saddlebrown #8B4513
salmon #FA8072
sandybrown #FA8072
seagreen #2E8B57
seashell #FFF5EE
sienna #A0522D
silver #C0C0C0
skyblue #87CEEB
slateblue #6A5ACD
slategray #708090
snow #FFFAFA
springgreen #00FF7F
steelblue #4682B4
tan #D2B48C
teal #008080
thistle #D8BFD8
tomato #FF6347
turquoise #40E0D0
violet #EE82EE
wheat #F5DEB3
white #FFFFFF
whitesmoke #F5F5F5
yellow #FFFF00
yellowgreen #9ACD32

View file

@ -1,54 +1,6 @@
#ifndef __PLATFORM
#define __PLATFORM
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
#ifdef __EMSCRIPTEN__
// WASM/Emscripten platform - no /proc filesystem, limited std::filesystem support
std::wstring executable_path()
{
// In WASM, the executable is at the root of the virtual filesystem
return L"/";
}
std::wstring executable_filename()
{
// In WASM, we use a fixed executable name
return L"/mcrogueface";
}
std::wstring working_path()
{
// In WASM, working directory is root of virtual filesystem
return L"/";
}
std::string narrow_string(std::wstring convertme)
{
// Simple conversion for ASCII/UTF-8 compatible strings
std::string result;
result.reserve(convertme.size());
for (wchar_t wc : convertme) {
if (wc < 128) {
result.push_back(static_cast<char>(wc));
} else {
// For non-ASCII, use a simple UTF-8 encoding
if (wc < 0x800) {
result.push_back(static_cast<char>(0xC0 | (wc >> 6)));
result.push_back(static_cast<char>(0x80 | (wc & 0x3F)));
} else {
result.push_back(static_cast<char>(0xE0 | (wc >> 12)));
result.push_back(static_cast<char>(0x80 | ((wc >> 6) & 0x3F)));
result.push_back(static_cast<char>(0x80 | (wc & 0x3F)));
}
}
}
return result;
}
#else
// Native Linux platform
std::wstring executable_path()
{
/*
@ -60,7 +12,7 @@ std::wstring executable_path()
return exec_path.wstring();
//size_t path_index = exec_path.find_last_of('/');
//return exec_path.substr(0, path_index);
}
std::wstring executable_filename()
@ -85,6 +37,4 @@ std::string narrow_string(std::wstring convertme)
return converter.to_bytes(convertme);
}
#endif // __EMSCRIPTEN__
#endif // __PLATFORM
#endif

View file

@ -1,12 +1,12 @@
#ifndef __PLATFORM
#define __PLATFORM
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
#include <windows.h>
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 0
#include <Windows.h>
std::wstring executable_path()
{
wchar_t buffer[MAX_PATH];
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
GetModuleFileName(NULL, buffer, MAX_PATH);
std::wstring exec_path = buffer;
size_t path_index = exec_path.find_last_of(L"\\/");
return exec_path.substr(0, path_index);
@ -15,7 +15,7 @@ std::wstring executable_path()
std::wstring executable_filename()
{
wchar_t buffer[MAX_PATH];
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
GetModuleFileName(NULL, buffer, MAX_PATH);
std::wstring exec_path = buffer;
return exec_path;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,860 +0,0 @@
# McRogueFace Emscripten & Renderer Abstraction Research
**Date**: 2026-01-30
**Branch**: `emscripten-mcrogueface`
**Related Issues**: #157 (True Headless), #158 (Emscripten/WASM)
## Executive Summary
This document analyzes the technical requirements for:
1. **SFML 2.6 → 3.0 migration** (modernization)
2. **Emscripten/WebAssembly compilation** (browser deployment)
Both goals share a common prerequisite: **renderer abstraction**. The codebase already has a partial abstraction via `sf::RenderTarget*` pointer, but SFML types are pervasive (1276 occurrences across 78 files).
**Key Insight**: This is a **build-time configuration**, not runtime switching. The standard McRogueFace binary remains a dynamic environment; Emscripten builds bundle assets and scripts at compile time.
---
## Current Architecture Analysis
### Existing Abstraction Strengths
1. **RenderTarget Pointer Pattern** (`GameEngine.h:156`)
```cpp
sf::RenderTarget* render_target;
// Points to either window.get() or headless_renderer->getRenderTarget()
```
This already decouples rendering logic from the specific backend.
2. **HeadlessRenderer** (`src/HeadlessRenderer.h`)
- Uses `sf::RenderTexture` internally
- Provides unified interface: `getRenderTarget()`, `display()`, `saveScreenshot()`
- Demonstrates the pattern for additional backends
3. **UIDrawable Hierarchy**
- Virtual `render(sf::Vector2f, sf::RenderTarget&)` method
- 7 drawable types: Frame, Caption, Sprite, Entity, Grid, Line, Circle, Arc
- Each manages its own SFML primitives internally
4. **Asset Wrappers**
- `PyTexture`, `PyFont`, `PyShader` wrap SFML types
- Python reference counting integrated
- Single point of change for asset loading APIs
### Current SFML Coupling Points
| Area | Count | Difficulty | Notes |
|------|-------|------------|-------|
| `sf::Vector2f` | ~200+ | Medium | Used everywhere for positions, sizes |
| `sf::Color` | ~100+ | Easy | Simple 4-byte struct replacement |
| `sf::FloatRect` | ~50+ | Medium | Bounds, intersection testing |
| `sf::RenderTexture` | ~20 | Hard | Shader effects, caching |
| `sf::Sprite/Text` | ~30 | Hard | Core rendering primitives |
| `sf::Event` | ~15 | Medium | Input system coupling |
| `sf::Keyboard/Mouse` | ~50+ | Easy | Enum mappings |
Total: **1276 occurrences across 78 files**
---
## SFML 3.0 Migration Analysis
### Breaking Changes Requiring Code Updates
#### 1. Vector Parameters (High Impact)
```cpp
// SFML 2.6
setPosition(10, 20);
sf::VideoMode(1024, 768, 32);
sf::FloatRect(x, y, w, h);
// SFML 3.0
setPosition({10, 20});
sf::VideoMode({1024, 768}, 32);
sf::FloatRect({x, y}, {w, h});
```
**Strategy**: Regex-based search/replace with manual verification.
#### 2. Rect Member Changes (Medium Impact)
```cpp
// SFML 2.6
rect.left, rect.top, rect.width, rect.height
rect.getPosition(), rect.getSize()
// SFML 3.0
rect.position.x, rect.position.y, rect.size.x, rect.size.y
rect.position, rect.size // direct access
rect.findIntersection() -> std::optional<Rect<T>>
```
#### 3. Resource Constructors (Low Impact)
```cpp
// SFML 2.6
sf::Sound sound; // default constructible
sound.setBuffer(buffer);
// SFML 3.0
sf::Sound sound(buffer); // requires buffer at construction
```
#### 4. Keyboard/Mouse Enum Scoping (Medium Impact)
```cpp
// SFML 2.6
sf::Keyboard::A
sf::Mouse::Left
// SFML 3.0
sf::Keyboard::Key::A
sf::Mouse::Button::Left
```
#### 5. Event Handling (Medium Impact)
```cpp
// SFML 2.6
sf::Event event;
while (window.pollEvent(event)) {
if (event.type == sf::Event::Closed) ...
}
// SFML 3.0
while (auto event = window.pollEvent()) {
if (event->is<sf::Event::Closed>()) ...
}
```
#### 6. CMake Target Changes
```cmake
# SFML 2.6
find_package(SFML 2 REQUIRED COMPONENTS graphics audio)
target_link_libraries(app sfml-graphics sfml-audio)
# SFML 3.0
find_package(SFML 3 REQUIRED COMPONENTS Graphics Audio)
target_link_libraries(app SFML::Graphics SFML::Audio)
```
### Migration Effort Estimate
| Phase | Files | Changes | Effort |
|-------|-------|---------|--------|
| CMakeLists.txt | 1 | Target names | 1 hour |
| Vector parameters | 30+ | ~200 calls | 4-8 hours |
| Rect refactoring | 20+ | ~50 usages | 2-4 hours |
| Event handling | 5 | ~15 sites | 2 hours |
| Keyboard/Mouse | 10 | ~50 enums | 2 hours |
| Resource constructors | 10 | ~30 sites | 2 hours |
| **Total** | - | - | **~15-25 hours** |
---
## Emscripten/VRSFML Analysis
### Why VRSFML Over Waiting for SFML 4.x?
1. **Available Now**: VRSFML is working today with browser demos
2. **Modern OpenGL**: Removes legacy calls, targets OpenGL ES 3.0+ (WebGL 2)
3. **SFML_GAME_LOOP Macro**: Handles blocking vs callback loop abstraction
4. **Performance**: 500k sprites @ 60FPS vs 3 FPS upstream (batching)
5. **SFML 4.x Timeline**: Unknown, potentially years away
### VRSFML API Differences from SFML
| Feature | SFML 2.6/3.0 | VRSFML |
|---------|--------------|--------|
| Default constructors | Allowed | Not allowed for resources |
| Texture ownership | Pointer in Sprite | Passed at draw time |
| Context management | Hidden global | Explicit `GraphicsContext` |
| Drawable base class | Polymorphic | Removed |
| Loading methods | `loadFromFile()` returns bool | Returns `std::optional` |
| Main loop | `while(running)` | `SFML_GAME_LOOP { }` |
### Main Loop Refactoring
Current blocking loop:
```cpp
void GameEngine::run() {
while (running) {
processEvents();
update();
render();
display();
}
}
```
Emscripten-compatible pattern:
```cpp
// Option A: VRSFML macro
SFML_GAME_LOOP {
processEvents();
update();
render();
display();
}
// Option B: Manual Emscripten integration
#ifdef __EMSCRIPTEN__
void mainLoopCallback() {
if (!game.running) {
emscripten_cancel_main_loop();
return;
}
game.doFrame();
}
emscripten_set_main_loop(mainLoopCallback, 0, 1);
#else
while (running) { doFrame(); }
#endif
```
**Recommendation**: Use preprocessor-based approach with `doFrame()` extraction for cleaner separation.
---
## Build-Time Configuration Strategy
### Normal Build (Desktop)
- Dynamic loading of assets from `assets/` directory
- Python scripts loaded from `scripts/` directory at runtime
- Full McRogueFace environment with dynamic game loading
### Emscripten Build (Web)
- Assets bundled via `--preload-file assets`
- Scripts bundled via `--preload-file scripts`
- Virtual filesystem (MEMFS/IDBFS)
- Optional: Script linting with Pyodide before bundling
- Single-purpose deployment (one game per build)
### CMake Configuration
```cmake
option(MCRF_BUILD_EMSCRIPTEN "Build for Emscripten/WebAssembly" OFF)
if(MCRF_BUILD_EMSCRIPTEN)
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/cmake/toolchains/emscripten.cmake)
add_definitions(-DMCRF_EMSCRIPTEN)
# Bundle assets
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} \
--preload-file ${CMAKE_SOURCE_DIR}/assets@/assets \
--preload-file ${CMAKE_SOURCE_DIR}/scripts@/scripts")
endif()
```
---
## Phased Implementation Plan
### Phase 0: Preparation (This PR)
- [ ] Create `docs/EMSCRIPTEN_RESEARCH.md` (this document)
- [ ] Update Gitea issues #157, #158 with findings
- [ ] Identify specific files requiring changes
- [ ] Create test matrix for rendering features
### Phase 1: Type Abstraction Layer
**Goal**: Isolate SFML types behind McRogueFace wrappers
```cpp
// src/types/McrfTypes.h
namespace mcrf {
using Vector2f = sf::Vector2f; // Alias initially, replace later
using Color = sf::Color;
using FloatRect = sf::FloatRect;
}
```
Changes:
- [ ] Create `src/types/` directory with wrapper types
- [ ] Gradually replace `sf::` with `mcrf::` namespace
- [ ] Update Common.h to provide both namespaces during transition
### Phase 2: Main Loop Extraction
**Goal**: Make game loop callback-compatible
- [ ] Extract `GameEngine::doFrame()` from `run()`
- [ ] Add `#ifdef __EMSCRIPTEN__` conditional in `run()`
- [ ] Test that desktop behavior is unchanged
### Phase 3: Render Backend Interface
**Goal**: Abstract RenderTarget operations
```cpp
class RenderBackend {
public:
virtual ~RenderBackend() = default;
virtual void clear(const Color& color) = 0;
virtual void draw(const Sprite& sprite) = 0;
virtual void draw(const Text& text) = 0;
virtual void display() = 0;
virtual bool isOpen() const = 0;
virtual Vector2u getSize() const = 0;
};
class SFMLBackend : public RenderBackend { ... };
class VRSFMLBackend : public RenderBackend { ... }; // Future
```
### Phase 4: SFML 3.0 Migration
**Goal**: Update to SFML 3.0 API
- [ ] Update CMakeLists.txt targets
- [ ] Fix vector parameter calls
- [ ] Fix rect member access
- [ ] Fix event handling
- [ ] Fix keyboard/mouse enums
- [ ] Test thoroughly
### Phase 5: VRSFML Integration (Experimental)
**Goal**: Add VRSFML as alternative backend
- [ ] Add VRSFML as submodule/dependency
- [ ] Implement VRSFMLBackend
- [ ] Add Emscripten CMake configuration
- [ ] Test in browser
### Phase 6: Python-in-WASM
**Goal**: Get Python scripting working in browser
**High Risk** - This is the major unknown:
- [ ] Build CPython for Emscripten
- [ ] Test `McRFPy_API` binding compatibility
- [ ] Evaluate Pyodide vs raw CPython
- [ ] Handle filesystem virtualization
- [ ] Test threading limitations
---
## Risk Assessment
| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| SFML 3.0 breaks unexpected code | Medium | Medium | Comprehensive test suite |
| VRSFML API too different | Low | High | Can fork/patch VRSFML |
| Python-in-WASM fails | Medium | Critical | Evaluate Pyodide early |
| Performance regression | Low | Medium | Benchmark before/after |
| Binary size too large | Medium | Medium | Lazy loading, stdlib trimming |
---
## References
### SFML 3.0
- [Migration Guide](https://www.sfml-dev.org/tutorials/3.0/getting-started/migrate/)
- [Changelog](https://www.sfml-dev.org/development/changelog/)
- [Release Notes](https://github.com/SFML/SFML/releases/tag/3.0.0)
### VRSFML/Emscripten
- [VRSFML Blog Post](https://vittorioromeo.com/index/blog/vrsfml.html)
- [VRSFML GitHub](https://github.com/vittorioromeo/VRSFML)
- [Browser Demos](https://vittorioromeo.github.io/VRSFML_HTML5_Examples/)
### Python WASM
- [PEP 776 - Python Emscripten Support](https://peps.python.org/pep-0776/)
- [CPython WASM Build Guide](https://github.com/python/cpython/blob/main/Tools/wasm/README.md)
- [Pyodide](https://github.com/pyodide/pyodide)
### Related Issues
- [SFML Emscripten Discussion #1494](https://github.com/SFML/SFML/issues/1494)
- [libtcod Emscripten #41](https://github.com/libtcod/libtcod/issues/41)
---
## Appendix A: File-by-File SFML Usage Inventory
### Critical Files (Must Abstract for Emscripten)
| File | SFML Types Used | Role | Abstraction Difficulty |
|------|-----------------|------|------------------------|
| `GameEngine.h/cpp` | RenderWindow, Clock, Font, Event | Main loop, window | **CRITICAL** |
| `HeadlessRenderer.h/cpp` | RenderTexture | Headless backend | **CRITICAL** |
| `UIDrawable.h/cpp` | Vector2f, RenderTarget, FloatRect | Base render interface | **HARD** |
| `UIFrame.h/cpp` | RectangleShape, Vector2f, Color | Container rendering | **HARD** |
| `UISprite.h/cpp` | Sprite, Texture, Vector2f | Texture display | **HARD** |
| `UICaption.h/cpp` | Text, Font, Vector2f, Color | Text rendering | **HARD** |
| `UIGrid.h/cpp` | RenderTexture, Sprite, Vector2f | Tile grid system | **HARD** |
| `UIEntity.h/cpp` | Sprite, Vector2f | Game entities | **HARD** |
| `UICircle.h/cpp` | CircleShape, Vector2f, Color | Circle shape | **MEDIUM** |
| `UILine.h/cpp` | VertexArray, Vector2f, Color | Line rendering | **MEDIUM** |
| `UIArc.h/cpp` | CircleShape segments, Vector2f | Arc shape | **MEDIUM** |
| `Scene.h/cpp` | Vector2f, RenderTarget | Scene management | **MEDIUM** |
| `SceneTransition.h/cpp` | RenderTexture, Sprite | Transitions | **MEDIUM** |
### Wrapper Files (Already Partially Abstracted)
| File | SFML Types Wrapped | Python API | Notes |
|------|-------------------|------------|-------|
| `PyVector.h/cpp` | sf::Vector2f | Vector | Ready for backend swap |
| `PyColor.h/cpp` | sf::Color | Color | Ready for backend swap |
| `PyTexture.h/cpp` | sf::Texture | Texture | Asset loading needs work |
| `PyFont.h/cpp` | sf::Font | Font | Asset loading needs work |
| `PyShader.h/cpp` | sf::Shader | Shader | Optional feature |
### Input System Files
| File | SFML Types Used | Notes |
|------|-----------------|-------|
| `ActionCode.h` | Keyboard::Key, Mouse::Button | Enum encoding only |
| `PyKey.h/cpp` | Keyboard::Key enum | 140+ key mappings |
| `PyMouseButton.h/cpp` | Mouse::Button enum | Simple enum |
| `PyKeyboard.h/cpp` | Keyboard::isKeyPressed | State queries |
| `PyMouse.h/cpp` | Mouse::getPosition | Position queries |
| `PyInputState.h/cpp` | None (pure enum) | No SFML dependency |
### Support Files (Low Priority)
| File | SFML Types Used | Notes |
|------|-----------------|-------|
| `Animation.h/cpp` | Vector2f, Color (as values) | Pure data animation |
| `GridLayers.h/cpp` | RenderTexture, Color | Layer caching |
| `IndexTexture.h/cpp` | Texture, IntRect | Legacy texture format |
| `Resources.h/cpp` | Font | Global font storage |
| `ProfilerOverlay.cpp` | Text, RectangleShape | Debug overlay |
| `McRFPy_Automation.h/cpp` | Various | Testing only |
---
## Appendix B: Recommended First Steps
### Immediate (Non-Breaking Changes)
1. **Extract `GameEngine::doFrame()`**
- Move loop body to separate method
- No API changes, just internal refactoring
- Enables future Emscripten callback integration
2. **Create type aliases in Common.h**
```cpp
namespace mcrf {
using Vector2f = sf::Vector2f;
using Vector2i = sf::Vector2i;
using Color = sf::Color;
using FloatRect = sf::FloatRect;
}
```
- Allows gradual migration from `sf::` to `mcrf::`
- No functional changes
3. **Document current render path**
- Add comments to key rendering functions
- Identify all `target.draw()` call sites
- Create rendering flow diagram
### Short-Term (Preparation for SFML 3.0)
1. **Audit vector parameter calls**
- Find all `setPosition(x, y)` style calls
- Prepare regex patterns for migration
2. **Audit rect member access**
- Find all `.left`, `.top`, `.width`, `.height` uses
- Prepare for `.position.x`, `.size.x` style
3. **Test suite expansion**
- Add rendering validation tests
- Screenshot comparison tests
- Animation correctness tests
---
## Appendix C: libtcod Architecture Analysis
**Key Finding**: libtcod uses a much simpler abstraction pattern than initially proposed.
### libtcod's Context Vtable Pattern
libtcod doesn't wrap every SDL type. Instead, it abstracts at the **context level** using a C-style vtable:
```c
struct TCOD_Context {
int type;
void* contextdata_; // Backend-specific data (opaque pointer)
// Function pointers - the "vtable"
void (*c_destructor_)(struct TCOD_Context* self);
TCOD_Error (*c_present_)(struct TCOD_Context* self,
const TCOD_Console* console,
const TCOD_ViewportOptions* viewport);
void (*c_pixel_to_tile_)(struct TCOD_Context* self, double* x, double* y);
TCOD_Error (*c_save_screenshot_)(struct TCOD_Context* self, const char* filename);
struct SDL_Window* (*c_get_sdl_window_)(struct TCOD_Context* self);
TCOD_Error (*c_set_tileset_)(struct TCOD_Context* self, TCOD_Tileset* tileset);
TCOD_Error (*c_screen_capture_)(struct TCOD_Context* self, ...);
// ... more operations
};
```
### How Backends Implement It
Each renderer fills in the function pointers:
```c
// In renderer_sdl2.c
context->c_destructor_ = sdl2_destructor;
context->c_present_ = sdl2_present;
context->c_get_sdl_window_ = sdl2_get_window;
// ...
// In renderer_xterm.c
context->c_destructor_ = xterm_destructor;
context->c_present_ = xterm_present;
// ...
```
### Conditional Compilation with NO_SDL
libtcod uses simple preprocessor guards:
```c
// In CMakeLists.txt
if(LIBTCOD_SDL3)
target_link_libraries(${PROJECT_NAME} PUBLIC SDL3::SDL3)
else()
target_compile_definitions(${PROJECT_NAME} PUBLIC NO_SDL)
endif()
// In source files
#ifndef NO_SDL
#include <SDL3/SDL.h>
// ... SDL-dependent code ...
#endif
```
**47 files** use this pattern. When building headless, SDL code is simply excluded.
### Why This Pattern Works
1. **Core functionality is SDL-independent**: Console manipulation, pathfinding, FOV, noise, BSP, etc. don't need SDL
2. **Only rendering needs abstraction**: The `TCOD_Context` is the single point of abstraction
3. **Minimal API surface**: Just ~10 function pointers instead of wrapping every primitive
4. **Backend-specific data is opaque**: `contextdata_` holds renderer-specific state
### Implications for McRogueFace
**libtcod's approach suggests we should NOT try to abstract every `sf::` type.**
Instead, consider:
1. **Keep SFML types internally** - `sf::Vector2f`, `sf::Color`, `sf::FloatRect` are fine
2. **Abstract at the RenderContext level** - One vtable for window/rendering operations
3. **Use `#ifndef NO_SFML` guards** - Compile-time backend selection
4. **Create alternative backend for Emscripten** - WebGL + canvas implementation
### Proposed McRogueFace Context Pattern
```cpp
struct McRF_RenderContext {
void* backend_data; // SFML or WebGL specific data
// Function pointers
void (*destroy)(McRF_RenderContext* self);
void (*clear)(McRF_RenderContext* self, uint32_t color);
void (*present)(McRF_RenderContext* self);
void (*draw_sprite)(McRF_RenderContext* self, const Sprite* sprite);
void (*draw_text)(McRF_RenderContext* self, const Text* text);
void (*draw_rect)(McRF_RenderContext* self, const Rect* rect);
bool (*poll_event)(McRF_RenderContext* self, Event* event);
void (*screenshot)(McRF_RenderContext* self, const char* path);
// ...
};
// SFML backend
McRF_RenderContext* mcrf_sfml_context_new(int width, int height, const char* title);
// Emscripten backend (future)
McRF_RenderContext* mcrf_webgl_context_new(const char* canvas_id);
```
### Comparison: Original Plan vs libtcod-Inspired Plan
| Aspect | Original Plan | libtcod-Inspired Plan |
|--------|---------------|----------------------|
| Type abstraction | Replace all `sf::*` with `mcrf::*` | Keep `sf::*` internally |
| Abstraction point | Every primitive type | Single Context object |
| Files affected | 78+ files | ~10 core files |
| Compile-time switching | Complex namespace aliasing | Simple `#ifndef NO_SFML` |
| Backend complexity | Full reimplementation | Focused vtable |
**Recommendation**: Adopt libtcod's simpler pattern. Focus abstraction on the rendering context, not on data types.
---
## Appendix D: Headless Build Experiment Results
**Experiment Date**: 2026-01-30
**Branch**: `emscripten-mcrogueface`
### Objective
Attempt to compile McRogueFace without SFML dependencies to identify true coupling points.
### What We Created
1. **`src/platform/HeadlessTypes.h`** - Complete SFML type stubs (~600 lines):
- Vector2f, Vector2i, Vector2u
- Color with standard color constants
- FloatRect, IntRect
- Time, Clock (with chrono-based implementation)
- Transform, Vertex, View
- Shape hierarchy (RectangleShape, CircleShape, etc.)
- Texture, Sprite, Font, Text stubs
- RenderTarget, RenderTexture, RenderWindow stubs
- Audio stubs (Sound, Music, SoundBuffer)
- Input stubs (Keyboard, Mouse, Event)
- Shader stub
2. **Modified `src/Common.h`** - Conditional include:
```cpp
#ifdef MCRF_HEADLESS
#include "platform/HeadlessTypes.h"
#else
#include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
#endif
```
### Build Attempt Result
**SUCCESS** - Headless build compiles after consolidating includes and adding stubs.
### Work Completed
#### 1. Consolidated SFML Includes
**15 files** had direct SFML includes that bypassed Common.h. All were modified to use `#include "Common.h"` instead:
| File | Original Include | Fixed |
|------|------------------|-------|
| `main.cpp` | `<SFML/Graphics.hpp>` | ✓ |
| `Animation.h` | `<SFML/Graphics.hpp>` | ✓ |
| `GridChunk.h` | `<SFML/Graphics.hpp>` | ✓ |
| `GridLayers.h` | `<SFML/Graphics.hpp>` | ✓ |
| `HeadlessRenderer.h` | `<SFML/Graphics.hpp>` | ✓ |
| `SceneTransition.h` | `<SFML/Graphics.hpp>` | ✓ |
| `McRFPy_Automation.h` | `<SFML/Graphics.hpp>`, `<SFML/Window.hpp>` | ✓ |
| `PyWindow.cpp` | `<SFML/Graphics.hpp>` | ✓ |
| `ActionCode.h` | `<SFML/Window/Keyboard.hpp>` | ✓ |
| `PyKey.h` | `<SFML/Window/Keyboard.hpp>` | ✓ |
| `PyMouseButton.h` | `<SFML/Window/Mouse.hpp>` | ✓ |
| `PyBSP.h` | `<SFML/System/Vector2.hpp>` | ✓ |
| `UIGridPathfinding.h` | `<SFML/System/Vector2.hpp>` | ✓ |
#### 2. Wrapped ImGui-SFML with Guards
ImGui-SFML is disabled entirely in headless builds since debug tools can't be accessed through the API:
| File | Changes |
|------|---------|
| `GameEngine.h` | Guarded includes and member variables |
| `GameEngine.cpp` | Guarded all ImGui::SFML calls |
| `ImGuiConsole.h/cpp` | Entire file wrapped with `#ifndef MCRF_HEADLESS` |
| `ImGuiSceneExplorer.h/cpp` | Entire file wrapped with `#ifndef MCRF_HEADLESS` |
| `McRFPy_API.cpp` | Guarded ImGuiConsole include and setEnabled call |
#### 3. Extended HeadlessTypes.h
The stub file grew from ~700 lines to ~900 lines with additional types and methods:
**Types Added:**
- `sf::Image` - For screenshot functionality
- `sf::Glsl::Vec3`, `sf::Glsl::Vec4` - For shader uniforms
- `sf::BlendMode` - For rendering states
- `sf::CurrentTextureType` - For shader texture binding
**Methods Added:**
- `Font::Info` struct and `Font::getInfo()`
- `Texture::update()` overloads
- `Texture::copyToImage()`
- `Transform::getInverse()`
- `RenderStates` constructors from Transform, BlendMode, Shader*
- `Music::getDuration()`, `getPlayingOffset()`, `setPlayingOffset()`
- `SoundBuffer::getDuration()`
- `RenderWindow::setMouseCursorGrabbed()`
- `sf::err()` stream function
- Keyboard aliases: `BackSpace`, `BackSlash`, `SemiColon`, `Dash`
### Build Commands
```bash
# Normal SFML build (default)
make
# Headless build (no SFML/ImGui dependencies)
mkdir build-headless && cd build-headless
cmake .. -DMCRF_HEADLESS=ON -DCMAKE_BUILD_TYPE=Release
make
```
### Key Insight
The libtcod approach of `#ifndef NO_SDL` guards works when **all platform includes go through a single point**. The consolidation of 15+ bypass points into Common.h was the prerequisite that made this work.
### Actual Effort
| Task | Files | Time |
|------|-------|------|
| Replace direct SFML includes with Common.h | 15 | ~30 min |
| Wrap ImGui-SFML in guards | 5 | ~20 min |
| Extend HeadlessTypes.h with missing stubs | 1 | ~1 hour |
| Fix compilation errors iteratively | - | ~1 hour |
**Total**: ~3 hours for clean headless compilation
### Completed Milestones
1. ✅ **Test Python bindings** - mcrfpy module loads and works in headless mode
- Vector, Color, Scene, Frame, Grid all functional
- libtcod integrations (BSP, pathfinding) available
2. ✅ **Add CMake option** - `option(MCRF_HEADLESS "Build without graphics" OFF)`
- Proper conditional compilation and linking
- No SFML symbols in headless binary
3. ✅ **Link-time validation** - `ldd` confirms zero SFML/OpenGL dependencies
4. ✅ **Binary size reduction** - Headless is 1.6 MB vs 2.5 MB normal build (36% smaller)
### Python Test Results (Headless Mode)
```python
# All these work in headless build:
import mcrfpy
v = mcrfpy.Vector(10, 20) # ✅
c = mcrfpy.Color(255, 128, 64) # ✅
scene = mcrfpy.Scene('test') # ✅
frame = mcrfpy.Frame(pos=(0,0)) # ✅
grid = mcrfpy.Grid(grid_size=(10,10)) # ✅
```
### Remaining Steps for Emscripten
1. ✅ **Main loop extraction** - `GameEngine::doFrame()` extracted with Emscripten callback support
- `run()` now uses `#ifdef __EMSCRIPTEN__` to choose between callback and blocking loop
- `emscripten_set_main_loop_arg()` integration ready
2. ✅ **Emscripten toolchain** - `emcmake cmake` works with headless mode
3. ✅ **Python-in-WASM** - Built CPython 3.14.2 for wasm32-emscripten target
- Uses official `Tools/wasm/emscripten build` script from CPython repo
- Produced libpython3.14.a (47MB static library)
- Also builds: libmpdec, libffi, libexpat for WASM
4. ✅ **libtcod-in-WASM** - Built libtcod-headless for Emscripten
- Uses `LIBTCOD_SDL3=OFF` to avoid SDL dependency
- Includes lodepng and utf8proc dependencies
5. ✅ **First successful WASM build** - mcrogueface.wasm (8.9MB) + mcrogueface.js (126KB)
- All 68 C++ source files compile with emcc
- Links: Python, libtcod, HACL crypto, expat, mpdec, ffi, zlib, bzip2, sqlite3
6. 🔲 **Python stdlib bundling** - Need to package Python stdlib for WASM filesystem
7. 🔲 **VRSFML integration** - Replace stubs with actual WebGL rendering
### First Emscripten Build Attempt (2026-01-31)
**Command:**
```bash
source ~/emsdk/emsdk_env.sh
emcmake cmake .. -DMCRF_HEADLESS=ON -DCMAKE_BUILD_TYPE=Release
emmake make -j8
```
**Result:** Build failed on Python headers.
**Key Errors:**
```
deps/Python/pyport.h:429:2: error: "LONG_BIT definition appears wrong for platform"
```
```
warning: shift count >= width of type [-Wshift-count-overflow]
_Py_STATIC_FLAG_BITS << 48 // 48-bit shift on 32-bit WASM!
```
**Root Cause:**
1. Desktop Python 3.14 headers assume 64-bit Linux with glibc
2. Emscripten targets 32-bit WASM with musl-based libc
3. Python's immortal reference counting uses `<< 48` shifts that overflow on 32-bit
4. `LONG_BIT` check fails because WASM's `long` is 32 bits
**Analysis:**
The HeadlessTypes.h stubs and game engine code compile fine. The blocker is exclusively the Python C API integration.
### Python-in-WASM Options
| Option | Complexity | Description |
|--------|------------|-------------|
| **Pyodide** | Medium | Pre-built Python WASM with package ecosystem |
| **CPython WASM** | High | Build CPython ourselves with Emscripten |
| **No-Python mode** | Low | New CMake option to exclude Python entirely |
**Pyodide Approach (Recommended):**
- Pyodide provides Python 3.12 compiled for WASM
- Would need to replace `deps/Python` with Pyodide headers
- `McRFPy_API` binding layer needs adaptation
- Pyodide handles asyncio, file system virtualization
- Active project with good documentation
### CPython WASM Build (Successful!)
**Date**: 2026-01-31
Used the official CPython WASM build process:
```bash
# From deps/cpython directory
./Tools/wasm/emscripten build
# This produces:
# - cross-build/wasm32-emscripten/build/python/libpython3.14.a
# - cross-build/wasm32-emscripten/prefix/lib/libmpdec.a
# - cross-build/wasm32-emscripten/prefix/lib/libffi.a
# - cross-build/wasm32-emscripten/build/python/Modules/expat/libexpat.a
```
**CMake Integration:**
```cmake
if(EMSCRIPTEN)
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
set(PYTHON_WASM_PREFIX "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/prefix")
# Force WASM-compatible pyconfig.h
add_compile_options(-include ${PYTHON_WASM_BUILD}/pyconfig.h)
# Link all Python dependencies
set(LINK_LIBS
${PYTHON_WASM_BUILD}/libpython3.14.a
${PYTHON_WASM_BUILD}/Modules/_hacl/*.o # HACL crypto not in libpython
${PYTHON_WASM_BUILD}/Modules/expat/libexpat.a
${PYTHON_WASM_PREFIX}/lib/libmpdec.a
${PYTHON_WASM_PREFIX}/lib/libffi.a
)
# Emscripten ports for common libraries
target_link_options(mcrogueface PRIVATE
-sUSE_ZLIB=1
-sUSE_BZIP2=1
-sUSE_SQLITE3=1
)
endif()
```
**No-Python Mode (For Testing):**
- Add `MCRF_NO_PYTHON` CMake option
- Allows testing WASM build without Python complexity
- Game engine would be pure C++ (no scripting)
- Useful for validating rendering, input, timing first
### Main Loop Architecture
The game loop now supports both desktop (blocking) and browser (callback) modes:
```cpp
// GameEngine::run() - build-time conditional
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(emscriptenMainLoopCallback, this, 0, 1);
#else
while (running) { doFrame(); }
#endif
// GameEngine::doFrame() - same code runs in both modes
void GameEngine::doFrame() {
metrics.resetPerFrame();
currentScene()->update();
testTimers();
// ... animations, input, rendering ...
currentFrame++;
frameTime = clock.restart().asSeconds();
}
```

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,13 +0,0 @@
"""McRogueFace - Animated Movement (basic)
Documentation: https://mcrogueface.github.io/cookbook/combat_animated_movement
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_animated_movement_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
if new_x != current_x:
anim = mcrfpy.Animation("x", float(new_x), duration, "easeInOut", callback=done)
else:
anim = mcrfpy.Animation("y", float(new_y), duration, "easeInOut", callback=done)

View file

@ -1,12 +0,0 @@
"""McRogueFace - Animated Movement (basic_2)
Documentation: https://mcrogueface.github.io/cookbook/combat_animated_movement
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_animated_movement_basic_2.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
current_anim = mcrfpy.Animation("x", 100.0, 0.5, "linear")
current_anim.start(entity)
# Later: current_anim = None # Let it complete or create new one

View file

@ -1,45 +0,0 @@
"""McRogueFace - Basic Enemy AI (basic)
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import random
def wander(enemy, grid):
"""Move randomly to an adjacent walkable tile."""
ex, ey = int(enemy.x), int(enemy.y)
# Get valid adjacent tiles
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
random.shuffle(directions)
for dx, dy in directions:
new_x, new_y = ex + dx, ey + dy
if is_walkable(grid, new_x, new_y) and not is_occupied(new_x, new_y):
enemy.x = new_x
enemy.y = new_y
return
# No valid moves - stay in place
def is_walkable(grid, x, y):
"""Check if a tile can be walked on."""
grid_w, grid_h = grid.grid_size
if x < 0 or x >= grid_w or y < 0 or y >= grid_h:
return False
return grid.at(x, y).walkable
def is_occupied(x, y, entities=None):
"""Check if a tile is occupied by another entity."""
if entities is None:
return False
for entity in entities:
if int(entity.x) == x and int(entity.y) == y:
return True
return False

View file

@ -1,11 +0,0 @@
"""McRogueFace - Basic Enemy AI (multi)
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
# Filter to cardinal directions only
path = [p for p in path if abs(p[0] - ex) + abs(p[1] - ey) == 1]

View file

@ -1,14 +0,0 @@
"""McRogueFace - Basic Enemy AI (multi_2)
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_multi_2.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def alert_nearby(x, y, radius, enemies):
for enemy in enemies:
dist = abs(enemy.entity.x - x) + abs(enemy.entity.y - y)
if dist <= radius and hasattr(enemy.ai, 'alert'):
enemy.ai.alert = True

View file

@ -1,82 +0,0 @@
"""McRogueFace - Melee Combat System (basic)
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class CombatLog:
"""Scrolling combat message log."""
def __init__(self, x, y, width, height, max_messages=10):
self.x = x
self.y = y
self.width = width
self.height = height
self.max_messages = max_messages
self.messages = []
self.captions = []
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
# Background
self.frame = mcrfpy.Frame(x, y, width, height)
self.frame.fill_color = mcrfpy.Color(0, 0, 0, 180)
ui.append(self.frame)
def add_message(self, text, color=None):
"""Add a message to the log."""
if color is None:
color = mcrfpy.Color(200, 200, 200)
self.messages.append((text, color))
# Keep only recent messages
if len(self.messages) > self.max_messages:
self.messages.pop(0)
self._refresh_display()
def _refresh_display(self):
"""Redraw all messages."""
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
# Remove old captions
for caption in self.captions:
try:
ui.remove(caption)
except:
pass
self.captions.clear()
# Create new captions
line_height = 18
for i, (text, color) in enumerate(self.messages):
caption = mcrfpy.Caption(text, self.x + 5, self.y + 5 + i * line_height)
caption.fill_color = color
ui.append(caption)
self.captions.append(caption)
def log_attack(self, attacker_name, defender_name, damage, killed=False, critical=False):
"""Log an attack event."""
if critical:
text = f"{attacker_name} CRITS {defender_name} for {damage}!"
color = mcrfpy.Color(255, 255, 0)
else:
text = f"{attacker_name} hits {defender_name} for {damage}."
color = mcrfpy.Color(200, 200, 200)
self.add_message(text, color)
if killed:
self.add_message(f"{defender_name} is defeated!", mcrfpy.Color(255, 100, 100))
# Global combat log
combat_log = None
def init_combat_log():
global combat_log
combat_log = CombatLog(10, 500, 400, 200)

View file

@ -1,15 +0,0 @@
"""McRogueFace - Melee Combat System (complete)
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_complete.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def die_with_animation(entity):
# Play death animation
anim = mcrfpy.Animation("opacity", 0.0, 0.5, "linear")
anim.start(entity)
# Remove after animation
mcrfpy.setTimer("remove", lambda dt: remove_entity(entity), 500)

View file

@ -1,14 +0,0 @@
"""McRogueFace - Melee Combat System (complete_2)
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_complete_2.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
@dataclass
class AdvancedFighter(Fighter):
fire_resist: float = 0.0
ice_resist: float = 0.0
physical_resist: float = 0.0

View file

@ -1,56 +0,0 @@
"""McRogueFace - Status Effects (basic)
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class StackableEffect(StatusEffect):
"""Effect that stacks intensity."""
def __init__(self, name, duration, intensity=1, max_stacks=5, **kwargs):
super().__init__(name, duration, **kwargs)
self.intensity = intensity
self.max_stacks = max_stacks
self.stacks = 1
def add_stack(self):
"""Add another stack."""
if self.stacks < self.max_stacks:
self.stacks += 1
return True
return False
class StackingEffectManager(EffectManager):
"""Effect manager with stacking support."""
def add_effect(self, effect):
if isinstance(effect, StackableEffect):
# Check for existing stacks
for existing in self.effects:
if existing.name == effect.name:
if existing.add_stack():
# Refresh duration
existing.duration = max(existing.duration, effect.duration)
return
else:
return # Max stacks
# Default behavior
super().add_effect(effect)
# Stacking poison example
def create_stacking_poison(base_damage=1, duration=5):
def on_tick(target):
# Find the poison effect to get stack count
effect = target.effects.get_effect("poison")
if effect:
damage = base_damage * effect.stacks
target.hp -= damage
print(f"{target.name} takes {damage} poison damage! ({effect.stacks} stacks)")
return StackableEffect("poison", duration, on_tick=on_tick, max_stacks=5)

View file

@ -1,16 +0,0 @@
"""McRogueFace - Status Effects (basic_2)
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic_2.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def apply_effect(self, effect):
if effect.name in self.immunities:
print(f"{self.name} is immune to {effect.name}!")
return
if effect.name in self.resistances:
effect.duration //= 2 # Half duration
self.effects.add_effect(effect)

View file

@ -1,12 +0,0 @@
"""McRogueFace - Status Effects (basic_3)
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic_3.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def serialize_effects(effect_manager):
return [{"name": e.name, "duration": e.duration}
for e in effect_manager.effects]

View file

@ -1,45 +0,0 @@
"""McRogueFace - Turn-Based Game Loop (combat_turn_system)
Documentation: https://mcrogueface.github.io/cookbook/combat_turn_system
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_turn_system.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def create_turn_order_ui(turn_manager, x=800, y=50):
"""Create a visual turn order display."""
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
# Background frame
frame = mcrfpy.Frame(x, y, 200, 300)
frame.fill_color = mcrfpy.Color(30, 30, 30, 200)
frame.outline = 2
frame.outline_color = mcrfpy.Color(100, 100, 100)
ui.append(frame)
# Title
title = mcrfpy.Caption("Turn Order", x + 10, y + 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
return frame
def update_turn_order_display(frame, turn_manager, x=800, y=50):
"""Update the turn order display."""
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
# Clear old entries (keep frame and title)
# In practice, store references to caption objects and update them
for i, actor_data in enumerate(turn_manager.actors):
actor = actor_data["actor"]
is_current = (i == turn_manager.current)
# Actor name/type
name = getattr(actor, 'name', f"Actor {i}")
color = mcrfpy.Color(255, 255, 0) if is_current else mcrfpy.Color(200, 200, 200)
caption = mcrfpy.Caption(name, x + 10, y + 40 + i * 25)
caption.fill_color = color
ui.append(caption)

View file

@ -1,118 +0,0 @@
"""McRogueFace - Color Pulse Effect (basic)
Documentation: https://mcrogueface.github.io/cookbook/effects_color_pulse
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_color_pulse_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class PulsingCell:
"""A cell that continuously pulses until stopped."""
def __init__(self, grid, x, y, color, period=1.0, max_alpha=180):
"""
Args:
grid: Grid with color layer
x, y: Cell position
color: RGB tuple
period: Time for one complete pulse cycle
max_alpha: Maximum alpha value (0-255)
"""
self.grid = grid
self.x = x
self.y = y
self.color = color
self.period = period
self.max_alpha = max_alpha
self.is_pulsing = False
self.pulse_id = 0
self.cell = None
self._setup_layer()
def _setup_layer(self):
"""Ensure color layer exists and get cell reference."""
color_layer = None
for layer in self.grid.layers:
if isinstance(layer, mcrfpy.ColorLayer):
color_layer = layer
break
if not color_layer:
self.grid.add_layer("color")
color_layer = self.grid.layers[-1]
self.cell = color_layer.at(self.x, self.y)
if self.cell:
self.cell.color = mcrfpy.Color(self.color[0], self.color[1],
self.color[2], 0)
def start(self):
"""Start continuous pulsing."""
if self.is_pulsing or not self.cell:
return
self.is_pulsing = True
self.pulse_id += 1
self._pulse_up()
def _pulse_up(self):
"""Animate alpha increasing."""
if not self.is_pulsing:
return
current_id = self.pulse_id
half_period = self.period / 2
anim = mcrfpy.Animation("a", float(self.max_alpha), half_period, "easeInOut")
anim.start(self.cell.color)
def next_phase(timer_name):
if self.is_pulsing and self.pulse_id == current_id:
self._pulse_down()
mcrfpy.Timer(f"pulse_up_{id(self)}_{current_id}",
next_phase, int(half_period * 1000), once=True)
def _pulse_down(self):
"""Animate alpha decreasing."""
if not self.is_pulsing:
return
current_id = self.pulse_id
half_period = self.period / 2
anim = mcrfpy.Animation("a", 0.0, half_period, "easeInOut")
anim.start(self.cell.color)
def next_phase(timer_name):
if self.is_pulsing and self.pulse_id == current_id:
self._pulse_up()
mcrfpy.Timer(f"pulse_down_{id(self)}_{current_id}",
next_phase, int(half_period * 1000), once=True)
def stop(self):
"""Stop pulsing and fade out."""
self.is_pulsing = False
if self.cell:
anim = mcrfpy.Animation("a", 0.0, 0.2, "easeOut")
anim.start(self.cell.color)
def set_color(self, color):
"""Change pulse color."""
self.color = color
if self.cell:
current_alpha = self.cell.color.a
self.cell.color = mcrfpy.Color(color[0], color[1], color[2], current_alpha)
# Usage
objective_pulse = PulsingCell(grid, 10, 10, (0, 255, 100), period=1.5)
objective_pulse.start()
# Later, when objective is reached:
objective_pulse.stop()

View file

@ -1,61 +0,0 @@
"""McRogueFace - Color Pulse Effect (multi)
Documentation: https://mcrogueface.github.io/cookbook/effects_color_pulse
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_color_pulse_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
def ripple_effect(grid, center_x, center_y, color, max_radius=5, duration=1.0):
"""
Create an expanding ripple effect.
Args:
grid: Grid with color layer
center_x, center_y: Ripple origin
color: RGB tuple
max_radius: Maximum ripple size
duration: Total animation time
"""
# Get color layer
color_layer = None
for layer in grid.layers:
if isinstance(layer, mcrfpy.ColorLayer):
color_layer = layer
break
if not color_layer:
grid.add_layer("color")
color_layer = grid.layers[-1]
step_duration = duration / max_radius
for radius in range(max_radius + 1):
# Get cells at this radius (ring, not filled)
ring_cells = []
for dy in range(-radius, radius + 1):
for dx in range(-radius, radius + 1):
dist_sq = dx * dx + dy * dy
# Include cells approximately on the ring edge
if radius * radius - radius <= dist_sq <= radius * radius + radius:
cell = color_layer.at(center_x + dx, center_y + dy)
if cell:
ring_cells.append(cell)
# Schedule this ring to animate
def animate_ring(timer_name, cells=ring_cells, c=color):
for cell in cells:
cell.color = mcrfpy.Color(c[0], c[1], c[2], 200)
# Fade out
anim = mcrfpy.Animation("a", 0.0, step_duration * 2, "easeOut")
anim.start(cell.color)
delay = int(radius * step_duration * 1000)
mcrfpy.Timer(f"ripple_{radius}", animate_ring, delay, once=True)
# Usage
ripple_effect(grid, 10, 10, (100, 200, 255), max_radius=6, duration=0.8)

View file

@ -1,41 +0,0 @@
"""McRogueFace - Damage Flash Effect (basic)
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Add a color layer to your grid (do this once during setup)
grid.add_layer("color")
color_layer = grid.layers[-1] # Get the color layer
def flash_cell(grid, x, y, color, duration=0.3):
"""Flash a grid cell with a color overlay."""
# Get the color layer (assumes it's the last layer added)
color_layer = None
for layer in grid.layers:
if isinstance(layer, mcrfpy.ColorLayer):
color_layer = layer
break
if not color_layer:
return
# Set cell to flash color
cell = color_layer.at(x, y)
cell.color = mcrfpy.Color(color[0], color[1], color[2], 200)
# Animate alpha back to 0
anim = mcrfpy.Animation("a", 0.0, duration, "easeOut")
anim.start(cell.color)
def damage_at_position(grid, x, y, duration=0.3):
"""Flash red at a grid position when damage occurs."""
flash_cell(grid, x, y, (255, 0, 0), duration)
# Usage when entity takes damage
damage_at_position(grid, int(enemy.x), int(enemy.y))

View file

@ -1,85 +0,0 @@
"""McRogueFace - Damage Flash Effect (complete)
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_complete.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class DamageEffects:
"""Manages visual damage feedback effects."""
# Color presets
DAMAGE_RED = (255, 50, 50)
HEAL_GREEN = (50, 255, 50)
POISON_PURPLE = (150, 50, 200)
FIRE_ORANGE = (255, 150, 50)
ICE_BLUE = (100, 200, 255)
def __init__(self, grid):
self.grid = grid
self.color_layer = None
self._setup_color_layer()
def _setup_color_layer(self):
"""Ensure grid has a color layer for effects."""
self.grid.add_layer("color")
self.color_layer = self.grid.layers[-1]
def flash_entity(self, entity, color, duration=0.3):
"""Flash an entity with a color tint."""
# Flash at entity's grid position
x, y = int(entity.x), int(entity.y)
self.flash_cell(x, y, color, duration)
def flash_cell(self, x, y, color, duration=0.3):
"""Flash a specific grid cell."""
if not self.color_layer:
return
cell = self.color_layer.at(x, y)
if cell:
cell.color = mcrfpy.Color(color[0], color[1], color[2], 180)
# Fade out
anim = mcrfpy.Animation("a", 0.0, duration, "easeOut")
anim.start(cell.color)
def damage(self, entity, amount, duration=0.3):
"""Standard damage flash."""
self.flash_entity(entity, self.DAMAGE_RED, duration)
def heal(self, entity, amount, duration=0.4):
"""Healing effect - green flash."""
self.flash_entity(entity, self.HEAL_GREEN, duration)
def poison(self, entity, duration=0.5):
"""Poison damage - purple flash."""
self.flash_entity(entity, self.POISON_PURPLE, duration)
def fire(self, entity, duration=0.3):
"""Fire damage - orange flash."""
self.flash_entity(entity, self.FIRE_ORANGE, duration)
def ice(self, entity, duration=0.4):
"""Ice damage - blue flash."""
self.flash_entity(entity, self.ICE_BLUE, duration)
def area_damage(self, center_x, center_y, radius, color, duration=0.4):
"""Flash all cells in a radius."""
for dy in range(-radius, radius + 1):
for dx in range(-radius, radius + 1):
if dx * dx + dy * dy <= radius * radius:
self.flash_cell(center_x + dx, center_y + dy, color, duration)
# Setup
effects = DamageEffects(grid)
# Usage examples
effects.damage(player, 10) # Red flash
effects.heal(player, 5) # Green flash
effects.poison(enemy) # Purple flash
effects.area_damage(5, 5, 3, effects.FIRE_ORANGE) # Area effect

View file

@ -1,25 +0,0 @@
"""McRogueFace - Damage Flash Effect (multi)
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
def multi_flash(grid, x, y, color, flashes=3, flash_duration=0.1):
"""Flash a cell multiple times for emphasis."""
delay = 0
for i in range(flashes):
# Schedule each flash with increasing delay
def do_flash(timer_name, fx=x, fy=y, fc=color, fd=flash_duration):
flash_cell(grid, fx, fy, fc, fd)
mcrfpy.Timer(f"flash_{x}_{y}_{i}", do_flash, int(delay * 1000), once=True)
delay += flash_duration * 1.5 # Gap between flashes
# Usage for critical hit
multi_flash(grid, int(enemy.x), int(enemy.y), (255, 255, 0), flashes=3)

View file

@ -1,42 +0,0 @@
"""McRogueFace - Floating Damage Numbers (effects_floating_text)
Documentation: https://mcrogueface.github.io/cookbook/effects_floating_text
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_floating_text.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class StackedFloatingText:
"""Prevents overlapping text by stacking vertically."""
def __init__(self, scene_name, grid=None):
self.manager = FloatingTextManager(scene_name, grid)
self.position_stack = {} # Track recent spawns per position
def spawn_stacked(self, x, y, text, color, **kwargs):
"""Spawn with automatic vertical stacking."""
key = (int(x), int(y))
# Calculate offset based on recent spawns at this position
offset = self.position_stack.get(key, 0)
actual_y = y - (offset * 20) # 20 pixels between stacked texts
self.manager.spawn(x, actual_y, text, color, **kwargs)
# Increment stack counter
self.position_stack[key] = offset + 1
# Reset stack after delay
def reset_stack(timer_name, k=key):
if k in self.position_stack:
self.position_stack[k] = max(0, self.position_stack[k] - 1)
mcrfpy.Timer(f"stack_reset_{x}_{y}_{offset}", reset_stack, 300, once=True)
# Usage
stacked = StackedFloatingText("game", grid)
# Rapid hits will stack vertically instead of overlapping
stacked.spawn_stacked(5, 5, "-10", (255, 0, 0), is_grid_pos=True)
stacked.spawn_stacked(5, 5, "-8", (255, 0, 0), is_grid_pos=True)
stacked.spawn_stacked(5, 5, "-12", (255, 0, 0), is_grid_pos=True)

View file

@ -1,65 +0,0 @@
"""McRogueFace - Path Animation (Multi-Step Movement) (effects_path_animation)
Documentation: https://mcrogueface.github.io/cookbook/effects_path_animation
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_path_animation.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class CameraFollowingPath:
"""Path animator that also moves the camera."""
def __init__(self, entity, grid, path, step_duration=0.2):
self.entity = entity
self.grid = grid
self.path = path
self.step_duration = step_duration
self.index = 0
self.on_complete = None
def start(self):
self.index = 0
self._next()
def _next(self):
if self.index >= len(self.path):
if self.on_complete:
self.on_complete(self)
return
x, y = self.path[self.index]
def done(anim, target):
self.index += 1
self._next()
# Animate entity
if self.entity.x != x:
anim = mcrfpy.Animation("x", float(x), self.step_duration,
"easeInOut", callback=done)
anim.start(self.entity)
elif self.entity.y != y:
anim = mcrfpy.Animation("y", float(y), self.step_duration,
"easeInOut", callback=done)
anim.start(self.entity)
else:
done(None, None)
return
# Animate camera to follow
cam_x = mcrfpy.Animation("center_x", (x + 0.5) * 16,
self.step_duration, "easeInOut")
cam_y = mcrfpy.Animation("center_y", (y + 0.5) * 16,
self.step_duration, "easeInOut")
cam_x.start(self.grid)
cam_y.start(self.grid)
# Usage
path = [(5, 5), (5, 10), (10, 10)]
mover = CameraFollowingPath(player, grid, path)
mover.on_complete = lambda m: print("Journey complete!")
mover.start()

View file

@ -1,166 +0,0 @@
"""McRogueFace - Scene Transition Effects (effects_scene_transitions)
Documentation: https://mcrogueface.github.io/cookbook/effects_scene_transitions
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_scene_transitions.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class TransitionManager:
"""Manages scene transitions with multiple effect types."""
def __init__(self, screen_width=1024, screen_height=768):
self.width = screen_width
self.height = screen_height
self.is_transitioning = False
def go_to(self, scene_name, effect="fade", duration=0.5, **kwargs):
"""
Transition to a scene with the specified effect.
Args:
scene_name: Target scene
effect: "fade", "flash", "wipe", "instant"
duration: Transition duration
**kwargs: Effect-specific options (color, direction)
"""
if self.is_transitioning:
return
self.is_transitioning = True
if effect == "instant":
mcrfpy.setScene(scene_name)
self.is_transitioning = False
elif effect == "fade":
color = kwargs.get("color", (0, 0, 0))
self._fade(scene_name, duration, color)
elif effect == "flash":
color = kwargs.get("color", (255, 255, 255))
self._flash(scene_name, duration, color)
elif effect == "wipe":
direction = kwargs.get("direction", "right")
color = kwargs.get("color", (0, 0, 0))
self._wipe(scene_name, duration, direction, color)
def _fade(self, scene, duration, color):
half = duration / 2
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
overlay = mcrfpy.Frame(0, 0, self.width, self.height)
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0)
overlay.z_index = 9999
ui.append(overlay)
anim = mcrfpy.Animation("opacity", 1.0, half, "easeIn")
anim.start(overlay)
def phase2(timer_name):
mcrfpy.setScene(scene)
new_ui = mcrfpy.sceneUI(scene)
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
new_overlay.z_index = 9999
new_ui.append(new_overlay)
anim2 = mcrfpy.Animation("opacity", 0.0, half, "easeOut")
anim2.start(new_overlay)
def cleanup(timer_name):
for i, elem in enumerate(new_ui):
if elem is new_overlay:
new_ui.remove(i)
break
self.is_transitioning = False
mcrfpy.Timer("fade_done", cleanup, int(half * 1000) + 50, once=True)
mcrfpy.Timer("fade_switch", phase2, int(half * 1000), once=True)
def _flash(self, scene, duration, color):
quarter = duration / 4
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
overlay = mcrfpy.Frame(0, 0, self.width, self.height)
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0)
overlay.z_index = 9999
ui.append(overlay)
anim = mcrfpy.Animation("opacity", 1.0, quarter, "easeOut")
anim.start(overlay)
def phase2(timer_name):
mcrfpy.setScene(scene)
new_ui = mcrfpy.sceneUI(scene)
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
new_overlay.z_index = 9999
new_ui.append(new_overlay)
anim2 = mcrfpy.Animation("opacity", 0.0, duration / 2, "easeIn")
anim2.start(new_overlay)
def cleanup(timer_name):
for i, elem in enumerate(new_ui):
if elem is new_overlay:
new_ui.remove(i)
break
self.is_transitioning = False
mcrfpy.Timer("flash_done", cleanup, int(duration * 500) + 50, once=True)
mcrfpy.Timer("flash_switch", phase2, int(quarter * 2000), once=True)
def _wipe(self, scene, duration, direction, color):
# Simplified wipe - right direction only for brevity
half = duration / 2
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
overlay = mcrfpy.Frame(0, 0, 0, self.height)
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
overlay.z_index = 9999
ui.append(overlay)
anim = mcrfpy.Animation("w", float(self.width), half, "easeInOut")
anim.start(overlay)
def phase2(timer_name):
mcrfpy.setScene(scene)
new_ui = mcrfpy.sceneUI(scene)
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
new_overlay.z_index = 9999
new_ui.append(new_overlay)
anim2 = mcrfpy.Animation("x", float(self.width), half, "easeInOut")
anim2.start(new_overlay)
def cleanup(timer_name):
for i, elem in enumerate(new_ui):
if elem is new_overlay:
new_ui.remove(i)
break
self.is_transitioning = False
mcrfpy.Timer("wipe_done", cleanup, int(half * 1000) + 50, once=True)
mcrfpy.Timer("wipe_switch", phase2, int(half * 1000), once=True)
# Usage
transitions = TransitionManager()
# Various transition styles
transitions.go_to("game", effect="fade", duration=0.5)
transitions.go_to("menu", effect="flash", color=(255, 255, 255), duration=0.4)
transitions.go_to("next_level", effect="wipe", direction="right", duration=0.6)
transitions.go_to("options", effect="instant")

View file

@ -1,38 +0,0 @@
"""McRogueFace - Screen Shake Effect (basic)
Documentation: https://mcrogueface.github.io/cookbook/effects_screen_shake
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_screen_shake_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
def screen_shake(frame, intensity=5, duration=0.2):
"""
Shake a frame/container by animating its position.
Args:
frame: The UI Frame to shake (often a container for all game elements)
intensity: Maximum pixel offset
duration: Total shake duration in seconds
"""
original_x = frame.x
original_y = frame.y
# Quick shake to offset position
shake_x = mcrfpy.Animation("x", float(original_x + intensity), duration / 4, "easeOut")
shake_x.start(frame)
# Schedule return to center
def return_to_center(timer_name):
anim = mcrfpy.Animation("x", float(original_x), duration / 2, "easeInOut")
anim.start(frame)
mcrfpy.Timer("shake_return", return_to_center, int(duration * 250), once=True)
# Usage - wrap your game content in a Frame
game_container = mcrfpy.Frame(0, 0, 1024, 768)
# ... add game elements to game_container.children ...
screen_shake(game_container, intensity=8, duration=0.3)

View file

@ -1,58 +0,0 @@
"""McRogueFace - Screen Shake Effect (multi)
Documentation: https://mcrogueface.github.io/cookbook/effects_screen_shake
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_screen_shake_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
import math
def directional_shake(shaker, direction_x, direction_y, intensity=10, duration=0.2):
"""
Shake in a specific direction (e.g., direction of impact).
Args:
shaker: ScreenShakeManager instance
direction_x, direction_y: Direction vector (will be normalized)
intensity: Shake strength
duration: Shake duration
"""
# Normalize direction
length = math.sqrt(direction_x * direction_x + direction_y * direction_y)
if length == 0:
return
dir_x = direction_x / length
dir_y = direction_y / length
# Shake in the direction, then opposite, then back
shaker._animate_position(
shaker.original_x + dir_x * intensity,
shaker.original_y + dir_y * intensity,
duration / 3
)
def reverse(timer_name):
shaker._animate_position(
shaker.original_x - dir_x * intensity * 0.5,
shaker.original_y - dir_y * intensity * 0.5,
duration / 3
)
def reset(timer_name):
shaker._animate_position(
shaker.original_x,
shaker.original_y,
duration / 3
)
shaker.is_shaking = False
mcrfpy.Timer("dir_shake_rev", reverse, int(duration * 333), once=True)
mcrfpy.Timer("dir_shake_reset", reset, int(duration * 666), once=True)
# Usage: shake away from impact direction
hit_from_x, hit_from_y = -1, 0 # Hit from the left
directional_shake(shaker, hit_from_x, hit_from_y, intensity=12)

View file

@ -1,74 +0,0 @@
"""McRogueFace - Cell Highlighting (Targeting) (animated)
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_animated.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class TargetingSystem:
"""Handle ability targeting with visual feedback."""
def __init__(self, grid, player):
self.grid = grid
self.player = player
self.highlights = HighlightManager(grid)
self.current_ability = None
self.valid_targets = set()
def start_targeting(self, ability):
"""Begin targeting for an ability."""
self.current_ability = ability
px, py = self.player.pos
# Get valid targets based on ability
if ability.target_type == 'self':
self.valid_targets = {(px, py)}
elif ability.target_type == 'adjacent':
self.valid_targets = get_adjacent(px, py)
elif ability.target_type == 'ranged':
self.valid_targets = get_radius_range(px, py, ability.range)
elif ability.target_type == 'line':
self.valid_targets = get_line_range(px, py, ability.range)
# Filter to visible tiles only
self.valid_targets = {
(x, y) for x, y in self.valid_targets
if grid.is_in_fov(x, y)
}
# Show valid targets
self.highlights.add('attack', self.valid_targets)
def update_hover(self, x, y):
"""Update when cursor moves."""
if not self.current_ability:
return
# Clear previous AoE preview
self.highlights.remove('danger')
if (x, y) in self.valid_targets:
# Valid target - highlight it
self.highlights.add('select', [(x, y)])
# Show AoE if applicable
if self.current_ability.aoe_radius > 0:
aoe = get_radius_range(x, y, self.current_ability.aoe_radius, True)
self.highlights.add('danger', aoe)
else:
self.highlights.remove('select')
def confirm_target(self, x, y):
"""Confirm target selection."""
if (x, y) in self.valid_targets:
self.cancel_targeting()
return (x, y)
return None
def cancel_targeting(self):
"""Cancel targeting mode."""
self.current_ability = None
self.valid_targets = set()
self.highlights.clear()

View file

@ -1,74 +0,0 @@
"""McRogueFace - Cell Highlighting (Targeting) (basic)
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def get_line_range(start_x, start_y, max_range):
"""Get cells in cardinal directions (ranged attack)."""
cells = set()
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
for dist in range(1, max_range + 1):
x = start_x + dx * dist
y = start_y + dy * dist
# Stop if wall blocks line of sight
if not grid.at(x, y).transparent:
break
cells.add((x, y))
return cells
def get_radius_range(center_x, center_y, radius, include_center=False):
"""Get cells within a radius (spell area)."""
cells = set()
for x in range(center_x - radius, center_x + radius + 1):
for y in range(center_y - radius, center_y + radius + 1):
# Euclidean distance
dist = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5
if dist <= radius:
if include_center or (x, y) != (center_x, center_y):
cells.add((x, y))
return cells
def get_cone_range(origin_x, origin_y, direction, length, spread):
"""Get cells in a cone (breath attack)."""
import math
cells = set()
# Direction angles (in radians)
angles = {
'n': -math.pi / 2,
's': math.pi / 2,
'e': 0,
'w': math.pi,
'ne': -math.pi / 4,
'nw': -3 * math.pi / 4,
'se': math.pi / 4,
'sw': 3 * math.pi / 4
}
base_angle = angles.get(direction, 0)
half_spread = math.radians(spread / 2)
for x in range(origin_x - length, origin_x + length + 1):
for y in range(origin_y - length, origin_y + length + 1):
dx = x - origin_x
dy = y - origin_y
dist = (dx * dx + dy * dy) ** 0.5
if dist > 0 and dist <= length:
angle = math.atan2(dy, dx)
angle_diff = abs((angle - base_angle + math.pi) % (2 * math.pi) - math.pi)
if angle_diff <= half_spread:
cells.add((x, y))
return cells

View file

@ -1,23 +0,0 @@
"""McRogueFace - Cell Highlighting (Targeting) (multi)
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def show_path_preview(start, end):
"""Highlight the path between two points."""
path = find_path(start, end) # Your pathfinding function
if path:
highlights.add('path', path)
# Highlight destination specially
highlights.add('select', [end])
def hide_path_preview():
"""Clear path display."""
highlights.remove('path')
highlights.remove('select')

View file

@ -1,31 +0,0 @@
"""McRogueFace - Dijkstra Distance Maps (basic)
Documentation: https://mcrogueface.github.io/cookbook/grid_dijkstra
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dijkstra_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def ai_flee(entity, threat_x, threat_y):
"""Move entity away from threat using Dijkstra map."""
grid.compute_dijkstra(threat_x, threat_y)
ex, ey = entity.pos
current_dist = grid.get_dijkstra_distance(ex, ey)
# Find neighbor with highest distance
best_move = None
best_dist = current_dist
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
nx, ny = ex + dx, ey + dy
if grid.at(nx, ny).walkable:
dist = grid.get_dijkstra_distance(nx, ny)
if dist > best_dist:
best_dist = dist
best_move = (nx, ny)
if best_move:
entity.pos = best_move

View file

@ -1,44 +0,0 @@
"""McRogueFace - Dijkstra Distance Maps (multi)
Documentation: https://mcrogueface.github.io/cookbook/grid_dijkstra
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dijkstra_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
# Cache Dijkstra maps when possible
class CachedDijkstra:
"""Cache Dijkstra computations."""
def __init__(self, grid):
self.grid = grid
self.cache = {}
self.cache_valid = False
def invalidate(self):
"""Call when map changes."""
self.cache = {}
self.cache_valid = False
def get_distance(self, from_x, from_y, to_x, to_y):
"""Get cached distance or compute."""
key = (to_x, to_y) # Cache by destination
if key not in self.cache:
self.grid.compute_dijkstra(to_x, to_y)
# Store all distances from this computation
self.cache[key] = self._snapshot_distances()
return self.cache[key].get((from_x, from_y), float('inf'))
def _snapshot_distances(self):
"""Capture current distance values."""
grid_w, grid_h = self.grid.grid_size
distances = {}
for x in range(grid_w):
for y in range(grid_h):
dist = self.grid.get_dijkstra_distance(x, y)
if dist != float('inf'):
distances[(x, y)] = dist
return distances

View file

@ -1,125 +0,0 @@
"""McRogueFace - Room and Corridor Generator (basic)
Documentation: https://mcrogueface.github.io/cookbook/grid_dungeon_generator
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dungeon_generator_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class BSPNode:
"""Node in a BSP tree for dungeon generation."""
MIN_SIZE = 6
def __init__(self, x, y, w, h):
self.x = x
self.y = y
self.w = w
self.h = h
self.left = None
self.right = None
self.room = None
def split(self):
"""Recursively split this node."""
if self.left or self.right:
return False
# Choose split direction
if self.w > self.h and self.w / self.h >= 1.25:
horizontal = False
elif self.h > self.w and self.h / self.w >= 1.25:
horizontal = True
else:
horizontal = random.random() < 0.5
max_size = (self.h if horizontal else self.w) - self.MIN_SIZE
if max_size <= self.MIN_SIZE:
return False
split = random.randint(self.MIN_SIZE, max_size)
if horizontal:
self.left = BSPNode(self.x, self.y, self.w, split)
self.right = BSPNode(self.x, self.y + split, self.w, self.h - split)
else:
self.left = BSPNode(self.x, self.y, split, self.h)
self.right = BSPNode(self.x + split, self.y, self.w - split, self.h)
return True
def create_rooms(self, grid):
"""Create rooms in leaf nodes and connect siblings."""
if self.left or self.right:
if self.left:
self.left.create_rooms(grid)
if self.right:
self.right.create_rooms(grid)
# Connect children
if self.left and self.right:
left_room = self.left.get_room()
right_room = self.right.get_room()
if left_room and right_room:
connect_points(grid, left_room.center, right_room.center)
else:
# Leaf node - create room
w = random.randint(3, self.w - 2)
h = random.randint(3, self.h - 2)
x = self.x + random.randint(1, self.w - w - 1)
y = self.y + random.randint(1, self.h - h - 1)
self.room = Room(x, y, w, h)
carve_room(grid, self.room)
def get_room(self):
"""Get a room from this node or its children."""
if self.room:
return self.room
left_room = self.left.get_room() if self.left else None
right_room = self.right.get_room() if self.right else None
if left_room and right_room:
return random.choice([left_room, right_room])
return left_room or right_room
def generate_bsp_dungeon(grid, iterations=4):
"""Generate a BSP-based dungeon."""
grid_w, grid_h = grid.grid_size
# Fill with walls
for x in range(grid_w):
for y in range(grid_h):
point = grid.at(x, y)
point.tilesprite = TILE_WALL
point.walkable = False
point.transparent = False
# Build BSP tree
root = BSPNode(0, 0, grid_w, grid_h)
nodes = [root]
for _ in range(iterations):
new_nodes = []
for node in nodes:
if node.split():
new_nodes.extend([node.left, node.right])
nodes = new_nodes or nodes
# Create rooms and corridors
root.create_rooms(grid)
# Collect all rooms
rooms = []
def collect_rooms(node):
if node.room:
rooms.append(node.room)
if node.left:
collect_rooms(node.left)
if node.right:
collect_rooms(node.right)
collect_rooms(root)
return rooms

View file

@ -1,148 +0,0 @@
"""McRogueFace - Room and Corridor Generator (complete)
Documentation: https://mcrogueface.github.io/cookbook/grid_dungeon_generator
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dungeon_generator_complete.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
import random
# Tile indices (adjust for your tileset)
TILE_FLOOR = 0
TILE_WALL = 1
TILE_DOOR = 2
TILE_STAIRS_DOWN = 3
TILE_STAIRS_UP = 4
class DungeonGenerator:
"""Procedural dungeon generator with rooms and corridors."""
def __init__(self, grid, seed=None):
self.grid = grid
self.grid_w, self.grid_h = grid.grid_size
self.rooms = []
if seed is not None:
random.seed(seed)
def generate(self, room_count=8, min_room=4, max_room=10):
"""Generate a complete dungeon level."""
self.rooms = []
# Fill with walls
self._fill_walls()
# Place rooms
attempts = 0
max_attempts = room_count * 10
while len(self.rooms) < room_count and attempts < max_attempts:
attempts += 1
# Random room size
w = random.randint(min_room, max_room)
h = random.randint(min_room, max_room)
# Random position (leaving border)
x = random.randint(1, self.grid_w - w - 2)
y = random.randint(1, self.grid_h - h - 2)
room = Room(x, y, w, h)
# Check overlap
if not any(room.intersects(r) for r in self.rooms):
self._carve_room(room)
# Connect to previous room
if self.rooms:
self._dig_corridor(self.rooms[-1].center, room.center)
self.rooms.append(room)
# Place stairs
if len(self.rooms) >= 2:
self._place_stairs()
return self.rooms
def _fill_walls(self):
"""Fill the entire grid with wall tiles."""
for x in range(self.grid_w):
for y in range(self.grid_h):
point = self.grid.at(x, y)
point.tilesprite = TILE_WALL
point.walkable = False
point.transparent = False
def _carve_room(self, room):
"""Carve out a room, making it walkable."""
for x in range(room.x, room.x + room.width):
for y in range(room.y, room.y + room.height):
self._set_floor(x, y)
def _set_floor(self, x, y):
"""Set a single tile as floor."""
if 0 <= x < self.grid_w and 0 <= y < self.grid_h:
point = self.grid.at(x, y)
point.tilesprite = TILE_FLOOR
point.walkable = True
point.transparent = True
def _dig_corridor(self, start, end):
"""Dig an L-shaped corridor between two points."""
x1, y1 = start
x2, y2 = end
# Randomly choose horizontal-first or vertical-first
if random.random() < 0.5:
# Horizontal then vertical
self._dig_horizontal(x1, x2, y1)
self._dig_vertical(y1, y2, x2)
else:
# Vertical then horizontal
self._dig_vertical(y1, y2, x1)
self._dig_horizontal(x1, x2, y2)
def _dig_horizontal(self, x1, x2, y):
"""Dig a horizontal tunnel."""
for x in range(min(x1, x2), max(x1, x2) + 1):
self._set_floor(x, y)
def _dig_vertical(self, y1, y2, x):
"""Dig a vertical tunnel."""
for y in range(min(y1, y2), max(y1, y2) + 1):
self._set_floor(x, y)
def _place_stairs(self):
"""Place stairs in first and last rooms."""
# Stairs up in first room
start_room = self.rooms[0]
sx, sy = start_room.center
point = self.grid.at(sx, sy)
point.tilesprite = TILE_STAIRS_UP
# Stairs down in last room
end_room = self.rooms[-1]
ex, ey = end_room.center
point = self.grid.at(ex, ey)
point.tilesprite = TILE_STAIRS_DOWN
return (sx, sy), (ex, ey)
def get_spawn_point(self):
"""Get a good spawn point for the player."""
if self.rooms:
return self.rooms[0].center
return (self.grid_w // 2, self.grid_h // 2)
def get_random_floor(self):
"""Get a random walkable floor tile."""
floors = []
for x in range(self.grid_w):
for y in range(self.grid_h):
if self.grid.at(x, y).walkable:
floors.append((x, y))
return random.choice(floors) if floors else None

View file

@ -1,20 +0,0 @@
"""McRogueFace - Basic Fog of War (grid_fog_of_war)
Documentation: https://mcrogueface.github.io/cookbook/grid_fog_of_war
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_fog_of_war.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
# Shadowcasting (default) - fast and produces nice results
grid.compute_fov(x, y, 10, mcrfpy.FOV.SHADOW)
# Recursive shadowcasting - slightly different corner behavior
grid.compute_fov(x, y, 10, mcrfpy.FOV.RECURSIVE_SHADOW)
# Diamond - simple but produces diamond-shaped FOV
grid.compute_fov(x, y, 10, mcrfpy.FOV.DIAMOND)
# Permissive - sees more tiles, good for tactical games
grid.compute_fov(x, y, 10, mcrfpy.FOV.PERMISSIVE)

View file

@ -1,114 +0,0 @@
"""McRogueFace - Multi-Layer Tiles (basic)
Documentation: https://mcrogueface.github.io/cookbook/grid_multi_layer
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_multi_layer_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class EffectLayer:
"""Manage visual effects with color overlays."""
def __init__(self, grid, z_index=2):
self.grid = grid
self.layer = grid.add_layer("color", z_index=z_index)
self.effects = {} # (x, y) -> effect_data
def add_effect(self, x, y, effect_type, duration=None, **kwargs):
"""Add a visual effect."""
self.effects[(x, y)] = {
'type': effect_type,
'duration': duration,
'time': 0,
**kwargs
}
def remove_effect(self, x, y):
"""Remove an effect."""
if (x, y) in self.effects:
del self.effects[(x, y)]
self.layer.set(x, y, mcrfpy.Color(0, 0, 0, 0))
def update(self, dt):
"""Update all effects."""
import math
to_remove = []
for (x, y), effect in self.effects.items():
effect['time'] += dt
# Check expiration
if effect['duration'] and effect['time'] >= effect['duration']:
to_remove.append((x, y))
continue
# Calculate color based on effect type
color = self._calculate_color(effect)
self.layer.set(x, y, color)
for pos in to_remove:
self.remove_effect(*pos)
def _calculate_color(self, effect):
"""Get color for an effect at current time."""
import math
t = effect['time']
effect_type = effect['type']
if effect_type == 'fire':
# Flickering orange/red
flicker = 0.7 + 0.3 * math.sin(t * 10)
return mcrfpy.Color(
255,
int(100 + 50 * math.sin(t * 8)),
0,
int(180 * flicker)
)
elif effect_type == 'poison':
# Pulsing green
pulse = 0.5 + 0.5 * math.sin(t * 3)
return mcrfpy.Color(0, 200, 0, int(100 * pulse))
elif effect_type == 'ice':
# Static blue with shimmer
shimmer = 0.8 + 0.2 * math.sin(t * 5)
return mcrfpy.Color(100, 150, 255, int(120 * shimmer))
elif effect_type == 'blood':
# Fading red
duration = effect.get('duration', 5)
fade = 1 - (t / duration) if duration else 1
return mcrfpy.Color(150, 0, 0, int(150 * fade))
elif effect_type == 'highlight':
# Pulsing highlight
pulse = 0.5 + 0.5 * math.sin(t * 4)
base = effect.get('color', mcrfpy.Color(255, 255, 0, 100))
return mcrfpy.Color(base.r, base.g, base.b, int(base.a * pulse))
return mcrfpy.Color(128, 128, 128, 50)
# Usage
effects = EffectLayer(grid)
# Add fire effect (permanent)
effects.add_effect(5, 5, 'fire')
# Add blood stain (fades over 10 seconds)
effects.add_effect(10, 10, 'blood', duration=10)
# Add poison cloud
for x in range(8, 12):
for y in range(8, 12):
effects.add_effect(x, y, 'poison', duration=5)
# Update in game loop
def game_update(runtime):
effects.update(0.016) # 60 FPS
mcrfpy.setTimer("effects", game_update, 16)

View file

@ -1,38 +0,0 @@
"""McRogueFace - Multi-Layer Tiles (complete)
Documentation: https://mcrogueface.github.io/cookbook/grid_multi_layer
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_multi_layer_complete.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
class OptimizedLayers:
"""Performance-optimized layer management."""
def __init__(self, grid):
self.grid = grid
self.dirty_effects = set() # Only update changed cells
self.batch_updates = []
def mark_dirty(self, x, y):
"""Mark a cell as needing update."""
self.dirty_effects.add((x, y))
def batch_set(self, layer, cells_and_values):
"""Queue batch updates."""
self.batch_updates.append((layer, cells_and_values))
def flush(self):
"""Apply all queued updates."""
for layer, updates in self.batch_updates:
for x, y, value in updates:
layer.set(x, y, value)
self.batch_updates = []
def update_dirty_only(self, effect_layer, effect_calculator):
"""Only update cells marked dirty."""
for x, y in self.dirty_effects:
color = effect_calculator(x, y)
effect_layer.set(x, y, color)
self.dirty_effects.clear()

View file

@ -1,89 +0,0 @@
"""HeightMap Hills and Craters Demo
Demonstrates: add_hill, dig_hill
Creates volcanic terrain with mountains and craters using ColorLayer visualization.
"""
import mcrfpy
from mcrfpy import automation
# Full screen grid: 60x48 tiles at 16x16 = 960x768
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def height_to_color(h):
"""Convert height value to terrain color."""
if h < 0.1:
return mcrfpy.Color(20, 40, int(80 + h * 400))
elif h < 0.3:
t = (h - 0.1) / 0.2
return mcrfpy.Color(int(40 + t * 30), int(60 + t * 40), 30)
elif h < 0.5:
t = (h - 0.3) / 0.2
return mcrfpy.Color(int(70 - t * 20), int(100 + t * 50), int(30 + t * 20))
elif h < 0.7:
t = (h - 0.5) / 0.2
return mcrfpy.Color(int(120 + t * 40), int(100 + t * 30), int(60 + t * 20))
elif h < 0.85:
t = (h - 0.7) / 0.15
return mcrfpy.Color(int(140 + t * 40), int(130 + t * 40), int(120 + t * 40))
else:
t = (h - 0.85) / 0.15
return mcrfpy.Color(int(180 + t * 75), int(180 + t * 75), int(180 + t * 75))
# Setup scene
scene = mcrfpy.Scene("hills_demo")
# Create grid with color layer
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
# Create heightmap
hmap = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.3)
# Add volcanic mountains - large hills
hmap.add_hill((15, 24), 18.0, 0.6) # Central volcano base
hmap.add_hill((15, 24), 10.0, 0.3) # Volcano peak
hmap.add_hill((45, 15), 12.0, 0.5) # Eastern mountain
hmap.add_hill((35, 38), 14.0, 0.45) # Southern mountain
hmap.add_hill((8, 10), 8.0, 0.35) # Small northern hill
# Create craters using dig_hill
hmap.dig_hill((15, 24), 5.0, 0.1) # Volcanic crater
hmap.dig_hill((45, 15), 4.0, 0.25) # Eastern crater
hmap.dig_hill((25, 30), 6.0, 0.05) # Impact crater (deep)
hmap.dig_hill((50, 40), 3.0, 0.2) # Small crater
# Add some smaller features for variety
for i in range(8):
x = 5 + (i * 7) % 55
y = 5 + (i * 11) % 40
hmap.add_hill((x, y), float(3 + (i % 4)), 0.15)
# Normalize to use full color range
hmap.normalize(0.0, 1.0)
# Apply heightmap to color layer
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
h = hmap.get((x, y))
color_layer.set((x, y), height_to_color(h))
# Title
title = mcrfpy.Caption(text="HeightMap: add_hill + dig_hill (volcanic terrain)", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
scene.activate()
# Take screenshot directly (works in headless mode)
automation.screenshot("procgen_01_heightmap_hills.png")
print("Screenshot saved: procgen_01_heightmap_hills.png")

View file

@ -1,124 +0,0 @@
"""HeightMap Noise Integration Demo
Demonstrates: add_noise, multiply_noise with NoiseSource
Shows terrain generation using different noise modes (flat, fbm, turbulence).
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def terrain_color(h):
"""Height-based terrain coloring."""
if h < 0.25:
# Water - deep to shallow blue
t = h / 0.25
return mcrfpy.Color(int(30 + t * 30), int(60 + t * 60), int(120 + t * 80))
elif h < 0.35:
# Beach/sand
t = (h - 0.25) / 0.1
return mcrfpy.Color(int(180 + t * 40), int(160 + t * 30), int(100 + t * 20))
elif h < 0.6:
# Grass - varies with height
t = (h - 0.35) / 0.25
return mcrfpy.Color(int(50 + t * 30), int(120 + t * 40), int(40 + t * 20))
elif h < 0.75:
# Forest/hills
t = (h - 0.6) / 0.15
return mcrfpy.Color(int(40 - t * 10), int(80 + t * 20), int(30 + t * 10))
elif h < 0.88:
# Rock/mountain
t = (h - 0.75) / 0.13
return mcrfpy.Color(int(100 + t * 40), int(90 + t * 40), int(80 + t * 40))
else:
# Snow peaks
t = (h - 0.88) / 0.12
return mcrfpy.Color(int(200 + t * 55), int(200 + t * 55), int(210 + t * 45))
def apply_to_layer(hmap, layer):
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
h = hmap.get((x, y))
layer.set(x, y, terrain_color(h))
def run_demo(runtime):
# Create three panels showing different noise modes
panel_width = GRID_WIDTH // 3
right_panel_width = GRID_WIDTH - 2 * panel_width # Handle non-divisible widths
# Create noise source with consistent seed
noise = mcrfpy.NoiseSource(
dimensions=2,
algorithm='simplex',
hurst=0.5,
lacunarity=2.0,
seed=42
)
# Left panel: Flat noise (single octave, raw)
left_hmap = mcrfpy.HeightMap((panel_width, GRID_HEIGHT), fill=0.0)
left_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='flat', octaves=1)
left_hmap.normalize(0.0, 1.0)
# Middle panel: FBM noise (fractal brownian motion - natural terrain)
mid_hmap = mcrfpy.HeightMap((panel_width, GRID_HEIGHT), fill=0.0)
mid_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='fbm', octaves=6)
mid_hmap.normalize(0.0, 1.0)
# Right panel: Turbulence (absolute value - clouds, marble)
right_hmap = mcrfpy.HeightMap((right_panel_width, GRID_HEIGHT), fill=0.0)
right_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='turbulence', octaves=6)
right_hmap.normalize(0.0, 1.0)
# Apply to color layer with panel divisions
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if x < panel_width:
h = left_hmap.get((x, y))
elif x < panel_width * 2:
h = mid_hmap.get((x - panel_width, y))
else:
h = right_hmap.get((x - panel_width * 2, y))
color_layer.set(((x, y)), terrain_color(h))
# Add divider lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_width - 1, y)), mcrfpy.Color(255, 255, 255, 100))
color_layer.set(((panel_width * 2 - 1, y)), mcrfpy.Color(255, 255, 255, 100))
# Setup scene
scene = mcrfpy.Scene("noise_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
# Labels for each panel
labels = [
("FLAT (raw)", 10),
("FBM (terrain)", GRID_WIDTH * CELL_SIZE // 3 + 10),
("TURBULENCE (clouds)", GRID_WIDTH * CELL_SIZE * 2 // 3 + 10)
]
for text, x in labels:
label = mcrfpy.Caption(text=text, pos=(x, 10))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_02_heightmap_noise.png")
print("Screenshot saved: procgen_02_heightmap_noise.png")

View file

@ -1,116 +0,0 @@
"""HeightMap Combination Operations Demo
Demonstrates: add, subtract, multiply, min, max, lerp, copy_from
Shows how heightmaps can be combined for complex terrain effects.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def value_to_color(h):
"""Simple grayscale with color tinting for visibility."""
h = max(0.0, min(1.0, h))
# Blue-white-red gradient for clear visualization
if h < 0.5:
t = h / 0.5
return mcrfpy.Color(int(50 * t), int(100 * t), int(200 - 100 * t))
else:
t = (h - 0.5) / 0.5
return mcrfpy.Color(int(50 + 200 * t), int(100 + 100 * t), int(100 - 50 * t))
def run_demo(runtime):
# Create 6 panels (2 rows x 3 columns)
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Create two base heightmaps for operations
noise1 = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
noise2 = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=123)
base1 = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
base1.add_noise(noise1, world_size=(10, 10), mode='fbm', octaves=4)
base1.normalize(0.0, 1.0)
base2 = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
base2.add_noise(noise2, world_size=(10, 10), mode='fbm', octaves=4)
base2.normalize(0.0, 1.0)
# Panel 1: ADD operation (combined terrain)
add_result = base1.copy_from(base1) # Actually need to create new
add_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
add_result.copy_from(base1).add(base2).normalize(0.0, 1.0)
# Panel 2: SUBTRACT operation (carving)
sub_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
sub_result.copy_from(base1).subtract(base2).normalize(0.0, 1.0)
# Panel 3: MULTIPLY operation (masking)
mul_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
mul_result.copy_from(base1).multiply(base2).normalize(0.0, 1.0)
# Panel 4: MIN operation (valleys)
min_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
min_result.copy_from(base1).min(base2)
# Panel 5: MAX operation (ridges)
max_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
max_result.copy_from(base1).max(base2)
# Panel 6: LERP operation (blending)
lerp_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
lerp_result.copy_from(base1).lerp(base2, 0.5)
# Apply panels to grid
panels = [
(add_result, 0, 0, "ADD"),
(sub_result, panel_w, 0, "SUBTRACT"),
(mul_result, panel_w * 2, 0, "MULTIPLY"),
(min_result, 0, panel_h, "MIN"),
(max_result, panel_w, panel_h, "MAX"),
(lerp_result, panel_w * 2, panel_h, "LERP(0.5)"),
]
for hmap, ox, oy, name in panels:
for y in range(panel_h):
for x in range(panel_w):
h = hmap.get((x, y))
color_layer.set(((ox + x, oy + y)), value_to_color(h))
# Add label
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Draw grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(255, 255, 255, 80))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(255, 255, 255, 80))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(255, 255, 255, 80))
# Setup
scene = mcrfpy.Scene("operations_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_03_heightmap_operations.png")
print("Screenshot saved: procgen_03_heightmap_operations.png")

View file

@ -1,116 +0,0 @@
"""HeightMap Transform Operations Demo
Demonstrates: scale, clamp, normalize, smooth, kernel_transform
Shows value manipulation and convolution effects.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def value_to_color(h):
"""Grayscale with enhanced contrast."""
h = max(0.0, min(1.0, h))
v = int(h * 255)
return mcrfpy.Color(v, v, v)
def run_demo(runtime):
# Create 6 panels showing different transforms
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Source noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
# Create base terrain with features
base = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
base.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=4)
base.add_hill((panel_w // 2, panel_h // 2), 8, 0.5)
base.normalize(0.0, 1.0)
# Panel 1: Original
original = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
original.copy_from(base)
# Panel 2: SCALE (amplify contrast)
scaled = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
scaled.copy_from(base).add_constant(-0.5).scale(2.0).clamp(0.0, 1.0)
# Panel 3: CLAMP (plateau effect)
clamped = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
clamped.copy_from(base).clamp(0.3, 0.7).normalize(0.0, 1.0)
# Panel 4: SMOOTH (blur/average)
smoothed = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
smoothed.copy_from(base).smooth(3)
# Panel 5: SHARPEN kernel
sharpened = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
sharpened.copy_from(base)
sharpen_kernel = {
(0, -1): -1.0, (-1, 0): -1.0, (0, 0): 5.0, (1, 0): -1.0, (0, 1): -1.0
}
sharpened.kernel_transform(sharpen_kernel).clamp(0.0, 1.0)
# Panel 6: EDGE DETECTION kernel
edges = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
edges.copy_from(base)
edge_kernel = {
(-1, -1): -1, (0, -1): -1, (1, -1): -1,
(-1, 0): -1, (0, 0): 8, (1, 0): -1,
(-1, 1): -1, (0, 1): -1, (1, 1): -1,
}
edges.kernel_transform(edge_kernel).normalize(0.0, 1.0)
# Apply to grid
panels = [
(original, 0, 0, "ORIGINAL"),
(scaled, panel_w, 0, "SCALE (contrast)"),
(clamped, panel_w * 2, 0, "CLAMP (plateau)"),
(smoothed, 0, panel_h, "SMOOTH (blur)"),
(sharpened, panel_w, panel_h, "SHARPEN kernel"),
(edges, panel_w * 2, panel_h, "EDGE DETECT"),
]
for hmap, ox, oy, name in panels:
for y in range(panel_h):
for x in range(panel_w):
h = hmap.get((x, y))
color_layer.set(((ox + x, oy + y)), value_to_color(h))
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 0)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(100, 100, 100))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(100, 100, 100))
# Setup
scene = mcrfpy.Scene("transforms_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_04_heightmap_transforms.png")
print("Screenshot saved: procgen_04_heightmap_transforms.png")

View file

@ -1,135 +0,0 @@
"""HeightMap Erosion and Terrain Generation Demo
Demonstrates: rain_erosion, mid_point_displacement, smooth
Shows natural terrain formation through erosion simulation.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def terrain_color(h):
"""Natural terrain coloring."""
if h < 0.2:
# Deep water
t = h / 0.2
return mcrfpy.Color(int(20 + t * 30), int(40 + t * 40), int(100 + t * 55))
elif h < 0.3:
# Shallow water
t = (h - 0.2) / 0.1
return mcrfpy.Color(int(50 + t * 50), int(80 + t * 60), int(155 + t * 40))
elif h < 0.35:
# Beach
t = (h - 0.3) / 0.05
return mcrfpy.Color(int(194 - t * 30), int(178 - t * 30), int(128 - t * 20))
elif h < 0.55:
# Lowland grass
t = (h - 0.35) / 0.2
return mcrfpy.Color(int(80 + t * 20), int(140 - t * 30), int(60 + t * 10))
elif h < 0.7:
# Highland grass/forest
t = (h - 0.55) / 0.15
return mcrfpy.Color(int(50 + t * 30), int(100 + t * 10), int(40 + t * 20))
elif h < 0.85:
# Rock
t = (h - 0.7) / 0.15
return mcrfpy.Color(int(100 + t * 30), int(95 + t * 30), int(85 + t * 35))
else:
# Snow
t = (h - 0.85) / 0.15
return mcrfpy.Color(int(180 + t * 75), int(185 + t * 70), int(190 + t * 65))
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Panel 1: Mid-point displacement (raw)
mpd_raw = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
mpd_raw.mid_point_displacement(roughness=0.6, seed=42)
mpd_raw.normalize(0.0, 1.0)
# Panel 2: Mid-point displacement + smoothing
mpd_smooth = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
mpd_smooth.mid_point_displacement(roughness=0.6, seed=42)
mpd_smooth.smooth(2)
mpd_smooth.normalize(0.0, 1.0)
# Panel 3: Mid-point + light erosion
mpd_light_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
mpd_light_erode.mid_point_displacement(roughness=0.6, seed=42)
mpd_light_erode.rain_erosion(drops=1000, erosion=0.05, sedimentation=0.03, seed=42)
mpd_light_erode.normalize(0.0, 1.0)
# Panel 4: Noise-based + moderate erosion
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=123)
noise_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
noise_erode.add_noise(noise, world_size=(12, 12), mode='fbm', octaves=5)
noise_erode.add_hill((panel_w // 2, panel_h // 2), 10, 0.4)
noise_erode.rain_erosion(drops=3000, erosion=0.1, sedimentation=0.05, seed=42)
noise_erode.normalize(0.0, 1.0)
# Panel 5: Heavy erosion (river valleys)
heavy_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
heavy_erode.mid_point_displacement(roughness=0.7, seed=99)
heavy_erode.rain_erosion(drops=8000, erosion=0.15, sedimentation=0.02, seed=42)
heavy_erode.normalize(0.0, 1.0)
# Panel 6: Extreme erosion (canyon-like)
extreme_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
extreme_erode.mid_point_displacement(roughness=0.5, seed=77)
extreme_erode.rain_erosion(drops=15000, erosion=0.2, sedimentation=0.01, seed=42)
extreme_erode.smooth(1)
extreme_erode.normalize(0.0, 1.0)
# Apply to grid
panels = [
(mpd_raw, 0, 0, "MPD Raw"),
(mpd_smooth, panel_w, 0, "MPD + Smooth"),
(mpd_light_erode, panel_w * 2, 0, "Light Erosion"),
(noise_erode, 0, panel_h, "Noise + Erosion"),
(heavy_erode, panel_w, panel_h, "Heavy Erosion"),
(extreme_erode, panel_w * 2, panel_h, "Extreme Erosion"),
]
for hmap, ox, oy, name in panels:
for y in range(panel_h):
for x in range(panel_w):
h = hmap.get((x, y))
color_layer.set(((ox + x, oy + y)), terrain_color(h))
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
# Setup
scene = mcrfpy.Scene("erosion_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_05_heightmap_erosion.png")
print("Screenshot saved: procgen_05_heightmap_erosion.png")

View file

@ -1,133 +0,0 @@
"""HeightMap Voronoi Demo
Demonstrates: add_voronoi with different coefficients
Shows cell-based patterns useful for biomes, regions, and organic structures.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def biome_color(h):
"""Color cells as distinct biomes."""
# Use value ranges to create distinct regions
h = max(0.0, min(1.0, h))
if h < 0.15:
return mcrfpy.Color(30, 60, 120) # Deep water
elif h < 0.25:
return mcrfpy.Color(50, 100, 180) # Shallow water
elif h < 0.35:
return mcrfpy.Color(194, 178, 128) # Beach/desert
elif h < 0.5:
return mcrfpy.Color(80, 160, 60) # Grassland
elif h < 0.65:
return mcrfpy.Color(40, 100, 40) # Forest
elif h < 0.8:
return mcrfpy.Color(100, 80, 60) # Hills
elif h < 0.9:
return mcrfpy.Color(130, 130, 130) # Mountains
else:
return mcrfpy.Color(240, 240, 250) # Snow
def cell_edges_color(h):
"""Highlight cell boundaries."""
h = max(0.0, min(1.0, h))
if h < 0.3:
return mcrfpy.Color(40, 40, 60)
elif h < 0.6:
return mcrfpy.Color(80, 80, 100)
else:
return mcrfpy.Color(200, 200, 220)
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Panel 1: Standard Voronoi (cell centers high)
# coefficients (1, 0) = distance to nearest point
v_standard = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_standard.add_voronoi(num_points=15, coefficients=(1.0, 0.0), seed=42)
v_standard.normalize(0.0, 1.0)
# Panel 2: Inverted (cell centers low, edges high)
# coefficients (-1, 0) = inverted distance
v_inverted = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_inverted.add_voronoi(num_points=15, coefficients=(-1.0, 0.0), seed=42)
v_inverted.normalize(0.0, 1.0)
# Panel 3: Cell difference (creates ridges)
# coefficients (1, -1) = distance to nearest - distance to second nearest
v_ridges = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_ridges.add_voronoi(num_points=15, coefficients=(1.0, -1.0), seed=42)
v_ridges.normalize(0.0, 1.0)
# Panel 4: Few large cells (biome-scale)
v_biomes = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_biomes.add_voronoi(num_points=6, coefficients=(1.0, -0.3), seed=99)
v_biomes.normalize(0.0, 1.0)
# Panel 5: Many small cells (texture-scale)
v_texture = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_texture.add_voronoi(num_points=50, coefficients=(1.0, -0.5), seed=77)
v_texture.normalize(0.0, 1.0)
# Panel 6: Voronoi + noise blend (natural regions)
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
v_natural = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
v_natural.add_voronoi(num_points=12, coefficients=(0.8, -0.4), seed=42)
v_natural.add_noise(noise, world_size=(15, 15), mode='fbm', octaves=3, scale=0.3)
v_natural.normalize(0.0, 1.0)
# Apply to grid
panels = [
(v_standard, 0, 0, "Standard (1,0)", biome_color),
(v_inverted, panel_w, 0, "Inverted (-1,0)", biome_color),
(v_ridges, panel_w * 2, 0, "Ridges (1,-1)", cell_edges_color),
(v_biomes, 0, panel_h, "Biomes (6 pts)", biome_color),
(v_texture, panel_w, panel_h, "Texture (50 pts)", cell_edges_color),
(v_natural, panel_w * 2, panel_h, "Voronoi + Noise", biome_color),
]
for hmap, ox, oy, name, color_func in panels:
for y in range(panel_h):
for x in range(panel_w):
h = hmap.get((x, y))
color_layer.set(((ox + x, oy + y)), color_func(h))
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(60, 60, 60))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(60, 60, 60))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(60, 60, 60))
# Setup
scene = mcrfpy.Scene("voronoi_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_06_heightmap_voronoi.png")
print("Screenshot saved: procgen_06_heightmap_voronoi.png")

View file

@ -1,158 +0,0 @@
"""HeightMap Bezier Curves Demo
Demonstrates: dig_bezier for rivers, roads, and paths
Shows path carving with variable width and depth.
"""
import mcrfpy
from mcrfpy import automation
import math
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def terrain_with_water(h):
"""Terrain coloring with water in low areas."""
if h < 0.15:
# Water (carved paths)
t = h / 0.15
return mcrfpy.Color(int(30 + t * 30), int(60 + t * 50), int(140 + t * 40))
elif h < 0.25:
# Shore/wet ground
t = (h - 0.15) / 0.1
return mcrfpy.Color(int(80 + t * 40), int(100 + t * 30), int(80 - t * 20))
elif h < 0.5:
# Lowland
t = (h - 0.25) / 0.25
return mcrfpy.Color(int(70 + t * 20), int(130 + t * 20), int(50 + t * 10))
elif h < 0.7:
# Highland
t = (h - 0.5) / 0.2
return mcrfpy.Color(int(60 + t * 30), int(110 - t * 20), int(45 + t * 15))
elif h < 0.85:
# Hills
t = (h - 0.7) / 0.15
return mcrfpy.Color(int(100 + t * 30), int(95 + t * 25), int(70 + t * 30))
else:
# Peaks
t = (h - 0.85) / 0.15
return mcrfpy.Color(int(150 + t * 60), int(150 + t * 60), int(155 + t * 60))
def run_demo(runtime):
panel_w = GRID_WIDTH // 2
panel_h = GRID_HEIGHT
# Left panel: River system
river_map = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
# Add terrain
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
river_map.add_noise(noise, world_size=(10, 10), mode='fbm', octaves=4, scale=0.3)
river_map.add_hill((panel_w // 2, 5), 12, 0.3) # Mountain source
river_map.normalize(0.3, 0.9)
# Main river - wide, flowing from top to bottom
river_map.dig_bezier(
points=((panel_w // 2, 2), (panel_w // 4, 15), (panel_w * 3 // 4, 30), (panel_w // 2, panel_h - 3)),
start_radius=2, end_radius=5,
start_height=0.1, end_height=0.05
)
# Tributary from left
river_map.dig_bezier(
points=((3, 20), (10, 18), (15, 22), (panel_w // 3, 20)),
start_radius=1, end_radius=2,
start_height=0.12, end_height=0.1
)
# Tributary from right
river_map.dig_bezier(
points=((panel_w - 3, 15), (panel_w - 8, 20), (panel_w - 12, 18), (panel_w * 2 // 3, 25)),
start_radius=1, end_radius=2,
start_height=0.12, end_height=0.1
)
# Right panel: Road network
road_map = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
road_map.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=3, scale=0.2)
road_map.normalize(0.35, 0.7)
# Main road - relatively straight
road_map.dig_bezier(
points=((5, panel_h // 2), (15, panel_h // 2 - 3), (panel_w - 15, panel_h // 2 + 3), (panel_w - 5, panel_h // 2)),
start_radius=2, end_radius=2,
start_height=0.25, end_height=0.25
)
# North-south crossing road
road_map.dig_bezier(
points=((panel_w // 2, 5), (panel_w // 2 + 5, 15), (panel_w // 2 - 5, 35), (panel_w // 2, panel_h - 5)),
start_radius=2, end_radius=2,
start_height=0.25, end_height=0.25
)
# Winding mountain path
road_map.dig_bezier(
points=((5, 8), (15, 5), (20, 15), (25, 10)),
start_radius=1, end_radius=1,
start_height=0.28, end_height=0.28
)
# Curved path to settlement
road_map.dig_bezier(
points=((panel_w - 5, panel_h - 8), (panel_w - 15, panel_h - 5), (panel_w - 10, panel_h - 15), (panel_w // 2 + 5, panel_h - 10)),
start_radius=1, end_radius=2,
start_height=0.27, end_height=0.26
)
# Apply to grid
for y in range(panel_h):
for x in range(panel_w):
# Left panel: rivers
h = river_map.get((x, y))
color_layer.set(((x, y)), terrain_with_water(h))
# Right panel: roads (use brown for roads)
h2 = road_map.get((x, y))
if h2 < 0.3:
# Road surface
t = h2 / 0.3
color = mcrfpy.Color(int(140 - t * 40), int(120 - t * 30), int(80 - t * 20))
else:
color = terrain_with_water(h2)
color_layer.set(((panel_w + x, y)), color)
# Divider
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
# Labels
labels = [("Rivers (dig_bezier)", 10, 10), ("Roads & Paths", panel_w * CELL_SIZE + 10, 10)]
for text, x, ypos in labels:
label = mcrfpy.Caption(text=text, pos=(x, ypos))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Setup
scene = mcrfpy.Scene("bezier_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_07_heightmap_bezier.png")
print("Screenshot saved: procgen_07_heightmap_bezier.png")

View file

@ -1,148 +0,0 @@
"""HeightMap Thresholds and ColorLayer Integration Demo
Demonstrates: threshold, threshold_binary, inverse, count_in_range
Also: ColorLayer.apply_ranges for multi-threshold coloring
Shows terrain classification and visualization techniques.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Create source terrain
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
source = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
source.add_noise(noise, world_size=(10, 10), mode='fbm', octaves=5)
source.add_hill((panel_w // 2, panel_h // 2), 8, 0.3)
source.normalize(0.0, 1.0)
# Create derived heightmaps
water_mask = source.threshold((0.0, 0.3)) # Returns NEW heightmap with values only in range
land_binary = source.threshold_binary((0.3, 1.0), value=1.0) # Binary mask
inverted = source.inverse() # Inverted values
# Count cells in ranges for classification stats
water_count = source.count_in_range((0.0, 0.3))
land_count = source.count_in_range((0.3, 0.7))
mountain_count = source.count_in_range((0.7, 1.0))
# IMPORTANT: Render apply_ranges FIRST since it affects the whole layer
# Panel 6: Using ColorLayer.apply_ranges (bottom-right)
# Create a full-size heightmap and copy source data to correct position
panel6_hmap = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=-1.0) # -1 won't match any range
for y in range(panel_h):
for x in range(panel_w):
h = source.get((x, y))
panel6_hmap.fill(h, pos=(panel_w * 2 + x, panel_h + y), size=(1, 1))
# apply_ranges colors cells based on height ranges
# Cells with -1.0 won't match any range and stay unchanged
color_layer.apply_ranges(panel6_hmap, [
((0.0, 0.2), (30, 80, 160)), # Deep water
((0.2, 0.3), ((60, 120, 180), (120, 160, 140))), # Gradient: shallow to shore
((0.3, 0.5), (80, 150, 60)), # Lowland
((0.5, 0.7), ((60, 120, 40), (100, 100, 80))), # Gradient: forest to hills
((0.7, 0.85), (130, 120, 110)), # Rock
((0.85, 1.0), ((180, 180, 190), (250, 250, 255))), # Gradient: rock to snow
])
# Now render the other 5 panels (they will overwrite only their regions)
# Panel 1 (top-left): Original grayscale
for y in range(panel_h):
for x in range(panel_w):
h = source.get((x, y))
v = int(h * 255)
color_layer.set(((x, y)), mcrfpy.Color(v, v, v))
# Panel 2 (top-middle): threshold() - shows only values in range 0.0-0.3
for y in range(panel_h):
for x in range(panel_w):
h = water_mask.get((x, y))
if h > 0:
# Values were preserved in 0.0-0.3 range
t = h / 0.3
color_layer.set(((panel_w + x, y)), mcrfpy.Color(
int(30 + t * 40), int(60 + t * 60), int(150 + t * 50)))
else:
# Outside threshold range - dark
color_layer.set(((panel_w + x, y)), mcrfpy.Color(20, 20, 30))
# Panel 3 (top-right): threshold_binary() - land mask
for y in range(panel_h):
for x in range(panel_w):
h = land_binary.get((x, y))
if h > 0:
color_layer.set(((panel_w * 2 + x, y)), mcrfpy.Color(80, 140, 60)) # Land
else:
color_layer.set(((panel_w * 2 + x, y)), mcrfpy.Color(40, 80, 150)) # Water
# Panel 4 (bottom-left): inverse()
for y in range(panel_h):
for x in range(panel_w):
h = inverted.get((x, y))
v = int(h * 255)
color_layer.set(((x, panel_h + y)), mcrfpy.Color(v, int(v * 0.8), int(v * 0.6)))
# Panel 5 (bottom-middle): Classification using count_in_range results
for y in range(panel_h):
for x in range(panel_w):
h = source.get((x, y))
if h < 0.3:
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(50, 100, 180)) # Water
elif h < 0.7:
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(70, 140, 50)) # Land
else:
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(140, 130, 120)) # Mountain
# Labels
labels = [
("Original (grayscale)", 5, 5),
("threshold(0-0.3)", panel_w * CELL_SIZE + 5, 5),
("threshold_binary(land)", panel_w * 2 * CELL_SIZE + 5, 5),
("inverse()", 5, panel_h * CELL_SIZE + 5),
(f"Classified (W:{water_count} L:{land_count} M:{mountain_count})", panel_w * CELL_SIZE + 5, panel_h * CELL_SIZE + 5),
("apply_ranges (biome)", panel_w * 2 * CELL_SIZE + 5, panel_h * CELL_SIZE + 5),
]
for text, x, y in labels:
label = mcrfpy.Caption(text=text, pos=(x, y))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Grid divider lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
# Setup
scene = mcrfpy.Scene("thresholds_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_08_heightmap_thresholds.png")
print("Screenshot saved: procgen_08_heightmap_thresholds.png")

View file

@ -1,130 +0,0 @@
"""BSP Dungeon Generation Demo
Demonstrates: BSP, split_recursive, leaves iteration, to_heightmap
Classic roguelike dungeon generation with rooms.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Create BSP tree covering the map
bsp = mcrfpy.BSP(pos=(1, 1), size=(GRID_WIDTH - 2, GRID_HEIGHT - 2))
# Split recursively to create rooms
# depth=4 creates up to 16 rooms, min_size ensures rooms aren't too small
bsp.split_recursive(depth=4, min_size=(8, 6), max_ratio=1.5, seed=42)
# Convert to heightmap for visualization
# shrink=1 leaves 1-tile border for walls
rooms_hmap = bsp.to_heightmap(
size=(GRID_WIDTH, GRID_HEIGHT),
select='leaves',
shrink=1,
value=1.0
)
# Fill background (walls)
color_layer.fill(mcrfpy.Color(40, 35, 45))
# Draw rooms
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if rooms_hmap.get((x, y)) > 0:
color_layer.set(((x, y)), mcrfpy.Color(80, 75, 70))
# Add some visual variety to rooms
room_colors = [
mcrfpy.Color(85, 80, 75),
mcrfpy.Color(75, 70, 65),
mcrfpy.Color(90, 85, 80),
mcrfpy.Color(70, 65, 60),
]
for i, leaf in enumerate(bsp.leaves()):
pos = leaf.pos
size = leaf.size
color = room_colors[i % len(room_colors)]
# Fill room interior (with shrink)
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
color_layer.set(((x, y)), color)
# Mark room center
cx, cy = leaf.center()
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
color_layer.set(((cx, cy)), mcrfpy.Color(200, 180, 100))
# Simple corridor generation: connect adjacent rooms
# Using adjacency graph
adjacency = bsp.adjacency
connected = set()
for leaf_idx in range(len(bsp)):
leaf = bsp.get_leaf(leaf_idx)
cx1, cy1 = leaf.center()
for neighbor_idx in adjacency[leaf_idx]:
if (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx)) in connected:
continue
connected.add((min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx)))
neighbor = bsp.get_leaf(neighbor_idx)
cx2, cy2 = neighbor.center()
# Draw L-shaped corridor
# Horizontal first, then vertical
x1, x2 = min(cx1, cx2), max(cx1, cx2)
for x in range(x1, x2 + 1):
if 0 <= x < GRID_WIDTH and 0 <= cy1 < GRID_HEIGHT:
color_layer.set(((x, cy1)), mcrfpy.Color(100, 95, 90))
y1, y2 = min(cy1, cy2), max(cy1, cy2)
for y in range(y1, y2 + 1):
if 0 <= cx2 < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
color_layer.set(((cx2, y)), mcrfpy.Color(100, 95, 90))
# Draw outer border
for x in range(GRID_WIDTH):
color_layer.set(((x, 0)), mcrfpy.Color(60, 50, 70))
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(60, 50, 70))
for y in range(GRID_HEIGHT):
color_layer.set(((0, y)), mcrfpy.Color(60, 50, 70))
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(60, 50, 70))
# Stats
stats = mcrfpy.Caption(
text=f"BSP Dungeon: {len(bsp)} rooms, depth=4, seed=42",
pos=(10, 10)
)
stats.fill_color = mcrfpy.Color(255, 255, 255)
stats.outline = 1
stats.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(stats)
# Setup
scene = mcrfpy.Scene("bsp_dungeon_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_10_bsp_dungeon.png")
print("Screenshot saved: procgen_10_bsp_dungeon.png")

View file

@ -1,178 +0,0 @@
"""BSP Traversal Orders Demo
Demonstrates: traverse() with different Traversal orders
Shows how traversal order affects leaf enumeration.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
traversal_orders = [
(mcrfpy.Traversal.PRE_ORDER, "PRE_ORDER", "Root first, then children"),
(mcrfpy.Traversal.IN_ORDER, "IN_ORDER", "Left, node, right"),
(mcrfpy.Traversal.POST_ORDER, "POST_ORDER", "Children before parent"),
(mcrfpy.Traversal.LEVEL_ORDER, "LEVEL_ORDER", "Breadth-first by level"),
(mcrfpy.Traversal.INVERTED_LEVEL_ORDER, "INV_LEVEL", "Deepest levels first"),
]
panels = [
(0, 0), (panel_w, 0), (panel_w * 2, 0),
(0, panel_h), (panel_w, panel_h), (panel_w * 2, panel_h)
]
# Distinct color palette for 8+ leaves
leaf_colors = [
mcrfpy.Color(220, 60, 60), # Red
mcrfpy.Color(60, 180, 60), # Green
mcrfpy.Color(60, 100, 220), # Blue
mcrfpy.Color(220, 180, 40), # Yellow
mcrfpy.Color(180, 60, 180), # Magenta
mcrfpy.Color(60, 200, 200), # Cyan
mcrfpy.Color(220, 120, 60), # Orange
mcrfpy.Color(160, 100, 200), # Purple
mcrfpy.Color(100, 200, 120), # Mint
mcrfpy.Color(200, 100, 140), # Pink
]
for panel_idx, (order, name, desc) in enumerate(traversal_orders):
if panel_idx >= 6:
break
ox, oy = panels[panel_idx]
# Create BSP for this panel
bsp = mcrfpy.BSP(pos=(ox + 2, oy + 4), size=(panel_w - 4, panel_h - 6))
bsp.split_recursive(depth=3, min_size=(5, 4), seed=42)
# Fill panel background (dark gray = walls)
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(40, 35, 45))
# Traverse and color ONLY LEAVES by their position in traversal
leaf_idx = 0
for node in bsp.traverse(order):
if not node.is_leaf:
continue # Skip branch nodes
color = leaf_colors[leaf_idx % len(leaf_colors)]
pos = node.pos
size = node.size
# Shrink by 1 to show walls between rooms
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
color_layer.set(((x, y)), color)
# Draw leaf index in center
cx, cy = node.center()
# Draw index as a darker spot
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
dark = mcrfpy.Color(color.r // 2, color.g // 2, color.b // 2)
color_layer.set(((cx, cy)), dark)
if cx + 1 < GRID_WIDTH:
color_layer.set(((cx + 1, cy)), dark)
leaf_idx += 1
# Add labels
label = mcrfpy.Caption(text=f"{name}", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
desc_label = mcrfpy.Caption(text=f"{desc} ({leaf_idx} leaves)", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
desc_label.outline = 1
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(desc_label)
# Panel 6: Show tree depth levels (branch AND leaf nodes)
ox, oy = panels[5]
bsp = mcrfpy.BSP(pos=(ox + 2, oy + 4), size=(panel_w - 4, panel_h - 6))
bsp.split_recursive(depth=3, min_size=(5, 4), seed=42)
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(40, 35, 45))
# Draw by level - deepest first so leaves are on top
level_colors = [
mcrfpy.Color(60, 40, 40), # Level 0 (root) - dark
mcrfpy.Color(80, 60, 50), # Level 1
mcrfpy.Color(100, 80, 60), # Level 2
mcrfpy.Color(140, 120, 80), # Level 3 (leaves usually)
]
# Use INVERTED_LEVEL_ORDER so leaves are drawn last
for node in bsp.traverse(mcrfpy.Traversal.INVERTED_LEVEL_ORDER):
level = node.level
color = level_colors[min(level, len(level_colors) - 1)]
# Make leaves brighter
if node.is_leaf:
color = mcrfpy.Color(
min(255, color.r + 80),
min(255, color.g + 80),
min(255, color.b + 60)
)
pos = node.pos
size = node.size
for y in range(pos[1], pos[1] + size[1]):
for x in range(pos[0], pos[0] + size[0]):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
# Draw border
if x == pos[0] or x == pos[0] + size[0] - 1 or \
y == pos[1] or y == pos[1] + size[1] - 1:
border = mcrfpy.Color(20, 20, 30)
color_layer.set(((x, y)), border)
else:
color_layer.set(((x, y)), color)
label = mcrfpy.Caption(text="BY LEVEL (depth)", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
desc_label = mcrfpy.Caption(text="Darker=root, Bright=leaves", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
desc_label.outline = 1
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(desc_label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(60, 60, 60))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(60, 60, 60))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(60, 60, 60))
# Setup
scene = mcrfpy.Scene("bsp_traversal_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_11_bsp_traversal.png")
print("Screenshot saved: procgen_11_bsp_traversal.png")

View file

@ -1,160 +0,0 @@
"""BSP Adjacency Graph Demo
Demonstrates: adjacency property, get_leaf, adjacent_tiles
Shows room connectivity for corridor generation.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Create dungeon BSP
bsp = mcrfpy.BSP(pos=(2, 2), size=(GRID_WIDTH - 4, GRID_HEIGHT - 4))
bsp.split_recursive(depth=3, min_size=(10, 8), max_ratio=1.4, seed=42)
# Fill with wall color
color_layer.fill(mcrfpy.Color(50, 45, 55))
# Generate distinct colors for each room
num_rooms = len(bsp)
room_colors = []
for i in range(num_rooms):
hue = (i * 137.5) % 360 # Golden angle for good distribution
# HSV to RGB (simplified, saturation=0.6, value=0.7)
h = hue / 60
c = 0.42 # 0.6 * 0.7
x = c * (1 - abs(h % 2 - 1))
m = 0.28 # 0.7 - c
if h < 1: r, g, b = c, x, 0
elif h < 2: r, g, b = x, c, 0
elif h < 3: r, g, b = 0, c, x
elif h < 4: r, g, b = 0, x, c
elif h < 5: r, g, b = x, 0, c
else: r, g, b = c, 0, x
room_colors.append(mcrfpy.Color(
int((r + m) * 255),
int((g + m) * 255),
int((b + m) * 255)
))
# Draw rooms with unique colors
for i, leaf in enumerate(bsp.leaves()):
pos = leaf.pos
size = leaf.size
color = room_colors[i]
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
color_layer.set(((x, y)), color)
# Room label
cx, cy = leaf.center()
label = mcrfpy.Caption(text=str(i), pos=(cx * CELL_SIZE - 4, cy * CELL_SIZE - 8))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Draw corridors using adjacency graph
adjacency = bsp.adjacency
connected = set()
corridor_color = mcrfpy.Color(100, 95, 90)
door_color = mcrfpy.Color(180, 140, 80)
for leaf_idx in range(num_rooms):
leaf = bsp.get_leaf(leaf_idx)
# Get adjacent_tiles for this leaf
adj_tiles = leaf.adjacent_tiles
for neighbor_idx in adjacency[leaf_idx]:
pair = (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx))
if pair in connected:
continue
connected.add(pair)
neighbor = bsp.get_leaf(neighbor_idx)
# Find shared wall tiles
if neighbor_idx in adj_tiles:
wall_tiles = adj_tiles[neighbor_idx]
if len(wall_tiles) > 0:
# Pick middle tile for door
mid_tile = wall_tiles[len(wall_tiles) // 2]
dx, dy = int(mid_tile.x), int(mid_tile.y)
# Draw door
color_layer.set(((dx, dy)), door_color)
# Simple corridor: connect room centers through door
cx1, cy1 = leaf.center()
cx2, cy2 = neighbor.center()
# Path from room 1 to door
for x in range(min(cx1, dx), max(cx1, dx) + 1):
color_layer.set(((x, cy1)), corridor_color)
for y in range(min(cy1, dy), max(cy1, dy) + 1):
color_layer.set(((dx, y)), corridor_color)
# Path from door to room 2
for x in range(min(dx, cx2), max(dx, cx2) + 1):
color_layer.set(((x, dy)), corridor_color)
for y in range(min(dy, cy2), max(dy, cy2) + 1):
color_layer.set(((cx2, y)), corridor_color)
else:
# Fallback: L-shaped corridor
cx1, cy1 = leaf.center()
cx2, cy2 = neighbor.center()
for x in range(min(cx1, cx2), max(cx1, cx2) + 1):
color_layer.set(((x, cy1)), corridor_color)
for y in range(min(cy1, cy2), max(cy1, cy2) + 1):
color_layer.set(((cx2, y)), corridor_color)
# Title and stats
title = mcrfpy.Caption(
text=f"BSP Adjacency: {num_rooms} rooms, {len(connected)} connections",
pos=(10, 10)
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Legend
legend = mcrfpy.Caption(
text="Numbers = room index, Gold = doors, Brown = corridors",
pos=(10, GRID_HEIGHT * CELL_SIZE - 25)
)
legend.fill_color = mcrfpy.Color(200, 200, 200)
legend.outline = 1
legend.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(legend)
# Setup
scene = mcrfpy.Scene("bsp_adjacency_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_12_bsp_adjacency.png")
print("Screenshot saved: procgen_12_bsp_adjacency.png")

View file

@ -1,178 +0,0 @@
"""BSP Shrink Parameter Demo
Demonstrates: to_heightmap with different shrink values
Shows room padding for walls and varied room sizes.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
# Use reasonable shrink values relative to room sizes
shrink_values = [
(0, "shrink=0", "Rooms fill BSP bounds"),
(1, "shrink=1", "Standard 1-tile walls"),
(2, "shrink=2", "Thick fortress walls"),
(3, "shrink=3", "Wide hallway spacing"),
(-1, "Random shrink", "Per-room variation"),
(-2, "Gradient", "Shrink by leaf index"),
]
panels = [
(0, 0), (panel_w, 0), (panel_w * 2, 0),
(0, panel_h), (panel_w, panel_h), (panel_w * 2, panel_h)
]
for panel_idx, (shrink, title, desc) in enumerate(shrink_values):
ox, oy = panels[panel_idx]
# Create BSP - use depth=2 for larger rooms, bigger min_size
bsp = mcrfpy.BSP(pos=(ox + 1, oy + 3), size=(panel_w - 2, panel_h - 4))
bsp.split_recursive(depth=2, min_size=(8, 6), seed=42)
# Fill panel background (stone wall)
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(50, 45, 55))
if shrink >= 0:
# Standard shrink value using to_heightmap
rooms_hmap = bsp.to_heightmap(
size=(GRID_WIDTH, GRID_HEIGHT),
select='leaves',
shrink=shrink,
value=1.0
)
# Draw floors with color based on shrink level
floor_colors = [
mcrfpy.Color(140, 120, 100), # shrink=0: tan/full
mcrfpy.Color(110, 100, 90), # shrink=1: gray-brown
mcrfpy.Color(90, 95, 100), # shrink=2: blue-gray
mcrfpy.Color(80, 90, 110), # shrink=3: slate
]
floor_color = floor_colors[min(shrink, len(floor_colors) - 1)]
for y in range(oy, oy + panel_h):
for x in range(ox, ox + panel_w):
if rooms_hmap.get((x, y)) > 0:
# Add subtle tile pattern
var = ((x + y) % 2) * 8
c = mcrfpy.Color(
floor_color.r + var,
floor_color.g + var,
floor_color.b + var
)
color_layer.set(((x, y)), c)
elif shrink == -1:
# Random shrink per room
import random
rand = random.Random(42)
for leaf in bsp.leaves():
room_shrink = rand.randint(0, 3)
pos = leaf.pos
size = leaf.size
x1 = pos[0] + room_shrink
y1 = pos[1] + room_shrink
x2 = pos[0] + size[0] - room_shrink
y2 = pos[1] + size[1] - room_shrink
if x2 > x1 and y2 > y1:
colors = [
mcrfpy.Color(160, 130, 100), # Full
mcrfpy.Color(130, 120, 100),
mcrfpy.Color(100, 110, 110),
mcrfpy.Color(80, 90, 100), # Most shrunk
]
floor_color = colors[room_shrink]
for y in range(y1, y2):
for x in range(x1, x2):
if ox <= x < ox + panel_w and oy <= y < oy + panel_h:
var = ((x + y) % 2) * 6
c = mcrfpy.Color(
floor_color.r + var,
floor_color.g + var,
floor_color.b + var
)
color_layer.set(((x, y)), c)
else:
# Gradient shrink by leaf index
leaves = list(bsp.leaves())
for i, leaf in enumerate(leaves):
# Shrink increases with leaf index
room_shrink = min(3, i)
pos = leaf.pos
size = leaf.size
x1 = pos[0] + room_shrink
y1 = pos[1] + room_shrink
x2 = pos[0] + size[0] - room_shrink
y2 = pos[1] + size[1] - room_shrink
if x2 > x1 and y2 > y1:
# Color gradient: warm to cool as shrink increases
t = i / max(1, len(leaves) - 1)
floor_color = mcrfpy.Color(
int(180 - t * 80),
int(120 + t * 20),
int(80 + t * 60)
)
for y in range(y1, y2):
for x in range(x1, x2):
if ox <= x < ox + panel_w and oy <= y < oy + panel_h:
var = ((x + y) % 2) * 6
c = mcrfpy.Color(
floor_color.r + var,
floor_color.g + var - 2,
floor_color.b + var
)
color_layer.set(((x, y)), c)
# Add labels
label = mcrfpy.Caption(text=title, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
label.fill_color = mcrfpy.Color(255, 255, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
desc_label = mcrfpy.Caption(text=desc, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
desc_label.outline = 1
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(desc_label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(30, 30, 35))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(30, 30, 35))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(30, 30, 35))
# Setup
scene = mcrfpy.Scene("bsp_shrink_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_13_bsp_shrink.png")
print("Screenshot saved: procgen_13_bsp_shrink.png")

View file

@ -1,150 +0,0 @@
"""BSP Manual Split Demo
Demonstrates: split_once for controlled layouts
Shows handcrafted room placement with manual BSP control.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Fill background
color_layer.fill(mcrfpy.Color(50, 45, 55))
# Create main BSP covering most of the map
bsp = mcrfpy.BSP(pos=(2, 2), size=(GRID_WIDTH - 4, GRID_HEIGHT - 4))
# Manual split strategy for a temple-like layout:
# 1. Split horizontally to create upper/lower sections
# 2. Upper section: main hall (large) + side rooms
# 3. Lower section: entrance + storage areas
# First split: horizontal, creating top (sanctuary) and bottom (entrance) areas
# Split at about 60% height
split_y = 2 + int((GRID_HEIGHT - 4) * 0.6)
bsp.split_once(horizontal=True, position=split_y)
# Now manually color the structure
root = bsp.root
# Get the two main regions
upper = root.left # Sanctuary area
lower = root.right # Entrance area
# Color the sanctuary (upper area) - golden temple floor
if upper:
pos, size = upper.pos, upper.size
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
# Create a pattern
if (x + y) % 4 == 0:
color_layer.set(((x, y)), mcrfpy.Color(180, 150, 80))
else:
color_layer.set(((x, y)), mcrfpy.Color(160, 130, 70))
# Add altar in center of sanctuary
cx, cy = upper.center()
for dy in range(-2, 3):
for dx in range(-3, 4):
nx, ny = cx + dx, cy + dy
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
if abs(dx) <= 1 and abs(dy) <= 1:
color_layer.set(((nx, ny)), mcrfpy.Color(200, 180, 100)) # Altar
else:
color_layer.set(((nx, ny)), mcrfpy.Color(140, 100, 60)) # Altar base
# Color the entrance (lower area) - stone floor
if lower:
pos, size = lower.pos, lower.size
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
base = 80 + ((x * 3 + y * 7) % 20)
color_layer.set(((x, y)), mcrfpy.Color(base, base - 5, base - 10))
# Add entrance path
cx = pos[0] + size[0] // 2
for y in range(pos[1] + size[1] - 1, pos[1], -1):
for dx in range(-2, 3):
nx = cx + dx
if pos[0] < nx < pos[0] + size[0] - 1:
color_layer.set(((nx, y)), mcrfpy.Color(100, 95, 85))
# Add pillars along the sides
if upper:
pos, size = upper.pos, upper.size
for y in range(pos[1] + 3, pos[1] + size[1] - 3, 4):
# Left pillars
color_layer.set(((pos[0] + 3, y)), mcrfpy.Color(120, 110, 100))
color_layer.set(((pos[0] + 3, y + 1)), mcrfpy.Color(120, 110, 100))
# Right pillars
color_layer.set(((pos[0] + size[0] - 4, y)), mcrfpy.Color(120, 110, 100))
color_layer.set(((pos[0] + size[0] - 4, y + 1)), mcrfpy.Color(120, 110, 100))
# Add side chambers using manual rectangles
# Left chamber
chamber_w, chamber_h = 8, 6
for y in range(10, 10 + chamber_h):
for x in range(4, 4 + chamber_w):
if x == 4 or x == 4 + chamber_w - 1 or y == 10 or y == 10 + chamber_h - 1:
continue # Skip border (walls)
color_layer.set(((x, y)), mcrfpy.Color(100, 80, 90)) # Purple-ish storage
# Right chamber
for y in range(10, 10 + chamber_h):
for x in range(GRID_WIDTH - 4 - chamber_w, GRID_WIDTH - 4):
if x == GRID_WIDTH - 4 - chamber_w or x == GRID_WIDTH - 5 or y == 10 or y == 10 + chamber_h - 1:
continue
color_layer.set(((x, y)), mcrfpy.Color(80, 100, 90)) # Green-ish treasury
# Connect chambers to main hall
hall_y = 12
for x in range(4 + chamber_w, 15):
color_layer.set(((x, hall_y)), mcrfpy.Color(90, 85, 80))
for x in range(GRID_WIDTH - 15, GRID_WIDTH - 4 - chamber_w):
color_layer.set(((x, hall_y)), mcrfpy.Color(90, 85, 80))
# Title
title = mcrfpy.Caption(text="BSP split_once: Temple Layout", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Labels for areas
labels = [
("SANCTUARY", GRID_WIDTH // 2 * CELL_SIZE - 40, 80),
("ENTRANCE", GRID_WIDTH // 2 * CELL_SIZE - 35, split_y * CELL_SIZE + 30),
("Storage", 50, 180),
("Treasury", (GRID_WIDTH - 10) * CELL_SIZE - 30, 180),
]
for text, x, y in labels:
lbl = mcrfpy.Caption(text=text, pos=(x, y))
lbl.fill_color = mcrfpy.Color(200, 200, 200)
lbl.outline = 1
lbl.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(lbl)
# Setup
scene = mcrfpy.Scene("bsp_manual_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_14_bsp_manual_split.png")
print("Screenshot saved: procgen_14_bsp_manual_split.png")

View file

@ -1,125 +0,0 @@
"""NoiseSource Algorithms Demo
Demonstrates: simplex, perlin, wavelet noise algorithms
Shows visual differences between noise types.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def value_to_terrain(h):
"""Convert noise value (-1 to 1) to terrain color."""
# Normalize from -1..1 to 0..1
h = (h + 1) / 2
h = max(0.0, min(1.0, h))
if h < 0.3:
t = h / 0.3
return mcrfpy.Color(int(30 + t * 40), int(60 + t * 60), int(140 + t * 40))
elif h < 0.45:
t = (h - 0.3) / 0.15
return mcrfpy.Color(int(70 + t * 120), int(120 + t * 60), int(100 - t * 60))
elif h < 0.6:
t = (h - 0.45) / 0.15
return mcrfpy.Color(int(60 + t * 20), int(130 + t * 20), int(50 + t * 10))
elif h < 0.75:
t = (h - 0.6) / 0.15
return mcrfpy.Color(int(50 + t * 50), int(110 - t * 20), int(40 + t * 20))
elif h < 0.88:
t = (h - 0.75) / 0.13
return mcrfpy.Color(int(100 + t * 40), int(95 + t * 35), int(80 + t * 40))
else:
t = (h - 0.88) / 0.12
return mcrfpy.Color(int(180 + t * 70), int(180 + t * 70), int(190 + t * 60))
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 2
algorithms = [
('simplex', "SIMPLEX", "Fast, no visible artifacts"),
('perlin', "PERLIN", "Classic, slight grid bias"),
('wavelet', "WAVELET", "Smooth, no tiling"),
]
# Top row: FBM (natural terrain)
# Bottom row: Raw noise (single octave)
for col, (algo, name, desc) in enumerate(algorithms):
ox = col * panel_w
# Create noise source
noise = mcrfpy.NoiseSource(
dimensions=2,
algorithm=algo,
hurst=0.5,
lacunarity=2.0,
seed=42
)
# Top panel: FBM
for y in range(panel_h):
for x in range(panel_w):
# Sample at world coordinates
wx = x * 0.15
wy = y * 0.15
val = noise.fbm((wx, wy), octaves=5)
color_layer.set(((ox + x, y)), value_to_terrain(val))
# Bottom panel: Raw (flat)
for y in range(panel_h):
for x in range(panel_w):
wx = x * 0.15
wy = y * 0.15
val = noise.get((wx, wy))
color_layer.set(((ox + x, panel_h + y)), value_to_terrain(val))
# Labels
top_label = mcrfpy.Caption(text=f"{name} (FBM)", pos=(ox * CELL_SIZE + 5, 5))
top_label.fill_color = mcrfpy.Color(255, 255, 255)
top_label.outline = 1
top_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(top_label)
bottom_label = mcrfpy.Caption(text=f"{name} (raw)", pos=(ox * CELL_SIZE + 5, panel_h * CELL_SIZE + 5))
bottom_label.fill_color = mcrfpy.Color(255, 255, 255)
bottom_label.outline = 1
bottom_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(bottom_label)
desc_label = mcrfpy.Caption(text=desc, pos=(ox * CELL_SIZE + 5, 22))
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
desc_label.outline = 1
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(desc_label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
# Setup
scene = mcrfpy.Scene("noise_algo_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_20_noise_algorithms.png")
print("Screenshot saved: procgen_20_noise_algorithms.png")

View file

@ -1,115 +0,0 @@
"""NoiseSource Parameters Demo
Demonstrates: hurst (roughness), lacunarity (frequency scaling), octaves
Shows how parameters affect terrain character.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def value_to_gray(h):
"""Simple grayscale visualization."""
h = (h + 1) / 2 # -1..1 to 0..1
h = max(0.0, min(1.0, h))
v = int(h * 255)
return mcrfpy.Color(v, v, v)
def run_demo(runtime):
panel_w = GRID_WIDTH // 3
panel_h = GRID_HEIGHT // 3
# 3x3 grid showing parameter variations
# Rows: different hurst values (roughness)
# Cols: different lacunarity values
hurst_values = [0.2, 0.5, 0.8]
lacunarity_values = [1.5, 2.0, 3.0]
for row, hurst in enumerate(hurst_values):
for col, lacunarity in enumerate(lacunarity_values):
ox = col * panel_w
oy = row * panel_h
# Create noise with these parameters
noise = mcrfpy.NoiseSource(
dimensions=2,
algorithm='simplex',
hurst=hurst,
lacunarity=lacunarity,
seed=42
)
# Sample using heightmap for efficiency
hmap = noise.sample(
size=(panel_w, panel_h),
world_origin=(0, 0),
world_size=(10, 10),
mode='fbm',
octaves=6
)
# Apply to color layer
for y in range(panel_h):
for x in range(panel_w):
h = hmap.get((x, y))
color_layer.set(((ox + x, oy + y)), value_to_gray(h))
# Parameter label
label = mcrfpy.Caption(
text=f"H={hurst} L={lacunarity}",
pos=(ox * CELL_SIZE + 3, oy * CELL_SIZE + 3)
)
label.fill_color = mcrfpy.Color(255, 255, 0)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Row/Column labels
row_labels = ["Low Hurst (rough)", "Mid Hurst (natural)", "High Hurst (smooth)"]
for row, text in enumerate(row_labels):
label = mcrfpy.Caption(text=text, pos=(5, row * panel_h * CELL_SIZE + panel_h * CELL_SIZE - 20))
label.fill_color = mcrfpy.Color(255, 200, 100)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
col_labels = ["Low Lacunarity", "Standard (2.0)", "High Lacunarity"]
for col, text in enumerate(col_labels):
label = mcrfpy.Caption(text=text, pos=(col * panel_w * CELL_SIZE + 5, GRID_HEIGHT * CELL_SIZE - 20))
label.fill_color = mcrfpy.Color(100, 200, 255)
label.outline = 1
label.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(label)
# Grid lines
for y in range(GRID_HEIGHT):
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(100, 100, 100))
for x in range(GRID_WIDTH):
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(100, 100, 100))
color_layer.set(((x, panel_h * 2 - 1)), mcrfpy.Color(100, 100, 100))
# Setup
scene = mcrfpy.Scene("noise_params_demo")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_21_noise_parameters.png")
print("Screenshot saved: procgen_21_noise_parameters.png")

View file

@ -1,163 +0,0 @@
"""Advanced: Cave-Carved Dungeon
Combines: BSP (room structure) + Noise (organic cave walls) + Erosion
Creates a dungeon where rooms have been carved from natural cave formations.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Step 1: Create base cave system using noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
cave_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
cave_map.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=4)
cave_map.normalize(0.0, 1.0)
# Step 2: Create BSP rooms
bsp = mcrfpy.BSP(pos=(3, 3), size=(GRID_WIDTH - 6, GRID_HEIGHT - 6))
bsp.split_recursive(depth=3, min_size=(10, 8), max_ratio=1.5, seed=42)
rooms_hmap = bsp.to_heightmap(
size=(GRID_WIDTH, GRID_HEIGHT),
select='leaves',
shrink=2,
value=1.0
)
# Step 3: Combine - rooms carve into cave, cave affects walls
combined = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
combined.copy_from(cave_map)
# Scale cave values to mid-range so rooms stand out
combined.scale(0.5)
combined.add_constant(0.2)
# Add room interiors (rooms become high values)
combined.max(rooms_hmap)
# Step 4: Apply GENTLE erosion for organic edges
# Use fewer drops and lower erosion rate
combined.rain_erosion(drops=100, erosion=0.02, sedimentation=0.01, seed=42)
# Re-normalize to ensure we use the full value range
combined.normalize(0.0, 1.0)
# Step 5: Create corridor connections
adjacency = bsp.adjacency
connected = set()
corridor_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
for leaf_idx in range(len(bsp)):
leaf = bsp.get_leaf(leaf_idx)
cx1, cy1 = leaf.center()
for neighbor_idx in adjacency[leaf_idx]:
pair = (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx))
if pair in connected:
continue
connected.add(pair)
neighbor = bsp.get_leaf(neighbor_idx)
cx2, cy2 = neighbor.center()
# Draw corridor using bezier for organic feel
mid_x = (cx1 + cx2) // 2 + ((leaf_idx * 3) % 5 - 2)
mid_y = (cy1 + cy2) // 2 + ((neighbor_idx * 7) % 5 - 2)
corridor_map.dig_bezier(
points=((cx1, cy1), (mid_x, cy1), (mid_x, cy2), (cx2, cy2)),
start_radius=1.5, end_radius=1.5,
start_height=0.0, end_height=0.0
)
# Add corridors - dig_bezier creates low values where corridors are
# We want high values there, so invert the corridor map logic
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
corr_val = corridor_map.get((x, y))
if corr_val < 0.5: # Corridor was dug here
current = combined.get((x, y))
combined.fill(max(current, 0.7), pos=(x, y), size=(1, 1))
# Step 6: Render with cave aesthetics
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
h = combined.get((x, y))
if h < 0.30:
# Solid rock/wall - darker
base = 30 + int(cave_map.get((x, y)) * 20)
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
elif h < 0.40:
# Cave wall edge (rough transition)
t = (h - 0.30) / 0.10
base = int(40 + t * 15)
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
elif h < 0.55:
# Cave floor (natural stone)
t = (h - 0.40) / 0.15
base = 65 + int(t * 20)
var = ((x * 7 + y * 11) % 10)
color_layer.set(((x, y)), mcrfpy.Color(base + var, base - 5 + var, base - 10))
elif h < 0.70:
# Corridor/worked passage
base = 85 + ((x + y) % 2) * 5
color_layer.set(((x, y)), mcrfpy.Color(base, base - 3, base - 6))
else:
# Room floor (finely worked stone)
base = 105 + ((x + y) % 2) * 8
color_layer.set(((x, y)), mcrfpy.Color(base, base - 8, base - 12))
# Mark room centers with special tile
for leaf in bsp.leaves():
cx, cy = leaf.center()
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
color_layer.set(((cx, cy)), mcrfpy.Color(160, 140, 120))
# Cross pattern
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = cx + dx, cy + dy
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
color_layer.set(((nx, ny)), mcrfpy.Color(140, 125, 105))
# Outer border
for x in range(GRID_WIDTH):
color_layer.set(((x, 0)), mcrfpy.Color(20, 15, 25))
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(20, 15, 25))
for y in range(GRID_HEIGHT):
color_layer.set(((0, y)), mcrfpy.Color(20, 15, 25))
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(20, 15, 25))
# Title
title = mcrfpy.Caption(text="Cave-Carved Dungeon: BSP + Noise + Erosion", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Setup
scene = mcrfpy.Scene("cave_dungeon")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_30_advanced_cave_dungeon.png")
print("Screenshot saved: procgen_30_advanced_cave_dungeon.png")

View file

@ -1,140 +0,0 @@
"""Advanced: Island Terrain Generation
Combines: Noise (base terrain) + Voronoi (biomes) + Hills + Erosion + Bezier (rivers)
Creates a tropical island with varied biomes and water features.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def biome_color(elevation, moisture):
"""Determine color based on elevation and moisture."""
if elevation < 0.25:
# Water
t = elevation / 0.25
return mcrfpy.Color(int(30 + t * 30), int(80 + t * 40), int(160 + t * 40))
elif elevation < 0.32:
# Beach
return mcrfpy.Color(220, 200, 150)
elif elevation < 0.5:
# Lowland - varies by moisture
if moisture < 0.3:
return mcrfpy.Color(180, 170, 110) # Desert/savanna
elif moisture < 0.6:
return mcrfpy.Color(80, 140, 60) # Grassland
else:
return mcrfpy.Color(40, 100, 50) # Rainforest
elif elevation < 0.7:
# Highland
if moisture < 0.4:
return mcrfpy.Color(100, 90, 70) # Dry hills
else:
return mcrfpy.Color(50, 90, 45) # Forest
elif elevation < 0.85:
# Mountain
return mcrfpy.Color(110, 105, 100)
else:
# Peak
return mcrfpy.Color(220, 225, 230)
def run_demo(runtime):
# Step 1: Create base elevation using noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
elevation = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
elevation.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=5)
elevation.normalize(0.0, 1.0)
# Step 2: Create island shape using radial falloff
cx, cy = GRID_WIDTH / 2, GRID_HEIGHT / 2
max_dist = min(cx, cy) * 0.85
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
dist = ((x - cx) ** 2 + (y - cy) ** 2) ** 0.5
falloff = max(0, 1 - (dist / max_dist) ** 1.5)
current = elevation.get((x, y))
elevation.fill(current * falloff, pos=(x, y), size=(1, 1))
# Step 3: Add central mountain range
elevation.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 15, 0.5)
elevation.add_hill((GRID_WIDTH // 2 - 8, GRID_HEIGHT // 2 + 3), 8, 0.3)
elevation.add_hill((GRID_WIDTH // 2 + 10, GRID_HEIGHT // 2 - 5), 6, 0.25)
# Step 4: Create moisture map using different noise
moisture_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=123)
moisture = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
moisture.add_noise(moisture_noise, world_size=(8, 8), mode='fbm', octaves=3)
moisture.normalize(0.0, 1.0)
# Step 5: Add voronoi for biome boundaries
biome_regions = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
biome_regions.add_voronoi(num_points=8, coefficients=(0.5, -0.3), seed=77)
biome_regions.normalize(0.0, 1.0)
# Blend voronoi into moisture
moisture.lerp(biome_regions, 0.4)
# Step 6: Apply erosion to elevation
elevation.rain_erosion(drops=2000, erosion=0.08, sedimentation=0.04, seed=42)
elevation.normalize(0.0, 1.0)
# Step 7: Carve rivers from mountains to sea
# Main river
elevation.dig_bezier(
points=((GRID_WIDTH // 2, GRID_HEIGHT // 2 - 5),
(GRID_WIDTH // 2 - 10, GRID_HEIGHT // 2),
(GRID_WIDTH // 4, GRID_HEIGHT // 2 + 5),
(5, GRID_HEIGHT // 2 + 8)),
start_radius=0.5, end_radius=2,
start_height=0.3, end_height=0.15
)
# Secondary river
elevation.dig_bezier(
points=((GRID_WIDTH // 2 + 5, GRID_HEIGHT // 2),
(GRID_WIDTH // 2 + 15, GRID_HEIGHT // 3),
(GRID_WIDTH - 15, GRID_HEIGHT // 4),
(GRID_WIDTH - 5, GRID_HEIGHT // 4 + 3)),
start_radius=0.5, end_radius=1.5,
start_height=0.32, end_height=0.18
)
# Step 8: Render
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
elev = elevation.get((x, y))
moist = moisture.get((x, y))
color_layer.set(((x, y)), biome_color(elev, moist))
# Title
title = mcrfpy.Caption(text="Island Terrain: Noise + Voronoi + Hills + Erosion + Rivers", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Setup
scene = mcrfpy.Scene("island")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_31_advanced_island.png")
print("Screenshot saved: procgen_31_advanced_island.png")

View file

@ -1,164 +0,0 @@
"""Advanced: Procedural City Map
Combines: BSP (city blocks/buildings) + Noise (terrain/parks) + Voronoi (districts)
Creates a city map with districts, buildings, roads, and parks.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Step 1: Create district map using voronoi
districts = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
districts.add_voronoi(num_points=6, coefficients=(1.0, 0.0), seed=42)
districts.normalize(0.0, 1.0)
# District types based on value
# 0.0-0.2: Residential (green-ish)
# 0.2-0.4: Commercial (blue-ish)
# 0.4-0.6: Industrial (gray)
# 0.6-0.8: Park/nature
# 0.8-1.0: Downtown (tall buildings)
# Step 2: Create building blocks using BSP
bsp = mcrfpy.BSP(pos=(1, 1), size=(GRID_WIDTH - 2, GRID_HEIGHT - 2))
bsp.split_recursive(depth=4, min_size=(6, 5), max_ratio=2.0, seed=42)
# Step 3: Create park areas using noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=99)
parks = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
parks.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=3)
parks.normalize(0.0, 1.0)
# Step 4: Render base (roads)
color_layer.fill(mcrfpy.Color(60, 60, 65)) # Asphalt
# Step 5: Draw buildings based on BSP and district type
for leaf in bsp.leaves():
pos = leaf.pos
size = leaf.size
cx, cy = leaf.center()
# Get district type at center
district_val = districts.get((cx, cy))
# Shrink for roads between buildings
shrink = 1
# Determine building style based on district
if district_val < 0.2:
# Residential
building_color = mcrfpy.Color(140, 160, 140)
roof_color = mcrfpy.Color(160, 100, 80)
shrink = 2 # More space between houses
elif district_val < 0.4:
# Commercial
building_color = mcrfpy.Color(120, 140, 170)
roof_color = mcrfpy.Color(80, 100, 130)
elif district_val < 0.6:
# Industrial
building_color = mcrfpy.Color(100, 100, 105)
roof_color = mcrfpy.Color(70, 70, 75)
elif district_val < 0.8:
# Park area - check noise for actual park placement
park_val = parks.get((cx, cy))
if park_val > 0.4:
# This block is a park
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
t = parks.get((x, y))
if t > 0.6:
color_layer.set(((x, y)), mcrfpy.Color(50, 120, 50)) # Trees
else:
color_layer.set(((x, y)), mcrfpy.Color(80, 150, 80)) # Grass
continue
else:
building_color = mcrfpy.Color(130, 150, 130)
roof_color = mcrfpy.Color(100, 80, 70)
else:
# Downtown
building_color = mcrfpy.Color(150, 155, 165)
roof_color = mcrfpy.Color(90, 95, 110)
shrink = 1 # Dense buildings
# Draw building
for y in range(pos[1] + shrink, pos[1] + size[1] - shrink):
for x in range(pos[0] + shrink, pos[0] + size[0] - shrink):
# Building edge (roof)
if y == pos[1] + shrink or y == pos[1] + size[1] - shrink - 1:
color_layer.set(((x, y)), roof_color)
elif x == pos[0] + shrink or x == pos[0] + size[0] - shrink - 1:
color_layer.set(((x, y)), roof_color)
else:
color_layer.set(((x, y)), building_color)
# Step 6: Add main roads (cross the city)
road_color = mcrfpy.Color(70, 70, 75)
marking_color = mcrfpy.Color(200, 200, 100)
# Horizontal main road
main_y = GRID_HEIGHT // 2
for x in range(GRID_WIDTH):
for dy in range(-1, 2):
if 0 <= main_y + dy < GRID_HEIGHT:
color_layer.set(((x, main_y + dy)), road_color)
# Road markings
if x % 4 == 0:
color_layer.set(((x, main_y)), marking_color)
# Vertical main road
main_x = GRID_WIDTH // 2
for y in range(GRID_HEIGHT):
for dx in range(-1, 2):
if 0 <= main_x + dx < GRID_WIDTH:
color_layer.set(((main_x + dx, y)), road_color)
if y % 4 == 0:
color_layer.set(((main_x, y)), marking_color)
# Intersection
for dy in range(-1, 2):
for dx in range(-1, 2):
color_layer.set(((main_x + dx, main_y + dy)), road_color)
# Step 7: Add a central plaza
plaza_x, plaza_y = main_x, main_y
for dy in range(-3, 4):
for dx in range(-4, 5):
nx, ny = plaza_x + dx, plaza_y + dy
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
if abs(dx) <= 1 and abs(dy) <= 1:
color_layer.set(((nx, ny)), mcrfpy.Color(180, 160, 140)) # Fountain
else:
color_layer.set(((nx, ny)), mcrfpy.Color(160, 150, 140)) # Plaza tiles
# Title
title = mcrfpy.Caption(text="Procedural City: BSP + Voronoi Districts + Noise Parks", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Setup
scene = mcrfpy.Scene("city")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_32_advanced_city.png")
print("Screenshot saved: procgen_32_advanced_city.png")

View file

@ -1,163 +0,0 @@
"""Advanced: Natural Cave System
Combines: Noise (cave formation) + Threshold (open areas) + Kernel (smoothing) + BSP (structured areas)
Creates organic cave networks with some structured rooms.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def run_demo(runtime):
# Step 1: Generate cave base using turbulent noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
cave_noise = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
cave_noise.add_noise(noise, world_size=(10, 8), mode='turbulence', octaves=4)
cave_noise.normalize(0.0, 1.0)
# Step 2: Create cave mask via threshold
# Values > 0.45 become open cave, rest is rock
cave_mask = cave_noise.threshold_binary((0.4, 1.0), 1.0)
# Step 3: Apply smoothing kernel to remove isolated pixels
smooth_kernel = {
(-1, -1): 1, (0, -1): 2, (1, -1): 1,
(-1, 0): 2, (0, 0): 4, (1, 0): 2,
(-1, 1): 1, (0, 1): 2, (1, 1): 1,
}
cave_mask.kernel_transform(smooth_kernel)
cave_mask.normalize(0.0, 1.0)
# Re-threshold after smoothing
cave_mask = cave_mask.threshold_binary((0.5, 1.0), 1.0)
# Step 4: Add some structured rooms using BSP in one corner
# This represents ancient ruins within the caves
bsp = mcrfpy.BSP(pos=(GRID_WIDTH - 22, GRID_HEIGHT - 18), size=(18, 14))
bsp.split_recursive(depth=2, min_size=(6, 5), seed=42)
ruins_hmap = bsp.to_heightmap(
size=(GRID_WIDTH, GRID_HEIGHT),
select='leaves',
shrink=1,
value=1.0
)
# Step 5: Combine caves and ruins
combined = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
combined.copy_from(cave_mask)
combined.max(ruins_hmap)
# Step 6: Add connecting tunnels from ruins to main cave
# Find a cave entrance point
tunnel_points = []
for y in range(GRID_HEIGHT - 18, GRID_HEIGHT - 10):
for x in range(GRID_WIDTH - 25, GRID_WIDTH - 20):
if cave_mask.get((x, y)) > 0.5:
tunnel_points.append((x, y))
break
if tunnel_points:
break
if tunnel_points:
tx, ty = tunnel_points[0]
# Carve tunnel to ruins entrance
combined.dig_bezier(
points=((tx, ty), (tx + 3, ty), (GRID_WIDTH - 22, ty + 2), (GRID_WIDTH - 20, GRID_HEIGHT - 15)),
start_radius=1.5, end_radius=1.5,
start_height=1.0, end_height=1.0
)
# Step 7: Add large cavern (central chamber)
combined.add_hill((GRID_WIDTH // 3, GRID_HEIGHT // 2), 8, 0.6)
# Step 8: Create water pools in low noise areas
water_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=99)
water_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
water_map.add_noise(water_noise, world_size=(15, 12), mode='fbm', octaves=3)
water_map.normalize(0.0, 1.0)
# Step 9: Render
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cave_val = combined.get((x, y))
water_val = water_map.get((x, y))
original_noise = cave_noise.get((x, y))
# Check if in ruins area
in_ruins = (x >= GRID_WIDTH - 22 and x < GRID_WIDTH - 4 and
y >= GRID_HEIGHT - 18 and y < GRID_HEIGHT - 4)
if cave_val < 0.3:
# Solid rock
base = 30 + int(original_noise * 25)
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
elif cave_val < 0.5:
# Cave wall edge
color_layer.set(((x, y)), mcrfpy.Color(45, 40, 50))
else:
# Open cave floor
if water_val > 0.7 and not in_ruins:
# Water pool
t = (water_val - 0.7) / 0.3
color_layer.set(((x, y)), mcrfpy.Color(
int(30 + t * 20), int(50 + t * 30), int(100 + t * 50)
))
elif in_ruins and ruins_hmap.get((x, y)) > 0.5:
# Ruins floor (worked stone)
base = 85 + ((x + y) % 3) * 5
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base))
else:
# Natural cave floor
base = 55 + int(original_noise * 20)
var = ((x * 3 + y * 5) % 8)
color_layer.set(((x, y)), mcrfpy.Color(base + var, base - 5 + var, base - 8))
# Glowing fungi spots
fungi_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=777)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if combined.get((x, y)) > 0.5: # Only in open areas
fungi_val = fungi_noise.get((x * 0.5, y * 0.5))
if fungi_val > 0.8:
color_layer.set(((x, y)), mcrfpy.Color(80, 180, 120))
# Border
for x in range(GRID_WIDTH):
color_layer.set(((x, 0)), mcrfpy.Color(20, 18, 25))
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(20, 18, 25))
for y in range(GRID_HEIGHT):
color_layer.set(((0, y)), mcrfpy.Color(20, 18, 25))
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(20, 18, 25))
# Title
title = mcrfpy.Caption(text="Cave System: Noise + Threshold + Kernel + BSP Ruins", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Setup
scene = mcrfpy.Scene("caves")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_33_advanced_caves.png")
print("Screenshot saved: procgen_33_advanced_caves.png")

View file

@ -1,187 +0,0 @@
"""Advanced: Volcanic Crater Region
Combines: Hills (mountains) + dig_hill (craters) + Voronoi (lava flows) + Erosion + Noise
Creates a volcanic landscape with active lava, ash fields, and rocky terrain.
"""
import mcrfpy
from mcrfpy import automation
GRID_WIDTH, GRID_HEIGHT = 64, 48
CELL_SIZE = 16
def volcanic_color(elevation, lava_intensity, ash_level):
"""Color based on elevation, lava presence, and ash coverage."""
# Lava overrides everything
if lava_intensity > 0.6:
t = (lava_intensity - 0.6) / 0.4
return mcrfpy.Color(
int(200 + t * 55),
int(80 + t * 80),
int(20 + t * 30)
)
elif lava_intensity > 0.4:
# Cooling lava
t = (lava_intensity - 0.4) / 0.2
return mcrfpy.Color(
int(80 + t * 120),
int(30 + t * 50),
int(20)
)
# Check for crater interior (very low elevation)
if elevation < 0.15:
t = elevation / 0.15
return mcrfpy.Color(int(40 + t * 30), int(20 + t * 20), int(10 + t * 15))
# Ash coverage
if ash_level > 0.6:
t = (ash_level - 0.6) / 0.4
base = int(60 + t * 40)
return mcrfpy.Color(base, base - 5, base - 10)
# Normal terrain by elevation
if elevation < 0.3:
# Volcanic plain
t = (elevation - 0.15) / 0.15
return mcrfpy.Color(int(50 + t * 30), int(40 + t * 25), int(35 + t * 20))
elif elevation < 0.5:
# Rocky slopes
t = (elevation - 0.3) / 0.2
return mcrfpy.Color(int(70 + t * 20), int(60 + t * 15), int(50 + t * 15))
elif elevation < 0.7:
# Mountain sides
t = (elevation - 0.5) / 0.2
return mcrfpy.Color(int(85 + t * 25), int(75 + t * 20), int(65 + t * 20))
elif elevation < 0.85:
# High slopes
t = (elevation - 0.7) / 0.15
return mcrfpy.Color(int(100 + t * 30), int(90 + t * 25), int(80 + t * 25))
else:
# Peaks
t = (elevation - 0.85) / 0.15
return mcrfpy.Color(int(130 + t * 50), int(120 + t * 50), int(115 + t * 50))
def run_demo(runtime):
# Step 1: Create base terrain with noise
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
terrain = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.3)
terrain.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=4, scale=0.2)
# Step 2: Add volcanic mountains
# Main volcano
terrain.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 20, 0.7)
terrain.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 12, 0.3) # Steep peak
# Secondary volcanoes
terrain.add_hill((15, 15), 10, 0.4)
terrain.add_hill((GRID_WIDTH - 12, GRID_HEIGHT - 15), 8, 0.35)
terrain.add_hill((10, GRID_HEIGHT - 10), 6, 0.25)
# Step 3: Create craters
terrain.dig_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 6, 0.1) # Main crater
terrain.dig_hill((15, 15), 4, 0.15) # Secondary crater
terrain.dig_hill((GRID_WIDTH - 12, GRID_HEIGHT - 15), 3, 0.18) # Third crater
# Step 4: Create lava flow pattern using voronoi
lava = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
lava.add_voronoi(num_points=12, coefficients=(1.0, -0.8), seed=77)
lava.normalize(0.0, 1.0)
# Lava originates from craters - enhance around crater centers
lava.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 8, 0.5)
lava.add_hill((15, 15), 5, 0.3)
# Lava flows downhill - multiply by inverted terrain
terrain_inv = terrain.inverse()
terrain_inv.normalize(0.0, 1.0)
lava.multiply(terrain_inv)
lava.normalize(0.0, 1.0)
# Step 5: Create ash distribution using noise
ash_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=123)
ash = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
ash.add_noise(ash_noise, world_size=(8, 6), mode='turbulence', octaves=3)
ash.normalize(0.0, 1.0)
# Ash settles on lower areas
ash.multiply(terrain_inv)
# Step 6: Apply erosion for realistic channels
terrain.rain_erosion(drops=1500, erosion=0.1, sedimentation=0.03, seed=42)
terrain.normalize(0.0, 1.0)
# Step 7: Add lava rivers from craters
lava.dig_bezier(
points=((GRID_WIDTH // 2, GRID_HEIGHT // 2 + 5),
(GRID_WIDTH // 2 - 5, GRID_HEIGHT // 2 + 15),
(GRID_WIDTH // 3, GRID_HEIGHT - 10),
(10, GRID_HEIGHT - 5)),
start_radius=2, end_radius=3,
start_height=0.9, end_height=0.7
)
lava.dig_bezier(
points=((GRID_WIDTH // 2 + 3, GRID_HEIGHT // 2 + 3),
(GRID_WIDTH // 2 + 15, GRID_HEIGHT // 2 + 8),
(GRID_WIDTH - 15, GRID_HEIGHT // 2 + 5),
(GRID_WIDTH - 5, GRID_HEIGHT // 2 + 10)),
start_radius=1.5, end_radius=2.5,
start_height=0.85, end_height=0.65
)
# Step 8: Render
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
elev = terrain.get((x, y))
lava_val = lava.get((x, y))
ash_val = ash.get((x, y))
color_layer.set(((x, y)), volcanic_color(elev, lava_val, ash_val))
# Add smoke/steam particles around crater rims
crater_centers = [
(GRID_WIDTH // 2, GRID_HEIGHT // 2, 6),
(15, 15, 4),
(GRID_WIDTH - 12, GRID_HEIGHT - 15, 3)
]
import math
for cx, cy, radius in crater_centers:
for angle in range(0, 360, 30):
rad = math.radians(angle)
px = int(cx + math.cos(rad) * radius)
py = int(cy + math.sin(rad) * radius)
if 0 <= px < GRID_WIDTH and 0 <= py < GRID_HEIGHT:
# Smoke color
color_layer.set(((px, py)), mcrfpy.Color(150, 140, 130, 180))
# Title
title = mcrfpy.Caption(text="Volcanic Region: Hills + Craters + Voronoi Lava + Erosion", pos=(10, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.outline = 1
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Setup
scene = mcrfpy.Scene("volcanic")
grid = mcrfpy.Grid(
grid_size=(GRID_WIDTH, GRID_HEIGHT),
pos=(0, 0),
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
layers={}
)
grid.fill_color = mcrfpy.Color(0, 0, 0)
color_layer = grid.add_layer("color", z_index=-1)
scene.children.append(grid)
scene.activate()
# Run the demo
run_demo(0)
# Take screenshot
automation.screenshot("procgen_34_advanced_volcanic.png")
print("Screenshot saved: procgen_34_advanced_volcanic.png")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Some files were not shown because too many files have changed in this diff Show more