Compare commits

..

5 commits

538 changed files with 2929 additions and 1846799 deletions

28
.gitignore vendored
View file

@ -8,31 +8,5 @@ PCbuild
obj
build
lib
__pycache__
obj
.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/

7
.gitmodules vendored
View file

@ -10,7 +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/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/`.

847
CLAUDE.md
View file

@ -1,847 +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)
## 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
- Entity Creation: `Entity()` with position and sprite properties
- Grid Management: `Grid()` for tilemap rendering
- Input Handling: `keypressScene()` for keyboard events
- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()`
- Timers: `Timer("name")` 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.12 shared libraries in `./lib/`
### Modifying Game Logic
- Game scripts are in `src/scripts/`
- Main game entry is `game.py`
- Entity behavior in `cos_entities.py`
- Level generation in `cos_level.py`
### Adding New Features
1. C++ API additions go in `src/McRFPy_API.cpp`
2. Expose to Python using the existing binding pattern
3. Update Python scripts to use new functionality
## Testing
### 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 and libtcod for roguelike utilities
- Python scripts are loaded at runtime from the `scripts/` directory
- Asset loading expects specific paths relative to the executable
- The game was created for 7DRL 2025 as "Crypt of Sokoban"
- Iterator implementations require careful handling of C++/Python boundaries
## Testing Guidelines
### Test-Driven Development
- **Always write tests first**: Create 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)
# 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: key names are "Num1", "Num2", "Escape", "Q", etc.
def on_key(key, state):
if key == "Num1" and state == "start":
demo_scene.activate()
```
## 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,157 +8,49 @@ project(McRogueFace)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# 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}/deps_linux/Python-3.11.1)
include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
# Python includes: use different paths for Windows vs Linux
if(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
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
)
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
list(APPEND SOURCES ${IMGUI_SOURCES})
# Find OpenGL (required by ImGui-SFML)
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()
# Create a list of libraries to link against
if(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)
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
# 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})
@ -175,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")

198
README.md
View file

@ -1,182 +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.
![ Image ]()
## Why?
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items.
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.
## Quick Start
## To-do
**Download**:
- The entire McRogueFace visual framework:
- **Sprite**: an image file or one sprite from a shared sprite sheet
- **Caption**: load a font, display text
- **Frame**: A rectangle; put other things on it to move or manage GUIs as modules
- **Grid**: A 2D array of tiles with zoom + position control
- **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path
- **Animation**: Change any property on any of the above over time
```bash
# Clone and build
git clone <wherever you found this repo>
cd McRogueFace
make
# Run the example game
cd build
./mcrogueface
```
## 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: Creating a Simple Scene
```python
import mcrfpy
# Create a new scene
intro = mcrfpy.Scene("intro")
# Add a text caption
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
caption.size = 48
caption.fill_color = (255, 255, 255)
# Add to scene
intro.children.append(caption)
# Switch to the scene
intro.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.12+
- SFML 2.6
- Linux or Windows (macOS untested)
## Project Structure
```
McRogueFace/
├── assets/ # Sprites, fonts, audio
├── build/ # Build output directory: zip + ship
│ ├─ (*)assets/ # (copied location of assets)
│ ├─ (*)scripts/ # (copied location of src/scripts)
│ └─ lib/ # SFML, TCOD libraries, Python + standard library / modules
├── deps/ # Python, SFML, and libtcod imports can be tossed in here to build
│ └─ platform/ # windows, linux subdirectories for OS-specific cpython config
├── docs/ # generated HTML, markdown docs
│ └─ stubs/ # .pyi files for editor integration
├── modules/ # git submodules, to build all of McRogueFace's dependencies from source
├── src/ # C++ engine source
│ └─ scripts/ # Python game scripts (copied during build)
└── tests/ # Automated test suite
└── tools/ # For the McRogueFace ecosystem: docs generation
```
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 - 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,223 +0,0 @@
# McRogueFace - Development Roadmap
## Project Status
**Current State**: Active development - C++ game engine with Python scripting
**Latest Release**: Alpha 0.1
**Issue Tracking**: See [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current tasks and bugs
---
## 🎯 Strategic Vision
### Engine Philosophy
- **C++ First**: Performance-critical code stays in C++
- **Python Close Behind**: Rich scripting without frame-rate impact
- **Game-Ready**: Each improvement should benefit actual game development
### Architecture Goals
1. **Clean Inheritance**: Drawable → UI components, proper type preservation
2. **Collection Consistency**: Uniform iteration, indexing, and search patterns
3. **Resource Management**: RAII everywhere, proper lifecycle handling
4. **Multi-Platform**: Windows/Linux feature parity maintained
---
## 🏗️ Architecture Decisions
### Three-Layer Grid Architecture
Following successful roguelike patterns (Caves of Qud, Cogmind, DCSS):
1. **Visual Layer** (UIGridPoint) - Sprites, colors, animations
2. **World State Layer** (TCODMap) - Walkability, transparency, physics
3. **Entity Perspective Layer** (UIGridPointState) - Per-entity FOV, knowledge
### Performance Architecture
Critical for large maps (1000x1000):
- **Spatial Hashing** for entity queries (not quadtrees!)
- **Batch Operations** with context managers (10-100x speedup)
- **Memory Pooling** for entities and components
- **Dirty Flag System** to avoid unnecessary updates
- **Zero-Copy NumPy Integration** via buffer protocol
### Key Insight from Research
"Minimizing Python/C++ boundary crossings matters more than individual function complexity"
- Batch everything possible
- Use context managers for logical operations
- Expose arrays, not individual cells
- Profile and optimize hot paths only
---
## 🚀 Development Phases
For detailed task tracking and current priorities, see the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).
### Phase 1: Foundation Stabilization ✅
**Status**: Complete
**Key Issues**: #7 (Safe Constructors), #71 (Base Class), #87 (Visibility), #88 (Opacity)
### Phase 2: Constructor & API Polish ✅
**Status**: Complete
**Key Features**: Pythonic API, tuple support, standardized defaults
### Phase 3: Entity Lifecycle Management ✅
**Status**: Complete
**Key Issues**: #30 (Entity.die()), #93 (Vector methods), #94 (Color helpers), #103 (Timer objects)
### Phase 4: Visibility & Performance ✅
**Status**: Complete
**Key Features**: AABB culling, name system, profiling tools
### Phase 5: Window/Scene Architecture ✅
**Status**: Complete
**Key Issues**: #34 (Window object), #61 (Scene object), #1 (Resize events), #105 (Scene transitions)
### Phase 6: Rendering Revolution ✅
**Status**: Complete
**Key Issues**: #50 (Grid backgrounds), #6 (RenderTexture), #8 (Viewport rendering)
### Phase 7: Documentation & Distribution ✅
**Status**: Complete (2025-10-30)
**Key Issues**: #85 (Docstrings), #86 (Parameter docs), #108 (Type stubs), #97 (API docs)
**Completed**: All classes and functions converted to MCRF_* macro system with automated HTML/Markdown/man page generation
See [current open issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues?state=open) for active work.
---
## 🔮 Future Vision: Pure Python Extension Architecture
### Concept: McRogueFace as a Traditional Python Package
**Status**: Long-term vision
**Complexity**: Major architectural overhaul
Instead of being a C++ application that embeds Python, McRogueFace could be redesigned as a pure Python extension module that can be installed via `pip install mcrogueface`.
### Technical Approach
1. **Separate Core Engine from Python Embedding**
- Extract SFML rendering, audio, and input into C++ extension modules
- Remove embedded CPython interpreter
- Use Python's C API to expose functionality
2. **Module Structure**
```
mcrfpy/
├── __init__.py # Pure Python coordinator
├── _core.so # C++ rendering/game loop extension
├── _sfml.so # SFML bindings
├── _audio.so # Audio system bindings
└── engine.py # Python game engine logic
```
3. **Inverted Control Flow**
- Python drives the main loop instead of C++
- C++ extensions handle performance-critical operations
- Python manages game logic, scenes, and entity systems
### Benefits
- **Standard Python Packaging**: `pip install mcrogueface`
- **Virtual Environment Support**: Works with venv, conda, poetry
- **Better IDE Integration**: Standard Python development workflow
- **Easier Testing**: Use pytest, standard Python testing tools
- **Cross-Python Compatibility**: Support multiple Python versions
- **Modular Architecture**: Users can import only what they need
### Challenges
- **Major Refactoring**: Complete restructure of codebase
- **Performance Considerations**: Python-driven main loop overhead
- **Build Complexity**: Multiple extension modules to compile
- **Platform Support**: Need wheels for many platform/Python combinations
- **API Stability**: Would need careful design to maintain compatibility
### Example Usage (Future Vision)
```python
import mcrfpy
from mcrfpy import Scene, Frame, Sprite, Grid
# Create game directly in Python
game = mcrfpy.Game(width=1024, height=768)
# Define scenes using Python classes
class MainMenu(Scene):
def on_enter(self):
self.ui.append(Frame(100, 100, 200, 50))
self.ui.append(Sprite("logo.png", x=400, y=100))
def on_keypress(self, key, pressed):
if key == "ENTER" and pressed:
self.game.set_scene("game")
# Run the game
game.add_scene("menu", MainMenu())
game.run()
```
This architecture would make McRogueFace a first-class Python citizen, following standard Python packaging conventions while maintaining high performance through C++ extensions.
---
## 📋 Major Feature Areas
For current status and detailed tasks, see the corresponding Gitea issue labels:
### Core Systems
- **UI/Rendering System**: Issues tagged `[Major Feature]` related to rendering
- **Grid/Entity System**: Pathfinding, FOV, entity management
- **Animation System**: Property animation, easing functions, callbacks
- **Scene/Window Management**: Scene lifecycle, transitions, viewport
### Performance Optimization
- **#115**: SpatialHash for 10,000+ entities
- **#116**: Dirty flag system
- **#113**: Batch operations for NumPy-style access
- **#117**: Memory pool for entities
### Advanced Features
- **#118**: Scene as Drawable (scenes can be drawn/animated)
- **#122**: Parent-Child UI System
- **#123**: Grid Subgrid System (256x256 chunks)
- **#124**: Grid Point Animation
- **#106**: Shader support
- **#107**: Particle system
### Documentation
- **#92**: Inline C++ documentation system
- **#91**: Python type stub files (.pyi)
- **#97**: Automated API documentation extraction
- **#126**: Generate perfectly consistent Python interface
---
## 📚 Resources
- **Issue Tracker**: [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues)
- **Source Code**: [Gitea Repository](https://gamedev.ffwf.net/gitea/john/McRogueFace)
- **Documentation**: See `CLAUDE.md` for build instructions and development guide
- **Tutorial**: See `roguelike_tutorial/` for implementation examples
- **Workflow**: See "Gitea-First Workflow" section in `CLAUDE.md` for issue management best practices
---
## 🔄 Development Workflow
**Gitea is the Single Source of Truth** for this project. Before starting any work:
1. **Check Gitea Issues** for existing tasks, bugs, or related work
2. **Create granular issues** for new features or problems
3. **Update issues** when work affects other systems
4. **Document discoveries** - if something is undocumented or misleading, create a task to fix it
5. **Cross-reference commits** with issue numbers (e.g., "Fixes #104")
See the "Gitea-First Workflow" section in `CLAUDE.md` for detailed guidelines on efficient development practices using the Gitea MCP tools.
---
*For current priorities, task tracking, and bug reports, please use the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).*

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,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

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,120 +0,0 @@
"""McRogueFace - Health Bar Widget (animated)
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_animated.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class AnimatedHealthBar:
"""Health bar with smooth fill animation."""
def __init__(self, x, y, w, h, current, maximum):
self.x = x
self.y = y
self.w = w
self.h = h
self.current = current
self.display_current = current # What's visually shown
self.maximum = maximum
self.timer_name = f"hp_anim_{id(self)}"
# Background
self.background = mcrfpy.Frame(x, y, w, h)
self.background.fill_color = mcrfpy.Color(40, 40, 40)
self.background.outline = 2
self.background.outline_color = mcrfpy.Color(60, 60, 60)
# Damage preview (shows recent damage in different color)
self.damage_fill = mcrfpy.Frame(x + 2, y + 2, w - 4, h - 4)
self.damage_fill.fill_color = mcrfpy.Color(180, 50, 50)
self.damage_fill.outline = 0
# Main fill
self.fill = mcrfpy.Frame(x + 2, y + 2, w - 4, h - 4)
self.fill.fill_color = mcrfpy.Color(50, 200, 50)
self.fill.outline = 0
self._update_display()
def _update_display(self):
"""Update the visual fill based on display_current."""
ratio = max(0, min(1, self.display_current / self.maximum))
self.fill.w = (self.w - 4) * ratio
# Color based on ratio
if ratio > 0.6:
self.fill.fill_color = mcrfpy.Color(50, 200, 50)
elif ratio > 0.3:
self.fill.fill_color = mcrfpy.Color(230, 180, 30)
else:
self.fill.fill_color = mcrfpy.Color(200, 50, 50)
def set_health(self, new_current, animate=True):
"""
Set health with optional animation.
Args:
new_current: New health value
animate: Whether to animate the transition
"""
old_current = self.current
self.current = max(0, min(self.maximum, new_current))
if not animate:
self.display_current = self.current
self._update_display()
return
# Show damage preview immediately
if self.current < old_current:
damage_ratio = self.current / self.maximum
self.damage_fill.w = (self.w - 4) * (old_current / self.maximum)
# Animate the fill
self._start_animation()
def _start_animation(self):
"""Start animating toward target health."""
mcrfpy.delTimer(self.timer_name)
def animate_step(dt):
# Lerp toward target
diff = self.current - self.display_current
if abs(diff) < 0.5:
self.display_current = self.current
mcrfpy.delTimer(self.timer_name)
# Also update damage preview
self.damage_fill.w = self.fill.w
else:
# Move 10% of the way each frame
self.display_current += diff * 0.1
self._update_display()
mcrfpy.setTimer(self.timer_name, animate_step, 16)
def damage(self, amount):
"""Apply damage with animation."""
self.set_health(self.current - amount, animate=True)
def heal(self, amount):
"""Apply healing with animation."""
self.set_health(self.current + amount, animate=True)
def add_to_scene(self, ui):
"""Add all frames to scene."""
ui.append(self.background)
ui.append(self.damage_fill)
ui.append(self.fill)
# Usage
hp_bar = AnimatedHealthBar(50, 50, 300, 30, current=100, maximum=100)
hp_bar.add_to_scene(ui)
# Damage will animate smoothly
hp_bar.damage(40)

View file

@ -1,43 +0,0 @@
"""McRogueFace - Health Bar Widget (basic)
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
mcrfpy.createScene("game")
mcrfpy.setScene("game")
ui = mcrfpy.sceneUI("game")
# Player health bar at top
player_hp = EnhancedHealthBar(10, 10, 300, 30, 100, 100)
player_hp.add_to_scene(ui)
# Enemy health bar
enemy_hp = EnhancedHealthBar(400, 10, 200, 20, 50, 50)
enemy_hp.add_to_scene(ui)
# Simulate combat
def combat_tick(dt):
import random
if random.random() < 0.3:
player_hp.damage(random.randint(5, 15))
if random.random() < 0.4:
enemy_hp.damage(random.randint(3, 8))
mcrfpy.setTimer("combat", combat_tick, 1000)
# Keyboard controls for testing
def on_key(key, state):
if state != "start":
return
if key == "H":
player_hp.heal(20)
elif key == "D":
player_hp.damage(10)
mcrfpy.keypressScene(on_key)

View file

@ -1,123 +0,0 @@
"""McRogueFace - Health Bar Widget (enhanced)
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_enhanced.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class EnhancedHealthBar:
"""Health bar with text display, color transitions, and animations."""
def __init__(self, x, y, w, h, current, maximum, show_text=True):
self.x = x
self.y = y
self.w = w
self.h = h
self.current = current
self.maximum = maximum
self.show_text = show_text
# Color thresholds (ratio -> color)
self.colors = {
0.6: mcrfpy.Color(50, 205, 50), # Green when > 60%
0.3: mcrfpy.Color(255, 165, 0), # Orange when > 30%
0.0: mcrfpy.Color(220, 20, 20), # Red when <= 30%
}
# Background frame with dark fill
self.background = mcrfpy.Frame(x, y, w, h)
self.background.fill_color = mcrfpy.Color(30, 30, 30)
self.background.outline = 2
self.background.outline_color = mcrfpy.Color(100, 100, 100)
# Fill frame (nested inside background conceptually)
padding = 2
self.fill = mcrfpy.Frame(
x + padding,
y + padding,
w - padding * 2,
h - padding * 2
)
self.fill.outline = 0
# Text label
self.label = None
if show_text:
self.label = mcrfpy.Caption(
"",
mcrfpy.default_font,
x + w / 2 - 20,
y + h / 2 - 8
)
self.label.fill_color = mcrfpy.Color(255, 255, 255)
self.label.outline = 1
self.label.outline_color = mcrfpy.Color(0, 0, 0)
self._update()
def _get_color_for_ratio(self, ratio):
"""Get the appropriate color based on health ratio."""
for threshold, color in sorted(self.colors.items(), reverse=True):
if ratio > threshold:
return color
# Return the lowest threshold color if ratio is 0 or below
return self.colors[0.0]
def _update(self):
"""Update fill width, color, and text."""
ratio = max(0, min(1, self.current / self.maximum))
# Update fill width (accounting for padding)
padding = 2
self.fill.w = (self.w - padding * 2) * ratio
# Update color based on ratio
self.fill.fill_color = self._get_color_for_ratio(ratio)
# Update text
if self.label:
self.label.text = f"{int(self.current)}/{int(self.maximum)}"
# Center the text
text_width = len(self.label.text) * 8 # Approximate
self.label.x = self.x + (self.w - text_width) / 2
def set_health(self, current, maximum=None):
"""Update health values."""
self.current = max(0, current)
if maximum is not None:
self.maximum = maximum
self._update()
def damage(self, amount):
"""Apply damage (convenience method)."""
self.set_health(self.current - amount)
def heal(self, amount):
"""Apply healing (convenience method)."""
self.set_health(min(self.maximum, self.current + amount))
def add_to_scene(self, ui):
"""Add all components to scene UI."""
ui.append(self.background)
ui.append(self.fill)
if self.label:
ui.append(self.label)
# Usage
mcrfpy.createScene("demo")
mcrfpy.setScene("demo")
ui = mcrfpy.sceneUI("demo")
# Create enhanced health bar
hp = EnhancedHealthBar(50, 50, 250, 25, current=100, maximum=100)
hp.add_to_scene(ui)
# Simulate damage
hp.damage(30) # Now 70/100, shows green
hp.damage(25) # Now 45/100, shows orange
hp.damage(20) # Now 25/100, shows red

View file

@ -1,108 +0,0 @@
"""McRogueFace - Health Bar Widget (multi)
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_multi.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class ResourceBar:
"""Generic resource bar that can represent any stat."""
def __init__(self, x, y, w, h, current, maximum,
fill_color, bg_color=None, label=""):
self.x = x
self.y = y
self.w = w
self.h = h
self.current = current
self.maximum = maximum
self.label_text = label
if bg_color is None:
bg_color = mcrfpy.Color(30, 30, 30)
# Background
self.background = mcrfpy.Frame(x, y, w, h)
self.background.fill_color = bg_color
self.background.outline = 1
self.background.outline_color = mcrfpy.Color(60, 60, 60)
# Fill
self.fill = mcrfpy.Frame(x + 1, y + 1, w - 2, h - 2)
self.fill.fill_color = fill_color
self.fill.outline = 0
# Label (left side)
self.label = mcrfpy.Caption(label, mcrfpy.default_font, x - 30, y + 2)
self.label.fill_color = mcrfpy.Color(200, 200, 200)
self._update()
def _update(self):
ratio = max(0, min(1, self.current / self.maximum))
self.fill.w = (self.w - 2) * ratio
def set_value(self, current, maximum=None):
self.current = max(0, current)
if maximum:
self.maximum = maximum
self._update()
def add_to_scene(self, ui):
if self.label_text:
ui.append(self.label)
ui.append(self.background)
ui.append(self.fill)
class PlayerStats:
"""Collection of resource bars for a player."""
def __init__(self, x, y):
bar_width = 200
bar_height = 18
spacing = 25
self.hp = ResourceBar(
x, y, bar_width, bar_height,
current=100, maximum=100,
fill_color=mcrfpy.Color(220, 50, 50),
label="HP"
)
self.mp = ResourceBar(
x, y + spacing, bar_width, bar_height,
current=50, maximum=50,
fill_color=mcrfpy.Color(50, 100, 220),
label="MP"
)
self.stamina = ResourceBar(
x, y + spacing * 2, bar_width, bar_height,
current=80, maximum=80,
fill_color=mcrfpy.Color(50, 180, 50),
label="SP"
)
def add_to_scene(self, ui):
self.hp.add_to_scene(ui)
self.mp.add_to_scene(ui)
self.stamina.add_to_scene(ui)
# Usage
mcrfpy.createScene("stats_demo")
mcrfpy.setScene("stats_demo")
ui = mcrfpy.sceneUI("stats_demo")
stats = PlayerStats(80, 20)
stats.add_to_scene(ui)
# Update individual stats
stats.hp.set_value(75)
stats.mp.set_value(30)
stats.stamina.set_value(60)

View file

@ -1,53 +0,0 @@
"""McRogueFace - Selection Menu Widget (basic)
Documentation: https://mcrogueface.github.io/cookbook/ui_menu
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_menu_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Setup
mcrfpy.createScene("main_menu")
mcrfpy.setScene("main_menu")
ui = mcrfpy.sceneUI("main_menu")
# Background
bg = mcrfpy.Frame(0, 0, 1024, 768)
bg.fill_color = mcrfpy.Color(20, 20, 35)
ui.append(bg)
# Title
title = mcrfpy.Caption("DUNGEON QUEST", mcrfpy.default_font, 350, 100)
title.fill_color = mcrfpy.Color(255, 200, 50)
ui.append(title)
# Menu
def start_game():
print("Starting game...")
def show_options():
print("Options...")
menu = Menu(
362, 250,
["New Game", "Continue", "Options", "Quit"],
lambda i, opt: {
0: start_game,
1: lambda: print("Continue..."),
2: show_options,
3: mcrfpy.exit
}.get(i, lambda: None)(),
title="Main Menu"
)
menu.add_to_scene(ui)
# Input
def on_key(key, state):
if state != "start":
return
menu.handle_key(key)
mcrfpy.keypressScene(on_key)

View file

@ -1,159 +0,0 @@
"""McRogueFace - Selection Menu Widget (enhanced)
Documentation: https://mcrogueface.github.io/cookbook/ui_menu
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_menu_enhanced.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class MenuBar:
"""Horizontal menu bar with dropdown submenus."""
def __init__(self, y=0, items=None):
"""
Create a menu bar.
Args:
y: Y position (usually 0 for top)
items: List of dicts with 'label' and 'options' keys
"""
self.y = y
self.items = items or []
self.selected_item = 0
self.dropdown_open = False
self.dropdown_selected = 0
self.item_width = 100
self.height = 30
# Main bar frame
self.bar = mcrfpy.Frame(0, y, 1024, self.height)
self.bar.fill_color = mcrfpy.Color(50, 50, 70)
self.bar.outline = 0
# Item captions
self.item_captions = []
for i, item in enumerate(items):
cap = mcrfpy.Caption(
item['label'],
mcrfpy.default_font,
10 + i * self.item_width,
y + 7
)
cap.fill_color = mcrfpy.Color(200, 200, 200)
self.item_captions.append(cap)
# Dropdown panel (hidden initially)
self.dropdown = None
self.dropdown_captions = []
def _update_highlight(self):
"""Update visual selection on bar."""
for i, cap in enumerate(self.item_captions):
if i == self.selected_item and self.dropdown_open:
cap.fill_color = mcrfpy.Color(255, 255, 100)
else:
cap.fill_color = mcrfpy.Color(200, 200, 200)
def _show_dropdown(self, ui):
"""Show dropdown for selected item."""
# Remove existing dropdown
self._hide_dropdown(ui)
item = self.items[self.selected_item]
options = item.get('options', [])
if not options:
return
x = 5 + self.selected_item * self.item_width
y = self.y + self.height
width = 150
height = len(options) * 25 + 10
self.dropdown = mcrfpy.Frame(x, y, width, height)
self.dropdown.fill_color = mcrfpy.Color(40, 40, 60, 250)
self.dropdown.outline = 1
self.dropdown.outline_color = mcrfpy.Color(80, 80, 100)
ui.append(self.dropdown)
self.dropdown_captions = []
for i, opt in enumerate(options):
cap = mcrfpy.Caption(
opt['label'],
mcrfpy.default_font,
x + 10,
y + 5 + i * 25
)
cap.fill_color = mcrfpy.Color(200, 200, 200)
self.dropdown_captions.append(cap)
ui.append(cap)
self.dropdown_selected = 0
self._update_dropdown_highlight()
def _hide_dropdown(self, ui):
"""Hide dropdown menu."""
if self.dropdown:
try:
ui.remove(self.dropdown)
except:
pass
self.dropdown = None
for cap in self.dropdown_captions:
try:
ui.remove(cap)
except:
pass
self.dropdown_captions = []
def _update_dropdown_highlight(self):
"""Update dropdown selection highlight."""
for i, cap in enumerate(self.dropdown_captions):
if i == self.dropdown_selected:
cap.fill_color = mcrfpy.Color(255, 255, 100)
else:
cap.fill_color = mcrfpy.Color(200, 200, 200)
def add_to_scene(self, ui):
ui.append(self.bar)
for cap in self.item_captions:
ui.append(cap)
def handle_key(self, key, ui):
"""Handle keyboard navigation."""
if not self.dropdown_open:
if key == "Left":
self.selected_item = (self.selected_item - 1) % len(self.items)
self._update_highlight()
elif key == "Right":
self.selected_item = (self.selected_item + 1) % len(self.items)
self._update_highlight()
elif key == "Return" or key == "Down":
self.dropdown_open = True
self._show_dropdown(ui)
self._update_highlight()
else:
if key == "Up":
options = self.items[self.selected_item].get('options', [])
self.dropdown_selected = (self.dropdown_selected - 1) % len(options)
self._update_dropdown_highlight()
elif key == "Down":
options = self.items[self.selected_item].get('options', [])
self.dropdown_selected = (self.dropdown_selected + 1) % len(options)
self._update_dropdown_highlight()
elif key == "Return":
opt = self.items[self.selected_item]['options'][self.dropdown_selected]
if opt.get('action'):
opt['action']()
self.dropdown_open = False
self._hide_dropdown(ui)
self._update_highlight()
elif key == "Escape":
self.dropdown_open = False
self._hide_dropdown(ui)
self._update_highlight()

View file

@ -1,54 +0,0 @@
"""McRogueFace - Message Log Widget (basic)
Documentation: https://mcrogueface.github.io/cookbook/ui_message_log
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_message_log_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Initialize
mcrfpy.createScene("game")
mcrfpy.setScene("game")
ui = mcrfpy.sceneUI("game")
# Create log at bottom of screen
log = EnhancedMessageLog(10, 500, 700, 250, line_height=20)
ui.append(log.frame)
# Simulate game events
def simulate_combat(dt):
import random
events = [
("You swing your sword!", "combat"),
("The orc dodges!", "combat"),
("Critical hit!", "combat"),
("You found a potion!", "loot"),
]
event = random.choice(events)
log.add(event[0], event[1])
# Add messages every 2 seconds for demo
mcrfpy.setTimer("combat_sim", simulate_combat, 2000)
# Keyboard controls
def on_key(key, state):
if state != "start":
return
if key == "PageUp":
log.scroll_up(3)
elif key == "PageDown":
log.scroll_down(3)
elif key == "C":
log.set_filter('combat')
elif key == "L":
log.set_filter('loot')
elif key == "A":
log.set_filter(None) # All
mcrfpy.keypressScene(on_key)
log.system("Press PageUp/PageDown to scroll")
log.system("Press C for combat, L for loot, A for all")

View file

@ -1,27 +0,0 @@
"""McRogueFace - Message Log Widget (enhanced)
Documentation: https://mcrogueface.github.io/cookbook/ui_message_log
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_message_log_enhanced.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
def handle_keys(key, state):
if state != "start":
return
if key == "PageUp":
log.scroll_up(5)
elif key == "PageDown":
log.scroll_down(5)
mcrfpy.keypressScene(handle_keys)
# Or with mouse scroll on the frame
def on_log_scroll(x, y, button, action):
# Note: You may need to implement scroll detection
# based on your input system
pass
log.frame.click = on_log_scroll

View file

@ -1,69 +0,0 @@
"""McRogueFace - Modal Dialog Widget (basic)
Documentation: https://mcrogueface.github.io/cookbook/ui_modal_dialog
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_modal_dialog_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Scene setup
mcrfpy.createScene("game")
mcrfpy.setScene("game")
ui = mcrfpy.sceneUI("game")
# Game background
bg = mcrfpy.Frame(0, 0, 1024, 768)
bg.fill_color = mcrfpy.Color(25, 35, 45)
ui.append(bg)
title = mcrfpy.Caption("My Game", mcrfpy.default_font, 450, 50)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Quit button
quit_btn = mcrfpy.Frame(430, 400, 160, 50)
quit_btn.fill_color = mcrfpy.Color(150, 50, 50)
quit_btn.outline = 2
quit_btn.outline_color = mcrfpy.Color(200, 100, 100)
ui.append(quit_btn)
quit_label = mcrfpy.Caption("Quit Game", mcrfpy.default_font, 460, 415)
quit_label.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(quit_label)
# Confirmation dialog
confirm_dialog = None
def show_quit_confirm():
global confirm_dialog
def on_response(index, label):
if label == "Yes":
mcrfpy.exit()
confirm_dialog = EnhancedDialog(
"Quit Game?",
"Are you sure you want to quit?\nUnsaved progress will be lost.",
["Yes", "No"],
DialogStyle.WARNING,
on_response
)
confirm_dialog.add_to_scene(ui)
confirm_dialog.show()
quit_btn.click = lambda x, y, b, a: show_quit_confirm() if a == "start" else None
def on_key(key, state):
if state != "start":
return
if confirm_dialog and confirm_dialog.handle_key(key):
return
if key == "Escape":
show_quit_confirm()
mcrfpy.keypressScene(on_key)

View file

@ -1,78 +0,0 @@
"""McRogueFace - Modal Dialog Widget (enhanced)
Documentation: https://mcrogueface.github.io/cookbook/ui_modal_dialog
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_modal_dialog_enhanced.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
class DialogManager:
"""Manages a queue of dialogs."""
def __init__(self, ui):
self.ui = ui
self.queue = []
self.current = None
def show(self, title, message, buttons=None, style=None, callback=None):
"""
Queue a dialog to show.
If no dialog is active, shows immediately.
Otherwise, queues for later.
"""
dialog_data = {
'title': title,
'message': message,
'buttons': buttons or ["OK"],
'style': style or DialogStyle.INFO,
'callback': callback
}
if self.current is None:
self._show_dialog(dialog_data)
else:
self.queue.append(dialog_data)
def _show_dialog(self, data):
"""Actually display a dialog."""
def on_close(index, label):
if data['callback']:
data['callback'](index, label)
self._on_dialog_closed()
self.current = EnhancedDialog(
data['title'],
data['message'],
data['buttons'],
data['style'],
on_close
)
self.current.add_to_scene(self.ui)
self.current.show()
def _on_dialog_closed(self):
"""Handle dialog close, show next if queued."""
self.current = None
if self.queue:
next_dialog = self.queue.pop(0)
self._show_dialog(next_dialog)
def handle_key(self, key):
"""Forward key events to current dialog."""
if self.current:
return self.current.handle_key(key)
return False
# Usage
manager = DialogManager(ui)
# Queue multiple dialogs
manager.show("First", "This is the first message")
manager.show("Second", "This appears after closing the first")
manager.show("Third", "And this is last", ["Done"])

View file

@ -1,65 +0,0 @@
"""McRogueFace - Tooltip on Hover (basic)
Documentation: https://mcrogueface.github.io/cookbook/ui_tooltip
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_tooltip_basic.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
mcrfpy.createScene("game")
mcrfpy.setScene("game")
ui = mcrfpy.sceneUI("game")
# Background
bg = mcrfpy.Frame(0, 0, 1024, 768)
bg.fill_color = mcrfpy.Color(25, 25, 35)
ui.append(bg)
# Create inventory slots with tooltips
class InventorySlot:
def __init__(self, x, y, item_name, item_desc, tooltip_mgr):
self.frame = mcrfpy.Frame(x, y, 50, 50)
self.frame.fill_color = mcrfpy.Color(50, 50, 60)
self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(80, 80, 90)
self.label = mcrfpy.Caption(item_name[:3], mcrfpy.default_font, x + 10, y + 15)
self.label.fill_color = mcrfpy.Color(200, 200, 200)
tooltip_mgr.register(self.frame, item_desc, title=item_name)
def add_to_scene(self, ui):
ui.append(self.frame)
ui.append(self.label)
# Setup tooltip manager
tips = TooltipManager()
tips.hover_delay = 300
# Create inventory
items = [
("Health Potion", "Restores 50 HP\nConsumable"),
("Mana Crystal", "Restores 30 MP\nConsumable"),
("Iron Key", "Opens iron doors\nQuest Item"),
("Gold Ring", "Worth 100 gold\nSell to merchant"),
]
slots = []
for i, (name, desc) in enumerate(items):
slot = InventorySlot(100 + i * 60, 100, name, desc, tips)
slot.add_to_scene(ui)
slots.append(slot)
# Add tooltip last
tips.add_to_scene(ui)
# Update loop
def update(dt):
from mcrfpy import automation
x, y = automation.position()
tips.update(x, y)
mcrfpy.setTimer("update", update, 50)

View file

@ -1,80 +0,0 @@
"""McRogueFace - Tooltip on Hover (multi)
Documentation: https://mcrogueface.github.io/cookbook/ui_tooltip
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_tooltip_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 create_info_icon(x, y, tooltip_text, ui):
"""
Create an info icon that shows tooltip on hover.
Args:
x, y: Position of the icon
tooltip_text: Text to show
ui: Scene UI to add elements to
"""
# Info icon (small circle with "i")
icon = mcrfpy.Frame(x, y, 20, 20)
icon.fill_color = mcrfpy.Color(70, 130, 180)
icon.outline = 1
icon.outline_color = mcrfpy.Color(100, 160, 210)
icon_label = mcrfpy.Caption("i", mcrfpy.default_font, x + 6, y + 2)
icon_label.fill_color = mcrfpy.Color(255, 255, 255)
# Tooltip (positioned to the right of icon)
tip_frame = mcrfpy.Frame(x + 25, y - 5, 180, 50)
tip_frame.fill_color = mcrfpy.Color(40, 40, 55, 240)
tip_frame.outline = 1
tip_frame.outline_color = mcrfpy.Color(80, 80, 100)
tip_frame.visible = False
tip_text = mcrfpy.Caption(tooltip_text, mcrfpy.default_font, x + 33, y + 3)
tip_text.fill_color = mcrfpy.Color(220, 220, 220)
tip_text.visible = False
# Hover behavior
def on_icon_hover(mx, my, button, action):
tip_frame.visible = True
tip_text.visible = True
icon.click = on_icon_hover
# Track when to hide
def check_hover(dt):
from mcrfpy import automation
mx, my = automation.position()
if not (icon.x <= mx <= icon.x + icon.w and
icon.y <= my <= icon.y + icon.h):
if tip_frame.visible:
tip_frame.visible = False
tip_text.visible = False
timer_name = f"info_hover_{id(icon)}"
mcrfpy.setTimer(timer_name, check_hover, 100)
# Add to scene
ui.append(icon)
ui.append(icon_label)
ui.append(tip_frame)
ui.append(tip_text)
return icon
# Usage
mcrfpy.createScene("info_demo")
mcrfpy.setScene("info_demo")
ui = mcrfpy.sceneUI("info_demo")
# Setting with info icon
setting_label = mcrfpy.Caption("Difficulty:", mcrfpy.default_font, 100, 100)
setting_label.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(setting_label)
create_info_icon(200, 98, "Affects enemy\nHP and damage", ui)

File diff suppressed because it is too large Load diff

View file

@ -1,532 +0,0 @@
"""Type stubs for McRogueFace Python API.
Core game engine interface for creating roguelike games with Python.
"""
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
# Type aliases
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid']
Transition = Union[str, None]
# Classes
class Color:
"""SFML Color Object for RGBA colors."""
r: int
g: int
b: int
a: int
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
def from_hex(self, hex_string: str) -> 'Color':
"""Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
...
def to_hex(self) -> str:
"""Convert color to hex string format."""
...
def lerp(self, other: 'Color', t: float) -> 'Color':
"""Linear interpolation between two colors."""
...
class Vector:
"""SFML Vector Object for 2D coordinates."""
x: float
y: float
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float, y: float) -> None: ...
def add(self, other: 'Vector') -> 'Vector': ...
def subtract(self, other: 'Vector') -> 'Vector': ...
def multiply(self, scalar: float) -> 'Vector': ...
def divide(self, scalar: float) -> 'Vector': ...
def distance(self, other: 'Vector') -> float: ...
def normalize(self) -> 'Vector': ...
def dot(self, other: 'Vector') -> float: ...
class Texture:
"""SFML Texture Object for images."""
def __init__(self, filename: str) -> None: ...
filename: str
width: int
height: int
sprite_count: int
class Font:
"""SFML Font Object for text rendering."""
def __init__(self, filename: str) -> None: ...
filename: str
family: str
class Drawable:
"""Base class for all drawable UI elements."""
x: float
y: float
visible: bool
z_index: int
name: str
pos: Vector
def get_bounds(self) -> Tuple[float, float, float, float]:
"""Get bounding box as (x, y, width, height)."""
...
def move(self, dx: float, dy: float) -> None:
"""Move by relative offset (dx, dy)."""
...
def resize(self, width: float, height: float) -> None:
"""Resize to new dimensions (width, height)."""
...
class Frame(Drawable):
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)
A rectangular frame UI element that can contain other drawable elements.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0,
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
outline: float = 0, click: Optional[Callable] = None,
children: Optional[List[UIElement]] = None) -> None: ...
w: float
h: float
fill_color: Color
outline_color: Color
outline: float
click: Optional[Callable[[float, float, int], None]]
children: 'UICollection'
clip_children: bool
class Caption(Drawable):
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)
A text display UI element with customizable font and styling.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, text: str = '', x: float = 0, y: float = 0,
font: Optional[Font] = None, fill_color: Optional[Color] = None,
outline_color: Optional[Color] = None, outline: float = 0,
click: Optional[Callable] = None) -> None: ...
text: str
font: Font
fill_color: Color
outline_color: Color
outline: float
click: Optional[Callable[[float, float, int], None]]
w: float # Read-only, computed from text
h: float # Read-only, computed from text
class Sprite(Drawable):
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)
A sprite UI element that displays a texture or portion of a texture atlas.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None,
sprite_index: int = 0, scale: float = 1.0,
click: Optional[Callable] = None) -> None: ...
texture: Texture
sprite_index: int
scale: float
click: Optional[Callable[[float, float, int], None]]
w: float # Read-only, computed from texture
h: float # Read-only, computed from texture
class Grid(Drawable):
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)
A grid-based tilemap UI element for rendering tile-based levels and game worlds.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20),
texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16,
scale: float = 1.0, click: Optional[Callable] = None) -> None: ...
grid_size: Tuple[int, int]
tile_width: int
tile_height: int
texture: Texture
scale: float
points: List[List['GridPoint']]
entities: 'EntityCollection'
background_color: Color
click: Optional[Callable[[int, int, int], None]]
def at(self, x: int, y: int) -> 'GridPoint':
"""Get grid point at tile coordinates."""
...
class GridPoint:
"""Grid point representing a single tile."""
texture_index: int
solid: bool
color: Color
class GridPointState:
"""State information for a grid point."""
texture_index: int
color: Color
class Entity(Drawable):
"""Entity(grid_x=0, grid_y=0, texture=None, sprite_index=0, name='')
Game entity that lives within a Grid.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, grid_x: float = 0, grid_y: float = 0, texture: Optional[Texture] = None,
sprite_index: int = 0, name: str = '') -> None: ...
grid_x: float
grid_y: float
texture: Texture
sprite_index: int
grid: Optional[Grid]
def at(self, grid_x: float, grid_y: float) -> None:
"""Move entity to grid position."""
...
def die(self) -> None:
"""Remove entity from its grid."""
...
def index(self) -> int:
"""Get index in parent grid's entity collection."""
...
class UICollection:
"""Collection of UI drawable elements (Frame, Caption, Sprite, Grid)."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> UIElement: ...
def __setitem__(self, index: int, value: UIElement) -> None: ...
def __delitem__(self, index: int) -> None: ...
def __contains__(self, item: UIElement) -> bool: ...
def __iter__(self) -> Any: ...
def __add__(self, other: 'UICollection') -> 'UICollection': ...
def __iadd__(self, other: 'UICollection') -> 'UICollection': ...
def append(self, item: UIElement) -> None: ...
def extend(self, items: List[UIElement]) -> None: ...
def remove(self, item: UIElement) -> None: ...
def index(self, item: UIElement) -> int: ...
def count(self, item: UIElement) -> int: ...
class EntityCollection:
"""Collection of Entity objects."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> Entity: ...
def __setitem__(self, index: int, value: Entity) -> None: ...
def __delitem__(self, index: int) -> None: ...
def __contains__(self, item: Entity) -> bool: ...
def __iter__(self) -> Any: ...
def __add__(self, other: 'EntityCollection') -> 'EntityCollection': ...
def __iadd__(self, other: 'EntityCollection') -> 'EntityCollection': ...
def append(self, item: Entity) -> None: ...
def extend(self, items: List[Entity]) -> None: ...
def remove(self, item: Entity) -> None: ...
def index(self, item: Entity) -> int: ...
def count(self, item: Entity) -> int: ...
class Scene:
"""Base class for object-oriented scenes."""
name: str
def __init__(self, name: str) -> None: ...
def activate(self) -> None:
"""Called when scene becomes active."""
...
def deactivate(self) -> None:
"""Called when scene becomes inactive."""
...
def get_ui(self) -> UICollection:
"""Get UI elements collection."""
...
def on_keypress(self, key: str, pressed: bool) -> None:
"""Handle keyboard events."""
...
def on_click(self, x: float, y: float, button: int) -> None:
"""Handle mouse clicks."""
...
def on_enter(self) -> None:
"""Called when entering the scene."""
...
def on_exit(self) -> None:
"""Called when leaving the scene."""
...
def on_resize(self, width: int, height: int) -> None:
"""Handle window resize events."""
...
def update(self, dt: float) -> None:
"""Update scene logic."""
...
class Timer:
"""Timer object for scheduled callbacks."""
name: str
interval: int
active: bool
def __init__(self, name: str, callback: Callable[[float], None], interval: int) -> None: ...
def pause(self) -> None:
"""Pause the timer."""
...
def resume(self) -> None:
"""Resume the timer."""
...
def cancel(self) -> None:
"""Cancel and remove the timer."""
...
class Window:
"""Window singleton for managing the game window."""
resolution: Tuple[int, int]
fullscreen: bool
vsync: bool
title: str
fps_limit: int
game_resolution: Tuple[int, int]
scaling_mode: str
@staticmethod
def get() -> 'Window':
"""Get the window singleton instance."""
...
class Animation:
"""Animation object for animating UI properties."""
target: Any
property: str
duration: float
easing: str
loop: bool
on_complete: Optional[Callable]
def __init__(self, target: Any, property: str, start_value: Any, end_value: Any,
duration: float, easing: str = 'linear', loop: bool = False,
on_complete: Optional[Callable] = None) -> None: ...
def start(self) -> None:
"""Start the animation."""
...
def update(self, dt: float) -> bool:
"""Update animation, returns True if still running."""
...
def get_current_value(self) -> Any:
"""Get the current interpolated value."""
...
# Module functions
def createSoundBuffer(filename: str) -> int:
"""Load a sound effect from a file and return its buffer ID."""
...
def loadMusic(filename: str) -> None:
"""Load and immediately play background music from a file."""
...
def setMusicVolume(volume: int) -> None:
"""Set the global music volume (0-100)."""
...
def setSoundVolume(volume: int) -> None:
"""Set the global sound effects volume (0-100)."""
...
def playSound(buffer_id: int) -> None:
"""Play a sound effect using a previously loaded buffer."""
...
def getMusicVolume() -> int:
"""Get the current music volume level (0-100)."""
...
def getSoundVolume() -> int:
"""Get the current sound effects volume level (0-100)."""
...
def sceneUI(scene: Optional[str] = None) -> UICollection:
"""Get all UI elements for a scene."""
...
def currentScene() -> str:
"""Get the name of the currently active scene."""
...
def setScene(scene: str, transition: Optional[str] = None, duration: float = 0.0) -> None:
"""Switch to a different scene with optional transition effect."""
...
def createScene(name: str) -> None:
"""Create a new empty scene."""
...
def keypressScene(handler: Callable[[str, bool], None]) -> None:
"""Set the keyboard event handler for the current scene."""
...
def setTimer(name: str, handler: Callable[[float], None], interval: int) -> None:
"""Create or update a recurring timer."""
...
def delTimer(name: str) -> None:
"""Stop and remove a timer."""
...
def exit() -> None:
"""Cleanly shut down the game engine and exit the application."""
...
def setScale(multiplier: float) -> None:
"""Scale the game window size (deprecated - use Window.resolution)."""
...
def find(name: str, scene: Optional[str] = None) -> Optional[UIElement]:
"""Find the first UI element with the specified name."""
...
def findAll(pattern: str, scene: Optional[str] = None) -> List[UIElement]:
"""Find all UI elements matching a name pattern (supports * wildcards)."""
...
def getMetrics() -> Dict[str, Union[int, float]]:
"""Get current performance metrics."""
...
# Submodule
class automation:
"""Automation API for testing and scripting."""
@staticmethod
def screenshot(filename: str) -> bool:
"""Save a screenshot to the specified file."""
...
@staticmethod
def position() -> Tuple[int, int]:
"""Get current mouse position as (x, y) tuple."""
...
@staticmethod
def size() -> Tuple[int, int]:
"""Get screen size as (width, height) tuple."""
...
@staticmethod
def onScreen(x: int, y: int) -> bool:
"""Check if coordinates are within screen bounds."""
...
@staticmethod
def moveTo(x: int, y: int, duration: float = 0.0) -> None:
"""Move mouse to absolute position."""
...
@staticmethod
def moveRel(xOffset: int, yOffset: int, duration: float = 0.0) -> None:
"""Move mouse relative to current position."""
...
@staticmethod
def dragTo(x: int, y: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse to position."""
...
@staticmethod
def dragRel(xOffset: int, yOffset: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse relative to current position."""
...
@staticmethod
def click(x: Optional[int] = None, y: Optional[int] = None, clicks: int = 1,
interval: float = 0.0, button: str = 'left') -> None:
"""Click mouse at position."""
...
@staticmethod
def mouseDown(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Press mouse button down."""
...
@staticmethod
def mouseUp(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Release mouse button."""
...
@staticmethod
def keyDown(key: str) -> None:
"""Press key down."""
...
@staticmethod
def keyUp(key: str) -> None:
"""Release key."""
...
@staticmethod
def press(key: str) -> None:
"""Press and release a key."""
...
@staticmethod
def typewrite(text: str, interval: float = 0.0) -> None:
"""Type text with optional interval between characters."""
...

View file

@ -1,209 +0,0 @@
"""Type stubs for McRogueFace Python API.
Auto-generated - do not edit directly.
"""
from typing import Any, List, Dict, Tuple, Optional, Callable, Union
# Module documentation
# McRogueFace Python API\n\nCore game engine interface for creating roguelike games with Python.\n\nThis module provides:\n- Scene management (createScene, setScene, currentScene)\n- UI components (Frame, Caption, Sprite, Grid)\n- Entity system for game objects\n- Audio playback (sound effects and music)\n- Timer system for scheduled events\n- Input handling\n- Performance metrics\n\nExample:\n import mcrfpy\n \n # Create a new scene\n mcrfpy.createScene('game')\n mcrfpy.setScene('game')\n \n # Add UI elements\n frame = mcrfpy.Frame(10, 10, 200, 100)\n caption = mcrfpy.Caption('Hello World', 50, 50)\n mcrfpy.sceneUI().extend([frame, caption])\n
# Classes
class Animation:
"""Animation object for animating UI properties"""
def __init__(selftype(self)) -> None: ...
def get_current_value(self, *args, **kwargs) -> Any: ...
def start(self, *args, **kwargs) -> Any: ...
def update(selfreturns True if still running) -> Any: ...
class Caption:
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Color:
"""SFML Color Object"""
def __init__(selftype(self)) -> None: ...
def from_hex(selfe.g., '#FF0000' or 'FF0000') -> Any: ...
def lerp(self, *args, **kwargs) -> Any: ...
def to_hex(self, *args, **kwargs) -> Any: ...
class Drawable:
"""Base class for all drawable UI elements"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Entity:
"""UIEntity objects"""
def __init__(selftype(self)) -> None: ...
def at(self, *args, **kwargs) -> Any: ...
def die(self, *args, **kwargs) -> Any: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def move(selfdx, dy) -> Any: ...
def path_to(selfx: int, y: int) -> bool: ...
def resize(selfwidth, height) -> Any: ...
def update_visibility(self) -> None: ...
class EntityCollection:
"""Iterable, indexable collection of Entities"""
def __init__(selftype(self)) -> None: ...
def append(self, *args, **kwargs) -> Any: ...
def count(self, *args, **kwargs) -> Any: ...
def extend(self, *args, **kwargs) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def remove(self, *args, **kwargs) -> Any: ...
class Font:
"""SFML Font Object"""
def __init__(selftype(self)) -> None: ...
class Frame:
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Grid:
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)"""
def __init__(selftype(self)) -> None: ...
def at(self, *args, **kwargs) -> Any: ...
def compute_astar_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
def compute_dijkstra(selfroot_x: int, root_y: int, diagonal_cost: float = 1.41) -> None: ...
def compute_fov(selfx: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None: ...
def find_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def get_dijkstra_distance(selfx: int, y: int) -> Optional[float]: ...
def get_dijkstra_path(selfx: int, y: int) -> List[Tuple[int, int]]: ...
def is_in_fov(selfx: int, y: int) -> bool: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class GridPoint:
"""UIGridPoint object"""
def __init__(selftype(self)) -> None: ...
class GridPointState:
"""UIGridPointState object"""
def __init__(selftype(self)) -> None: ...
class Scene:
"""Base class for object-oriented scenes"""
def __init__(selftype(self)) -> None: ...
def activate(self, *args, **kwargs) -> Any: ...
def get_ui(self, *args, **kwargs) -> Any: ...
def register_keyboard(selfalternative to overriding on_keypress) -> Any: ...
class Sprite:
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Texture:
"""SFML Texture Object"""
def __init__(selftype(self)) -> None: ...
class Timer:
"""Timer object for scheduled callbacks"""
def __init__(selftype(self)) -> None: ...
def cancel(self, *args, **kwargs) -> Any: ...
def pause(self, *args, **kwargs) -> Any: ...
def restart(self, *args, **kwargs) -> Any: ...
def resume(self, *args, **kwargs) -> Any: ...
class UICollection:
"""Iterable, indexable collection of UI objects"""
def __init__(selftype(self)) -> None: ...
def append(self, *args, **kwargs) -> Any: ...
def count(self, *args, **kwargs) -> Any: ...
def extend(self, *args, **kwargs) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def remove(self, *args, **kwargs) -> Any: ...
class UICollectionIter:
"""Iterator for a collection of UI objects"""
def __init__(selftype(self)) -> None: ...
class UIEntityCollectionIter:
"""Iterator for a collection of UI objects"""
def __init__(selftype(self)) -> None: ...
class Vector:
"""SFML Vector Object"""
def __init__(selftype(self)) -> None: ...
def angle(self, *args, **kwargs) -> Any: ...
def copy(self, *args, **kwargs) -> Any: ...
def distance_to(self, *args, **kwargs) -> Any: ...
def dot(self, *args, **kwargs) -> Any: ...
def magnitude(self, *args, **kwargs) -> Any: ...
def magnitude_squared(self, *args, **kwargs) -> Any: ...
def normalize(self, *args, **kwargs) -> Any: ...
class Window:
"""Window singleton for accessing and modifying the game window properties"""
def __init__(selftype(self)) -> None: ...
def center(self, *args, **kwargs) -> Any: ...
def get(self, *args, **kwargs) -> Any: ...
def screenshot(self, *args, **kwargs) -> Any: ...
# Functions
def createScene(name: str) -> None: ...
def createSoundBuffer(filename: str) -> int: ...
def currentScene() -> str: ...
def delTimer(name: str) -> None: ...
def exit() -> None: ...
def find(name: str, scene: str = None) -> UIDrawable | None: ...
def findAll(pattern: str, scene: str = None) -> list: ...
def getMetrics() -> dict: ...
def getMusicVolume() -> int: ...
def getSoundVolume() -> int: ...
def keypressScene(handler: callable) -> None: ...
def loadMusic(filename: str) -> None: ...
def playSound(buffer_id: int) -> None: ...
def sceneUI(scene: str = None) -> list: ...
def setMusicVolume(volume: int) -> None: ...
def setScale(multiplier: float) -> None: ...
def setScene(scene: str, transition: str = None, duration: float = 0.0) -> None: ...
def setSoundVolume(volume: int) -> None: ...
def setTimer(name: str, handler: callable, interval: int) -> None: ...
# Constants
FOV_BASIC: int
FOV_DIAMOND: int
FOV_PERMISSIVE_0: int
FOV_PERMISSIVE_1: int
FOV_PERMISSIVE_2: int
FOV_PERMISSIVE_3: int
FOV_PERMISSIVE_4: int
FOV_PERMISSIVE_5: int
FOV_PERMISSIVE_6: int
FOV_PERMISSIVE_7: int
FOV_PERMISSIVE_8: int
FOV_RESTRICTIVE: int
FOV_SHADOW: int
default_font: Any
default_texture: Any

View file

@ -1,24 +0,0 @@
"""Type stubs for McRogueFace automation API."""
from typing import Optional, Tuple
def click(x=None, y=None, clicks=1, interval=0.0, button='left') -> Any: ...
def doubleClick(x=None, y=None) -> Any: ...
def dragRel(xOffset, yOffset, duration=0.0, button='left') -> Any: ...
def dragTo(x, y, duration=0.0, button='left') -> Any: ...
def hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c')) -> Any: ...
def keyDown(key) -> Any: ...
def keyUp(key) -> Any: ...
def middleClick(x=None, y=None) -> Any: ...
def mouseDown(x=None, y=None, button='left') -> Any: ...
def mouseUp(x=None, y=None, button='left') -> Any: ...
def moveRel(xOffset, yOffset, duration=0.0) -> Any: ...
def moveTo(x, y, duration=0.0) -> Any: ...
def onScreen(x, y) -> Any: ...
def position() - Get current mouse position as (x, y) -> Any: ...
def rightClick(x=None, y=None) -> Any: ...
def screenshot(filename) -> Any: ...
def scroll(clicks, x=None, y=None) -> Any: ...
def size() - Get screen size as (width, height) -> Any: ...
def tripleClick(x=None, y=None) -> Any: ...
def typewrite(message, interval=0.0) -> Any: ...

View file

View file

@ -1,289 +0,0 @@
"""
ai.py - Enemy AI System for McRogueFace Roguelike
Simple AI behaviors for enemies: chase player when visible, wander otherwise.
Uses A* pathfinding via entity.path_to() for movement.
"""
from typing import List, Tuple, Optional, TYPE_CHECKING
import random
from entities import Enemy, Player, Actor
from combat import melee_attack, CombatResult
if TYPE_CHECKING:
from dungeon import Dungeon
class AIBehavior:
"""Base class for AI behaviors."""
def take_turn(self, enemy: Enemy, player: Player, dungeon: 'Dungeon',
enemies: List[Enemy]) -> Optional[CombatResult]:
"""
Execute one turn of AI behavior.
Args:
enemy: The enemy taking a turn
player: The player to potentially chase/attack
dungeon: The dungeon map
enemies: List of all enemies (for collision avoidance)
Returns:
CombatResult if combat occurred, None otherwise
"""
raise NotImplementedError
class BasicChaseAI(AIBehavior):
"""
Simple chase AI: If player is visible, move toward them.
If adjacent, attack. Otherwise, stand still or wander.
"""
def __init__(self, sight_range: int = 8):
"""
Args:
sight_range: How far the enemy can see
"""
self.sight_range = sight_range
def can_see_player(self, enemy: Enemy, player: Player,
dungeon: 'Dungeon') -> bool:
"""Check if enemy can see the player."""
# Simple distance check combined with line of sight
distance = enemy.distance_to(player)
if distance > self.sight_range:
return False
# Check line of sight using Bresenham's line
return self._has_line_of_sight(enemy.x, enemy.y, player.x, player.y, dungeon)
def _has_line_of_sight(self, x1: int, y1: int, x2: int, y2: int,
dungeon: 'Dungeon') -> bool:
"""
Check if there's a clear line of sight between two points.
Uses Bresenham's line algorithm.
"""
dx = abs(x2 - x1)
dy = abs(y2 - y1)
x, y = x1, y1
sx = 1 if x1 < x2 else -1
sy = 1 if y1 < y2 else -1
if dx > dy:
err = dx / 2
while x != x2:
if not dungeon.is_transparent(x, y):
return False
err -= dy
if err < 0:
y += sy
err += dx
x += sx
else:
err = dy / 2
while y != y2:
if not dungeon.is_transparent(x, y):
return False
err -= dx
if err < 0:
x += sx
err += dy
y += sy
return True
def get_path_to_player(self, enemy: Enemy, player: Player) -> List[Tuple[int, int]]:
"""
Get a path from enemy to player using A* pathfinding.
Uses the entity's built-in path_to method.
"""
try:
path = enemy.entity.path_to(player.x, player.y)
# Convert path to list of tuples
return [(int(p[0]), int(p[1])) for p in path] if path else []
except (AttributeError, TypeError):
# Fallback: simple direction-based movement
return []
def is_position_blocked(self, x: int, y: int, dungeon: 'Dungeon',
enemies: List[Enemy], player: Player) -> bool:
"""Check if a position is blocked by terrain or another actor."""
# Check terrain
if not dungeon.is_walkable(x, y):
return True
# Check player position
if player.x == x and player.y == y:
return True
# Check other enemies
for other in enemies:
if other.is_alive and other.x == x and other.y == y:
return True
return False
def move_toward(self, enemy: Enemy, target_x: int, target_y: int,
dungeon: 'Dungeon', enemies: List[Enemy],
player: Player) -> bool:
"""
Move one step toward the target position.
Returns True if movement occurred, False otherwise.
"""
# Try pathfinding first
path = self.get_path_to_player(enemy, player)
if path and len(path) > 1:
# First element is current position, second is next step
next_x, next_y = path[1]
else:
# Fallback: move in the general direction
dx = 0
dy = 0
if target_x < enemy.x:
dx = -1
elif target_x > enemy.x:
dx = 1
if target_y < enemy.y:
dy = -1
elif target_y > enemy.y:
dy = 1
next_x = enemy.x + dx
next_y = enemy.y + dy
# Check if the position is blocked
if not self.is_position_blocked(next_x, next_y, dungeon, enemies, player):
enemy.move_to(next_x, next_y)
return True
# Try moving in just one axis
if next_x != enemy.x:
if not self.is_position_blocked(next_x, enemy.y, dungeon, enemies, player):
enemy.move_to(next_x, enemy.y)
return True
if next_y != enemy.y:
if not self.is_position_blocked(enemy.x, next_y, dungeon, enemies, player):
enemy.move_to(enemy.x, next_y)
return True
return False
def take_turn(self, enemy: Enemy, player: Player, dungeon: 'Dungeon',
enemies: List[Enemy]) -> Optional[CombatResult]:
"""Execute the enemy's turn."""
if not enemy.is_alive:
return None
# Check if adjacent to player (can attack)
if enemy.distance_to(player) == 1:
return melee_attack(enemy, player)
# Check if can see player
if self.can_see_player(enemy, player, dungeon):
# Move toward player
self.move_toward(enemy, player.x, player.y, dungeon, enemies, player)
return None
class WanderingAI(BasicChaseAI):
"""
AI that wanders randomly when it can't see the player.
More active than BasicChaseAI.
"""
def __init__(self, sight_range: int = 8, wander_chance: float = 0.3):
"""
Args:
sight_range: How far the enemy can see
wander_chance: Probability of wandering each turn (0.0 to 1.0)
"""
super().__init__(sight_range)
self.wander_chance = wander_chance
def wander(self, enemy: Enemy, dungeon: 'Dungeon',
enemies: List[Enemy], player: Player) -> bool:
"""
Move in a random direction.
Returns True if movement occurred.
"""
# Random direction
directions = [
(-1, 0), (1, 0), (0, -1), (0, 1), # Cardinal
(-1, -1), (1, -1), (-1, 1), (1, 1) # Diagonal
]
random.shuffle(directions)
for dx, dy in directions:
new_x = enemy.x + dx
new_y = enemy.y + dy
if not self.is_position_blocked(new_x, new_y, dungeon, enemies, player):
enemy.move_to(new_x, new_y)
return True
return False
def take_turn(self, enemy: Enemy, player: Player, dungeon: 'Dungeon',
enemies: List[Enemy]) -> Optional[CombatResult]:
"""Execute the enemy's turn with wandering behavior."""
if not enemy.is_alive:
return None
# Check if adjacent to player (can attack)
if enemy.distance_to(player) == 1:
return melee_attack(enemy, player)
# Check if can see player
if self.can_see_player(enemy, player, dungeon):
# Chase player
self.move_toward(enemy, player.x, player.y, dungeon, enemies, player)
else:
# Wander randomly
if random.random() < self.wander_chance:
self.wander(enemy, dungeon, enemies, player)
return None
# Default AI instance
default_ai = WanderingAI(sight_range=8, wander_chance=0.3)
def process_enemy_turns(enemies: List[Enemy], player: Player,
dungeon: 'Dungeon',
ai: AIBehavior = None) -> List[CombatResult]:
"""
Process turns for all enemies.
Args:
enemies: List of all enemies
player: The player
dungeon: The dungeon map
ai: AI behavior to use (defaults to WanderingAI)
Returns:
List of combat results from this round of enemy actions
"""
if ai is None:
ai = default_ai
results = []
for enemy in enemies:
if enemy.is_alive:
result = ai.take_turn(enemy, player, dungeon, enemies)
if result:
results.append(result)
return results

View file

@ -1,187 +0,0 @@
"""
combat.py - Combat System for McRogueFace Roguelike
Handles attack resolution, damage calculation, and combat outcomes.
"""
from dataclasses import dataclass
from typing import Tuple, Optional
import random
from entities import Actor, Player, Enemy
from constants import (
MSG_PLAYER_ATTACK, MSG_PLAYER_KILL, MSG_PLAYER_MISS,
MSG_ENEMY_ATTACK, MSG_ENEMY_MISS
)
@dataclass
class CombatResult:
"""
Result of a combat action.
Attributes:
attacker: The attacking actor
defender: The defending actor
damage: Damage dealt (after defense)
killed: Whether the defender was killed
message: Human-readable result message
message_color: Color tuple for the message
"""
attacker: Actor
defender: Actor
damage: int
killed: bool
message: str
message_color: Tuple[int, int, int, int]
def calculate_damage(attack: int, defense: int, variance: float = 0.2) -> int:
"""
Calculate damage with some randomness.
Args:
attack: Attacker's attack power
defense: Defender's defense value
variance: Random variance as percentage (0.2 = +/-20%)
Returns:
Final damage amount (minimum 0)
"""
# Base damage is attack vs defense
base_damage = attack - defense
# Add some variance
if base_damage > 0:
variance_amount = int(base_damage * variance)
damage = base_damage + random.randint(-variance_amount, variance_amount)
else:
# Small chance to do 1 damage even with high defense
damage = 1 if random.random() < 0.1 else 0
return max(0, damage)
def attack(attacker: Actor, defender: Actor) -> CombatResult:
"""
Perform an attack from one actor to another.
Args:
attacker: The actor making the attack
defender: The actor being attacked
Returns:
CombatResult with outcome details
"""
# Calculate damage
damage = calculate_damage(
attacker.fighter.attack,
defender.fighter.defense
)
# Apply damage
actual_damage = defender.fighter.take_damage(damage + defender.fighter.defense)
# Note: take_damage applies defense internally, so we add it back
# Actually, we calculated damage already reduced by defense, so just apply it:
defender.fighter.hp = max(0, defender.fighter.hp - damage + actual_damage)
# Simplified: just use take_damage properly
# Reset and do it right:
# Apply raw damage (defense already calculated)
defender.fighter.hp = max(0, defender.fighter.hp - damage)
killed = not defender.is_alive
# Generate message based on attacker/defender types
if isinstance(attacker, Player):
if killed:
message = MSG_PLAYER_KILL % defender.name
color = (255, 255, 100, 255) # Yellow for kills
elif damage > 0:
message = MSG_PLAYER_ATTACK % (defender.name, damage)
color = (255, 255, 255, 255) # White for hits
else:
message = MSG_PLAYER_MISS % defender.name
color = (150, 150, 150, 255) # Gray for misses
else:
if damage > 0:
message = MSG_ENEMY_ATTACK % (attacker.name, damage)
color = (255, 100, 100, 255) # Red for enemy hits
else:
message = MSG_ENEMY_MISS % attacker.name
color = (150, 150, 150, 255) # Gray for misses
return CombatResult(
attacker=attacker,
defender=defender,
damage=damage,
killed=killed,
message=message,
message_color=color
)
def melee_attack(attacker: Actor, defender: Actor) -> CombatResult:
"""
Perform a melee attack (bump attack).
This is the standard roguelike bump-to-attack.
Args:
attacker: The actor making the attack
defender: The actor being attacked
Returns:
CombatResult with outcome details
"""
return attack(attacker, defender)
def try_attack(attacker: Actor, target_x: int, target_y: int,
enemies: list, player: Optional[Player] = None) -> Optional[CombatResult]:
"""
Attempt to attack whatever is at the target position.
Args:
attacker: The actor making the attack
target_x: X coordinate to attack
target_y: Y coordinate to attack
enemies: List of Enemy actors
player: The player (if attacker is an enemy)
Returns:
CombatResult if something was attacked, None otherwise
"""
# Check if player is attacking
if isinstance(attacker, Player):
# Look for enemy at position
for enemy in enemies:
if enemy.is_alive and enemy.x == target_x and enemy.y == target_y:
return melee_attack(attacker, enemy)
else:
# Enemy attacking - check if player is at position
if player and player.x == target_x and player.y == target_y:
return melee_attack(attacker, player)
return None
def process_kill(attacker: Actor, defender: Actor) -> int:
"""
Process the aftermath of killing an enemy.
Args:
attacker: The actor that made the kill
defender: The actor that was killed
Returns:
XP gained (if attacker is player and defender is enemy)
"""
xp_gained = 0
if isinstance(attacker, Player) and isinstance(defender, Enemy):
xp_gained = defender.xp_reward
attacker.gain_xp(xp_gained)
# Remove the dead actor from the grid
defender.remove()
return xp_gained

View file

@ -1,210 +0,0 @@
"""
constants.py - Game Constants for McRogueFace Complete Roguelike Template
All configuration values in one place for easy tweaking.
"""
# =============================================================================
# WINDOW AND DISPLAY
# =============================================================================
SCREEN_WIDTH = 1024
SCREEN_HEIGHT = 768
# Grid display area (where the dungeon is rendered)
GRID_X = 0
GRID_Y = 0
GRID_WIDTH = 800
GRID_HEIGHT = 600
# Tile dimensions (must match your texture)
TILE_WIDTH = 16
TILE_HEIGHT = 16
# =============================================================================
# DUNGEON GENERATION
# =============================================================================
# Size of the dungeon in tiles
DUNGEON_WIDTH = 80
DUNGEON_HEIGHT = 45
# Room size constraints
ROOM_MIN_SIZE = 6
ROOM_MAX_SIZE = 12
MAX_ROOMS = 15
# Enemy spawning per room
MAX_ENEMIES_PER_ROOM = 3
MIN_ENEMIES_PER_ROOM = 0
# =============================================================================
# SPRITE INDICES (for kenney_tinydungeon.png - 16x16 tiles)
# Adjust these if using a different tileset
# =============================================================================
# Terrain
SPRITE_FLOOR = 48 # Dungeon floor
SPRITE_WALL = 33 # Wall tile
SPRITE_STAIRS_DOWN = 50 # Stairs going down
SPRITE_DOOR = 49 # Door tile
# Player sprites
SPRITE_PLAYER = 84 # Player character (knight)
# Enemy sprites
SPRITE_GOBLIN = 111 # Goblin enemy
SPRITE_ORC = 112 # Orc enemy
SPRITE_TROLL = 116 # Troll enemy
# Items (for future expansion)
SPRITE_POTION = 89 # Health potion
SPRITE_CHEST = 91 # Treasure chest
# =============================================================================
# COLORS (R, G, B, A)
# =============================================================================
# Map colors
COLOR_DARK_WALL = (50, 50, 100, 255)
COLOR_DARK_FLOOR = (30, 30, 50, 255)
COLOR_LIGHT_WALL = (100, 100, 150, 255)
COLOR_LIGHT_FLOOR = (80, 80, 100, 255)
# FOV overlay colors
COLOR_FOG = (0, 0, 0, 200) # Unexplored areas
COLOR_REMEMBERED = (0, 0, 0, 128) # Seen but not visible
COLOR_VISIBLE = (0, 0, 0, 0) # Currently visible (transparent)
# UI Colors
COLOR_UI_BG = (20, 20, 30, 230)
COLOR_UI_BORDER = (80, 80, 120, 255)
COLOR_TEXT = (255, 255, 255, 255)
COLOR_TEXT_HIGHLIGHT = (255, 255, 100, 255)
# Health bar colors
COLOR_HP_BAR_BG = (80, 0, 0, 255)
COLOR_HP_BAR_FILL = (0, 180, 0, 255)
COLOR_HP_BAR_WARNING = (180, 180, 0, 255)
COLOR_HP_BAR_CRITICAL = (180, 0, 0, 255)
# Message log colors
COLOR_MSG_DEFAULT = (255, 255, 255, 255)
COLOR_MSG_DAMAGE = (255, 100, 100, 255)
COLOR_MSG_HEAL = (100, 255, 100, 255)
COLOR_MSG_INFO = (100, 100, 255, 255)
COLOR_MSG_IMPORTANT = (255, 255, 100, 255)
# =============================================================================
# PLAYER STATS
# =============================================================================
PLAYER_START_HP = 30
PLAYER_START_ATTACK = 5
PLAYER_START_DEFENSE = 2
# =============================================================================
# ENEMY STATS
# Each enemy type: (hp, attack, defense, xp_reward, name)
# =============================================================================
ENEMY_STATS = {
'goblin': {
'hp': 10,
'attack': 3,
'defense': 0,
'xp': 35,
'sprite': SPRITE_GOBLIN,
'name': 'Goblin'
},
'orc': {
'hp': 16,
'attack': 4,
'defense': 1,
'xp': 50,
'sprite': SPRITE_ORC,
'name': 'Orc'
},
'troll': {
'hp': 24,
'attack': 6,
'defense': 2,
'xp': 100,
'sprite': SPRITE_TROLL,
'name': 'Troll'
}
}
# Enemy spawn weights per dungeon level
# Format: {level: [(enemy_type, weight), ...]}
# Higher weight = more likely to spawn
ENEMY_SPAWN_WEIGHTS = {
1: [('goblin', 100)],
2: [('goblin', 80), ('orc', 20)],
3: [('goblin', 60), ('orc', 40)],
4: [('goblin', 40), ('orc', 50), ('troll', 10)],
5: [('goblin', 20), ('orc', 50), ('troll', 30)],
}
# Default weights for levels beyond those defined
DEFAULT_SPAWN_WEIGHTS = [('goblin', 10), ('orc', 50), ('troll', 40)]
# =============================================================================
# FOV (Field of View) SETTINGS
# =============================================================================
FOV_RADIUS = 8 # How far the player can see
FOV_LIGHT_WALLS = True # Whether walls at FOV edge are visible
# =============================================================================
# INPUT KEYS
# Key names as returned by McRogueFace keypressScene
# =============================================================================
KEY_UP = ['Up', 'W', 'Numpad8']
KEY_DOWN = ['Down', 'S', 'Numpad2']
KEY_LEFT = ['Left', 'A', 'Numpad4']
KEY_RIGHT = ['Right', 'D', 'Numpad6']
# Diagonal movement (numpad)
KEY_UP_LEFT = ['Numpad7']
KEY_UP_RIGHT = ['Numpad9']
KEY_DOWN_LEFT = ['Numpad1']
KEY_DOWN_RIGHT = ['Numpad3']
# Actions
KEY_WAIT = ['Period', 'Numpad5'] # Skip turn
KEY_DESCEND = ['Greater', 'Space'] # Go down stairs (> key or space)
# =============================================================================
# GAME MESSAGES
# =============================================================================
MSG_WELCOME = "Welcome to the dungeon! Find the stairs to descend deeper."
MSG_DESCEND = "You descend the stairs to level %d..."
MSG_PLAYER_ATTACK = "You attack the %s for %d damage!"
MSG_PLAYER_KILL = "You have slain the %s!"
MSG_PLAYER_MISS = "You attack the %s but do no damage."
MSG_ENEMY_ATTACK = "The %s attacks you for %d damage!"
MSG_ENEMY_MISS = "The %s attacks you but does no damage."
MSG_BLOCKED = "You can't move there!"
MSG_STAIRS = "You see stairs leading down here. Press > or Space to descend."
MSG_DEATH = "You have died! Press R to restart."
MSG_NO_STAIRS = "There are no stairs here."
# =============================================================================
# UI LAYOUT
# =============================================================================
# Health bar
HP_BAR_X = 10
HP_BAR_Y = 620
HP_BAR_WIDTH = 200
HP_BAR_HEIGHT = 24
# Message log
MSG_LOG_X = 10
MSG_LOG_Y = 660
MSG_LOG_WIDTH = 780
MSG_LOG_HEIGHT = 100
MSG_LOG_MAX_LINES = 5
# Dungeon level display
LEVEL_DISPLAY_X = 700
LEVEL_DISPLAY_Y = 620
# =============================================================================
# ASSET PATHS
# =============================================================================
TEXTURE_PATH = "assets/kenney_tinydungeon.png"
FONT_PATH = "assets/JetbrainsMono.ttf"

View file

@ -1,298 +0,0 @@
"""
dungeon.py - Procedural Dungeon Generation for McRogueFace
Generates a roguelike dungeon with rooms connected by corridors.
Includes stairs placement for multi-level progression.
"""
import random
from dataclasses import dataclass
from typing import List, Tuple, Optional
from constants import (
DUNGEON_WIDTH, DUNGEON_HEIGHT,
ROOM_MIN_SIZE, ROOM_MAX_SIZE, MAX_ROOMS,
SPRITE_FLOOR, SPRITE_WALL, SPRITE_STAIRS_DOWN,
MAX_ENEMIES_PER_ROOM, MIN_ENEMIES_PER_ROOM,
ENEMY_SPAWN_WEIGHTS, DEFAULT_SPAWN_WEIGHTS
)
@dataclass
class Rect:
"""A rectangle representing a room in the dungeon."""
x: int
y: int
width: int
height: int
@property
def x2(self) -> int:
return self.x + self.width
@property
def y2(self) -> int:
return self.y + self.height
@property
def center(self) -> Tuple[int, int]:
"""Return the center coordinates of this room."""
center_x = (self.x + self.x2) // 2
center_y = (self.y + self.y2) // 2
return center_x, center_y
def intersects(self, other: 'Rect') -> bool:
"""Check if this room overlaps with another (with 1 tile buffer)."""
return (self.x <= other.x2 + 1 and self.x2 + 1 >= other.x and
self.y <= other.y2 + 1 and self.y2 + 1 >= other.y)
def inner(self) -> Tuple[int, int, int, int]:
"""Return the inner area of the room (excluding walls)."""
return self.x + 1, self.y + 1, self.width - 2, self.height - 2
class Tile:
"""Represents a single tile in the dungeon."""
def __init__(self, walkable: bool = False, transparent: bool = False,
sprite: int = SPRITE_WALL):
self.walkable = walkable
self.transparent = transparent
self.sprite = sprite
self.explored = False
self.visible = False
class Dungeon:
"""
The dungeon map with rooms, corridors, and tile data.
Attributes:
width: Width of the dungeon in tiles
height: Height of the dungeon in tiles
level: Current dungeon depth
tiles: 2D array of Tile objects
rooms: List of rooms (Rect objects)
player_start: Starting position for the player
stairs_pos: Position of the stairs down
"""
def __init__(self, width: int = DUNGEON_WIDTH, height: int = DUNGEON_HEIGHT,
level: int = 1):
self.width = width
self.height = height
self.level = level
self.tiles: List[List[Tile]] = []
self.rooms: List[Rect] = []
self.player_start: Tuple[int, int] = (0, 0)
self.stairs_pos: Tuple[int, int] = (0, 0)
# Initialize all tiles as walls
self._init_tiles()
def _init_tiles(self) -> None:
"""Fill the dungeon with wall tiles."""
self.tiles = [
[Tile(walkable=False, transparent=False, sprite=SPRITE_WALL)
for _ in range(self.height)]
for _ in range(self.width)
]
def in_bounds(self, x: int, y: int) -> bool:
"""Check if coordinates are within dungeon bounds."""
return 0 <= x < self.width and 0 <= y < self.height
def is_walkable(self, x: int, y: int) -> bool:
"""Check if a tile can be walked on."""
if not self.in_bounds(x, y):
return False
return self.tiles[x][y].walkable
def is_transparent(self, x: int, y: int) -> bool:
"""Check if a tile allows light to pass through."""
if not self.in_bounds(x, y):
return False
return self.tiles[x][y].transparent
def get_tile(self, x: int, y: int) -> Optional[Tile]:
"""Get the tile at the given position."""
if not self.in_bounds(x, y):
return None
return self.tiles[x][y]
def set_tile(self, x: int, y: int, walkable: bool, transparent: bool,
sprite: int) -> None:
"""Set properties of a tile."""
if self.in_bounds(x, y):
tile = self.tiles[x][y]
tile.walkable = walkable
tile.transparent = transparent
tile.sprite = sprite
def carve_room(self, room: Rect) -> None:
"""Carve out a room in the dungeon (make tiles walkable)."""
inner_x, inner_y, inner_w, inner_h = room.inner()
for x in range(inner_x, inner_x + inner_w):
for y in range(inner_y, inner_y + inner_h):
self.set_tile(x, y, walkable=True, transparent=True,
sprite=SPRITE_FLOOR)
def carve_tunnel_h(self, x1: int, x2: int, y: int) -> None:
"""Carve a horizontal tunnel."""
for x in range(min(x1, x2), max(x1, x2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite=SPRITE_FLOOR)
def carve_tunnel_v(self, y1: int, y2: int, x: int) -> None:
"""Carve a vertical tunnel."""
for y in range(min(y1, y2), max(y1, y2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite=SPRITE_FLOOR)
def connect_rooms(self, room1: Rect, room2: Rect) -> None:
"""Connect two rooms with an L-shaped corridor."""
x1, y1 = room1.center
x2, y2 = room2.center
# Randomly choose to go horizontal then vertical, or vice versa
if random.random() < 0.5:
self.carve_tunnel_h(x1, x2, y1)
self.carve_tunnel_v(y1, y2, x2)
else:
self.carve_tunnel_v(y1, y2, x1)
self.carve_tunnel_h(x1, x2, y2)
def place_stairs(self) -> None:
"""Place stairs in the last room."""
if self.rooms:
# Stairs go in the center of the last room
self.stairs_pos = self.rooms[-1].center
x, y = self.stairs_pos
self.set_tile(x, y, walkable=True, transparent=True,
sprite=SPRITE_STAIRS_DOWN)
def generate(self) -> None:
"""Generate the dungeon using BSP-style room placement."""
self._init_tiles()
self.rooms.clear()
for _ in range(MAX_ROOMS):
# Random room dimensions
w = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
h = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
# Random position (ensure room fits in dungeon)
x = random.randint(1, self.width - w - 1)
y = random.randint(1, self.height - h - 1)
new_room = Rect(x, y, w, h)
# Check for intersections with existing rooms
if any(new_room.intersects(other) for other in self.rooms):
continue
# Room is valid - carve it out
self.carve_room(new_room)
if self.rooms:
# Connect to previous room
self.connect_rooms(self.rooms[-1], new_room)
else:
# First room - player starts here
self.player_start = new_room.center
self.rooms.append(new_room)
# Place stairs in the last room
self.place_stairs()
def get_spawn_positions(self) -> List[Tuple[int, int]]:
"""
Get valid spawn positions for enemies.
Returns positions from all rooms except the first (player start).
"""
positions = []
for room in self.rooms[1:]: # Skip first room (player start)
inner_x, inner_y, inner_w, inner_h = room.inner()
for x in range(inner_x, inner_x + inner_w):
for y in range(inner_y, inner_y + inner_h):
# Don't spawn on stairs
if (x, y) != self.stairs_pos:
positions.append((x, y))
return positions
def get_enemy_spawns(self) -> List[Tuple[str, int, int]]:
"""
Determine which enemies to spawn and where.
Returns list of (enemy_type, x, y) tuples.
"""
spawns = []
# Get spawn weights for this level
weights = ENEMY_SPAWN_WEIGHTS.get(self.level, DEFAULT_SPAWN_WEIGHTS)
# Create weighted list for random selection
enemy_types = []
for enemy_type, weight in weights:
enemy_types.extend([enemy_type] * weight)
# Spawn enemies in each room (except the first)
for room in self.rooms[1:]:
num_enemies = random.randint(MIN_ENEMIES_PER_ROOM, MAX_ENEMIES_PER_ROOM)
# Scale up enemies slightly with dungeon level
num_enemies = min(num_enemies + (self.level - 1) // 2, MAX_ENEMIES_PER_ROOM + 2)
inner_x, inner_y, inner_w, inner_h = room.inner()
used_positions = set()
for _ in range(num_enemies):
# Find an unused position
attempts = 0
while attempts < 20:
x = random.randint(inner_x, inner_x + inner_w - 1)
y = random.randint(inner_y, inner_y + inner_h - 1)
if (x, y) not in used_positions and (x, y) != self.stairs_pos:
enemy_type = random.choice(enemy_types)
spawns.append((enemy_type, x, y))
used_positions.add((x, y))
break
attempts += 1
return spawns
def apply_to_grid(self, grid) -> None:
"""
Apply the dungeon data to a McRogueFace Grid object.
Args:
grid: A mcrfpy.Grid object to update
"""
for x in range(self.width):
for y in range(self.height):
tile = self.tiles[x][y]
point = grid.at(x, y)
point.tilesprite = tile.sprite
point.walkable = tile.walkable
point.transparent = tile.transparent
def generate_dungeon(level: int = 1) -> Dungeon:
"""
Convenience function to generate a new dungeon.
Args:
level: The dungeon depth (affects enemy spawns)
Returns:
A fully generated Dungeon object
"""
dungeon = Dungeon(level=level)
dungeon.generate()
return dungeon

View file

@ -1,319 +0,0 @@
"""
entities.py - Player and Enemy Entity Definitions
Defines the game actors with stats, rendering, and basic behaviors.
Uses composition with McRogueFace Entity objects for rendering.
"""
from dataclasses import dataclass, field
from typing import Optional, List, Tuple, TYPE_CHECKING
import mcrfpy
from constants import (
PLAYER_START_HP, PLAYER_START_ATTACK, PLAYER_START_DEFENSE,
SPRITE_PLAYER, ENEMY_STATS, FOV_RADIUS
)
if TYPE_CHECKING:
from dungeon import Dungeon
@dataclass
class Fighter:
"""
Combat statistics component for entities that can fight.
Attributes:
hp: Current hit points
max_hp: Maximum hit points
attack: Attack power
defense: Damage reduction
"""
hp: int
max_hp: int
attack: int
defense: int
@property
def is_alive(self) -> bool:
"""Check if this fighter is still alive."""
return self.hp > 0
@property
def hp_percent(self) -> float:
"""Return HP as a percentage (0.0 to 1.0)."""
if self.max_hp <= 0:
return 0.0
return self.hp / self.max_hp
def heal(self, amount: int) -> int:
"""
Heal by the given amount, up to max_hp.
Returns:
The actual amount healed.
"""
old_hp = self.hp
self.hp = min(self.hp + amount, self.max_hp)
return self.hp - old_hp
def take_damage(self, amount: int) -> int:
"""
Take damage, reduced by defense.
Args:
amount: Raw damage before defense calculation
Returns:
The actual damage taken after defense.
"""
# Defense reduces damage, minimum 0
actual_damage = max(0, amount - self.defense)
self.hp = max(0, self.hp - actual_damage)
return actual_damage
class Actor:
"""
Base class for all game actors (player and enemies).
Wraps a McRogueFace Entity and adds game logic.
"""
def __init__(self, x: int, y: int, sprite: int, name: str,
texture: mcrfpy.Texture, grid: mcrfpy.Grid,
fighter: Fighter):
"""
Create a new actor.
Args:
x: Starting X position
y: Starting Y position
sprite: Sprite index for rendering
name: Display name of this actor
texture: Texture for the entity sprite
grid: Grid to add the entity to
fighter: Combat statistics
"""
self.name = name
self.fighter = fighter
self.grid = grid
self._x = x
self._y = y
# Create the McRogueFace entity
self.entity = mcrfpy.Entity((x, y), texture, sprite)
grid.entities.append(self.entity)
@property
def x(self) -> int:
return self._x
@x.setter
def x(self, value: int) -> None:
self._x = value
self.entity.pos = (value, self._y)
@property
def y(self) -> int:
return self._y
@y.setter
def y(self, value: int) -> None:
self._y = value
self.entity.pos = (self._x, value)
@property
def pos(self) -> Tuple[int, int]:
return (self._x, self._y)
@pos.setter
def pos(self, value: Tuple[int, int]) -> None:
self._x, self._y = value
self.entity.pos = value
@property
def is_alive(self) -> bool:
return self.fighter.is_alive
def move(self, dx: int, dy: int) -> None:
"""Move by the given delta."""
self.x += dx
self.y += dy
def move_to(self, x: int, y: int) -> None:
"""Move to an absolute position."""
self.pos = (x, y)
def distance_to(self, other: 'Actor') -> int:
"""Calculate Manhattan distance to another actor."""
return abs(self.x - other.x) + abs(self.y - other.y)
def remove(self) -> None:
"""Remove this actor's entity from the grid."""
try:
idx = self.entity.index()
self.grid.entities.remove(idx)
except (ValueError, RuntimeError):
pass # Already removed
class Player(Actor):
"""
The player character with additional player-specific functionality.
"""
def __init__(self, x: int, y: int, texture: mcrfpy.Texture,
grid: mcrfpy.Grid):
fighter = Fighter(
hp=PLAYER_START_HP,
max_hp=PLAYER_START_HP,
attack=PLAYER_START_ATTACK,
defense=PLAYER_START_DEFENSE
)
super().__init__(
x=x, y=y,
sprite=SPRITE_PLAYER,
name="Player",
texture=texture,
grid=grid,
fighter=fighter
)
self.xp = 0
self.level = 1
self.dungeon_level = 1
def gain_xp(self, amount: int) -> bool:
"""
Gain experience points.
Args:
amount: XP to gain
Returns:
True if the player leveled up
"""
self.xp += amount
xp_to_level = self.xp_for_next_level
if self.xp >= xp_to_level:
self.level_up()
return True
return False
@property
def xp_for_next_level(self) -> int:
"""XP required for the next level."""
return self.level * 100
def level_up(self) -> None:
"""Level up the player, improving stats."""
self.level += 1
# Improve stats
hp_increase = 5
attack_increase = 1
defense_increase = 1 if self.level % 3 == 0 else 0
self.fighter.max_hp += hp_increase
self.fighter.hp += hp_increase # Heal the increase amount
self.fighter.attack += attack_increase
self.fighter.defense += defense_increase
def update_fov(self, dungeon: 'Dungeon') -> None:
"""
Update field of view based on player position.
Uses entity.update_visibility() for TCOD FOV calculation.
"""
# Update the entity's visibility data
self.entity.update_visibility()
# Apply FOV to dungeon tiles
for x in range(dungeon.width):
for y in range(dungeon.height):
state = self.entity.at(x, y)
tile = dungeon.get_tile(x, y)
if tile:
tile.visible = state.visible
if state.visible:
tile.explored = True
class Enemy(Actor):
"""
An enemy actor with AI behavior.
"""
def __init__(self, x: int, y: int, enemy_type: str,
texture: mcrfpy.Texture, grid: mcrfpy.Grid):
"""
Create a new enemy.
Args:
x: Starting X position
y: Starting Y position
enemy_type: Key into ENEMY_STATS dictionary
texture: Texture for the entity sprite
grid: Grid to add the entity to
"""
stats = ENEMY_STATS.get(enemy_type, ENEMY_STATS['goblin'])
fighter = Fighter(
hp=stats['hp'],
max_hp=stats['hp'],
attack=stats['attack'],
defense=stats['defense']
)
super().__init__(
x=x, y=y,
sprite=stats['sprite'],
name=stats['name'],
texture=texture,
grid=grid,
fighter=fighter
)
self.enemy_type = enemy_type
self.xp_reward = stats['xp']
# AI state
self.target: Optional[Actor] = None
self.path: List[Tuple[int, int]] = []
def create_player(x: int, y: int, texture: mcrfpy.Texture,
grid: mcrfpy.Grid) -> Player:
"""
Factory function to create the player.
Args:
x: Starting X position
y: Starting Y position
texture: Texture for player sprite
grid: Grid to add player to
Returns:
A new Player instance
"""
return Player(x, y, texture, grid)
def create_enemy(x: int, y: int, enemy_type: str,
texture: mcrfpy.Texture, grid: mcrfpy.Grid) -> Enemy:
"""
Factory function to create an enemy.
Args:
x: Starting X position
y: Starting Y position
enemy_type: Type of enemy ('goblin', 'orc', 'troll')
texture: Texture for enemy sprite
grid: Grid to add enemy to
Returns:
A new Enemy instance
"""
return Enemy(x, y, enemy_type, texture, grid)

View file

@ -1,313 +0,0 @@
"""
game.py - Main Entry Point for McRogueFace Complete Roguelike Template
This is the main game file that ties everything together:
- Scene setup
- Input handling
- Game loop
- Level transitions
To run: Copy this template to your McRogueFace scripts/ directory
and rename to game.py (or import from game.py).
"""
import mcrfpy
from typing import List, Optional
# Import game modules
from constants import (
SCREEN_WIDTH, SCREEN_HEIGHT,
GRID_X, GRID_Y, GRID_WIDTH, GRID_HEIGHT,
DUNGEON_WIDTH, DUNGEON_HEIGHT,
TEXTURE_PATH, FONT_PATH,
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT,
KEY_UP_LEFT, KEY_UP_RIGHT, KEY_DOWN_LEFT, KEY_DOWN_RIGHT,
KEY_WAIT, KEY_DESCEND,
MSG_WELCOME, MSG_DESCEND, MSG_BLOCKED, MSG_STAIRS, MSG_DEATH, MSG_NO_STAIRS,
FOV_RADIUS, COLOR_FOG, COLOR_REMEMBERED, COLOR_VISIBLE
)
from dungeon import Dungeon, generate_dungeon
from entities import Player, Enemy, create_player, create_enemy
from turns import TurnManager, GameState
from ui import GameUI, DeathScreen
class Game:
"""
Main game class that manages the complete roguelike experience.
"""
def __init__(self):
"""Initialize the game."""
# Load resources
self.texture = mcrfpy.Texture(TEXTURE_PATH, 16, 16)
self.font = mcrfpy.Font(FONT_PATH)
# Create scene
mcrfpy.createScene("game")
self.ui_collection = mcrfpy.sceneUI("game")
# Create grid
self.grid = mcrfpy.Grid(
DUNGEON_WIDTH, DUNGEON_HEIGHT,
self.texture,
GRID_X, GRID_Y,
GRID_WIDTH, GRID_HEIGHT
)
self.ui_collection.append(self.grid)
# Game state
self.dungeon: Optional[Dungeon] = None
self.player: Optional[Player] = None
self.enemies: List[Enemy] = []
self.turn_manager: Optional[TurnManager] = None
self.current_level = 1
# UI
self.game_ui = GameUI(self.font)
self.game_ui.add_to_scene(self.ui_collection)
self.death_screen: Optional[DeathScreen] = None
self.game_over = False
# Set up input handling
mcrfpy.keypressScene(self.handle_keypress)
# Start the game
self.new_game()
# Switch to game scene
mcrfpy.setScene("game")
def new_game(self) -> None:
"""Start a new game from level 1."""
self.current_level = 1
self.game_over = False
# Clear any death screen
if self.death_screen:
self.death_screen.remove_from_scene(self.ui_collection)
self.death_screen = None
# Generate first level
self.generate_level()
# Welcome message
self.game_ui.clear_messages()
self.game_ui.add_message(MSG_WELCOME, (255, 255, 100, 255))
def generate_level(self) -> None:
"""Generate a new dungeon level."""
# Clear existing entities from grid
while len(self.grid.entities) > 0:
self.grid.entities.remove(0)
self.enemies.clear()
# Generate dungeon
self.dungeon = generate_dungeon(self.current_level)
self.dungeon.apply_to_grid(self.grid)
# Create player at start position
start_x, start_y = self.dungeon.player_start
self.player = create_player(start_x, start_y, self.texture, self.grid)
self.player.dungeon_level = self.current_level
# Spawn enemies
enemy_spawns = self.dungeon.get_enemy_spawns()
for enemy_type, x, y in enemy_spawns:
enemy = create_enemy(x, y, enemy_type, self.texture, self.grid)
self.enemies.append(enemy)
# Set up turn manager
self.turn_manager = TurnManager(self.player, self.enemies, self.dungeon)
self.turn_manager.on_message = self.game_ui.add_message
self.turn_manager.on_player_death = self.on_player_death
# Update FOV
self.update_fov()
# Center camera on player
self.center_camera()
# Update UI
self.game_ui.update_level(self.current_level)
self.update_ui()
def descend(self) -> None:
"""Go down to the next dungeon level."""
# Check if player is on stairs
if self.player.pos != self.dungeon.stairs_pos:
self.game_ui.add_message(MSG_NO_STAIRS, (150, 150, 150, 255))
return
self.current_level += 1
self.game_ui.add_message(MSG_DESCEND % self.current_level, (100, 100, 255, 255))
# Keep player stats
old_hp = self.player.fighter.hp
old_max_hp = self.player.fighter.max_hp
old_attack = self.player.fighter.attack
old_defense = self.player.fighter.defense
old_xp = self.player.xp
old_level = self.player.level
# Generate new level
self.generate_level()
# Restore player stats
self.player.fighter.hp = old_hp
self.player.fighter.max_hp = old_max_hp
self.player.fighter.attack = old_attack
self.player.fighter.defense = old_defense
self.player.xp = old_xp
self.player.level = old_level
self.update_ui()
def update_fov(self) -> None:
"""Update field of view and apply to grid tiles."""
if not self.player or not self.dungeon:
return
# Use entity's built-in FOV calculation
self.player.entity.update_visibility()
# Apply visibility to tiles
for x in range(self.dungeon.width):
for y in range(self.dungeon.height):
point = self.grid.at(x, y)
tile = self.dungeon.get_tile(x, y)
if tile:
state = self.player.entity.at(x, y)
if state.visible:
# Currently visible
tile.explored = True
tile.visible = True
point.color_overlay = mcrfpy.Color(*COLOR_VISIBLE)
elif tile.explored:
# Explored but not visible
tile.visible = False
point.color_overlay = mcrfpy.Color(*COLOR_REMEMBERED)
else:
# Never seen
point.color_overlay = mcrfpy.Color(*COLOR_FOG)
def center_camera(self) -> None:
"""Center the camera on the player."""
if self.player:
self.grid.center = (self.player.x, self.player.y)
def update_ui(self) -> None:
"""Update all UI elements."""
if self.player:
self.game_ui.update_hp(
self.player.fighter.hp,
self.player.fighter.max_hp
)
def on_player_death(self) -> None:
"""Handle player death."""
self.game_over = True
self.game_ui.add_message(MSG_DEATH, (255, 0, 0, 255))
# Show death screen
self.death_screen = DeathScreen(self.font)
self.death_screen.add_to_scene(self.ui_collection)
def handle_keypress(self, key: str, state: str) -> None:
"""
Handle keyboard input.
Args:
key: Key name
state: "start" for key down, "end" for key up
"""
# Only handle key down events
if state != "start":
return
# Handle restart when dead
if self.game_over:
if key == "R":
self.new_game()
return
# Handle movement
dx, dy = 0, 0
if key in KEY_UP:
dy = -1
elif key in KEY_DOWN:
dy = 1
elif key in KEY_LEFT:
dx = -1
elif key in KEY_RIGHT:
dx = 1
elif key in KEY_UP_LEFT:
dx, dy = -1, -1
elif key in KEY_UP_RIGHT:
dx, dy = 1, -1
elif key in KEY_DOWN_LEFT:
dx, dy = -1, 1
elif key in KEY_DOWN_RIGHT:
dx, dy = 1, 1
elif key in KEY_WAIT:
# Skip turn
self.turn_manager.handle_wait()
self.after_turn()
return
elif key in KEY_DESCEND:
# Try to descend
self.descend()
return
elif key == "Escape":
# Quit game
mcrfpy.exit()
return
# Process movement/attack
if dx != 0 or dy != 0:
if self.turn_manager.handle_player_action(dx, dy):
self.after_turn()
else:
# Movement was blocked
self.game_ui.add_message(MSG_BLOCKED, (150, 150, 150, 255))
def after_turn(self) -> None:
"""Called after each player turn."""
# Update FOV
self.update_fov()
# Center camera
self.center_camera()
# Update UI
self.update_ui()
# Check if standing on stairs
if self.player.pos == self.dungeon.stairs_pos:
self.game_ui.add_message(MSG_STAIRS, (100, 255, 100, 255))
# Clean up dead enemies
self.enemies = [e for e in self.enemies if e.is_alive]
# =============================================================================
# ENTRY POINT
# =============================================================================
# Global game instance
game: Optional[Game] = None
def start_game():
"""Start the game."""
global game
game = Game()
# Auto-start when this script is loaded
start_game()

View file

@ -1,232 +0,0 @@
"""
turns.py - Turn Management System for McRogueFace Roguelike
Handles the turn-based game flow: player turn, then enemy turns.
"""
from enum import Enum, auto
from typing import List, Optional, Callable, TYPE_CHECKING
from entities import Player, Enemy
from combat import try_attack, process_kill, CombatResult
from ai import process_enemy_turns
if TYPE_CHECKING:
from dungeon import Dungeon
class GameState(Enum):
"""Current state of the game."""
PLAYER_TURN = auto() # Waiting for player input
ENEMY_TURN = auto() # Processing enemy actions
PLAYER_DEAD = auto() # Player has died
VICTORY = auto() # Player has won (optional)
LEVEL_TRANSITION = auto() # Moving to next level
class TurnManager:
"""
Manages the turn-based game loop.
The game follows this flow:
1. Player takes action (move or attack)
2. If action was valid, enemies take turns
3. Check for game over conditions
4. Return to step 1
"""
def __init__(self, player: Player, enemies: List[Enemy], dungeon: 'Dungeon'):
"""
Initialize the turn manager.
Args:
player: The player entity
enemies: List of all enemies
dungeon: The dungeon map
"""
self.player = player
self.enemies = enemies
self.dungeon = dungeon
self.state = GameState.PLAYER_TURN
self.turn_count = 0
# Callbacks for game events
self.on_message: Optional[Callable[[str, tuple], None]] = None
self.on_player_death: Optional[Callable[[], None]] = None
self.on_enemy_death: Optional[Callable[[Enemy], None]] = None
self.on_turn_end: Optional[Callable[[int], None]] = None
def reset(self, player: Player, enemies: List[Enemy], dungeon: 'Dungeon') -> None:
"""Reset the turn manager with new game state."""
self.player = player
self.enemies = enemies
self.dungeon = dungeon
self.state = GameState.PLAYER_TURN
self.turn_count = 0
def add_message(self, message: str, color: tuple = (255, 255, 255, 255)) -> None:
"""Add a message to the log via callback."""
if self.on_message:
self.on_message(message, color)
def handle_player_action(self, dx: int, dy: int) -> bool:
"""
Handle a player movement or attack action.
Args:
dx: X direction (-1, 0, or 1)
dy: Y direction (-1, 0, or 1)
Returns:
True if the action consumed a turn, False otherwise
"""
if self.state != GameState.PLAYER_TURN:
return False
target_x = self.player.x + dx
target_y = self.player.y + dy
# Check for attack
result = try_attack(self.player, target_x, target_y, self.enemies)
if result:
# Player attacked something
self.add_message(result.message, result.message_color)
if result.killed:
# Process kill
xp = process_kill(self.player, result.defender)
self.enemies.remove(result.defender)
if xp > 0:
self.add_message(f"You gain {xp} XP!", (255, 255, 100, 255))
if self.on_enemy_death:
self.on_enemy_death(result.defender)
# Action consumed a turn
self._end_player_turn()
return True
# No attack - try to move
if self.dungeon.is_walkable(target_x, target_y):
# Check for enemy blocking
blocked = False
for enemy in self.enemies:
if enemy.is_alive and enemy.x == target_x and enemy.y == target_y:
blocked = True
break
if not blocked:
self.player.move_to(target_x, target_y)
self._end_player_turn()
return True
# Movement blocked
return False
def handle_wait(self) -> bool:
"""
Handle the player choosing to wait (skip turn).
Returns:
True (always consumes a turn)
"""
if self.state != GameState.PLAYER_TURN:
return False
self.add_message("You wait...", (150, 150, 150, 255))
self._end_player_turn()
return True
def _end_player_turn(self) -> None:
"""End the player's turn and process enemy turns."""
self.state = GameState.ENEMY_TURN
self._process_enemy_turns()
def _process_enemy_turns(self) -> None:
"""Process all enemy turns."""
# Get combat results from enemy actions
results = process_enemy_turns(
self.enemies,
self.player,
self.dungeon
)
# Report results
for result in results:
self.add_message(result.message, result.message_color)
# Check if player died
if not self.player.is_alive:
self.state = GameState.PLAYER_DEAD
if self.on_player_death:
self.on_player_death()
else:
# End turn
self.turn_count += 1
self.state = GameState.PLAYER_TURN
if self.on_turn_end:
self.on_turn_end(self.turn_count)
def is_player_turn(self) -> bool:
"""Check if it's the player's turn."""
return self.state == GameState.PLAYER_TURN
def is_game_over(self) -> bool:
"""Check if the game is over (player dead)."""
return self.state == GameState.PLAYER_DEAD
def get_enemy_count(self) -> int:
"""Get the number of living enemies."""
return sum(1 for e in self.enemies if e.is_alive)
class ActionResult:
"""Result of a player action."""
def __init__(self, success: bool, message: str = "",
color: tuple = (255, 255, 255, 255)):
self.success = success
self.message = message
self.color = color
def try_move_or_attack(player: Player, dx: int, dy: int,
dungeon: 'Dungeon', enemies: List[Enemy]) -> ActionResult:
"""
Attempt to move or attack in a direction.
This is a simpler, standalone function for games that don't want
the full TurnManager.
Args:
player: The player
dx: X direction
dy: Y direction
dungeon: The dungeon map
enemies: List of enemies
Returns:
ActionResult indicating success and any message
"""
target_x = player.x + dx
target_y = player.y + dy
# Check for attack
for enemy in enemies:
if enemy.is_alive and enemy.x == target_x and enemy.y == target_y:
result = try_attack(player, target_x, target_y, enemies)
if result:
if result.killed:
process_kill(player, enemy)
enemies.remove(enemy)
return ActionResult(True, result.message, result.message_color)
# Check for movement
if dungeon.is_walkable(target_x, target_y):
player.move_to(target_x, target_y)
return ActionResult(True)
return ActionResult(False, "You can't move there!", (150, 150, 150, 255))

View file

@ -1,330 +0,0 @@
"""
ui.py - User Interface Components for McRogueFace Roguelike
Contains the health bar and message log UI elements.
"""
from typing import List, Tuple, Optional
from dataclasses import dataclass
import mcrfpy
from constants import (
HP_BAR_X, HP_BAR_Y, HP_BAR_WIDTH, HP_BAR_HEIGHT,
MSG_LOG_X, MSG_LOG_Y, MSG_LOG_WIDTH, MSG_LOG_HEIGHT, MSG_LOG_MAX_LINES,
LEVEL_DISPLAY_X, LEVEL_DISPLAY_Y,
COLOR_UI_BG, COLOR_UI_BORDER, COLOR_TEXT,
COLOR_HP_BAR_BG, COLOR_HP_BAR_FILL, COLOR_HP_BAR_WARNING, COLOR_HP_BAR_CRITICAL,
COLOR_MSG_DEFAULT
)
@dataclass
class Message:
"""A message in the message log."""
text: str
color: Tuple[int, int, int, int]
class HealthBar:
"""
Visual health bar displaying player HP.
Uses nested Frames: an outer background frame and an inner fill frame
that resizes based on HP percentage.
"""
def __init__(self, x: int = HP_BAR_X, y: int = HP_BAR_Y,
width: int = HP_BAR_WIDTH, height: int = HP_BAR_HEIGHT,
font: mcrfpy.Font = None):
"""
Create a health bar.
Args:
x: X position
y: Y position
width: Total width of the bar
height: Height of the bar
font: Font for the HP text
"""
self.x = x
self.y = y
self.width = width
self.height = height
self.font = font or mcrfpy.default_font
# Background frame
self.bg_frame = mcrfpy.Frame(x, y, width, height)
self.bg_frame.fill_color = mcrfpy.Color(*COLOR_HP_BAR_BG)
self.bg_frame.outline = 2
self.bg_frame.outline_color = mcrfpy.Color(*COLOR_UI_BORDER)
# Fill frame (inside background)
self.fill_frame = mcrfpy.Frame(x + 2, y + 2, width - 4, height - 4)
self.fill_frame.fill_color = mcrfpy.Color(*COLOR_HP_BAR_FILL)
self.fill_frame.outline = 0
# HP text
self.hp_text = mcrfpy.Caption("HP: 0 / 0", self.font, x + 8, y + 4)
self.hp_text.fill_color = mcrfpy.Color(*COLOR_TEXT)
self._max_fill_width = width - 4
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
"""Add all health bar components to a scene."""
ui.append(self.bg_frame)
ui.append(self.fill_frame)
ui.append(self.hp_text)
def update(self, current_hp: int, max_hp: int) -> None:
"""
Update the health bar display.
Args:
current_hp: Current hit points
max_hp: Maximum hit points
"""
# Calculate fill percentage
if max_hp <= 0:
percent = 0.0
else:
percent = max(0.0, min(1.0, current_hp / max_hp))
# Update fill bar width
self.fill_frame.w = int(self._max_fill_width * percent)
# Update color based on HP percentage
if percent > 0.6:
color = COLOR_HP_BAR_FILL
elif percent > 0.3:
color = COLOR_HP_BAR_WARNING
else:
color = COLOR_HP_BAR_CRITICAL
self.fill_frame.fill_color = mcrfpy.Color(*color)
# Update text
self.hp_text.text = f"HP: {current_hp} / {max_hp}"
class MessageLog:
"""
Scrolling message log displaying game events.
Uses a Frame container with Caption children for each line.
"""
def __init__(self, x: int = MSG_LOG_X, y: int = MSG_LOG_Y,
width: int = MSG_LOG_WIDTH, height: int = MSG_LOG_HEIGHT,
max_lines: int = MSG_LOG_MAX_LINES,
font: mcrfpy.Font = None):
"""
Create a message log.
Args:
x: X position
y: Y position
width: Width of the log
height: Height of the log
max_lines: Maximum number of visible lines
font: Font for the messages
"""
self.x = x
self.y = y
self.width = width
self.height = height
self.max_lines = max_lines
self.font = font or mcrfpy.default_font
# Container frame
self.frame = mcrfpy.Frame(x, y, width, height)
self.frame.fill_color = mcrfpy.Color(*COLOR_UI_BG)
self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(*COLOR_UI_BORDER)
# Message storage
self.messages: List[Message] = []
self.captions: List[mcrfpy.Caption] = []
# Line height (approximate based on font)
self.line_height = 18
# Create caption objects for each line
self._init_captions()
def _init_captions(self) -> None:
"""Initialize caption objects for message display."""
for i in range(self.max_lines):
caption = mcrfpy.Caption(
"",
self.font,
self.x + 5,
self.y + 5 + i * self.line_height
)
caption.fill_color = mcrfpy.Color(*COLOR_MSG_DEFAULT)
self.captions.append(caption)
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
"""Add the message log to a scene."""
ui.append(self.frame)
for caption in self.captions:
ui.append(caption)
def add_message(self, text: str,
color: Tuple[int, int, int, int] = COLOR_MSG_DEFAULT) -> None:
"""
Add a message to the log.
Args:
text: Message text
color: Text color as (R, G, B, A)
"""
self.messages.append(Message(text, color))
# Trim old messages
if len(self.messages) > 100:
self.messages = self.messages[-100:]
# Update display
self._update_display()
def _update_display(self) -> None:
"""Update the displayed messages."""
# Get the most recent messages
recent = self.messages[-self.max_lines:]
for i, caption in enumerate(self.captions):
if i < len(recent):
msg = recent[i]
caption.text = msg.text
caption.fill_color = mcrfpy.Color(*msg.color)
else:
caption.text = ""
def clear(self) -> None:
"""Clear all messages."""
self.messages.clear()
self._update_display()
class LevelDisplay:
"""Simple display showing current dungeon level."""
def __init__(self, x: int = LEVEL_DISPLAY_X, y: int = LEVEL_DISPLAY_Y,
font: mcrfpy.Font = None):
"""
Create a level display.
Args:
x: X position
y: Y position
font: Font for the text
"""
self.font = font or mcrfpy.default_font
self.caption = mcrfpy.Caption("Level: 1", self.font, x, y)
self.caption.fill_color = mcrfpy.Color(*COLOR_TEXT)
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
"""Add to a scene."""
ui.append(self.caption)
def update(self, level: int) -> None:
"""Update the displayed level."""
self.caption.text = f"Dungeon Level: {level}"
class GameUI:
"""
Container for all UI elements.
Provides a single point of access for updating the entire UI.
"""
def __init__(self, font: mcrfpy.Font = None):
"""
Create the game UI.
Args:
font: Font for all UI elements
"""
self.font = font or mcrfpy.default_font
# Create UI components
self.health_bar = HealthBar(font=self.font)
self.message_log = MessageLog(font=self.font)
self.level_display = LevelDisplay(font=self.font)
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
"""Add all UI elements to a scene."""
self.health_bar.add_to_scene(ui)
self.message_log.add_to_scene(ui)
self.level_display.add_to_scene(ui)
def update_hp(self, current_hp: int, max_hp: int) -> None:
"""Update the health bar."""
self.health_bar.update(current_hp, max_hp)
def add_message(self, text: str,
color: Tuple[int, int, int, int] = COLOR_MSG_DEFAULT) -> None:
"""Add a message to the log."""
self.message_log.add_message(text, color)
def update_level(self, level: int) -> None:
"""Update the dungeon level display."""
self.level_display.update(level)
def clear_messages(self) -> None:
"""Clear the message log."""
self.message_log.clear()
class DeathScreen:
"""Game over screen shown when player dies."""
def __init__(self, font: mcrfpy.Font = None):
"""
Create the death screen.
Args:
font: Font for text
"""
self.font = font or mcrfpy.default_font
self.elements: List = []
# Semi-transparent overlay
self.overlay = mcrfpy.Frame(0, 0, 1024, 768)
self.overlay.fill_color = mcrfpy.Color(0, 0, 0, 180)
self.elements.append(self.overlay)
# Death message
self.death_text = mcrfpy.Caption(
"YOU HAVE DIED",
self.font,
362, 300
)
self.death_text.fill_color = mcrfpy.Color(255, 0, 0, 255)
self.death_text.outline = 2
self.death_text.outline_color = mcrfpy.Color(0, 0, 0, 255)
self.elements.append(self.death_text)
# Restart prompt
self.restart_text = mcrfpy.Caption(
"Press R to restart",
self.font,
400, 400
)
self.restart_text.fill_color = mcrfpy.Color(200, 200, 200, 255)
self.elements.append(self.restart_text)
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
"""Add death screen elements to a scene."""
for element in self.elements:
ui.append(element)
def remove_from_scene(self, ui: mcrfpy.UICollection) -> None:
"""Remove death screen elements from a scene."""
for element in self.elements:
try:
ui.remove(element)
except (ValueError, RuntimeError):
pass

View file

@ -1,176 +0,0 @@
"""
McRogueFace Minimal Template
============================
A starting point for simple roguelike prototypes.
This template demonstrates:
- Scene object pattern (preferred OOP approach)
- Grid-based movement with boundary checking
- Keyboard input handling
- Entity positioning on a grid
Usage:
Place this file in your McRogueFace scripts directory and run McRogueFace.
Use arrow keys to move the @ symbol. Press Escape to exit.
"""
import mcrfpy
# =============================================================================
# CONSTANTS
# =============================================================================
# Grid dimensions (in tiles)
GRID_WIDTH: int = 20
GRID_HEIGHT: int = 15
# Tile size in pixels (must match your sprite sheet)
TILE_SIZE: int = 16
# CP437 sprite indices (standard roguelike character mapping)
# In CP437, character codes map to sprite indices: '@' = 64, '.' = 46, etc.
SPRITE_PLAYER: int = 64 # '@' symbol
SPRITE_FLOOR: int = 46 # '.' symbol
# Colors (RGBA tuples)
COLOR_BACKGROUND: tuple[int, int, int] = (20, 20, 30)
# =============================================================================
# GAME STATE
# =============================================================================
# Player position in grid coordinates
player_x: int = GRID_WIDTH // 2
player_y: int = GRID_HEIGHT // 2
# Reference to player entity (set during setup)
player_entity: mcrfpy.Entity = None
# =============================================================================
# MOVEMENT LOGIC
# =============================================================================
def try_move(dx: int, dy: int) -> bool:
"""
Attempt to move the player by (dx, dy) tiles.
Args:
dx: Horizontal movement (-1 = left, +1 = right, 0 = none)
dy: Vertical movement (-1 = up, +1 = down, 0 = none)
Returns:
True if movement succeeded, False if blocked by boundary
"""
global player_x, player_y
new_x = player_x + dx
new_y = player_y + dy
# Boundary checking: ensure player stays within grid
if 0 <= new_x < GRID_WIDTH and 0 <= new_y < GRID_HEIGHT:
player_x = new_x
player_y = new_y
# Update the entity's position on the grid
player_entity.x = player_x
player_entity.y = player_y
return True
return False
# =============================================================================
# INPUT HANDLING
# =============================================================================
def handle_keypress(key: str, action: str) -> None:
"""
Handle keyboard input for the game scene.
Args:
key: The key that was pressed (e.g., "Up", "Down", "Escape", "a", "W")
action: Either "start" (key pressed) or "end" (key released)
Note:
We only process on "start" to avoid double-triggering on key release.
"""
if action != "start":
return
# Movement keys (both arrow keys and WASD)
if key == "Up" or key == "W" or key == "w":
try_move(0, -1)
elif key == "Down" or key == "S" or key == "s":
try_move(0, 1)
elif key == "Left" or key == "A" or key == "a":
try_move(-1, 0)
elif key == "Right" or key == "D" or key == "d":
try_move(1, 0)
# Exit on Escape
elif key == "Escape":
mcrfpy.exit()
# =============================================================================
# SCENE SETUP
# =============================================================================
def setup_game() -> mcrfpy.Scene:
"""
Create and configure the game scene.
Returns:
The configured Scene object, ready to be activated.
"""
global player_entity
# Create the scene using the OOP pattern (preferred over createScene)
scene = mcrfpy.Scene("game")
# Load the sprite sheet texture
# Adjust the path and tile size to match your assets
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", TILE_SIZE, TILE_SIZE)
# Create the game grid
# Grid(pos, size, grid_size) where:
# pos = pixel position on screen
# size = pixel dimensions of the grid display
# grid_size = number of tiles (columns, rows)
grid = mcrfpy.Grid(
pos=(32, 32),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture
)
grid.fill_color = mcrfpy.Color(*COLOR_BACKGROUND)
# Fill the grid with floor tiles
for x in range(GRID_WIDTH):
for y in range(GRID_HEIGHT):
point = grid.at(x, y)
point.tilesprite = SPRITE_FLOOR
point.walkable = True
point.transparent = True
# Create the player entity
player_entity = mcrfpy.Entity(
pos=(player_x, player_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player_entity)
# Add the grid to the scene's UI
scene.children.append(grid)
# Set up keyboard input handler for this scene
scene.on_key = handle_keypress
return scene
# =============================================================================
# MAIN ENTRY POINT
# =============================================================================
# Create and activate the game scene
game_scene = setup_game()
game_scene.activate()

View file

@ -1,138 +0,0 @@
"""
constants.py - Roguelike Template Constants
This module defines all the constants used throughout the roguelike template,
including sprite indices for CP437 tileset, colors for FOV system, and
game configuration values.
CP437 is the classic IBM PC character set commonly used in traditional roguelikes.
The sprite indices correspond to ASCII character codes in a CP437 tileset.
"""
import mcrfpy
# =============================================================================
# SPRITE INDICES (CP437 Character Codes)
# =============================================================================
# These indices correspond to characters in a CP437-style tileset.
# The default McRogueFace tileset uses 16x16 sprites arranged in a grid.
# Terrain sprites
SPRITE_FLOOR = 46 # '.' - Standard floor tile
SPRITE_WALL = 35 # '#' - Wall/obstacle tile
SPRITE_DOOR_CLOSED = 43 # '+' - Closed door
SPRITE_DOOR_OPEN = 47 # '/' - Open door
SPRITE_STAIRS_DOWN = 62 # '>' - Stairs going down
SPRITE_STAIRS_UP = 60 # '<' - Stairs going up
# Player sprite
SPRITE_PLAYER = 64 # '@' - The classic roguelike player symbol
# Enemy sprites
SPRITE_ORC = 111 # 'o' - Orc enemy
SPRITE_TROLL = 84 # 'T' - Troll enemy
SPRITE_GOBLIN = 103 # 'g' - Goblin enemy
SPRITE_RAT = 114 # 'r' - Giant rat
SPRITE_SNAKE = 115 # 's' - Snake
SPRITE_ZOMBIE = 90 # 'Z' - Zombie
# Item sprites
SPRITE_POTION = 33 # '!' - Potion
SPRITE_SCROLL = 63 # '?' - Scroll
SPRITE_GOLD = 36 # '$' - Gold/treasure
SPRITE_WEAPON = 41 # ')' - Weapon
SPRITE_ARMOR = 91 # '[' - Armor
SPRITE_RING = 61 # '=' - Ring
# =============================================================================
# FOV/VISIBILITY COLORS
# =============================================================================
# These colors are applied as overlays to grid tiles to create the fog of war
# effect. The alpha channel determines how much of the original tile shows through.
# Fully visible - no overlay (alpha = 0 means completely transparent overlay)
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
# Previously explored but not currently visible - dim blue-gray overlay
# This creates the "memory" effect where you can see the map layout
# but not current enemy positions
COLOR_EXPLORED = mcrfpy.Color(50, 50, 80, 180)
# Never seen - completely black (alpha = 255 means fully opaque)
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
# =============================================================================
# TILE COLORS
# =============================================================================
# Base colors for different tile types (applied to the tile's color property)
COLOR_FLOOR = mcrfpy.Color(50, 50, 50) # Dark gray floor
COLOR_WALL = mcrfpy.Color(100, 100, 100) # Lighter gray walls
COLOR_FLOOR_LIT = mcrfpy.Color(100, 90, 70) # Warm lit floor
COLOR_WALL_LIT = mcrfpy.Color(130, 110, 80) # Warm lit walls
# =============================================================================
# ENTITY COLORS
# =============================================================================
# Colors applied to entity sprites
COLOR_PLAYER = mcrfpy.Color(255, 255, 255) # White player
COLOR_ORC = mcrfpy.Color(63, 127, 63) # Green orc
COLOR_TROLL = mcrfpy.Color(0, 127, 0) # Darker green troll
COLOR_GOBLIN = mcrfpy.Color(127, 127, 0) # Yellow-green goblin
# =============================================================================
# GAME CONFIGURATION
# =============================================================================
# Map dimensions (in tiles)
MAP_WIDTH = 80
MAP_HEIGHT = 45
# Room generation parameters
ROOM_MIN_SIZE = 6 # Minimum room dimension
ROOM_MAX_SIZE = 12 # Maximum room dimension
MAX_ROOMS = 30 # Maximum number of rooms to generate
# FOV settings
FOV_RADIUS = 8 # How far the player can see
# Display settings
GRID_PIXEL_WIDTH = 1024 # Grid display width in pixels
GRID_PIXEL_HEIGHT = 768 # Grid display height in pixels
# Sprite size (should match your tileset)
SPRITE_WIDTH = 16
SPRITE_HEIGHT = 16
# =============================================================================
# ENEMY DEFINITIONS
# =============================================================================
# Dictionary of enemy types with their properties for easy spawning
ENEMY_TYPES = {
"orc": {
"sprite": SPRITE_ORC,
"color": COLOR_ORC,
"name": "Orc",
"hp": 10,
"power": 3,
"defense": 0,
},
"troll": {
"sprite": SPRITE_TROLL,
"color": COLOR_TROLL,
"name": "Troll",
"hp": 16,
"power": 4,
"defense": 1,
},
"goblin": {
"sprite": SPRITE_GOBLIN,
"color": COLOR_GOBLIN,
"name": "Goblin",
"hp": 6,
"power": 2,
"defense": 0,
},
}

View file

@ -1,340 +0,0 @@
"""
dungeon.py - Procedural Dungeon Generation
This module provides classic roguelike dungeon generation using the
"rooms and corridors" algorithm:
1. Generate random non-overlapping rectangular rooms
2. Connect rooms with L-shaped corridors
3. Mark tiles as walkable/transparent based on terrain type
The algorithm is simple but effective, producing dungeons similar to
the original Rogue game.
"""
from __future__ import annotations
import random
from typing import Iterator, Tuple, List, TYPE_CHECKING
if TYPE_CHECKING:
import mcrfpy
from constants import (
MAP_WIDTH, MAP_HEIGHT,
ROOM_MIN_SIZE, ROOM_MAX_SIZE, MAX_ROOMS,
SPRITE_FLOOR, SPRITE_WALL,
COLOR_FLOOR, COLOR_WALL,
)
class RectangularRoom:
"""
A rectangular room in the dungeon.
This class represents a single room and provides utilities for
working with room geometry. Rooms are defined by their top-left
corner (x1, y1) and bottom-right corner (x2, y2).
Attributes:
x1, y1: Top-left corner coordinates
x2, y2: Bottom-right corner coordinates
"""
def __init__(self, x: int, y: int, width: int, height: int) -> None:
"""
Create a new rectangular room.
Args:
x: X coordinate of the top-left corner
y: Y coordinate of the top-left corner
width: Width of the room in tiles
height: Height of the room in tiles
"""
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self) -> Tuple[int, int]:
"""
Return the center coordinates of the room.
This is useful for connecting rooms with corridors and
for placing the player in the starting room.
Returns:
Tuple of (center_x, center_y)
"""
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self) -> Tuple[slice, slice]:
"""
Return the inner area of the room as a pair of slices.
The inner area excludes the walls (1 tile border), giving
the floor area where entities can be placed.
Returns:
Tuple of (x_slice, y_slice) for array indexing
"""
# Add 1 to exclude the walls on all sides
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
def intersects(self, other: RectangularRoom) -> bool:
"""
Check if this room overlaps with another room.
Used during generation to ensure rooms don't overlap.
Args:
other: Another RectangularRoom to check against
Returns:
True if the rooms overlap, False otherwise
"""
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def inner_tiles(self) -> Iterator[Tuple[int, int]]:
"""
Iterate over all floor tile coordinates in the room.
Yields coordinates for the interior of the room (excluding walls).
Yields:
Tuples of (x, y) coordinates
"""
for x in range(self.x1 + 1, self.x2):
for y in range(self.y1 + 1, self.y2):
yield x, y
def tunnel_between(
start: Tuple[int, int],
end: Tuple[int, int]
) -> Iterator[Tuple[int, int]]:
"""
Generate an L-shaped tunnel between two points.
The tunnel goes horizontally first, then vertically (or vice versa,
chosen randomly). This creates the classic roguelike corridor style.
Args:
start: Starting (x, y) coordinates
end: Ending (x, y) coordinates
Yields:
Tuples of (x, y) coordinates for each tile in the tunnel
"""
x1, y1 = start
x2, y2 = end
# Randomly choose whether to go horizontal-first or vertical-first
if random.random() < 0.5:
# Horizontal first, then vertical
corner_x, corner_y = x2, y1
else:
# Vertical first, then horizontal
corner_x, corner_y = x1, y2
# Generate the horizontal segment
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
# Generate the vertical segment
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
# Generate to the endpoint (if needed)
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
def generate_dungeon(
max_rooms: int = MAX_ROOMS,
room_min_size: int = ROOM_MIN_SIZE,
room_max_size: int = ROOM_MAX_SIZE,
map_width: int = MAP_WIDTH,
map_height: int = MAP_HEIGHT,
) -> List[RectangularRoom]:
"""
Generate a dungeon using the rooms-and-corridors algorithm.
This function creates a list of non-overlapping rooms. The actual
tile data should be applied to a Grid using populate_grid().
Algorithm:
1. Try to place MAX_ROOMS rooms randomly
2. Reject rooms that overlap existing rooms
3. Connect each new room to the previous room with a corridor
Args:
max_rooms: Maximum number of rooms to generate
room_min_size: Minimum room dimension
room_max_size: Maximum room dimension
map_width: Width of the dungeon in tiles
map_height: Height of the dungeon in tiles
Returns:
List of RectangularRoom objects representing the dungeon layout
"""
rooms: List[RectangularRoom] = []
for _ in range(max_rooms):
# Random room dimensions
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
# Random position (ensuring room fits within map bounds)
x = random.randint(0, map_width - room_width - 1)
y = random.randint(0, map_height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
# Check if this room overlaps with any existing room
if any(new_room.intersects(other) for other in rooms):
continue # Skip this room, try again
# Room is valid, add it
rooms.append(new_room)
return rooms
def populate_grid(grid: mcrfpy.Grid, rooms: List[RectangularRoom]) -> None:
"""
Apply dungeon layout to a McRogueFace Grid.
This function:
1. Fills the entire grid with walls
2. Carves out floor tiles for each room
3. Carves corridors connecting adjacent rooms
4. Sets walkable/transparent flags appropriately
Args:
grid: The McRogueFace Grid to populate
rooms: List of RectangularRoom objects from generate_dungeon()
"""
grid_width, grid_height = grid.grid_size
# Step 1: Fill entire map with walls
for x in range(grid_width):
for y in range(grid_height):
point = grid.at(x, y)
point.tilesprite = SPRITE_WALL
point.walkable = False
point.transparent = False
point.color = COLOR_WALL
# Step 2: Carve out rooms
for room in rooms:
for x, y in room.inner_tiles():
# Bounds check (room might extend past grid)
if 0 <= x < grid_width and 0 <= y < grid_height:
point = grid.at(x, y)
point.tilesprite = SPRITE_FLOOR
point.walkable = True
point.transparent = True
point.color = COLOR_FLOOR
# Step 3: Carve corridors between adjacent rooms
for i in range(1, len(rooms)):
# Connect each room to the previous room
start = rooms[i - 1].center
end = rooms[i].center
for x, y in tunnel_between(start, end):
if 0 <= x < grid_width and 0 <= y < grid_height:
point = grid.at(x, y)
point.tilesprite = SPRITE_FLOOR
point.walkable = True
point.transparent = True
point.color = COLOR_FLOOR
def get_random_floor_position(
grid: mcrfpy.Grid,
rooms: List[RectangularRoom],
exclude_first_room: bool = False
) -> Tuple[int, int]:
"""
Get a random walkable floor position for entity placement.
This is useful for placing enemies, items, or other entities
in valid floor locations.
Args:
grid: The populated Grid to search
rooms: List of rooms (used for faster random selection)
exclude_first_room: If True, won't return positions from the
first room (where the player usually starts)
Returns:
Tuple of (x, y) coordinates of a walkable floor tile
"""
available_rooms = rooms[1:] if exclude_first_room and len(rooms) > 1 else rooms
if not available_rooms:
# Fallback: find any walkable tile
grid_width, grid_height = grid.grid_size
walkable_tiles = []
for x in range(grid_width):
for y in range(grid_height):
if grid.at(x, y).walkable:
walkable_tiles.append((x, y))
return random.choice(walkable_tiles) if walkable_tiles else (1, 1)
# Pick a random room and a random position within it
room = random.choice(available_rooms)
floor_tiles = list(room.inner_tiles())
return random.choice(floor_tiles)
def get_spawn_positions(
rooms: List[RectangularRoom],
count: int,
exclude_first_room: bool = True
) -> List[Tuple[int, int]]:
"""
Get multiple spawn positions for enemies.
Distributes enemies across different rooms for better gameplay.
Args:
rooms: List of rooms from dungeon generation
count: Number of positions to generate
exclude_first_room: If True, won't spawn in the player's starting room
Returns:
List of (x, y) coordinate tuples
"""
available_rooms = rooms[1:] if exclude_first_room and len(rooms) > 1 else rooms
if not available_rooms:
return []
positions = []
for i in range(count):
# Cycle through rooms to distribute enemies
room = available_rooms[i % len(available_rooms)]
floor_tiles = list(room.inner_tiles())
# Try to avoid placing on the same tile
available_tiles = [t for t in floor_tiles if t not in positions]
if available_tiles:
positions.append(random.choice(available_tiles))
elif floor_tiles:
positions.append(random.choice(floor_tiles))
return positions

View file

@ -1,364 +0,0 @@
"""
entities.py - Entity Management for Roguelike Template
This module provides entity creation and management utilities for the
roguelike template. Entities in McRogueFace are game objects that exist
on a Grid, such as the player, enemies, items, and NPCs.
The module includes:
- Entity factory functions for creating common entity types
- Helper functions for entity management
- Simple data containers for entity stats (for future expansion)
Note: McRogueFace entities are simple position + sprite objects. For
complex game logic like AI, combat, and inventory, you'll want to wrap
them in Python classes that reference the underlying Entity.
"""
from __future__ import annotations
from typing import Tuple, Optional, List, Dict, Any, TYPE_CHECKING
from dataclasses import dataclass
if TYPE_CHECKING:
import mcrfpy
from constants import (
SPRITE_PLAYER, SPRITE_ORC, SPRITE_TROLL, SPRITE_GOBLIN,
COLOR_PLAYER, COLOR_ORC, COLOR_TROLL, COLOR_GOBLIN,
ENEMY_TYPES,
)
@dataclass
class EntityStats:
"""
Optional stats container for game entities.
This dataclass can be used to track stats for entities that need them.
Attach it to your entity wrapper class for combat, leveling, etc.
Attributes:
hp: Current hit points
max_hp: Maximum hit points
power: Attack power
defense: Damage reduction
name: Display name for the entity
"""
hp: int = 10
max_hp: int = 10
power: int = 3
defense: int = 0
name: str = "Unknown"
@property
def is_alive(self) -> bool:
"""Check if the entity is still alive."""
return self.hp > 0
def take_damage(self, amount: int) -> int:
"""
Apply damage, accounting for defense.
Args:
amount: Raw damage amount
Returns:
Actual damage dealt after defense
"""
actual_damage = max(0, amount - self.defense)
self.hp = max(0, self.hp - actual_damage)
return actual_damage
def heal(self, amount: int) -> int:
"""
Heal the entity.
Args:
amount: Amount to heal
Returns:
Actual amount healed (may be less if near max HP)
"""
old_hp = self.hp
self.hp = min(self.max_hp, self.hp + amount)
return self.hp - old_hp
def create_player(
grid: mcrfpy.Grid,
texture: mcrfpy.Texture,
x: int,
y: int
) -> mcrfpy.Entity:
"""
Create and place the player entity on the grid.
The player uses the classic '@' symbol (sprite index 64 in CP437).
Args:
grid: The Grid to place the player on
texture: The texture/tileset to use
x: Starting X position
y: Starting Y position
Returns:
The created player Entity
"""
import mcrfpy
player = mcrfpy.Entity(
pos=(x, y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
return player
def create_enemy(
grid: mcrfpy.Grid,
texture: mcrfpy.Texture,
x: int,
y: int,
enemy_type: str = "orc"
) -> Tuple[mcrfpy.Entity, EntityStats]:
"""
Create an enemy entity with associated stats.
Enemy types are defined in constants.py. Currently available:
- "orc": Standard enemy, balanced stats
- "troll": Tough enemy, high HP and power
- "goblin": Weak enemy, low stats
Args:
grid: The Grid to place the enemy on
texture: The texture/tileset to use
x: X position
y: Y position
enemy_type: Key from ENEMY_TYPES dict
Returns:
Tuple of (Entity, EntityStats) for the created enemy
"""
import mcrfpy
# Get enemy definition, default to orc if not found
enemy_def = ENEMY_TYPES.get(enemy_type, ENEMY_TYPES["orc"])
entity = mcrfpy.Entity(
pos=(x, y),
texture=texture,
sprite_index=enemy_def["sprite"]
)
grid.entities.append(entity)
stats = EntityStats(
hp=enemy_def["hp"],
max_hp=enemy_def["hp"],
power=enemy_def["power"],
defense=enemy_def["defense"],
name=enemy_def["name"]
)
return entity, stats
def create_enemies_in_rooms(
grid: mcrfpy.Grid,
texture: mcrfpy.Texture,
rooms: list,
enemies_per_room: int = 2,
skip_first_room: bool = True
) -> List[Tuple[mcrfpy.Entity, EntityStats]]:
"""
Populate dungeon rooms with enemies.
This helper function places random enemies throughout the dungeon,
typically skipping the first room (where the player starts).
Args:
grid: The Grid to populate
texture: The texture/tileset to use
rooms: List of RectangularRoom objects from dungeon generation
enemies_per_room: Maximum enemies to spawn per room
skip_first_room: If True, don't spawn enemies in the first room
Returns:
List of (Entity, EntityStats) tuples for all created enemies
"""
import random
enemies = []
enemy_type_keys = list(ENEMY_TYPES.keys())
# Iterate through rooms, optionally skipping the first
rooms_to_populate = rooms[1:] if skip_first_room else rooms
for room in rooms_to_populate:
# Random number of enemies (0 to enemies_per_room)
num_enemies = random.randint(0, enemies_per_room)
# Get available floor tiles in this room
floor_tiles = list(room.inner_tiles())
for _ in range(num_enemies):
if not floor_tiles:
break
# Pick a random position and remove it from available
pos = random.choice(floor_tiles)
floor_tiles.remove(pos)
# Pick a random enemy type (weighted toward weaker enemies)
if random.random() < 0.8:
enemy_type = "orc" # 80% orcs
else:
enemy_type = "troll" # 20% trolls
x, y = pos
entity, stats = create_enemy(grid, texture, x, y, enemy_type)
enemies.append((entity, stats))
return enemies
def get_blocking_entity_at(
entities: List[mcrfpy.Entity],
x: int,
y: int
) -> Optional[mcrfpy.Entity]:
"""
Check if there's a blocking entity at the given position.
Useful for collision detection - checks if an entity exists at
the target position before moving there.
Args:
entities: List of entities to check
x: X coordinate to check
y: Y coordinate to check
Returns:
The entity at that position, or None if empty
"""
for entity in entities:
if entity.pos[0] == x and entity.pos[1] == y:
return entity
return None
def move_entity(
entity: mcrfpy.Entity,
grid: mcrfpy.Grid,
dx: int,
dy: int,
entities: List[mcrfpy.Entity] = None
) -> bool:
"""
Attempt to move an entity by a delta.
Checks for:
- Grid bounds
- Walkable terrain
- Other blocking entities (if entities list provided)
Args:
entity: The entity to move
grid: The grid for terrain collision
dx: Delta X (-1, 0, or 1 typically)
dy: Delta Y (-1, 0, or 1 typically)
entities: Optional list of entities to check for collision
Returns:
True if movement succeeded, False otherwise
"""
dest_x = entity.pos[0] + dx
dest_y = entity.pos[1] + dy
# Check grid bounds
grid_width, grid_height = grid.grid_size
if not (0 <= dest_x < grid_width and 0 <= dest_y < grid_height):
return False
# Check if tile is walkable
if not grid.at(dest_x, dest_y).walkable:
return False
# Check for blocking entities
if entities and get_blocking_entity_at(entities, dest_x, dest_y):
return False
# Move is valid
entity.pos = (dest_x, dest_y)
return True
def distance_between(
entity1: mcrfpy.Entity,
entity2: mcrfpy.Entity
) -> float:
"""
Calculate the Chebyshev distance between two entities.
Chebyshev distance (also called chessboard distance) counts
diagonal moves as 1, which is standard for roguelikes.
Args:
entity1: First entity
entity2: Second entity
Returns:
Distance in tiles (diagonal = 1)
"""
dx = abs(entity1.pos[0] - entity2.pos[0])
dy = abs(entity1.pos[1] - entity2.pos[1])
return max(dx, dy)
def entities_in_radius(
center: mcrfpy.Entity,
entities: List[mcrfpy.Entity],
radius: float
) -> List[mcrfpy.Entity]:
"""
Find all entities within a given radius of a center entity.
Uses Chebyshev distance for roguelike-style radius.
Args:
center: The entity to search around
entities: List of entities to check
radius: Maximum distance in tiles
Returns:
List of entities within the radius (excluding center)
"""
nearby = []
for entity in entities:
if entity is not center:
if distance_between(center, entity) <= radius:
nearby.append(entity)
return nearby
def remove_entity(
entity: mcrfpy.Entity,
grid: mcrfpy.Grid
) -> bool:
"""
Remove an entity from a grid.
Args:
entity: The entity to remove
grid: The grid containing the entity
Returns:
True if removal succeeded, False otherwise
"""
try:
idx = entity.index()
grid.entities.remove(idx)
return True
except (ValueError, AttributeError):
return False

View file

@ -1,290 +0,0 @@
"""
game.py - Roguelike Template Main Entry Point
A minimal but complete roguelike starter using McRogueFace.
This template demonstrates:
- Scene and grid setup
- Procedural dungeon generation
- Player entity with keyboard movement
- Enemy entities (static, no AI)
- Field of view using TCOD via Entity.update_visibility()
- FOV visualization with grid color overlays
Run with: ./mcrogueface
Controls:
- Arrow keys / WASD: Move player
- Escape: Quit game
The template is designed to be extended. Good next steps:
- Add enemy AI (chase player, pathfinding)
- Implement combat system
- Add items and inventory
- Add multiple dungeon levels
"""
import mcrfpy
from typing import List, Tuple
# Import our template modules
from constants import (
MAP_WIDTH, MAP_HEIGHT,
SPRITE_WIDTH, SPRITE_HEIGHT,
FOV_RADIUS,
COLOR_VISIBLE, COLOR_EXPLORED, COLOR_UNKNOWN,
SPRITE_PLAYER,
)
from dungeon import generate_dungeon, populate_grid, RectangularRoom
from entities import (
create_player,
create_enemies_in_rooms,
move_entity,
EntityStats,
)
# =============================================================================
# GAME STATE
# =============================================================================
# Global game state - in a larger game, you'd use a proper state management
# system, but for a template this keeps things simple and visible.
class GameState:
"""Container for all game state."""
def __init__(self):
# Core game objects (set during initialization)
self.grid: mcrfpy.Grid = None
self.player: mcrfpy.Entity = None
self.rooms: List[RectangularRoom] = []
self.enemies: List[Tuple[mcrfpy.Entity, EntityStats]] = []
# Texture reference
self.texture: mcrfpy.Texture = None
# Global game state instance
game = GameState()
# =============================================================================
# FOV (FIELD OF VIEW) SYSTEM
# =============================================================================
def update_fov() -> None:
"""
Update the field of view based on player position.
This function:
1. Calls update_visibility() on the player entity to compute FOV using TCOD
2. Applies color overlays to tiles based on visibility state
The FOV creates the classic roguelike effect where:
- Visible tiles are fully bright (no overlay)
- Previously seen tiles are dimmed (remembered layout)
- Never-seen tiles are completely dark
TCOD handles the actual FOV computation based on the grid's
walkable and transparent flags set during dungeon generation.
"""
if not game.player or not game.grid:
return
# Tell McRogueFace/TCOD to recompute visibility from player position
game.player.update_visibility()
grid_width, grid_height = game.grid.grid_size
# Apply visibility colors to each tile
for x in range(grid_width):
for y in range(grid_height):
point = game.grid.at(x, y)
# Get the player's visibility state for this tile
state = game.player.at(x, y)
if state.visible:
# Currently visible - no overlay (full brightness)
point.color_overlay = COLOR_VISIBLE
elif state.discovered:
# Previously seen - dimmed overlay (memory)
point.color_overlay = COLOR_EXPLORED
else:
# Never seen - completely dark
point.color_overlay = COLOR_UNKNOWN
# =============================================================================
# INPUT HANDLING
# =============================================================================
def handle_keys(key: str, state: str) -> None:
"""
Handle keyboard input for player movement and game controls.
This is the main input handler registered with McRogueFace.
It processes key events and updates game state accordingly.
Args:
key: The key that was pressed (e.g., "W", "Up", "Escape")
state: Either "start" (key pressed) or "end" (key released)
"""
# Only process key press events, not releases
if state != "start":
return
# Movement deltas: (dx, dy)
movement = {
# Arrow keys
"Up": (0, -1),
"Down": (0, 1),
"Left": (-1, 0),
"Right": (1, 0),
# WASD keys
"W": (0, -1),
"S": (0, 1),
"A": (-1, 0),
"D": (1, 0),
# Numpad (for diagonal movement if desired)
"Numpad8": (0, -1),
"Numpad2": (0, 1),
"Numpad4": (-1, 0),
"Numpad6": (1, 0),
"Numpad7": (-1, -1),
"Numpad9": (1, -1),
"Numpad1": (-1, 1),
"Numpad3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
# Get list of all entity objects for collision checking
all_entities = [e for e, _ in game.enemies]
# Attempt to move the player
if move_entity(game.player, game.grid, dx, dy, all_entities):
# Movement succeeded - update FOV
update_fov()
# Center camera on player
px, py = game.player.pos
game.grid.center = (px, py)
elif key == "Escape":
# Quit the game
mcrfpy.exit()
# =============================================================================
# GAME INITIALIZATION
# =============================================================================
def initialize_game() -> None:
"""
Set up the game world.
This function:
1. Creates the scene and loads resources
2. Generates the dungeon layout
3. Creates and places all entities
4. Initializes the FOV system
5. Sets up input handling
"""
# Create the game scene
mcrfpy.createScene("game")
ui = mcrfpy.sceneUI("game")
# Load the tileset texture
# The default McRogueFace texture works great for roguelikes
game.texture = mcrfpy.Texture(
"assets/kenney_tinydungeon.png",
SPRITE_WIDTH,
SPRITE_HEIGHT
)
# Create the grid (tile-based game world)
# Using keyword arguments for clarity - this is the preferred style
game.grid = mcrfpy.Grid(
pos=(0, 0), # Screen position in pixels
size=(1024, 768), # Display size in pixels
grid_size=(MAP_WIDTH, MAP_HEIGHT), # Map size in tiles
texture=game.texture
)
ui.append(game.grid)
# Generate dungeon layout
game.rooms = generate_dungeon()
# Apply dungeon to grid (sets tiles, walkable flags, etc.)
populate_grid(game.grid, game.rooms)
# Place player in the center of the first room
if game.rooms:
start_x, start_y = game.rooms[0].center
else:
# Fallback if no rooms generated
start_x, start_y = MAP_WIDTH // 2, MAP_HEIGHT // 2
game.player = create_player(
grid=game.grid,
texture=game.texture,
x=start_x,
y=start_y
)
# Center camera on player
game.grid.center = (start_x, start_y)
# Spawn enemies in other rooms
game.enemies = create_enemies_in_rooms(
grid=game.grid,
texture=game.texture,
rooms=game.rooms,
enemies_per_room=2,
skip_first_room=True
)
# Initial FOV calculation
update_fov()
# Register input handler
mcrfpy.keypressScene(handle_keys)
# Switch to game scene
mcrfpy.setScene("game")
# =============================================================================
# MAIN ENTRY POINT
# =============================================================================
def main() -> None:
"""
Main entry point for the roguelike template.
This function is called when the script starts. It initializes
the game and McRogueFace handles the game loop automatically.
"""
initialize_game()
# Display welcome message
print("=" * 50)
print(" ROGUELIKE TEMPLATE")
print("=" * 50)
print("Controls:")
print(" Arrow keys / WASD - Move")
print(" Escape - Quit")
print()
print(f"Dungeon generated with {len(game.rooms)} rooms")
print(f"Enemies spawned: {len(game.enemies)}")
print("=" * 50)
# Run the game
if __name__ == "__main__":
main()
else:
# McRogueFace runs game.py directly, not as __main__
main()

View file

@ -1,30 +0,0 @@
"""McRogueFace - Part 0: Setting Up McRogueFace
Documentation: https://mcrogueface.github.io/tutorial/part_00_setup
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_00_setup/part_00_setup.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Create a Scene object - this is the preferred approach
scene = mcrfpy.Scene("hello")
# Create a caption to display text
title = mcrfpy.Caption(
pos=(512, 300),
text="Hello, Roguelike!"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 32
# Add the caption to the scene's UI collection
scene.children.append(title)
# Activate the scene to display it
scene.activate()
# Note: There is no run() function!
# The engine is already running - your script is imported by it.

View file

@ -1,119 +0,0 @@
"""McRogueFace - Part 1: The '@' and the Dungeon Grid
Documentation: https://mcrogueface.github.io/tutorial/part_01_grid_movement
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# Sprite indices for CP437 tileset
SPRITE_AT = 64 # '@' - player character
SPRITE_FLOOR = 46 # '.' - floor tile
# Grid dimensions (in tiles)
GRID_WIDTH = 20
GRID_HEIGHT = 15
# Create the scene
scene = mcrfpy.Scene("game")
# Load the texture (sprite sheet)
# Parameters: path, sprite_width, sprite_height
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
# The grid displays tiles and contains entities
grid = mcrfpy.Grid(
pos=(100, 80), # Position on screen (pixels)
size=(640, 480), # Display size (pixels)
zoom = 2.0,
grid_size=(GRID_WIDTH, GRID_HEIGHT), # Size in tiles
texture=texture
)
# Fill the grid with floor tiles
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
# Create the player entity at the center of the grid
player = mcrfpy.Entity(
grid_pos=(GRID_WIDTH // 2, GRID_HEIGHT // 2), # Grid coordinates, not pixels!
texture=texture,
sprite_index=SPRITE_AT
)
# Add the player to the grid
# Option 1: Use the grid parameter in constructor
# player = mcrfpy.Entity(grid_pos=(10, 7), texture=texture, sprite_index=SPRITE_AT, grid=grid)
# Option 2: Append to grid.entities (what we will use)
grid.entities.append(player)
# Add the grid to the scene
scene.children.append(grid)
# Add a title caption
title = mcrfpy.Caption(
pos=(100, 20),
text="Part 1: Grid Movement - Use Arrow Keys or WASD"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 18
scene.children.append(title)
# Add a position display
pos_display = mcrfpy.Caption(
pos=(100, 50),
text=f"Player Position: ({player.x}, {player.y})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input to move the player.
Args:
key: The key that was pressed (e.g., "W", "Up", "Space")
action: Either "start" (key pressed) or "end" (key released)
"""
# Only respond to key press, not release
if action != "start":
return
# Get current player position
px, py = int(player.x), int(player.y)
# Calculate new position based on key
if key == "W" or key == "Up":
py -= 1 # Up decreases Y
elif key == "S" or key == "Down":
py += 1 # Down increases Y
elif key == "A" or key == "Left":
px -= 1 # Left decreases X
elif key == "D" or key == "Right":
px += 1 # Right increases X
elif key == "Escape":
mcrfpy.exit()
return
# Update player position
player.x = px
player.y = py
# Update the position display
pos_display.text = f"Player Position: ({player.x}, {player.y})"
# Set the key handler on the scene
# This is the preferred approach - works on ANY scene, not just the active one
scene.on_key = handle_keys
# Activate the scene
scene.activate()
print("Part 1 loaded! Use WASD or Arrow keys to move.")

View file

@ -1,206 +0,0 @@
"""McRogueFace - Part 2: Walls, Floors, and Collision
Documentation: https://mcrogueface.github.io/tutorial/part_02_tiles_collision
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py
This code is extracted from the McRogueFace documentation and can be
run directly with: ./mcrogueface path/to/this/file.py
"""
import mcrfpy
# =============================================================================
# Constants
# =============================================================================
# Sprite indices for CP437 tileset
SPRITE_WALL = 35 # '#' - wall
SPRITE_FLOOR = 46 # '.' - floor
SPRITE_PLAYER = 64 # '@' - player
# Grid dimensions
GRID_WIDTH = 30
GRID_HEIGHT = 20
# =============================================================================
# Map Creation
# =============================================================================
def create_map(grid: mcrfpy.Grid) -> None:
"""Fill the grid with walls and floors.
Creates a simple room with walls around the edges and floor in the middle.
"""
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
# Place walls around the edges
if x == 0 or x == GRID_WIDTH - 1 or y == 0 or y == GRID_HEIGHT - 1:
cell.tilesprite = SPRITE_WALL
cell.walkable = False
else:
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
# Add some interior walls to make it interesting
# Vertical wall
for y in range(5, 15):
cell = grid.at(10, y)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
# Horizontal wall
for x in range(15, 25):
cell = grid.at(x, 10)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
# Leave gaps for doorways
grid.at(10, 10).tilesprite = SPRITE_FLOOR
grid.at(10, 10).walkable = True
grid.at(20, 10).tilesprite = SPRITE_FLOOR
grid.at(20, 10).walkable = True
# =============================================================================
# Collision Detection
# =============================================================================
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
"""Check if a position is valid for movement.
Args:
grid: The game grid
x: Target X coordinate (in tiles)
y: Target Y coordinate (in tiles)
Returns:
True if the position is walkable, False otherwise
"""
# Check grid bounds first
if x < 0 or x >= GRID_WIDTH:
return False
if y < 0 or y >= GRID_HEIGHT:
return False
# Check if the tile is walkable
cell = grid.at(x, y)
return cell.walkable
# =============================================================================
# Game Setup
# =============================================================================
# Create the scene
scene = mcrfpy.Scene("game")
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
grid = mcrfpy.Grid(
pos=(80, 100),
size=(720, 480),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture,
zoom=1.5
)
# Build the map
create_map(grid)
# Create the player in the center of the left room
player = mcrfpy.Entity(
grid_pos=(5, 10),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
# Add grid to scene
scene.children.append(grid)
# =============================================================================
# UI Elements
# =============================================================================
title = mcrfpy.Caption(
pos=(80, 20),
text="Part 2: Walls and Collision"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 24
scene.children.append(title)
instructions = mcrfpy.Caption(
pos=(80, 55),
text="WASD or Arrow Keys to move | Walls block movement"
)
instructions.fill_color = mcrfpy.Color(180, 180, 180)
instructions.font_size = 16
scene.children.append(instructions)
pos_display = mcrfpy.Caption(
pos=(80, 600),
text=f"Position: ({int(player.x)}, {int(player.y)})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
status_display = mcrfpy.Caption(
pos=(400, 600),
text="Status: Ready"
)
status_display.fill_color = mcrfpy.Color(100, 200, 100)
status_display.font_size = 16
scene.children.append(status_display)
# =============================================================================
# Input Handling
# =============================================================================
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input with collision detection."""
if action != "start":
return
# Get current position
px, py = int(player.x), int(player.y)
# Calculate intended new position
new_x, new_y = px, py
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
elif key == "Escape":
mcrfpy.exit()
return
else:
return # Ignore other keys
# Check collision before moving
if can_move_to(grid, new_x, new_y):
player.x = new_x
player.y = new_y
pos_display.text = f"Position: ({new_x}, {new_y})"
status_display.text = "Status: Moved"
status_display.fill_color = mcrfpy.Color(100, 200, 100)
else:
status_display.text = "Status: Blocked!"
status_display.fill_color = mcrfpy.Color(200, 100, 100)
scene.on_key = handle_keys
# =============================================================================
# Start the Game
# =============================================================================
scene.activate()
print("Part 2 loaded! Try walking into walls.")

View file

@ -1,356 +0,0 @@
"""McRogueFace - Part 3: Procedural Dungeon Generation
Documentation: https://mcrogueface.github.io/tutorial/part_03_dungeon_generation
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.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
# =============================================================================
# Constants
# =============================================================================
# Sprite indices for CP437 tileset
SPRITE_WALL = 35 # '#' - wall
SPRITE_FLOOR = 46 # '.' - floor
SPRITE_PLAYER = 64 # '@' - player
# Grid dimensions
GRID_WIDTH = 50
GRID_HEIGHT = 35
# Room generation parameters
ROOM_MIN_SIZE = 6
ROOM_MAX_SIZE = 12
MAX_ROOMS = 8
# =============================================================================
# Room Class
# =============================================================================
class RectangularRoom:
"""A rectangular room with its position and size."""
def __init__(self, x: int, y: int, width: int, height: int):
"""Create a new room.
Args:
x: Left edge X coordinate
y: Top edge Y coordinate
width: Room width in tiles
height: Room height in tiles
"""
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self) -> tuple[int, int]:
"""Return the center coordinates of the room."""
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self) -> tuple[slice, slice]:
"""Return the inner area of the room (excluding walls).
The inner area is one tile smaller on each side to leave room
for walls between adjacent rooms.
"""
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
def intersects(self, other: "RectangularRoom") -> bool:
"""Check if this room overlaps with another room.
Args:
other: Another RectangularRoom to check against
Returns:
True if the rooms overlap, False otherwise
"""
return (
self.x1 <= other.x2 and
self.x2 >= other.x1 and
self.y1 <= other.y2 and
self.y2 >= other.y1
)
# =============================================================================
# Dungeon Generation
# =============================================================================
def fill_with_walls(grid: mcrfpy.Grid) -> None:
"""Fill the entire grid with wall tiles."""
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
cell.transparent = False
def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None:
"""Carve out a room by setting its inner tiles to floor.
Args:
grid: The game grid
room: The room to carve
"""
inner_x, inner_y = room.inner
for y in range(inner_y.start, inner_y.stop):
for x in range(inner_x.start, inner_x.stop):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
"""Carve a horizontal tunnel.
Args:
grid: The game grid
x1: Starting X coordinate
x2: Ending X coordinate
y: Y coordinate of the tunnel
"""
start_x = min(x1, x2)
end_x = max(x1, x2)
for x in range(start_x, end_x + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
"""Carve a vertical tunnel.
Args:
grid: The game grid
y1: Starting Y coordinate
y2: Ending Y coordinate
x: X coordinate of the tunnel
"""
start_y = min(y1, y2)
end_y = max(y1, y2)
for y in range(start_y, end_y + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_l_tunnel(
grid: mcrfpy.Grid,
start: tuple[int, int],
end: tuple[int, int]
) -> None:
"""Carve an L-shaped tunnel between two points.
Randomly chooses to go horizontal-then-vertical or vertical-then-horizontal.
Args:
grid: The game grid
start: Starting (x, y) coordinates
end: Ending (x, y) coordinates
"""
x1, y1 = start
x2, y2 = end
# Randomly choose whether to go horizontal or vertical first
if random.random() < 0.5:
# Horizontal first, then vertical
carve_tunnel_horizontal(grid, x1, x2, y1)
carve_tunnel_vertical(grid, y1, y2, x2)
else:
# Vertical first, then horizontal
carve_tunnel_vertical(grid, y1, y2, x1)
carve_tunnel_horizontal(grid, x1, x2, y2)
def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
"""Generate a dungeon with rooms and tunnels.
Args:
grid: The game grid to generate the dungeon in
Returns:
The (x, y) coordinates where the player should start
"""
# Start with all walls
fill_with_walls(grid)
rooms: list[RectangularRoom] = []
for _ in range(MAX_ROOMS):
# Random room dimensions
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
# Random position (leaving 1-tile border)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
# Check for overlap with existing rooms
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue # Skip this room, try another
# No overlap - carve out the room
carve_room(grid, new_room)
# Connect to previous room with a tunnel
if rooms:
# Tunnel from this room's center to the previous room's center
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
# Return the center of the first room as the player start position
if rooms:
return rooms[0].center
else:
# Fallback if no rooms were generated
return GRID_WIDTH // 2, GRID_HEIGHT // 2
# =============================================================================
# Collision Detection
# =============================================================================
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
"""Check if a position is valid for movement."""
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
return False
return grid.at(x, y).walkable
# =============================================================================
# Game Setup
# =============================================================================
# Create the scene
scene = mcrfpy.Scene("game")
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
grid = mcrfpy.Grid(
pos=(50, 80),
size=(800, 560),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture,
zoom=1.0
)
# Generate the dungeon and get player start position
player_start_x, player_start_y = generate_dungeon(grid)
# Create the player at the starting position
player = mcrfpy.Entity(
grid_pos=(player_start_x, player_start_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
# Add grid to scene
scene.children.append(grid)
# =============================================================================
# UI Elements
# =============================================================================
title = mcrfpy.Caption(
pos=(50, 15),
text="Part 3: Procedural Dungeon Generation"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 24
scene.children.append(title)
instructions = mcrfpy.Caption(
pos=(50, 50),
text="WASD/Arrows: Move | R: Regenerate dungeon | Escape: Quit"
)
instructions.fill_color = mcrfpy.Color(180, 180, 180)
instructions.font_size = 16
scene.children.append(instructions)
pos_display = mcrfpy.Caption(
pos=(50, 660),
text=f"Position: ({int(player.x)}, {int(player.y)})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
room_display = mcrfpy.Caption(
pos=(400, 660),
text="Press R to regenerate the dungeon"
)
room_display.fill_color = mcrfpy.Color(100, 200, 100)
room_display.font_size = 16
scene.children.append(room_display)
# =============================================================================
# Input Handling
# =============================================================================
def regenerate_dungeon() -> None:
"""Generate a new dungeon and reposition the player."""
new_x, new_y = generate_dungeon(grid)
player.x = new_x
player.y = new_y
pos_display.text = f"Position: ({new_x}, {new_y})"
room_display.text = "New dungeon generated!"
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input."""
if action != "start":
return
px, py = int(player.x), int(player.y)
new_x, new_y = px, py
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
elif key == "R":
regenerate_dungeon()
return
elif key == "Escape":
mcrfpy.exit()
return
else:
return
if can_move_to(grid, new_x, new_y):
player.x = new_x
player.y = new_y
pos_display.text = f"Position: ({new_x}, {new_y})"
scene.on_key = handle_keys
# =============================================================================
# Start the Game
# =============================================================================
scene.activate()
print("Part 3 loaded! Explore the dungeon or press R to regenerate.")

View file

@ -1,363 +0,0 @@
"""McRogueFace - Part 4: Field of View
Documentation: https://mcrogueface.github.io/tutorial/part_04_fov
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_04_fov/part_04_fov.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
# =============================================================================
# Constants
# =============================================================================
# Sprite indices for CP437 tileset
SPRITE_WALL = 35 # '#' - wall
SPRITE_FLOOR = 46 # '.' - floor
SPRITE_PLAYER = 64 # '@' - player
# Grid dimensions
GRID_WIDTH = 50
GRID_HEIGHT = 35
# Room generation parameters
ROOM_MIN_SIZE = 6
ROOM_MAX_SIZE = 12
MAX_ROOMS = 8
# FOV settings
FOV_RADIUS = 8
# Visibility colors (applied as overlays)
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) # Fully transparent - show tile
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) # Dark blue tint - dimmed
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) # Solid black - hidden
# =============================================================================
# Room Class (from Part 3)
# =============================================================================
class RectangularRoom:
"""A rectangular room with its position and size."""
def __init__(self, x: int, y: int, width: int, height: int):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self) -> tuple[int, int]:
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self) -> tuple[slice, slice]:
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
def intersects(self, other: "RectangularRoom") -> bool:
return (
self.x1 <= other.x2 and
self.x2 >= other.x1 and
self.y1 <= other.y2 and
self.y2 >= other.y1
)
# =============================================================================
# Exploration Tracking
# =============================================================================
# Track which tiles have been discovered (seen at least once)
explored: list[list[bool]] = []
def init_explored() -> None:
"""Initialize the explored array to all False."""
global explored
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
def mark_explored(x: int, y: int) -> None:
"""Mark a tile as explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
explored[y][x] = True
def is_explored(x: int, y: int) -> bool:
"""Check if a tile has been explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
return explored[y][x]
return False
# =============================================================================
# Dungeon Generation (from Part 3, with transparent property)
# =============================================================================
def fill_with_walls(grid: mcrfpy.Grid) -> None:
"""Fill the entire grid with wall tiles."""
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
cell.transparent = False # Walls block line of sight
def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None:
"""Carve out a room by setting its inner tiles to floor."""
inner_x, inner_y = room.inner
for y in range(inner_y.start, inner_y.stop):
for x in range(inner_x.start, inner_x.stop):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True # Floors allow line of sight
def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
"""Carve a horizontal tunnel."""
for x in range(min(x1, x2), max(x1, x2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
"""Carve a vertical tunnel."""
for y in range(min(y1, y2), max(y1, y2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_l_tunnel(
grid: mcrfpy.Grid,
start: tuple[int, int],
end: tuple[int, int]
) -> None:
"""Carve an L-shaped tunnel between two points."""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
carve_tunnel_horizontal(grid, x1, x2, y1)
carve_tunnel_vertical(grid, y1, y2, x2)
else:
carve_tunnel_vertical(grid, y1, y2, x1)
carve_tunnel_horizontal(grid, x1, x2, y2)
def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
"""Generate a dungeon with rooms and tunnels."""
fill_with_walls(grid)
init_explored() # Reset exploration when generating new dungeon
rooms: list[RectangularRoom] = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(grid, new_room)
if rooms:
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
if rooms:
return rooms[0].center
return GRID_WIDTH // 2, GRID_HEIGHT // 2
# =============================================================================
# Field of View
# =============================================================================
def update_fov(grid: mcrfpy.Grid, fov_layer, player_x: int, player_y: int) -> None:
"""Update the field of view visualization.
Args:
grid: The game grid
fov_layer: The ColorLayer for FOV visualization
player_x: Player's X position
player_y: Player's Y position
"""
# Compute FOV from player position
grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
# Update each tile's visibility
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if grid.is_in_fov(x, y):
# Currently visible - mark as explored and show clearly
mark_explored(x, y)
fov_layer.set(x, y, COLOR_VISIBLE)
elif is_explored(x, y):
# Previously seen but not currently visible - show dimmed
fov_layer.set(x, y, COLOR_DISCOVERED)
else:
# Never seen - hide completely
fov_layer.set(x, y, COLOR_UNKNOWN)
# =============================================================================
# Collision Detection
# =============================================================================
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
"""Check if a position is valid for movement."""
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
return False
return grid.at(x, y).walkable
# =============================================================================
# Game Setup
# =============================================================================
# Create the scene
scene = mcrfpy.Scene("game")
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
grid = mcrfpy.Grid(
pos=(50, 80),
size=(800, 560),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture,
zoom=1.0
)
# Generate the dungeon
player_start_x, player_start_y = generate_dungeon(grid)
# Add a color layer for FOV visualization (below entities)
fov_layer = grid.add_layer("color", z_index=-1)
# Initialize the FOV layer to all black (unknown)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Create the player
player = mcrfpy.Entity(
grid_pos=(player_start_x, player_start_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
# Calculate initial FOV
update_fov(grid, fov_layer, player_start_x, player_start_y)
# Add grid to scene
scene.children.append(grid)
# =============================================================================
# UI Elements
# =============================================================================
title = mcrfpy.Caption(
pos=(50, 15),
text="Part 4: Field of View"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 24
scene.children.append(title)
instructions = mcrfpy.Caption(
pos=(50, 50),
text="WASD/Arrows: Move | R: Regenerate | Escape: Quit"
)
instructions.fill_color = mcrfpy.Color(180, 180, 180)
instructions.font_size = 16
scene.children.append(instructions)
pos_display = mcrfpy.Caption(
pos=(50, 660),
text=f"Position: ({int(player.x)}, {int(player.y)})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
fov_display = mcrfpy.Caption(
pos=(400, 660),
text=f"FOV Radius: {FOV_RADIUS}"
)
fov_display.fill_color = mcrfpy.Color(100, 200, 100)
fov_display.font_size = 16
scene.children.append(fov_display)
# =============================================================================
# Input Handling
# =============================================================================
def regenerate_dungeon() -> None:
"""Generate a new dungeon and reposition the player."""
new_x, new_y = generate_dungeon(grid)
player.x = new_x
player.y = new_y
# Reset FOV layer to unknown
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Calculate new FOV
update_fov(grid, fov_layer, new_x, new_y)
pos_display.text = f"Position: ({new_x}, {new_y})"
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input."""
if action != "start":
return
px, py = int(player.x), int(player.y)
new_x, new_y = px, py
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
elif key == "R":
regenerate_dungeon()
return
elif key == "Escape":
mcrfpy.exit()
return
else:
return
if can_move_to(grid, new_x, new_y):
player.x = new_x
player.y = new_y
pos_display.text = f"Position: ({new_x}, {new_y})"
# Update FOV after movement
update_fov(grid, fov_layer, new_x, new_y)
scene.on_key = handle_keys
# =============================================================================
# Start the Game
# =============================================================================
scene.activate()
print("Part 4 loaded! Explore the dungeon - watch the fog of war!")

View file

@ -1,685 +0,0 @@
"""McRogueFace - Part 5: Placing Enemies
Documentation: https://mcrogueface.github.io/tutorial/part_05_enemies
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_05_enemies/part_05_enemies.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
# =============================================================================
# Constants
# =============================================================================
# Sprite indices for CP437 tileset
SPRITE_WALL = 35 # '#' - wall
SPRITE_FLOOR = 46 # '.' - floor
SPRITE_PLAYER = 64 # '@' - player
# Enemy sprites (lowercase letters in CP437)
SPRITE_GOBLIN = 103 # 'g'
SPRITE_ORC = 111 # 'o'
SPRITE_TROLL = 116 # 't'
# Grid dimensions
GRID_WIDTH = 50
GRID_HEIGHT = 35
# Room generation parameters
ROOM_MIN_SIZE = 6
ROOM_MAX_SIZE = 12
MAX_ROOMS = 8
# Enemy spawn parameters
MAX_ENEMIES_PER_ROOM = 3
# FOV settings
FOV_RADIUS = 8
# Visibility colors
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180)
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
# =============================================================================
# Enemy Data
# =============================================================================
# Enemy templates - stats for each enemy type
ENEMY_TEMPLATES = {
"goblin": {
"sprite": SPRITE_GOBLIN,
"hp": 6,
"max_hp": 6,
"attack": 3,
"defense": 0,
"color": mcrfpy.Color(100, 200, 100) # Greenish
},
"orc": {
"sprite": SPRITE_ORC,
"hp": 10,
"max_hp": 10,
"attack": 4,
"defense": 1,
"color": mcrfpy.Color(100, 150, 100) # Darker green
},
"troll": {
"sprite": SPRITE_TROLL,
"hp": 16,
"max_hp": 16,
"attack": 6,
"defense": 2,
"color": mcrfpy.Color(50, 150, 50) # Dark green
}
}
# Global storage for entity data
# Maps entity objects to their data dictionaries
entity_data: dict = {}
# Global references
player = None
grid = None
fov_layer = None
# =============================================================================
# Room Class (from Part 3)
# =============================================================================
class RectangularRoom:
"""A rectangular room with its position and size."""
def __init__(self, x: int, y: int, width: int, height: int):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self) -> tuple[int, int]:
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self) -> tuple[slice, slice]:
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
def intersects(self, other: "RectangularRoom") -> bool:
return (
self.x1 <= other.x2 and
self.x2 >= other.x1 and
self.y1 <= other.y2 and
self.y2 >= other.y1
)
# =============================================================================
# Exploration Tracking (from Part 4)
# =============================================================================
explored: list[list[bool]] = []
def init_explored() -> None:
"""Initialize the explored array to all False."""
global explored
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
def mark_explored(x: int, y: int) -> None:
"""Mark a tile as explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
explored[y][x] = True
def is_explored(x: int, y: int) -> bool:
"""Check if a tile has been explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
return explored[y][x]
return False
# =============================================================================
# Dungeon Generation (from Part 4)
# =============================================================================
def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
"""Fill the entire grid with wall tiles."""
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
cell.transparent = False
def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None:
"""Carve out a room by setting its inner tiles to floor."""
inner_x, inner_y = room.inner
for y in range(inner_y.start, inner_y.stop):
for x in range(inner_x.start, inner_x.stop):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
"""Carve a horizontal tunnel."""
for x in range(min(x1, x2), max(x1, x2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
"""Carve a vertical tunnel."""
for y in range(min(y1, y2), max(y1, y2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_l_tunnel(
target_grid: mcrfpy.Grid,
start: tuple[int, int],
end: tuple[int, int]
) -> None:
"""Carve an L-shaped tunnel between two points."""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
carve_tunnel_horizontal(target_grid, x1, x2, y1)
carve_tunnel_vertical(target_grid, y1, y2, x2)
else:
carve_tunnel_vertical(target_grid, y1, y2, x1)
carve_tunnel_horizontal(target_grid, x1, x2, y2)
# =============================================================================
# Enemy Management
# =============================================================================
def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, texture: mcrfpy.Texture) -> mcrfpy.Entity:
"""Spawn an enemy at the given position.
Args:
target_grid: The game grid
x: X position in tiles
y: Y position in tiles
enemy_type: Type of enemy ("goblin", "orc", or "troll")
texture: The texture to use for the sprite
Returns:
The created enemy Entity
"""
template = ENEMY_TEMPLATES[enemy_type]
enemy = mcrfpy.Entity(
grid_pos=(x, y),
texture=texture,
sprite_index=template["sprite"]
)
# Start hidden until player sees them
enemy.visible = False
# Add to grid
target_grid.entities.append(enemy)
# Store enemy data
entity_data[enemy] = {
"type": enemy_type,
"name": enemy_type.capitalize(),
"hp": template["hp"],
"max_hp": template["max_hp"],
"attack": template["attack"],
"defense": template["defense"],
"is_player": False
}
return enemy
def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, texture: mcrfpy.Texture) -> None:
"""Spawn random enemies in a room.
Args:
target_grid: The game grid
room: The room to spawn enemies in
texture: The texture to use for sprites
"""
# Random number of enemies (0 to MAX_ENEMIES_PER_ROOM)
num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM)
for _ in range(num_enemies):
# Random position within the room's inner area
inner_x, inner_y = room.inner
x = random.randint(inner_x.start, inner_x.stop - 1)
y = random.randint(inner_y.start, inner_y.stop - 1)
# Check if position is already occupied
if get_blocking_entity_at(target_grid, x, y) is not None:
continue # Skip this spawn attempt
# Choose enemy type based on weighted random
roll = random.random()
if roll < 0.6:
enemy_type = "goblin" # 60% chance
elif roll < 0.9:
enemy_type = "orc" # 30% chance
else:
enemy_type = "troll" # 10% chance
spawn_enemy(target_grid, x, y, enemy_type, texture)
def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> mcrfpy.Entity | None:
"""Get any entity that blocks movement at the given position.
Args:
target_grid: The game grid
x: X position to check
y: Y position to check
Returns:
The blocking entity, or None if no entity blocks this position
"""
for entity in target_grid.entities:
if int(entity.x) == x and int(entity.y) == y:
return entity
return None
def clear_enemies(target_grid: mcrfpy.Grid) -> None:
"""Remove all enemies from the grid."""
global entity_data
# Get list of enemies to remove (not the player)
enemies_to_remove = []
for entity in target_grid.entities:
if entity in entity_data and not entity_data[entity].get("is_player", False):
enemies_to_remove.append(entity)
# Remove from grid and entity_data
for enemy in enemies_to_remove:
# Find and remove from grid.entities
for i, e in enumerate(target_grid.entities):
if e == enemy:
target_grid.entities.remove(i)
break
# Remove from entity_data
if enemy in entity_data:
del entity_data[enemy]
# =============================================================================
# Entity Visibility
# =============================================================================
def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
"""Update visibility of all entities based on FOV.
Entities outside the player's field of view are hidden.
"""
global player
for entity in target_grid.entities:
# Player is always visible
if entity == player:
entity.visible = True
continue
# Other entities are only visible if in FOV
ex, ey = int(entity.x), int(entity.y)
entity.visible = target_grid.is_in_fov(ex, ey)
# =============================================================================
# Field of View (from Part 4)
# =============================================================================
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
"""Update the field of view visualization."""
# Compute FOV from player position
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
# Update each tile's visibility
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if target_grid.is_in_fov(x, y):
mark_explored(x, y)
target_fov_layer.set(x, y, COLOR_VISIBLE)
elif is_explored(x, y):
target_fov_layer.set(x, y, COLOR_DISCOVERED)
else:
target_fov_layer.set(x, y, COLOR_UNKNOWN)
# Update entity visibility
update_entity_visibility(target_grid)
# =============================================================================
# Collision Detection
# =============================================================================
def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int) -> bool:
"""Check if a position is valid for movement.
A position is valid if:
1. It is within grid bounds
2. The tile is walkable
3. No entity is blocking it
"""
# Check bounds
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
return False
# Check tile walkability
if not target_grid.at(x, y).walkable:
return False
# Check for blocking entities
if get_blocking_entity_at(target_grid, x, y) is not None:
return False
return True
# =============================================================================
# Dungeon Generation with Enemies
# =============================================================================
def generate_dungeon(target_grid: mcrfpy.Grid, texture: mcrfpy.Texture) -> tuple[int, int]:
"""Generate a dungeon with rooms, tunnels, and enemies.
Args:
target_grid: The game grid
texture: The texture for entity sprites
Returns:
The (x, y) coordinates where the player should start
"""
# Clear any existing enemies
clear_enemies(target_grid)
# Fill with walls
fill_with_walls(target_grid)
init_explored()
rooms: list[RectangularRoom] = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(target_grid, new_room)
if rooms:
carve_l_tunnel(target_grid, new_room.center, rooms[-1].center)
# Spawn enemies in all rooms except the first (player starting room)
spawn_enemies_in_room(target_grid, new_room, texture)
rooms.append(new_room)
if rooms:
return rooms[0].center
return GRID_WIDTH // 2, GRID_HEIGHT // 2
# =============================================================================
# Game Setup
# =============================================================================
# Create the scene
scene = mcrfpy.Scene("game")
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
grid = mcrfpy.Grid(
pos=(50, 80),
size=(800, 560),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture,
zoom=1.0
)
# Generate the dungeon (without player first to get starting position)
fill_with_walls(grid)
init_explored()
rooms: list[RectangularRoom] = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(grid, new_room)
if rooms:
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
# Get player starting position
if rooms:
player_start_x, player_start_y = rooms[0].center
else:
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
# Add FOV layer
fov_layer = grid.add_layer("color", z_index=-1)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Create the player
player = mcrfpy.Entity(
grid_pos=(player_start_x, player_start_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
# Store player data
entity_data[player] = {
"type": "player",
"name": "Player",
"hp": 30,
"max_hp": 30,
"attack": 5,
"defense": 2,
"is_player": True
}
# Now spawn enemies in rooms (except the first one)
for i, room in enumerate(rooms):
if i == 0:
continue # Skip player's starting room
spawn_enemies_in_room(grid, room, texture)
# Calculate initial FOV
update_fov(grid, fov_layer, player_start_x, player_start_y)
# Add grid to scene
scene.children.append(grid)
# =============================================================================
# UI Elements
# =============================================================================
title = mcrfpy.Caption(
pos=(50, 15),
text="Part 5: Placing Enemies"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 24
scene.children.append(title)
instructions = mcrfpy.Caption(
pos=(50, 50),
text="WASD/Arrows: Move | R: Regenerate | Escape: Quit"
)
instructions.fill_color = mcrfpy.Color(180, 180, 180)
instructions.font_size = 16
scene.children.append(instructions)
pos_display = mcrfpy.Caption(
pos=(50, 660),
text=f"Position: ({int(player.x)}, {int(player.y)})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
status_display = mcrfpy.Caption(
pos=(400, 660),
text="Explore the dungeon..."
)
status_display.fill_color = mcrfpy.Color(100, 200, 100)
status_display.font_size = 16
scene.children.append(status_display)
# =============================================================================
# Input Handling
# =============================================================================
def regenerate_dungeon() -> None:
"""Generate a new dungeon and reposition the player."""
global player, grid, fov_layer, rooms
# Clear enemies
clear_enemies(grid)
# Regenerate dungeon structure
fill_with_walls(grid)
init_explored()
rooms = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(grid, new_room)
if rooms:
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
# Reposition player
if rooms:
new_x, new_y = rooms[0].center
else:
new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
player.x = new_x
player.y = new_y
# Spawn new enemies
for i, room in enumerate(rooms):
if i == 0:
continue
spawn_enemies_in_room(grid, room, texture)
# Reset FOV layer
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Update FOV
update_fov(grid, fov_layer, new_x, new_y)
pos_display.text = f"Position: ({new_x}, {new_y})"
status_display.text = "New dungeon generated!"
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input."""
global player, grid, fov_layer
if action != "start":
return
px, py = int(player.x), int(player.y)
new_x, new_y = px, py
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
elif key == "R":
regenerate_dungeon()
return
elif key == "Escape":
mcrfpy.exit()
return
else:
return
# Check for blocking entity (potential combat target)
blocker = get_blocking_entity_at(grid, new_x, new_y)
if blocker is not None and blocker != player:
# For now, just report that we bumped into an enemy
if blocker in entity_data:
enemy_name = entity_data[blocker]["name"]
status_display.text = f"A {enemy_name} blocks your path!"
status_display.fill_color = mcrfpy.Color(200, 150, 100)
return
# Check if we can move
if can_move_to(grid, new_x, new_y):
player.x = new_x
player.y = new_y
pos_display.text = f"Position: ({new_x}, {new_y})"
status_display.text = "Exploring..."
status_display.fill_color = mcrfpy.Color(100, 200, 100)
# Update FOV after movement
update_fov(grid, fov_layer, new_x, new_y)
scene.on_key = handle_keys
# =============================================================================
# Start the Game
# =============================================================================
scene.activate()
print("Part 5 loaded! Enemies lurk in the dungeon...")

View file

@ -1,940 +0,0 @@
"""McRogueFace - Part 6: Combat System
Documentation: https://mcrogueface.github.io/tutorial/part_06_combat
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_06_combat/part_06_combat.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
from dataclasses import dataclass
from typing import Optional
# =============================================================================
# Constants
# =============================================================================
# Sprite indices for CP437 tileset
SPRITE_WALL = 35 # '#' - wall
SPRITE_FLOOR = 46 # '.' - floor
SPRITE_PLAYER = 64 # '@' - player
SPRITE_CORPSE = 37 # '%' - remains
# Enemy sprites
SPRITE_GOBLIN = 103 # 'g'
SPRITE_ORC = 111 # 'o'
SPRITE_TROLL = 116 # 't'
# Grid dimensions
GRID_WIDTH = 50
GRID_HEIGHT = 35
# Room generation parameters
ROOM_MIN_SIZE = 6
ROOM_MAX_SIZE = 12
MAX_ROOMS = 8
# Enemy spawn parameters
MAX_ENEMIES_PER_ROOM = 3
# FOV settings
FOV_RADIUS = 8
# Visibility colors
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180)
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
# Message log settings
MAX_MESSAGES = 5
# =============================================================================
# Fighter Component
# =============================================================================
@dataclass
class Fighter:
"""Combat stats for an entity."""
hp: int
max_hp: int
attack: int
defense: int
name: str
is_player: bool = False
@property
def is_alive(self) -> bool:
"""Check if this fighter is still alive."""
return self.hp > 0
def take_damage(self, amount: int) -> int:
"""Apply damage and return actual damage taken."""
actual_damage = min(self.hp, amount)
self.hp -= actual_damage
return actual_damage
def heal(self, amount: int) -> int:
"""Heal and return actual amount healed."""
actual_heal = min(self.max_hp - self.hp, amount)
self.hp += actual_heal
return actual_heal
# =============================================================================
# Enemy Templates
# =============================================================================
ENEMY_TEMPLATES = {
"goblin": {
"sprite": SPRITE_GOBLIN,
"hp": 6,
"attack": 3,
"defense": 0,
"color": mcrfpy.Color(100, 200, 100)
},
"orc": {
"sprite": SPRITE_ORC,
"hp": 10,
"attack": 4,
"defense": 1,
"color": mcrfpy.Color(100, 150, 100)
},
"troll": {
"sprite": SPRITE_TROLL,
"hp": 16,
"attack": 6,
"defense": 2,
"color": mcrfpy.Color(50, 150, 50)
}
}
# =============================================================================
# Global State
# =============================================================================
# Entity data storage
entity_data: dict[mcrfpy.Entity, Fighter] = {}
# Global references
player: Optional[mcrfpy.Entity] = None
grid: Optional[mcrfpy.Grid] = None
fov_layer = None
texture: Optional[mcrfpy.Texture] = None
# Game state
game_over: bool = False
# Message log
messages: list[tuple[str, mcrfpy.Color]] = []
# =============================================================================
# Room Class
# =============================================================================
class RectangularRoom:
"""A rectangular room with its position and size."""
def __init__(self, x: int, y: int, width: int, height: int):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self) -> tuple[int, int]:
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self) -> tuple[slice, slice]:
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
def intersects(self, other: "RectangularRoom") -> bool:
return (
self.x1 <= other.x2 and
self.x2 >= other.x1 and
self.y1 <= other.y2 and
self.y2 >= other.y1
)
# =============================================================================
# Exploration Tracking
# =============================================================================
explored: list[list[bool]] = []
def init_explored() -> None:
"""Initialize the explored array to all False."""
global explored
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
def mark_explored(x: int, y: int) -> None:
"""Mark a tile as explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
explored[y][x] = True
def is_explored(x: int, y: int) -> bool:
"""Check if a tile has been explored."""
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
return explored[y][x]
return False
# =============================================================================
# Message Log
# =============================================================================
def add_message(text: str, color: mcrfpy.Color = None) -> None:
"""Add a message to the log.
Args:
text: The message text
color: Optional color (defaults to white)
"""
if color is None:
color = mcrfpy.Color(255, 255, 255)
messages.append((text, color))
# Keep only the most recent messages
while len(messages) > MAX_MESSAGES:
messages.pop(0)
# Update the message display
update_message_display()
def update_message_display() -> None:
"""Update the message log UI."""
if message_log_caption is None:
return
# Combine messages into a single string
lines = []
for text, color in messages:
lines.append(text)
message_log_caption.text = "\n".join(lines)
def clear_messages() -> None:
"""Clear all messages."""
global messages
messages = []
update_message_display()
# =============================================================================
# Dungeon Generation
# =============================================================================
def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
"""Fill the entire grid with wall tiles."""
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_WALL
cell.walkable = False
cell.transparent = False
def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None:
"""Carve out a room by setting its inner tiles to floor."""
inner_x, inner_y = room.inner
for y in range(inner_y.start, inner_y.stop):
for x in range(inner_x.start, inner_x.stop):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
"""Carve a horizontal tunnel."""
for x in range(min(x1, x2), max(x1, x2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
"""Carve a vertical tunnel."""
for y in range(min(y1, y2), max(y1, y2) + 1):
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
cell = target_grid.at(x, y)
cell.tilesprite = SPRITE_FLOOR
cell.walkable = True
cell.transparent = True
def carve_l_tunnel(
target_grid: mcrfpy.Grid,
start: tuple[int, int],
end: tuple[int, int]
) -> None:
"""Carve an L-shaped tunnel between two points."""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
carve_tunnel_horizontal(target_grid, x1, x2, y1)
carve_tunnel_vertical(target_grid, y1, y2, x2)
else:
carve_tunnel_vertical(target_grid, y1, y2, x1)
carve_tunnel_horizontal(target_grid, x1, x2, y2)
# =============================================================================
# Entity Management
# =============================================================================
def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
"""Spawn an enemy at the given position."""
template = ENEMY_TEMPLATES[enemy_type]
enemy = mcrfpy.Entity(
grid_pos=(x, y),
texture=tex,
sprite_index=template["sprite"]
)
enemy.visible = False
target_grid.entities.append(enemy)
# Create Fighter component for this enemy
entity_data[enemy] = Fighter(
hp=template["hp"],
max_hp=template["hp"],
attack=template["attack"],
defense=template["defense"],
name=enemy_type.capitalize(),
is_player=False
)
return enemy
def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None:
"""Spawn random enemies in a room."""
num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM)
for _ in range(num_enemies):
inner_x, inner_y = room.inner
x = random.randint(inner_x.start, inner_x.stop - 1)
y = random.randint(inner_y.start, inner_y.stop - 1)
if get_entity_at(target_grid, x, y) is not None:
continue
roll = random.random()
if roll < 0.6:
enemy_type = "goblin"
elif roll < 0.9:
enemy_type = "orc"
else:
enemy_type = "troll"
spawn_enemy(target_grid, x, y, enemy_type, tex)
def get_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]:
"""Get any entity at the given position."""
for entity in target_grid.entities:
if int(entity.x) == x and int(entity.y) == y:
# Check if this entity is alive (or is a non-Fighter entity)
if entity in entity_data:
if entity_data[entity].is_alive:
return entity
else:
return entity
return None
def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]:
"""Get any living entity that blocks movement at the given position."""
for entity in target_grid.entities:
if entity == exclude:
continue
if int(entity.x) == x and int(entity.y) == y:
if entity in entity_data and entity_data[entity].is_alive:
return entity
return None
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
"""Remove an entity from the grid and data storage."""
# Find and remove from grid
for i, e in enumerate(target_grid.entities):
if e == entity:
target_grid.entities.remove(i)
break
# Remove from entity data
if entity in entity_data:
del entity_data[entity]
def clear_enemies(target_grid: mcrfpy.Grid) -> None:
"""Remove all enemies from the grid."""
enemies_to_remove = []
for entity in target_grid.entities:
if entity in entity_data and not entity_data[entity].is_player:
enemies_to_remove.append(entity)
for enemy in enemies_to_remove:
remove_entity(target_grid, enemy)
# =============================================================================
# Combat System
# =============================================================================
def calculate_damage(attacker: Fighter, defender: Fighter) -> int:
"""Calculate damage dealt from attacker to defender.
Args:
attacker: The attacking Fighter
defender: The defending Fighter
Returns:
The amount of damage to deal (minimum 0)
"""
damage = max(0, attacker.attack - defender.defense)
return damage
def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
"""Execute an attack from one entity to another.
Args:
attacker_entity: The entity performing the attack
defender_entity: The entity being attacked
"""
global game_over
attacker = entity_data.get(attacker_entity)
defender = entity_data.get(defender_entity)
if attacker is None or defender is None:
return
# Calculate and apply damage
damage = calculate_damage(attacker, defender)
defender.take_damage(damage)
# Generate combat message
if damage > 0:
if attacker.is_player:
add_message(
f"You hit the {defender.name} for {damage} damage!",
mcrfpy.Color(200, 200, 200)
)
else:
add_message(
f"The {attacker.name} hits you for {damage} damage!",
mcrfpy.Color(255, 150, 150)
)
else:
if attacker.is_player:
add_message(
f"You hit the {defender.name} but deal no damage.",
mcrfpy.Color(150, 150, 150)
)
else:
add_message(
f"The {attacker.name} hits you but deals no damage.",
mcrfpy.Color(150, 150, 200)
)
# Check for death
if not defender.is_alive:
handle_death(defender_entity, defender)
def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
"""Handle the death of an entity.
Args:
entity: The entity that died
fighter: The Fighter component of the dead entity
"""
global game_over, grid
if fighter.is_player:
# Player death
add_message("You have died!", mcrfpy.Color(255, 50, 50))
add_message("Press R to restart or Escape to quit.", mcrfpy.Color(200, 200, 200))
game_over = True
# Change player sprite to corpse
entity.sprite_index = SPRITE_CORPSE
else:
# Enemy death
add_message(f"The {fighter.name} dies!", mcrfpy.Color(100, 255, 100))
# Replace with corpse
entity.sprite_index = SPRITE_CORPSE
# Mark as dead (hp is already 0)
# Remove blocking but keep visual corpse
# Actually remove the entity and its data
remove_entity(grid, entity)
# Update HP display
update_hp_display()
# =============================================================================
# Field of View
# =============================================================================
def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
"""Update visibility of all entities based on FOV."""
global player
for entity in target_grid.entities:
if entity == player:
entity.visible = True
continue
ex, ey = int(entity.x), int(entity.y)
entity.visible = target_grid.is_in_fov(ex, ey)
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
"""Update the field of view visualization."""
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
if target_grid.is_in_fov(x, y):
mark_explored(x, y)
target_fov_layer.set(x, y, COLOR_VISIBLE)
elif is_explored(x, y):
target_fov_layer.set(x, y, COLOR_DISCOVERED)
else:
target_fov_layer.set(x, y, COLOR_UNKNOWN)
update_entity_visibility(target_grid)
# =============================================================================
# Movement and Actions
# =============================================================================
def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool:
"""Check if a position is valid for movement."""
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
return False
if not target_grid.at(x, y).walkable:
return False
blocker = get_blocking_entity_at(target_grid, x, y, exclude=mover)
if blocker is not None:
return False
return True
def try_move_or_attack(dx: int, dy: int) -> None:
"""Attempt to move the player or attack if blocked by enemy.
Args:
dx: Change in X position (-1, 0, or 1)
dy: Change in Y position (-1, 0, or 1)
"""
global player, grid, fov_layer, game_over
if game_over:
return
px, py = int(player.x), int(player.y)
target_x = px + dx
target_y = py + dy
# Check bounds
if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT:
return
# Check for blocking entity
blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
if blocker is not None:
# Attack the blocking entity
perform_attack(player, blocker)
# After player attacks, enemies take their turn
enemy_turn()
elif grid.at(target_x, target_y).walkable:
# Move to the empty tile
player.x = target_x
player.y = target_y
pos_display.text = f"Position: ({target_x}, {target_y})"
# Update FOV after movement
update_fov(grid, fov_layer, target_x, target_y)
# Enemies take their turn after player moves
enemy_turn()
# Update HP display
update_hp_display()
# =============================================================================
# Enemy AI
# =============================================================================
def enemy_turn() -> None:
"""Execute enemy actions."""
global player, grid, game_over
if game_over:
return
player_x, player_y = int(player.x), int(player.y)
# Collect enemies that can act
enemies = []
for entity in grid.entities:
if entity == player:
continue
if entity in entity_data and entity_data[entity].is_alive:
enemies.append(entity)
for enemy in enemies:
fighter = entity_data.get(enemy)
if fighter is None or not fighter.is_alive:
continue
ex, ey = int(enemy.x), int(enemy.y)
# Only act if in player's FOV (aware of player)
if not grid.is_in_fov(ex, ey):
continue
# Check if adjacent to player
dx = player_x - ex
dy = player_y - ey
if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0):
# Adjacent - attack!
perform_attack(enemy, player)
else:
# Not adjacent - try to move toward player
move_toward_player(enemy, ex, ey, player_x, player_y)
def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None:
"""Move an enemy one step toward the player.
Uses simple greedy movement - not true pathfinding.
"""
global grid
# Calculate direction to player
dx = 0
dy = 0
if px < ex:
dx = -1
elif px > ex:
dx = 1
if py < ey:
dy = -1
elif py > ey:
dy = 1
# Try to move in the desired direction
# First try the combined direction
new_x = ex + dx
new_y = ey + dy
if can_move_to(grid, new_x, new_y, enemy):
enemy.x = new_x
enemy.y = new_y
elif dx != 0 and can_move_to(grid, ex + dx, ey, enemy):
# Try horizontal only
enemy.x = ex + dx
elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy):
# Try vertical only
enemy.y = ey + dy
# If all fail, enemy stays in place
# =============================================================================
# UI Updates
# =============================================================================
def update_hp_display() -> None:
"""Update the HP display in the UI."""
global player
if hp_display is None or player is None:
return
if player in entity_data:
fighter = entity_data[player]
hp_display.text = f"HP: {fighter.hp}/{fighter.max_hp}"
# Color based on health percentage
hp_percent = fighter.hp / fighter.max_hp
if hp_percent > 0.6:
hp_display.fill_color = mcrfpy.Color(100, 255, 100)
elif hp_percent > 0.3:
hp_display.fill_color = mcrfpy.Color(255, 255, 100)
else:
hp_display.fill_color = mcrfpy.Color(255, 100, 100)
# =============================================================================
# Game Setup
# =============================================================================
# Create the scene
scene = mcrfpy.Scene("game")
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create the grid
grid = mcrfpy.Grid(
pos=(50, 80),
size=(800, 480),
grid_size=(GRID_WIDTH, GRID_HEIGHT),
texture=texture,
zoom=1.0
)
# Generate initial dungeon structure
fill_with_walls(grid)
init_explored()
rooms: list[RectangularRoom] = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(grid, new_room)
if rooms:
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
# Get player starting position
if rooms:
player_start_x, player_start_y = rooms[0].center
else:
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
# Add FOV layer
fov_layer = grid.add_layer("color", z_index=-1)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Create the player
player = mcrfpy.Entity(
grid_pos=(player_start_x, player_start_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
# Create player Fighter component
entity_data[player] = Fighter(
hp=30,
max_hp=30,
attack=5,
defense=2,
name="Player",
is_player=True
)
# Spawn enemies in all rooms except the first
for i, room in enumerate(rooms):
if i == 0:
continue
spawn_enemies_in_room(grid, room, texture)
# Calculate initial FOV
update_fov(grid, fov_layer, player_start_x, player_start_y)
# Add grid to scene
scene.children.append(grid)
# =============================================================================
# UI Elements
# =============================================================================
title = mcrfpy.Caption(
pos=(50, 15),
text="Part 6: Combat System"
)
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 24
scene.children.append(title)
instructions = mcrfpy.Caption(
pos=(50, 50),
text="WASD/Arrows: Move/Attack | R: Restart | Escape: Quit"
)
instructions.fill_color = mcrfpy.Color(180, 180, 180)
instructions.font_size = 16
scene.children.append(instructions)
# Position display
pos_display = mcrfpy.Caption(
pos=(50, 580),
text=f"Position: ({int(player.x)}, {int(player.y)})"
)
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
pos_display.font_size = 16
scene.children.append(pos_display)
# HP display
hp_display = mcrfpy.Caption(
pos=(300, 580),
text="HP: 30/30"
)
hp_display.fill_color = mcrfpy.Color(100, 255, 100)
hp_display.font_size = 16
scene.children.append(hp_display)
# Message log (positioned below the grid)
message_log_caption = mcrfpy.Caption(
pos=(50, 610),
text=""
)
message_log_caption.fill_color = mcrfpy.Color(200, 200, 200)
message_log_caption.font_size = 14
scene.children.append(message_log_caption)
# Initial message
add_message("Welcome to the dungeon! Find and defeat the enemies.", mcrfpy.Color(100, 100, 255))
# =============================================================================
# Input Handling
# =============================================================================
def restart_game() -> None:
"""Restart the game with a new dungeon."""
global player, grid, fov_layer, game_over, entity_data, rooms
game_over = False
# Clear all entities and data
entity_data.clear()
# Remove all entities from grid
while len(grid.entities) > 0:
grid.entities.remove(0)
# Regenerate dungeon
fill_with_walls(grid)
init_explored()
clear_messages()
rooms = []
for _ in range(MAX_ROOMS):
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
x = random.randint(1, GRID_WIDTH - room_width - 2)
y = random.randint(1, GRID_HEIGHT - room_height - 2)
new_room = RectangularRoom(x, y, room_width, room_height)
overlaps = False
for other_room in rooms:
if new_room.intersects(other_room):
overlaps = True
break
if overlaps:
continue
carve_room(grid, new_room)
if rooms:
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
rooms.append(new_room)
# Get new player starting position
if rooms:
new_x, new_y = rooms[0].center
else:
new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
# Recreate player
player = mcrfpy.Entity(
grid_pos=(new_x, new_y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
entity_data[player] = Fighter(
hp=30,
max_hp=30,
attack=5,
defense=2,
name="Player",
is_player=True
)
# Spawn enemies
for i, room in enumerate(rooms):
if i == 0:
continue
spawn_enemies_in_room(grid, room, texture)
# Reset FOV layer
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
fov_layer.set(x, y, COLOR_UNKNOWN)
# Update displays
update_fov(grid, fov_layer, new_x, new_y)
pos_display.text = f"Position: ({new_x}, {new_y})"
update_hp_display()
add_message("A new adventure begins!", mcrfpy.Color(100, 100, 255))
def handle_keys(key: str, action: str) -> None:
"""Handle keyboard input."""
global game_over
if action != "start":
return
# Handle restart
if key == "R":
restart_game()
return
if key == "Escape":
mcrfpy.exit()
return
# Ignore other input if game is over
if game_over:
return
# Movement and attack
if key == "W" or key == "Up":
try_move_or_attack(0, -1)
elif key == "S" or key == "Down":
try_move_or_attack(0, 1)
elif key == "A" or key == "Left":
try_move_or_attack(-1, 0)
elif key == "D" or key == "Right":
try_move_or_attack(1, 0)
scene.on_key = handle_keys
# =============================================================================
# Start the Game
# =============================================================================
scene.activate()
print("Part 6 loaded! Combat is now active. Good luck!")

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