Compare commits

..

156 commits

Author SHA1 Message Date
8628ac164b BSP: add room adjacency graph for corridor generation (closes #210)
New features:
- bsp.adjacency[i] returns tuple of neighbor leaf indices
- bsp.get_leaf(index) returns BSPNode by leaf index (O(1) lookup)
- node.leaf_index returns this leaf's index (0..n-1) or None
- node.adjacent_tiles[j] returns tuple of Vector wall tiles bordering neighbor j

Implementation details:
- Lazy-computed adjacency cache with generation-based invalidation
- O(n²) pairwise adjacency check on first access
- Wall tiles computed per-direction (not symmetric) for correct perspective
- Supports 'in' operator: `5 in leaf.adjacent_tiles`

Code review fixes applied:
- split_once now increments generation to invalidate cache
- Wall tile cache uses (self, neighbor) key, not symmetric
- Added sq_contains for 'in' operator support
- Documented wall tile semantics (tiles on THIS leaf's boundary)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:43:57 -05:00
5a86602789 HeightMap - kernel_transform (#198) 2026-01-12 21:42:34 -05:00
2b12d1fc70 Update to combination operations (#194) - allowing targeted, partial regions on source or target 2026-01-12 20:56:39 -05:00
e5d0eb4847 Noise, combination, and sampling: first pass at #207, #208, #194, #209 2026-01-12 19:01:20 -05:00
6caf3dcd05 BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation

API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count

Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
8699bba9e6 BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
  split_horizontal, split_position; navigation via left/right/parent/sibling;
  contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options

Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
a4b1ab7d68 Grid layers: add HeightMap-based procedural generation methods
TileLayer (closes #200):
- apply_threshold(source, range, tile): Set tile index where heightmap value is in range
- apply_ranges(source, ranges): Apply multiple tile assignments in one pass

ColorLayer (closes #201):
- apply_threshold(source, range, color): Set fixed color where value is in range
- apply_gradient(source, range, color_low, color_high): Interpolate colors based on value
- apply_ranges(source, ranges): Apply multiple color assignments (fixed or gradient)

All methods return self for chaining. HeightMap size must match layer dimensions.
Later ranges override earlier ones if overlapping. Cells not matching any range are unchanged.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:35:44 -05:00
b7c5262abf HeightMap: improve dig_hill/dig_bezier API clarity
Rename parameters for clearer semantics:
- dig_hill: depth -> target_height
- dig_bezier: start_depth/end_depth -> start_height/end_height

The libtcod "dig" functions set terrain TO a target height, not
relative to current values. "target_height" makes this clearer.

Also add warnings for likely user errors:
- add_hill/dig_hill/dig_bezier with radius <= 0 (no-op)
- smooth with iterations <= 0 already raises ValueError

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:21:58 -05:00
f2711e553f HeightMap: add terrain generation methods (closes #195)
Add seven terrain generation methods wrapping libtcod heightmap functions:
- add_hill(center, radius, height): Add smooth hill
- dig_hill(center, radius, depth): Dig crater (use negative depth)
- add_voronoi(num_points, coefficients, seed): Voronoi-based features
- mid_point_displacement(roughness, seed): Diamond-square terrain
- rain_erosion(drops, erosion, sedimentation, seed): Erosion simulation
- dig_bezier(points, start_radius, end_radius, start_depth, end_depth): Carve paths
- smooth(iterations): Average neighboring cells

All methods return self for chaining. Includes 24 unit tests.

Note: dig_hill and dig_bezier use libtcod's "dig" semantics - use negative
depth values to actually dig below current terrain level.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:00:08 -05:00
d92d5f0274 HeightMap: add threshold operations that return new HeightMaps (closes #197)
Add three methods that create NEW HeightMap objects:
- threshold(range): preserve original values where in range, 0.0 elsewhere
- threshold_binary(range, value=1.0): set uniform value where in range
- inverse(): return (1.0 - value) for each cell

These operations are immutable - they preserve the original HeightMap.
Useful for masking operations with Grid.apply_threshold/apply_ranges.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 21:49:28 -05:00
b98b2be012 HeightMap: improve API consistency and add subscript support
Position argument flexibility:
- get(), get_interpolated(), get_slope(), get_normal() now accept:
  - Two separate args: hmap.get(5, 5)
  - Tuple: hmap.get((5, 5))
  - List: hmap.get([5, 5])
  - Vector: hmap.get(mcrfpy.Vector(5, 5))
- Uses PyPositionHelper for standardized parsing

Subscript support:
- Add __getitem__ as shorthand for get(): hmap[5, 5] or hmap[(5, 5)]

Range validation:
- count_in_range() now raises ValueError when min > max
- count_in_range() accepts both tuple and list

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 21:43:44 -05:00
c2877c8053 Replace deprecated PyWeakref_GetObject with PyWeakref_GetRef (closes #191)
PyWeakref_GetObject was deprecated in Python 3.13 and will be removed
in 3.15. The new PyWeakref_GetRef API returns a strong reference directly
and uses integer return codes for error handling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 20:48:06 -05:00
a81430991c libtcod as SYSTEM include, to ignore deprecations 2026-01-11 20:44:46 -05:00
bf8557798a Grid: add apply_threshold and apply_ranges for HeightMap (closes #199)
Add methods to apply HeightMap data to Grid walkable/transparent properties:
- apply_threshold(source, range, walkable, transparent): Apply properties
  to cells where HeightMap value is in the specified range
- apply_ranges(source, ranges): Apply multiple threshold rules in one pass

Features:
- Size mismatch between HeightMap and Grid raises ValueError
- Both methods return self for chaining
- Uses dynamic type lookup via module for HeightMap type checking
- First matching range wins in apply_ranges
- Cells not matching any range remain unchanged
- TCOD map is synced after changes for FOV/pathfinding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 20:42:47 -05:00
8d6d564d6b HeightMap: add query methods (closes #196)
Add methods to query HeightMap values and statistics:
- get(pos): Get height value at integer coordinates
- get_interpolated(pos): Get bilinearly interpolated height at float coords
- get_slope(pos): Get slope angle (0 to pi/2) at position
- get_normal(pos, water_level): Get surface normal vector
- min_max(): Get (min, max) tuple of all values
- count_in_range(range): Count cells with values in range

All methods include proper bounds checking and error messages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 20:42:33 -05:00
87444c2fd0 HeightMap: add GRID_MAX limit and input validation
Fixes potential integer overflow and invalid input issues:

- Add GRID_MAX constant (8192) to Common.h for global use
- Validate HeightMap dimensions against GRID_MAX to prevent
  integer overflow in w*h calculations (65536*65536 = 0)
- Add min > max validation for clamp() and normalize()
- Add unit tests for all new validation cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 20:26:04 -05:00
c095be4b73 HeightMap: core class with scalar operations (closes #193)
Implement the foundational HeightMap class for procedural generation:

- HeightMap(size, fill=0.0) constructor with libtcod backend
- Immutable size property after construction
- Scalar operations returning self for method chaining:
  - fill(value), clear()
  - add_constant(value), scale(factor)
  - clamp(min=0.0, max=1.0), normalize(min=0.0, max=1.0)

Includes procedural generation spec document and unit tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 20:07:55 -05:00
b32f5af28c UIGridPathfinding: clear and separate A-star and Djikstra path systems 2026-01-10 22:09:45 -05:00
9eacedc624 Input Enums instead of strings. 2026-01-10 21:31:20 -05:00
d9411f94a4 Version bump: 0.2.0-prerelease-7drl2026 (d6ef29f) -> 0.2.1-prerelease-7drl2026 2026-01-10 08:55:50 -05:00
d6ef29f3cd Grid code quality improvements
* Grid [x, y] subscript - convenience for `.at()`
* Extract UIEntityCollection - cleanup of UIGrid.cpp
* Thread-safe type cache - PyTypeCache
* Exception-safe extend() - validate before modify
2026-01-10 08:37:31 -05:00
a77ac6c501 Monkey Patch support + Robust callback tracking
McRogueFace needs to accept callable objects (properties on C++ objects)
and also support subclassing (getattr on user objects). Only direct
properties were supported previously, now shadowing a callback by name
will allow custom objects to "just work".
- Added CallbackCache struct and is_python_subclass flag to UIDrawable.h
- Created metaclass for tracking class-level callback changes
- Updated all UI type init functions to detect subclasses
- Modified PyScene.cpp event dispatch to try subclass methods
2026-01-09 21:37:23 -05:00
1d11b020b0 Implement Scene subclass on_key callback support
Scene subclasses can now define on_key(self, key, state) methods that
receive keyboard events, matching the existing on_enter, on_exit, and
update lifecycle callbacks.

Changes:
- Rename call_on_keypress to call_on_key (consistent naming with property)
- Add triggerKeyEvent helper in McRFPy_API
- Call triggerKeyEvent from GameEngine when key_callable is not set
- Fix condition to check key_callable.isNone() (not just pointer existence)
- Handle both bound methods and instance-assigned callables

Usage:
    class GameScene(mcrfpy.Scene):
        def on_key(self, key, state):
            if key == "Escape" and state == "end":
                quit_game()

Property assignment (scene.on_key = callable) still works and takes
precedence when key_callable is set via the property setter.

Includes comprehensive test: tests/unit/scene_subclass_on_key_test.py

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 15:51:20 -05:00
b6eb70748a Remove YAGNI methods from performance systems
GridChunk: Remove getWorldBounds, markAllDirty, getVisibleChunks
- getWorldBounds: Chunk visibility handled by isVisible() instead
- markAllDirty: GridLayers uses per-cell markDirty() pattern
- getVisibleChunks: GridLayers computes visible range inline
- Keep dirtyChunks() for diagnostics

GridLayers: Remove getChunkCoords
- Trivial helper replaced by inline division throughout codebase

SpatialHash: Remove queryRect, totalEntities, cleanBucket
- queryRect: Exceeds #115 scope (only queryRadius required)
- totalEntities: Redundant with separate entity count tracking
- cleanBucket: Dead code - expired weak_ptrs cleaned during remove/update

All removals identified via cppcheck static analysis. Core functionality
of each system remains intact and actively used.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 15:40:13 -05:00
ae27e7deee delete unused file 2026-01-09 15:33:52 -05:00
2c320effc6 hashing bugfix: '<<' rather than '<<=' operator was used 2026-01-09 13:45:36 -05:00
a7ada7d65b distribution packaging 2026-01-09 12:00:59 -05:00
e6fa62f35d set version string for 7DRL2026 prerelease 2026-01-09 07:01:29 -05:00
ed85ccdf33 update to cpython 3.14.2 2026-01-09 07:00:15 -05:00
08c7c797a3 asset cleanup 2026-01-08 22:52:34 -05:00
1438044c6a mingw toolchain and final fixes for Windows. Closes #162 2026-01-08 21:16:27 -05:00
1f002e820c long -> intptr_t for casts. WIP: mingw cross-compilation for Windows (see #162) 2026-01-08 10:41:24 -05:00
2f4ebf3420 tests for the last few issues (these test scripts should work with recent APIs, while the rest of the test suite needs an overhaul) 2026-01-08 10:31:21 -05:00
a57f0875f8 Code editor window, lockable positions; send console output to the code editor to select and cut/copy output. Closes #170 2026-01-06 22:42:20 -05:00
75127ac9d1 mcrfpy.Mouse: a new class built for symmetry with mcrfpy.Keyboard. Closes #186 2026-01-06 21:39:01 -05:00
b0b17f4633 timer fixes: timers managed by engine can run in the background. Closes #180 2026-01-06 20:13:51 -05:00
2c20455003 support for Scene object as parent, from Python: closes #183 2026-01-06 14:04:53 -05:00
7e47050d6f bugfixes for .parent property - partial #183 solution 2026-01-06 10:21:50 -05:00
a4c2c04343 bugfix: segfault in Grid.at() due to internal types not exported to module
After #184/#189 made GridPoint and GridPointState internal-only types,
code using PyObject_GetAttrString(mcrf_module, "GridPoint") would get
NULL and crash when dereferencing.

Fixed by using the type directly via &mcrfpydef::PyUIGridPointType
instead of looking it up in the module.

Affected functions:
- UIGrid::py_at()
- UIGridPointState::get_point()
- UIEntity::at()
- UIGridPointState_to_PyObject()

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 04:38:56 -05:00
f9b6cdef1c Python API improvements: Vectors, bounds, window singleton, hidden types
- #177: GridPoint.grid_pos property returns (x, y) tuple
- #179: Grid.grid_size returns Vector instead of tuple
- #181: Grid.center returns Vector instead of tuple
- #182: Caption.size/w/h read-only properties for text dimensions
- #184: mcrfpy.window singleton for window access
- #185: Removed get_bounds() method, use .bounds property instead
- #188: bounds/global_bounds return (pos, size) as pair of Vectors
- #189: Hide internal types from module namespace (iterators, collections)

Also fixed critical bug: Changed static PyTypeObject to inline in headers
to ensure single instance across translation units (was causing segfaults).

Closes #177, closes #179, closes #181, closes #182, closes #184, closes #185, closes #188, closes #189

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 23:00:48 -05:00
c6233fa47f Expand TileLayer and ColorLayer __init__ documentation; closes #190
Enhanced tp_doc strings for both layer types to include:
- What happens when grid_size=None (inherits from parent Grid)
- That layers are created via Grid.add_layer() rather than directly
- FOV-related methods for ColorLayer
- Tile index -1 meaning no tile/transparent for TileLayer
- fill_rect method documentation
- Comprehensive usage examples

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 22:24:36 -05:00
84d73e6aef Update old format scene/timer examples 2026-01-05 11:42:22 -05:00
02c512402e bugfix: segfault due to use of uninitialized Vector class reference 2026-01-05 10:31:41 -05:00
d2e4791f5a Positions are always mcrfpy.Vector, Vector/tuple/iterables expected as inputs, and for position-only inputs we permit x,y args to prevent requiring double-parens 2026-01-05 10:16:16 -05:00
016ca693b5 ImGui cleanup order: prevent error on exit by performing ImGui::SFML::Shutdown() before window close 2026-01-04 16:34:47 -05:00
9ab618079a .animate helper: create and start an animation directly on a target. Preferred use pattern; closes #175 2026-01-04 15:32:14 -05:00
d878c8684d Easing functions as enum 2026-01-04 12:59:28 -05:00
357c2ac7d7 Animation fixes: 0-duration edge case, integer value bug resolution 2026-01-04 00:45:16 -05:00
cec76b63dc Timer overhaul: update tests 2026-01-03 22:44:53 -05:00
5d41292bf6 Timer refactor: stopwatch-like semantics, mcrfpy.timers collection closes #173
Major Timer API improvements:
- Add `stopped` flag to Timer C++ class for proper state management
- Add `start()` method to restart stopped timers (preserves callback)
- Add `stop()` method that removes from engine but preserves callback
- Make `active` property read-write (True=start/resume, False=pause)
- Add `start=True` init parameter to create timers in stopped state
- Add `mcrfpy.timers` module-level collection (tuple of active timers)
- One-shot timers now set stopped=true instead of clearing callback
- Remove deprecated `setTimer()` and `delTimer()` module functions

Timer callbacks now receive (timer, runtime) instead of just (runtime).
Updated all tests to use new Timer API and callback signature.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 22:09:18 -05:00
fc95fc2844 scene transitions via Scene object 2026-01-03 13:53:18 -05:00
40c0eb2693 scripts - use scene object API 2026-01-03 11:02:40 -05:00
d7e34a3f72 Remove old scene management methods 2026-01-03 11:01:42 -05:00
48359b5a48 draft tutorial revisions 2026-01-03 11:01:10 -05:00
838da4571d update tests: new scene API 2026-01-03 10:59:52 -05:00
f62362032e feat: Grid camera defaults to tile (0,0) at top-left + center_camera() method (#169)
Changes:
- Default Grid center now positions tile (0,0) at widget's top-left corner
- Added center_camera() method to center grid's middle tile at view center
- Added center_camera((tile_x, tile_y)) to position tile at top-left of widget
- Uses NaN as sentinel to detect if user provided center values in kwargs
- Animation-compatible: center_camera() just sets center property, no special state

Behavior:
- center_camera() → grid's center tile at view center
- center_camera((0, 0)) → tile (0,0) at top-left corner
- center_camera((5, 10)) → tile (5,10) at top-left corner

Before: Grid(size=(320,240)) showed 3/4 of content off-screen (center=0,0)
After: Grid(size=(320,240)) shows tile (0,0) at top-left (center=160,120)

Closes #169

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 17:22:26 -05:00
05f28ef7cd Add 14-part tutorial Python files (extracted, tested)
Tutorial scripts extracted from documentation, with fixes:
- Asset filename: kenney_roguelike.png → kenney_tinydungeon.png
- Entity keyword: pos= → grid_pos= (tile coordinates)
- Frame.size property → Frame.resize() method
- Removed sprite_color (deferred to shader support)

All 14 parts pass smoke testing (import + 2-frame run).

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 16:21:09 -05:00
e64c5c147f docs: Fix property extraction and add Scene documentation
Doc generator fixes (tools/generate_dynamic_docs.py):
- Add types.GetSetDescriptorType detection for C++ extension properties
- All 22 classes with properties now have documented Properties sections
- Read-only detection via "read-only" docstring convention

Scene class documentation (src/PySceneObject.h):
- Expanded tp_doc with constructor, properties, lifecycle callbacks
- Documents key advantage: on_key works on ANY scene
- Includes usage examples for basic and subclass patterns

CLAUDE.md additions:
- New section "Adding Documentation for New Python Types"
- Step-by-step guide for tp_doc, PyMethodDef, PyGetSetDef
- Documentation extraction details and troubleshooting

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:48:21 -05:00
b863698f6e test: Add comprehensive Scene object API test
Demonstrates the object-oriented Scene API as alternative to module-level
functions. Key features tested:

- Scene object creation and properties (name, active, children)
- scene.activate() vs mcrfpy.setScene()
- scene.on_key property - can be set on ANY scene, not just current
- Scene visual properties (pos, visible, opacity)
- Subclassing for lifecycle callbacks (on_enter, on_exit, update)

The on_key advantage resolves confusion with keypressScene() which only
works on the currently active scene.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:48:00 -05:00
9f481a2e4a fix: Update test files to use current API patterns
Migrates test suite to current API:
- Frame(x, y, w, h) → Frame(pos=(x, y), size=(w, h))
- Caption("text", x, y) → Caption(pos=(x, y), text="text")
- caption.size → caption.font_size
- Entity(x, y, ...) → Entity((x, y), ...)
- Grid(w, h, ...) → Grid(grid_size=(w, h), ...)
- cell.color → ColorLayer system

Tests now serve as valid API usage examples.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:47:48 -05:00
c025cd7da3 feat: Add Sound/Music classes, keyboard state, version (#66, #160, #164)
Replace module-level audio functions with proper OOP API:
- mcrfpy.Sound: Wraps sf::SoundBuffer + sf::Sound for short effects
- mcrfpy.Music: Wraps sf::Music for streaming long tracks
- Both support: volume, loop, playing, duration, play/pause/stop
- Music adds position property for seeking

Add mcrfpy.keyboard singleton for real-time modifier state:
- shift, ctrl, alt, system properties (bool, read-only)
- Queries sf::Keyboard::isKeyPressed() directly

Add mcrfpy.__version__ = "1.0.0" for version identity

Remove old audio API entirely (no deprecation - unused in codebase):
- createSoundBuffer, loadMusic, playSound
- setMusicVolume, getMusicVolume, setSoundVolume, getSoundVolume

closes #66, closes #160, closes #164

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 16:24:27 -05:00
335efc5514 feat: Implement enhanced action economy for LLM agent orchestration (#156)
- Add action economy system with free (LOOK, SPEAK) vs turn-ending (GO, WAIT, TAKE) actions
- Implement LOOK action with detailed descriptions for doors, objects, entities, directions
- Add SPEAK/ANNOUNCE speech system with room-wide and proximity-based message delivery
- Create multi-tile pathing with FOV interrupt detection (path cancels when new entity visible)
- Implement TAKE action with adjacency requirement and clear error messages
- Add conversation history and error feedback loop so agents learn from failed actions
- Create structured simulation logging for offline viewer replay
- Document offline viewer requirements in OFFLINE_VIEWER_SPEC.md
- Fix import path in 1_multi_agent_demo.py for standalone execution

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 20:50:00 -05:00
85e90088d5 fix: Register keypressScene after setScene (closes #143)
keypressScene() sets the handler for the CURRENT scene, so we must
call setScene() first to make focus_demo the active scene before
registering the key handler.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 15:35:48 -05:00
b6ec0fe7ab feat: Add focus system demo for #143
Implements a comprehensive Python-level focus management system showing:
- FocusManager: central coordinator for keyboard routing, tab cycling, modal stack
- ModifierTracker: workaround for tracking Shift/Ctrl/Alt state (#160)
- FocusableGrid: WASD movement in a grid with player marker
- TextInputWidget: text entry with cursor, backspace, home/end
- MenuIcon: icons that open modal dialogs on Space/Enter

Features demonstrated:
- Click-to-focus on any widget
- Tab/Shift+Tab cycling through focusable widgets
- Visual focus indicators (blue outline)
- Keyboard routing to focused widget
- Modal dialog push/pop stack
- Escape to close modals

Addresses #143

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 15:30:17 -05:00
89986323f8 docs: Add missing Drawable callbacks and Scene.on_key to stubs
Add to Drawable base class:
- on_click, on_enter, on_exit, on_move callbacks (#140, #141)
- hovered read-only property (#140)

Add to Scene class:
- children property (#151)
- on_key handler property

Discovered while defining implementation details for #143.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 14:49:17 -05:00
da6f4a3e62 docs: Add Line/Circle/Arc to stubs and fix click→on_click
- Add Line, Circle, Arc class definitions to type stubs
- Update UIElement type alias to include new drawable types
- Rename click kwarg to on_click throughout stubs (matches #126 change)
- Update UICollection docstring to list all drawable types

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 14:36:54 -05:00
c9c7375827 refactor: Rename click kwarg to on_click for API consistency (closes #126)
BREAKING CHANGE: Constructor keyword argument renamed from `click` to
`on_click` for all UIDrawable types (Frame, Caption, Sprite, Grid, Line,
Circle, Arc).

Before: Frame(pos=(0,0), size=(100,100), click=handler)
After:  Frame(pos=(0,0), size=(100,100), on_click=handler)

The property name was already `on_click` - this makes the constructor
kwarg match, completing the callback naming standardization from #139.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 14:31:22 -05:00
58efffd2fd feat: Animation property locking prevents conflicting animations (closes #120)
Add AnimationConflictMode enum with three modes:
- REPLACE (default): Complete existing animation and start new one
- QUEUE: Wait for existing animation to complete before starting
- ERROR: Raise RuntimeError if property is already being animated

Changes:
- AnimationManager now tracks property locks per (target, property) pair
- Animation.start() accepts optional conflict_mode parameter
- Queued animations start automatically when property becomes free
- Updated type stubs with ConflictMode type alias

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 13:21:50 -05:00
366ccecb7d chore: Extend benchmark to test 5000 entities
Validate SpatialHash scalability with larger entity counts.
Results at 5,000 entities:
- N×N visibility: 216.9× faster (431ms → 2ms)
- Single query: 37.4× faster (0.11ms → 0.003ms)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 00:46:43 -05:00
7d57ce2608 feat: Implement SpatialHash for O(1) entity spatial queries (closes #115)
Add SpatialHash class for efficient spatial queries on entities:
- New SpatialHash.h/cpp with bucket-based spatial hashing
- Grid.entities_in_radius(x, y, radius) method for O(k) queries
- Automatic spatial hash updates on entity add/remove/move

Benchmark results at 2,000 entities:
- Single query: 16.2× faster (0.044ms → 0.003ms)
- N×N visibility: 104.8× faster (74ms → 1ms)

This enables efficient range queries for AI, visibility, and
collision detection without scanning all entities.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 00:44:07 -05:00
8f2407b518 fix: EntityCollection iterator O(n²) → O(n) with 100× speedup (closes #159)
Problem: EntityCollection iterator used index-based access on std::list,
causing O(n) traversal per element access (O(n²) total for iteration).

Root cause: Each call to next() started from begin() and advanced index steps:
  std::advance(l_begin, self->index-1);  // O(index) for linked list!

Solution:
- Store actual std::list iterators (current, end) instead of index
- Increment iterator directly in next() - O(1) operation
- Cache Entity and Iterator type lookups to avoid repeated dict lookups

Benchmark results (2,000 entities):
- Before: 13.577ms via EntityCollection
- After:  0.131ms via EntityCollection
- Speedup: 103×

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-28 00:30:31 -05:00
fcc0376f31 feat: Add entity scale benchmark for #115 and #117
Benchmark suite measuring entity performance at scale:
- B1: Entity creation (measures allocation overhead)
- B2: Full iteration (measures cache locality)
- B3: Single range query (measures O(n) scan cost)
- B4: N×N visibility (the "what can everyone see" problem)
- B5: Movement churn (baseline for spatial index overhead)

Key findings at 2,000 entities on 100×100 grid:
- Creation: 75k entities/sec
- Range query: 0.05ms (O(n) - checks all entities)
- N×N visibility: 128ms total (O(n²))
- EntityCollection iteration 60× slower than direct iteration

Addresses #115, addresses #117

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-27 23:24:31 -05:00
3e07334aa5 Oops, remove token 2025-12-26 20:19:55 -05:00
71c91e19a5 feat: Add consistent Scene API with module-level properties (closes #151)
Replaces module-level scene functions with more Pythonic OO interface:

Scene class changes:
- Add `scene.children` property (replaces get_ui() method)
- Add `scene.on_key` getter/setter (matches on_click pattern)
- Remove get_ui() method

Module-level properties:
- Add `mcrfpy.current_scene` (getter returns Scene, setter activates)
- Add `mcrfpy.scenes` (read-only tuple of all Scene objects)

Implementation uses custom module type (McRFPyModuleType) inheriting
from PyModule_Type with tp_setattro for property assignment support.

New usage:
  scene = mcrfpy.Scene("game")
  mcrfpy.current_scene = scene
  scene.on_key = handler
  ui = scene.children

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-22 22:15:03 -05:00
de739037f0 feat: Add TurnOrchestrator for multi-turn LLM simulation (addresses #156)
TurnOrchestrator: Coordinates multi-agent turn-based simulation
- Perspective switching with FOV layer updates
- Screenshot capture per agent per turn
- Pluggable LLM query callback
- SimulationStep/SimulationLog for full context capture
- JSON save/load with replay support

New demos:
- 2_integrated_demo.py: WorldGraph + action execution integration
- 3_multi_turn_demo.py: Complete multi-turn simulation with logging

Updated 1_multi_agent_demo.py with action parser/executor integration.

Tested with Qwen2.5-VL-32B: agents successfully navigate based on
WorldGraph descriptions and VLM visual input.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-14 12:53:48 -05:00
2890528e21 feat: Add action parser and executor for LLM agent actions
ActionParser: Extracts structured actions from LLM text responses
- Regex patterns for GO, WAIT, LOOK, TAKE, DROP, PUSH, USE, etc.
- Direction normalization (N→NORTH, UP→NORTH)
- Handles "Action: GO EAST" and fallback patterns
- 12 unit tests covering edge cases

ActionExecutor: Executes parsed actions in the game world
- Movement with collision detection (walls, entities)
- Boundary checking
- ActionResult with path data for animation replay

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-14 12:53:39 -05:00
e45760c2ac feat: Add WorldGraph for deterministic room descriptions (closes #155)
Implements Python-side room graph data structures for LLM agent environments:
- Room, Door, WorldObject dataclasses with full metadata
- WorldGraph class with spatial queries (room_at, get_exits)
- Deterministic text generation (describe_room, describe_exits)
- Available action enumeration based on room state
- Factory functions for test scenarios (two_room, button_door)

Example output:
"You are in the guard room. The air is musty. On the ground you see
a brass key. Exits: east (the armory)."

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-14 12:53:30 -05:00
b1b3773680 docs: Update CLAUDE.md with wiki workflow references
- Link to Development Workflow wiki page
- Clarify documentation update procedures

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 09:22:15 -05:00
5b637a48a7 fix: Correct right mouse button action name from 'rclick' to 'right'
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 09:22:07 -05:00
d761b53d48 docs: Update grid demo and regenerate API docs
- grid_demo.py: Updated for new layer-based rendering
- Screenshots: Refreshed demo screenshots
- API docs: Regenerated with latest method signatures

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 09:21:43 -05:00
4713b62535 feat: Add VLLM integration demos for multi-agent research (#156)
- 0_basic_vllm_demo.py: Single agent with FOV, grounded text, VLLM query
- 1_multi_agent_demo.py: Three agents with perspective cycling

Features demonstrated:
- Headless step() + screenshot() for AI-driven gameplay
- ColorLayer.apply_perspective() for per-agent fog of war
- Grounded text generation based on entity visibility
- Sequential VLLM queries with vision model support
- Proper FOV reset between perspective switches

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 09:21:25 -05:00
f2f8d6422f Add warning when starting benchmark in headless mode
The benchmark API captures per-frame data from the game loop, which is
bypassed when using step()-based simulation control. This warning
informs users to use Python's time module for headless performance
measurement instead.

Also adds test_headless_benchmark.py which verifies:
- step() and screenshot() don't produce benchmark frames
- Wall-clock timing for headless operations
- Complex scene throughput measurement

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 22:20:19 -05:00
60ffa68d04 feat: Add mcrfpy.step() and synchronous screenshot for headless mode (closes #153)
Implements Python-controlled simulation advancement for headless mode:

- Add mcrfpy.step(dt) to advance simulation by dt seconds
- step(None) advances to next scheduled event (timer/animation)
- Timers use simulation_time in headless mode for deterministic behavior
- automation.screenshot() now renders synchronously in headless mode
  (captures current state, not previous frame)

This enables LLM agent orchestration (#156) by allowing:
- Set perspective, take screenshot, query LLM - all synchronous
- Deterministic simulation control without frame timing issues
- Event-driven advancement with step(None)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 21:56:47 -05:00
f33e79a123 feat: Add GridPoint.entities and GridPointState.point properties
GridPoint.entities (#114):
- Returns list of entities at this grid cell position
- Enables convenient cell-based entity queries without manual iteration
- Example: grid.at(5, 5).entities → [<Entity>, <Entity>]

GridPointState.point (#16):
- Returns GridPoint if entity has discovered this cell, None otherwise
- Respects entity's perspective: undiscovered cells return None
- Enables entity.at(x,y).point.walkable style access
- Live reference: changes to GridPoint are immediately visible

This provides a simpler solution for #16 without the complexity of
caching stale GridPoint copies. The visible/discovered flags indicate
whether the entity "should" trust the data; Python can implement
memory systems if needed.

closes #114, closes #16

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 21:04:03 -05:00
a529e5eac3 feat: Add ColorLayer perspective methods and patrol demo (addresses #113)
ColorLayer enhancements:
- fill_rect(x, y, w, h, color): Fill rectangular region
- draw_fov(source, radius, fov, visible, discovered, unknown): One-time FOV draw
- apply_perspective(entity, visible, discovered, unknown): Bind layer to entity
- update_perspective(): Refresh layer from bound entity's gridstate
- clear_perspective(): Remove entity binding

New demo: tests/demo/perspective_patrol_demo.py
- Entity patrols around 10x10 central obstacle
- FOV layer shows visible/discovered/unknown states
- [R] to reset vision, [Space] to pause, [Q] to quit
- Demonstrates fog of war memory system

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 16:26:30 -05:00
c5b4200dea feat: Add entity.visible_entities() and improve entity.updateVisibility() (closes #113)
Phase 3 of Agent POV Integration:

Entity.updateVisibility() improvements:
- Now uses grid.fov_algorithm and grid.fov_radius instead of hardcoded values
- Updates any ColorLayers bound to this entity via apply_perspective()
- Properly triggers layer FOV recomputation when entity moves

New Entity.visible_entities(fov=None, radius=None) method:
- Returns list of other entities visible from this entity's position
- Optional fov parameter to override grid's FOV algorithm
- Optional radius parameter to override grid's fov_radius
- Useful for AI decision-making and line-of-sight checks

Test coverage in test_perspective_binding.py:
- Tests entity movement with bound layers
- Tests visible_entities with wall occlusion
- Tests radius override limiting visibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 15:55:18 -05:00
018e73590f feat: Implement FOV enum and layer draw_fov for #114 and #113
Phase 1 - FOV Enum System:
- Create PyFOV.h/cpp with mcrfpy.FOV IntEnum (BASIC, DIAMOND, SHADOW, etc.)
- Add mcrfpy.default_fov module property initialized to FOV.BASIC
- Add grid.fov and grid.fov_radius properties for per-grid defaults
- Remove deprecated module-level FOV_* constants (breaking change)

Phase 2 - Layer Operations:
- Implement ColorLayer.fill_rect(pos, size, color) for rectangle fills
- Implement TileLayer.fill_rect(pos, size, index) for tile rectangle fills
- Implement ColorLayer.draw_fov(source, radius, fov, visible, discovered, unknown)
  to paint FOV-based visibility on color layers using parent grid's TCOD map

The FOV enum uses Python's IntEnum for type safety while maintaining
backward compatibility with integer values. Tests updated to use new API.

Addresses #114 (FOV enum), #113 (layer operations)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 15:18:10 -05:00
0545dd4861 Tests for cached rendering performance 2025-11-28 23:28:13 -05:00
42fcd3417e refactor: Remove layer-related GridPoint properties, fix layer z-index
- Remove color, color_overlay, tilesprite, tile_overlay, uisprite from
  UIGridPoint - these are now accessed through named layers
- Keep only walkable and transparent as protected GridPoint properties
- Update isProtectedLayerName() to only protect walkable/transparent
- Fix default layer z-index to -1 (below entities) instead of 0
- Remove dead rendering code from GridChunk (layers handle rendering)
- Update cos_level.py demo to use explicit layer definitions
- Update UITestScene.cpp to use layer API instead of GridPoint properties

Part of #150 - Grid layer system migration

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 23:21:39 -05:00
a258613faa feat: Migrate Grid to user-driven layer rendering (closes #150)
- Add `layers` dict parameter to Grid constructor for explicit layer definitions
  - `layers={"ground": "color", "terrain": "tile"}` creates named layers
  - `layers={}` creates empty grid (entities + pathfinding only)
  - Default creates single TileLayer named "tilesprite" for backward compat

- Implement dynamic GridPoint property access via layer names
  - `grid.at(x,y).layer_name = value` routes to corresponding layer
  - Protected names (walkable, transparent, etc.) still use GridPoint

- Remove base layer rendering from UIGrid::render()
  - Layers are now the sole source of grid rendering
  - Old chunk_manager remains for GridPoint data access
  - FOV overlay unchanged

- Update test to use explicit `layers={}` parameter

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 23:04:09 -05:00
9469c04b01 feat: Implement chunk-based Grid rendering for large grids (closes #123)
Adds a sub-grid system where grids larger than 64x64 cells are automatically
divided into 64x64 chunks, each with its own RenderTexture for incremental
rendering. This significantly improves performance for large grids by:

- Only re-rendering dirty chunks when cells are modified
- Caching rendered chunk textures between frames
- Viewport culling at the chunk level (skip invisible chunks entirely)

Implementation details:
- GridChunk class manages individual 64x64 cell regions with dirty tracking
- ChunkManager organizes chunks and routes cell access appropriately
- UIGrid::at() method transparently routes through chunks for large grids
- UIGrid::render() uses chunk-based blitting for large grids
- Compile-time CHUNK_SIZE (64) and CHUNK_THRESHOLD (64) constants
- Small grids (<= 64x64) continue to use flat storage (no regression)

Benchmark results show ~2x improvement in base layer render time for 100x100
grids (0.45ms -> 0.22ms) due to chunk caching.

Note: Dynamic layers (#147) still use full-grid textures; extending chunk
system to layers is tracked separately as #150.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 22:33:16 -05:00
abb3316ac1 feat: Add dirty flag and RenderTexture caching for Grid layers (closes #148)
Implement per-layer dirty tracking and RenderTexture caching for
ColorLayer and TileLayer. Each layer now maintains its own cached
texture and only re-renders when content changes.

Key changes:
- Add dirty flag, cached_texture, and cached_sprite to GridLayer base
- Implement renderToTexture() for both ColorLayer and TileLayer
- Mark layers dirty on: set(), fill(), resize(), texture change
- Viewport changes (center/zoom) just blit cached texture portion
- Fallback to direct rendering if texture creation fails
- Add regression test with performance benchmarks

Expected performance improvement: Static layers render once, then
viewport panning/zooming only requires texture blitting instead of
re-rendering all cells.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 21:44:33 -05:00
4b05a95efe feat: Add dynamic layer system for Grid (closes #147)
Implements ColorLayer and TileLayer classes with z_index ordering:
- ColorLayer: stores RGBA color per cell for overlays, fog of war, etc.
- TileLayer: stores sprite index per cell with optional texture
- z_index < 0: renders below entities
- z_index >= 0: renders above entities

Python API:
- grid.add_layer(type, z_index, texture) - create layer
- grid.remove_layer(layer) - remove layer
- grid.layers - list of layers sorted by z_index
- grid.layer(z_index) - get layer by z_index
- layer.at(x,y) / layer.set(x,y,value) - cell access
- layer.fill(value) - fill entire layer

Layers are allocated separately from UIGridPoint, reducing memory
for grids that don't need all features. Base grid retains walkable/
transparent arrays for TCOD pathfinding.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 21:35:38 -05:00
f769c6c5f5 fix: Remove O(n²) list-building from compute_fov() (closes #146)
compute_fov() was iterating through the entire grid to build a Python
list of visible cells, causing O(grid_size) performance instead of
O(radius²). On a 1000×1000 grid this was 15.76ms vs 0.48ms.

The fix returns None instead - users should use is_in_fov() to query
visibility, which is the pattern already used by existing code.

Performance: 33x speedup (15.76ms → 0.48ms on 1M cell grid)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 21:26:32 -05:00
68f8349fe8 feat: Implement texture caching system with dirty flag optimization (closes #144)
- Add cache_subtree property on Frame for opt-in RenderTexture caching
- Add PyTexture::from_rendered() factory for runtime texture creation
- Add snapshot= parameter to Sprite for creating sprites from Frame content
- Implement content_dirty vs composite_dirty distinction:
  - markContentDirty(): content changed, invalidate self and ancestors
  - markCompositeDirty(): position changed, ancestors need recomposite only
- Update all UIDrawable position setters to use markCompositeDirty()
- Add quick exit workaround for cleanup segfaults

Benchmark: deep_nesting_cached is 3.7x faster (0.09ms vs 0.35ms)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 19:30:24 -05:00
8583db7225 feat: Add work_time_ms to benchmark logging for load analysis
Track actual work time separately from frame time to determine
system load percentage:
- work_time_ms: Time spent doing actual work before display()
- sleep_time = frame_time_ms - work_time_ms

This allows calculating load percentage:
  load% = (work_time / frame_time) * 100

Example at 60fps with light load:
- frame_time: 16.67ms, work_time: 2ms
- load: 12%, sleep: 14.67ms

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 16:13:40 -05:00
a7fef2aeb6 feat: Add benchmark logging system for performance analysis (closes #104)
Add Python API for capturing performance data to JSON files:
- mcrfpy.start_benchmark() - start capturing frame data
- mcrfpy.end_benchmark() - stop and return filename
- mcrfpy.log_benchmark(msg) - add log message to current frame

The benchmark system captures per-frame data including:
- Frame timing (frame_time_ms, fps, timestamp)
- Detailed timing breakdown (grid_render, entity_render, python, animation, fov)
- Draw call and element counts
- User log messages attached to frames

Output JSON format supports analysis tools and includes:
- Benchmark metadata (PID, timestamps, duration, total frames)
- Full frame-by-frame metrics array

Also refactors ProfilingMetrics from nested GameEngine struct to
top-level struct for easier forward declaration.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 16:05:55 -05:00
219a559c35 feat: Add dirty flag propagation to all UIDrawables and expand metrics API (#144, #104)
- Add markDirty() calls to setProperty() methods in:
  - UISprite: position, scale, sprite_index changes
  - UICaption: position, font_size, colors, text changes
  - UIGrid: position, size, center, zoom, color changes
  - UILine: thickness, position, endpoints, color changes
  - UICircle: radius, position, colors changes
  - UIArc: radius, angles, position, color changes
  - UIEntity: position changes propagate to parent grid

- Expand getMetrics() Python API to include detailed timing breakdown:
  - grid_render_time, entity_render_time, fov_overlay_time
  - python_time, animation_time
  - grid_cells_rendered, entities_rendered, total_entities

- Add comprehensive benchmark suite (tests/benchmarks/benchmark_suite.py):
  - 6 scenarios: empty, static UI, animated UI, mixed, deep hierarchy, grid stress
  - Automated metrics collection and performance assessment
  - Timing breakdown percentages

This enables proper dirty flag propagation for the upcoming texture caching
system (#144) and provides infrastructure for performance benchmarking (#104).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 15:44:09 -05:00
6c496b8732 feat: Implement comprehensive mouse event system
Implements multiple mouse event improvements for UI elements:

- Mouse enter/exit events (#140): on_enter, on_exit callbacks and
  hovered property for all UIDrawable types (Frame, Caption, Sprite, Grid)
- Headless click events (#111): Track simulated mouse position for
  automation testing in headless mode
- Mouse move events (#141): on_move callback fires continuously while
  mouse is within element bounds
- Grid cell events (#142): on_cell_enter, on_cell_exit, on_cell_click
  callbacks with cell coordinates (x, y), plus hovered_cell property

Includes comprehensive tests for all new functionality.

Closes #140, closes #111, closes #141, closes #142

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 23:08:31 -05:00
6d5a5e9e16 feat: Add AABB/hit testing foundation (#138)
C++ additions:
- get_global_bounds(): returns bounds in screen coordinates
- contains_point(x, y): hit test using global bounds

Python properties (on all UIDrawable types):
- bounds: (x, y, w, h) tuple in local coordinates
- global_bounds: (x, y, w, h) tuple in screen coordinates

These enable the mouse event system (#140, #141, #142) by providing
a way to determine which drawable is under the mouse cursor.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 22:36:08 -05:00
52a655399e refactor: Rename click property to on_click (closes #139)
Breaking change: callback property standardized to on_* pattern.
- `drawable.click` → `drawable.on_click`

Updated all C++ bindings (8 files) and Python test usages.
Note: src/scripts changes tracked separately (in .gitignore).

This establishes the naming pattern for future callbacks:
on_click, on_enter, on_exit, on_move, on_key, etc.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 22:31:53 -05:00
e9b5a8301d feat: Add entity.grid property and fix auto-removal bug
UIEntity now has a `.grid` property with getter/setter:
- entity.grid          # Get current grid (or None)
- entity.grid = grid   # Move to new grid (auto-removes from old)
- entity.grid = None   # Remove from current grid

Also fixes UIEntityCollection.append() to properly implement the
documented "single grid only" behavior - entities are now correctly
removed from their old grid when appended to a new one.

This matches the parent property pattern used for UIDrawables.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 21:08:31 -05:00
41a704a010 refactor: Use property setter pattern for parent assignment
Instead of separate getParent()/setParent()/removeFromParent() methods,
the parent property now supports the Pythonic getter/setter pattern:
- child.parent       # Get parent (or None)
- child.parent = f   # Set parent (adds to f.children)
- child.parent = None # Remove from parent

This matches the existing pattern used by the click property callback.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 21:01:11 -05:00
e3d8f54d46 feat: Implement Phase A UI hierarchy foundations (closes #122, #102, #116, #118)
Parent-Child UI System (#122):
- Add parent weak_ptr to UIDrawable for hierarchy tracking
- Add setParent(), getParent(), removeFromParent() methods
- UICollection now tracks owner and sets parent on append/insert
- Auto-remove from old parent when adding to new collection

Global Position Property (#102):
- Add get_global_position() that walks up parent chain
- Expose as read-only 'global_position' property on all UI types
- Add UIDRAWABLE_PARENT_GETSETTERS macro for consistent bindings

Dirty Flag System (#116):
- Modify markDirty() to propagate up the parent chain
- Add isDirty() and clearDirty() methods for render optimization

Scene as Drawable (#118):
- Add position, visible, opacity properties to Scene
- Add setProperty()/getProperty() for animation support
- Apply scene transformations in PyScene::render()
- Fix lifecycle callbacks to clear errors when methods don't exist
- Add GameEngine::getScene() public accessor

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 16:33:17 -05:00
bfadab7486 Crypt of Sokoban - update mcrfpy API usage to recent changes 2025-11-27 07:43:03 -05:00
bbc744f8dc feat: Add self-contained venv support for pip packages (closes #137)
- Set sys.executable in PyConfig for subprocess/pip calls
- Detect sibling venv/ directory and prepend site-packages to sys.path
- Add mcrf_venv.py reference implementation for bootstrapping pip
- Supports both Linux (lib/python3.14/site-packages) and Windows (Lib/site-packages)

Usage: ./mcrogueface -m pip install numpy
Or via Python: mcrf_venv.pip_install("numpy")

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 22:01:09 -05:00
3f6ea4fe33 feat: Add ImGui-based developer console overlay
Integrates Dear ImGui for an in-game debug console that replaces the
blocking Python REPL. Press ~ (grave/tilde) to toggle the console.

Features:
- Python code execution without blocking the game loop
- Output capture with color coding (yellow=input, red=errors, gray=output)
- Expression results show repr() automatically
- Command history navigation with up/down arrows
- Word wrapping for long output lines
- Auto-scroll that doesn't fight manual scrolling
- mcrfpy.setDevConsole(bool) API to disable for shipping

Technical changes:
- Update imgui submodule to v1.89.9 (stable)
- Update imgui-sfml submodule to 2.6.x branch (SFML 2.x compatible)
- Add ImGui sources to CMakeLists.txt with OpenGL dependency
- Integrate ImGui lifecycle into GameEngine
- Add ImGuiConsole class for console overlay

closes #36, closes #65, closes #75

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 20:03:58 -05:00
8e2c603c54 fix: Update cpython submodule to v3.14.0 and fix flaky tests
- Update cpython submodule from v3.12.2 to v3.14.0
- Fix test_timer_object.py: Add delTimer call to prevent double-cancel
- Fix test_viewport_scaling.py: Handle headless mode for window resize

Test suite now achieves 100% pass rate (129/129 tests).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 18:43:32 -05:00
a703bce196 Merge branch 'origin/master' - combine double-execution fixes
Both branches fixed the --exec double-execution bug with complementary approaches:
- origin/master: Added executeStartupScripts() method for cleaner separation
- HEAD: Avoided engine recreation to preserve state

This merge keeps the best of both: executeStartupScripts() called on the
existing engine without recreation.

Also accepts deletion of flaky test_viewport_visual.py from origin/master.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 18:03:15 -05:00
28396b65c9 feat: Migrate to Python 3.14 (closes #135)
Replace deprecated Python C API calls with modern PyConfig-based initialization:
- PySys_SetArgvEx() -> PyConfig.argv (deprecated since 3.11)
- Py_InspectFlag -> PyConfig.inspect (deprecated since 3.12)

Fix critical memory safety bugs discovered during migration:
- PyColor::from_arg() and PyVector::from_arg() now return new references
  instead of borrowed references, preventing use-after-free when callers
  call Py_DECREF on the result
- GameEngine::testTimers() now holds a local shared_ptr copy during
  callback execution, preventing use-after-free when timer callbacks
  call delTimer() on themselves

Fix double script execution bug with --exec flag:
- Scripts were running twice because GameEngine constructor executed them,
  then main.cpp deleted and recreated the engine
- Now reuses existing engine and just sets auto_exit_after_exec flag

Update test syntax to use keyword arguments for Frame/Caption constructors.

Test results: 127/130 passing (97.7%)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 17:48:12 -05:00
ce0be78b73 fix: Resolve --exec double script execution bug
Scripts passed to --exec were executing twice because GameEngine
constructor ran scripts, and main.cpp created two GameEngine instances.

- Move exec_scripts from constructor to new executeStartupScripts() method
- Call executeStartupScripts() once after final engine setup in main.cpp
- Remove double-execution workarounds from tests
- Delete duplicate test_viewport_visual.py (flaky due to race condition)
- Fix test constructor syntax and callback signatures

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 13:20:22 -05:00
b173f59f22 docs: Add comprehensive build documentation
- BUILD_FROM_SOURCE.md: Complete guide for building from source
  - Quick build option using pre-built build_deps archive
  - Full build instructions for all dependencies
  - libtcod-headless integration (no SDL required)
  - Instructions for creating build_deps archives for releases
  - Troubleshooting section

- README.md: Add "Building from Source" section
  - Quick reference for common build scenarios
  - Links to full build guide

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 11:28:10 -05:00
8854d5b366 feat: Replace libtcod with libtcod-headless fork (closes #134)
Replace upstream libtcod with jmccardle/libtcod-headless fork that:
- Builds without SDL dependency (NO_SDL compile flag)
- Uses vendored dependencies (lodepng, utf8proc, stb)
- Provides all core algorithms (FOV, pathfinding, BSP, noise)

Changes:
- Update .gitmodules to use libtcod-headless (2.2.1-headless branch)
- Add NO_SDL compile definition to CMakeLists.txt
- Remove old libtcod submodule

Build instructions: deps/libtcod symlink should point to
modules/libtcod-headless/src/libtcod (configured during build setup)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 11:22:48 -05:00
19ded088b0 feat: Exit on first Python callback exception (closes #133)
By default, McRogueFace now exits with code 1 on the first unhandled
exception in timer, click, key, or animation callbacks. This prevents
repeated exception output that wastes resources in AI-driven development.

Changes:
- Add exit_on_exception config flag (default: true)
- Add --continue-after-exceptions CLI flag to preserve old behavior
- Update exception handlers in Timer, PyCallable, and Animation
- Signal game loop via McRFPy_API atomic flags
- Return proper exit code from main()

Before: Timer exceptions repeated 1000+ times until timeout
After: Single traceback, clean exit with code 1

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 10:26:30 -05:00
9028bf485e fix: Correct test to use del for index-based removal
The test was incorrectly using scene_ui.remove(-1) expecting it to
remove the element at index -1. However, Python's list.remove(x)
removes the first occurrence of VALUE x, not by index.

Changed to use `del scene_ui[-1]` which is the correct Python idiom
for removing an element by index.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 09:48:05 -05:00
f041a0c8ca feat: Add Vector convenience features - indexing, tuple comparison, floor
Implements issue #109 improvements to mcrfpy.Vector:

- Sequence protocol: v[0], v[1], v[-1], v[-2], len(v), tuple(v), x,y = v
- Tuple comparison: v == (5, 6), v != (1, 2) works bidirectionally
- .floor() method: returns new Vector with floored coordinates
- .int property: returns (int(floor(x)), int(floor(y))) tuple for dict keys

The sequence protocol enables unpacking and iteration, making Vector
interoperable with code expecting tuples. The tuple comparison fixes
compatibility issues where functions returning Vector broke code expecting
tuple comparison (e.g., in Crypt of Sokoban).

Closes #109

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 09:37:14 -05:00
afcb54d9fe fix: Make UICollection/EntityCollection match Python list semantics
Breaking change: UICollection.remove() now takes a value (element) instead
of an index, matching Python's list.remove() behavior.

New methods added to both UICollection and EntityCollection:
- pop([index]) -> element: Remove and return element at index (default: last)
- insert(index, element): Insert element at position

Semantic fixes:
- remove(element): Now removes first occurrence of element (was: remove by index)
- All methods now have docstrings documenting behavior

Note on z_index sorting: The collections are sorted by z_index before each
render. Using index-based operations (pop, insert) with non-default z_index
values may produce unexpected results. Use name-based .find() for stable
element access when z_index sorting is in use.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 08:08:43 -05:00
deb5d81ab6 feat: Add .find() method to UICollection and EntityCollection
Implements name-based search for UI elements and entities:
- Exact match returns single element or None
- Wildcard patterns (prefix*, *suffix, *contains*) return list
- Recursive search for nested Frame children (UICollection only)

API:
  ui.find("player_frame")           # exact match
  ui.find("enemy*")                 # starts with
  ui.find("*_button", recursive=True)  # recursive search
  grid.entities.find("*goblin*")    # entity search

Closes #41, closes #40

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 05:24:55 -05:00
51e96c0c6b fix: Refine geometry demos for 1024x768 and fix animations
- Fix timer restart when switching between animated demo scenes
- Update all demos from 800x600 to 1024x768 resolution
- Add screen_angle_between() for correct arc angles in screen coords
- Fix arc directions by accounting for screen Y inversion
- Reposition labels to avoid text overlaps
- Shift solar system center down to prevent moon orbit overflow
- Reposition ship/target in pathfinding demo to avoid sun clipping
- Scale menu screen to fill 1024x768 with wider buttons
- Regenerate all demo screenshots

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 04:54:13 -05:00
576481957a cleanup: remove partial tutorial 2025-11-26 04:53:31 -05:00
198686cba9 feat: Add geometry module demo system for orbital mechanics
Creates comprehensive visual demonstrations of the geometry module:

Static demos:
- Bresenham algorithms: circle/line rasterization on grid cells
- Angle calculations: line elements showing angles between points,
  waypoint viability with angle thresholds, orbit exit headings
- Pathfinding: planets with surfaces and orbit rings, optimal
  path using orbital slingshots vs direct path comparison

Animated demos:
- Solar system: planets orbiting star with discrete time steps,
  nested moon orbit, position updates every second
- Pathfinding through moving system: ship navigates to target
  using orbital intercepts, anticipating planetary motion

Includes 5 screenshot outputs demonstrating each feature.

Run: ./mcrogueface --headless --exec tests/geometry_demo/geometry_main.py
Interactive: ./mcrogueface tests/geometry_demo/geometry_main.py

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 00:46:38 -05:00
bc95cb1f0b feat: Add geometry module for orbital mechanics and spatial calculations
Implements issue #130 with:
- Basic utilities: distance, angle_between, normalize_angle, lerp, clamp
- Grid algorithms: bresenham_circle, bresenham_line, filled_circle
- OrbitalBody class with recursive positioning (star -> planet -> moon)
- OrbitingShip class for relative ship positioning on orbit rings
- Pathfinding helpers: nearest_orbit_entry, optimal_exit_heading,
  is_viable_waypoint, line_of_sight_blocked
- Comprehensive test suite (25+ tests)

Designed for Pinships turn-based space roguelike with:
- Discrete time steps (planets move in whole grid squares)
- Deterministic position projection
- Free orbital movement while in orbit
- Support for nested orbits (moons of moons)

closes #130

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 00:26:14 -05:00
e5e796bad9 refactor: comprehensive test suite overhaul and demo system
Major changes:
- Reorganized tests/ into unit/, integration/, regression/, benchmarks/, demo/
- Deleted 73 failing/outdated tests, kept 126 passing tests (100% pass rate)
- Created demo system with 6 feature screens (Caption, Frame, Primitives, Grid, Animation, Color)
- Updated .gitignore to track tests/ directory
- Updated CLAUDE.md with comprehensive testing guidelines and API quick reference

Demo system features:
- Interactive menu navigation (press 1-6 for demos, ESC to return)
- Headless screenshot generation for CI
- Per-feature demonstration screens with code examples

Testing infrastructure:
- tests/run_tests.py - unified test runner with timeout support
- tests/demo/demo_main.py - interactive/headless demo runner
- All tests are headless-compliant

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 23:37:05 -05:00
4d6808e34d feat: Add UIDrawable children collection to Grid
Grid now supports a `children` collection for arbitrary UIDrawable elements
(speech bubbles, effects, highlights, path visualization, etc.) that
automatically transform with the grid's camera (pan/zoom).

Key features:
- Children positioned in grid-world pixel coordinates
- Render after entities, before FOV overlay (proper z-ordering)
- Sorted by z_index, culled when outside visible region
- Click detection transforms through grid camera
- Automatically clipped to grid boundaries via RenderTexture

Python API:
  grid.children.append(caption)  # Speech bubble follows grid camera
  grid.children.append(circle)   # Highlight indicator

Closes #132

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 21:52:37 -05:00
311dc02f1d feat: Add UILine, UICircle, and UIArc drawing primitives
Implement new UIDrawable-derived classes for vector graphics:

- UILine: Thick line segments using sf::ConvexShape for proper thickness
  - Properties: start, end, color, thickness
  - Supports click detection along the line

- UICircle: Filled and outlined circles using sf::CircleShape
  - Properties: radius, center, fill_color, outline_color, outline
  - Full property system for animations

- UIArc: Arc segments for orbital paths and partial circles
  - Properties: center, radius, start_angle, end_angle, color, thickness
  - Uses sf::VertexArray with TriangleStrip for smooth rendering
  - Supports arbitrary angle spans including negative (reverse) arcs

All primitives integrate with the Python API through mcrfpy module:
- Added to PyObjectsEnum for type identification
- Full getter/setter support for all properties
- Added to UICollection for scene management
- Support for visibility, opacity, z_index, name, and click handling

closes #128, closes #129

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 21:42:33 -05:00
acef21593b workaround for gitea label API bugs 2025-11-03 10:59:56 -05:00
354107fc50 version bump for forgejo-mcp binary 2025-11-03 10:59:22 -05:00
8042630cca docs: add comprehensive Gitea label system documentation and MCP tool limitations
- Added complete label category breakdown (System, Priority, Type/Scope, Workflow)
- Documented all 22 labels with descriptions and usage guidelines
- Added example label combinations for common scenarios
- Documented MCP tool label application issues (see #131)
- Provided label ID reference for documentation purposes
- Strong recommendation to apply labels manually via web interface

Related to #131

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 09:23:05 -04:00
8f8b72da4a feat: auto-exit in --headless --exec mode when script completes
Closes #127

Previously, `./mcrogueface --headless --exec <script>` would hang
indefinitely after the script completed because the game loop ran
continuously. This required external timeouts and explicit mcrfpy.exit()
calls in every automation script.

This commit adds automatic exit detection for headless+exec mode:
- Added `auto_exit_after_exec` flag to McRogueFaceConfig
- Set flag automatically when both --headless and --exec are present
- Game loop exits when no timers remain (timers.empty())

Benefits:
- Documentation generation scripts work without explicit exit calls
- Testing scripts don't need timeout wrappers
- Clean process termination for automation
- Backward compatible (scripts with mcrfpy.exit() continue working)

Changes:
- src/McRogueFaceConfig.h: Add auto_exit_after_exec flag
- src/main.cpp: Set flag and recreate engine with modified config
- src/GameEngine.cpp: Check timers.empty() in game loop
- ROADMAP.md: Mark Phase 7 as complete (2025-10-30)
- CLAUDE.md: Add instruction about closing issues with commit messages

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 22:52:52 -04:00
4e94291cfb docs: Complete Phase 7 documentation system with parser fixes and man pages
Fixed critical documentation generation bugs and added complete multi-format
output support. All documentation now generates cleanly from MCRF_* macros.

## Parser Fixes (tools/generate_dynamic_docs.py)

Fixed parse_docstring() function:
- Added "Raises:" section support (was missing entirely)
- Fixed function name duplication in headings
  - Was: `createSoundBuffercreateSoundBuffer(...)`
  - Now: `createSoundBuffer(filename: str) -> int`
- Proper section separation between Returns and Raises
- Handles MCRF_* macro format correctly

Changes:
- Rewrote parse_docstring() to parse by section markers
- Fixed markdown generation (lines 514-539)
- Fixed HTML generation (lines 385-413, 446-473)
- Added "raises" field to parsed output dict

## Man Page Generation

New files:
- tools/generate_man_page.sh - Pandoc wrapper for man page generation
- docs/mcrfpy.3 - Unix man page (section 3 for library functions)

Uses pandoc with metadata:
- Section 3 (library functions)
- Git version tag in footer
- Current date in header

## Master Orchestration Script

New file: tools/generate_all_docs.sh

Single command generates all documentation formats:
- HTML API reference (docs/api_reference_dynamic.html)
- Markdown API reference (docs/API_REFERENCE_DYNAMIC.md)
- Unix man page (docs/mcrfpy.3)
- Type stubs (stubs/mcrfpy.pyi via generate_stubs_v2.py)

Includes error checking (set -e) and helpful output messages.

## Documentation Updates (CLAUDE.md)

Updated "Regenerating Documentation" section:
- Documents new ./tools/generate_all_docs.sh master script
- Lists all output files with descriptions
- Notes pandoc as system requirement
- Clarifies generate_stubs_v2.py is preferred (has @overload support)

## Type Stub Decision

Assessed generate_stubs.py vs generate_stubs_v2.py:
- generate_stubs.py has critical bugs (missing commas in method signatures)
- generate_stubs_v2.py produces high-quality manually-maintained stubs
- Decision: Keep v2, use it in master script

## Files Modified

Modified:
- CLAUDE.md (25 lines changed)
- tools/generate_dynamic_docs.py (121 lines changed)
- docs/api_reference_dynamic.html (359 lines changed)

Created:
- tools/generate_all_docs.sh (28 lines)
- tools/generate_man_page.sh (12 lines)
- docs/mcrfpy.3 (1070 lines)
- stubs/mcrfpy.pyi (532 lines)
- stubs/mcrfpy/__init__.pyi (213 lines)
- stubs/mcrfpy/automation.pyi (24 lines)
- stubs/py.typed (0 bytes)

Total: 2159 insertions, 225 deletions

## Testing

Verified:
- Man page viewable with `man docs/mcrfpy.3`
- No function name duplication in docs/API_REFERENCE_DYNAMIC.md
- Raises sections properly separated from Returns
- Master script successfully generates all formats

## Related Issues

Addresses requirements from Phase 7 documentation issues.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 21:20:50 -04:00
621d719c25 docs: Phase 3 - Convert 19 module functions to MCRF_FUNCTION macros
Converted all module-level functions in McRFPy_API.cpp to use the MCRF_*
documentation macro system:

Audio functions (7):
- createSoundBuffer, loadMusic, setMusicVolume, setSoundVolume
- playSound, getMusicVolume, getSoundVolume

Scene functions (5):
- sceneUI, currentScene, setScene, createScene, keypressScene

Timer functions (2):
- setTimer, delTimer

Utility functions (5):
- exit, setScale, find, findAll, getMetrics

Each function now uses:
- MCRF_SIG for signatures
- MCRF_DESC for descriptions
- MCRF_ARG for parameters
- MCRF_RETURNS for return values
- MCRF_RAISES for exceptions
- MCRF_NOTE for additional details

Phase 4 assessment: PyCallable.cpp and PythonObjectCache.cpp contain only
internal C++ implementation with no Python API to document.

All conversions tested and verified with test_phase3_docs.py.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 19:38:22 -04:00
29aa6e62be docs: convert Phase 2 classes to documentation macros (Animation, Window, SceneObject)
Converted 3 files to use MCRF_* documentation macros:
- PyAnimation.cpp: 5 methods + 5 properties
- PyWindow.cpp: 3 methods + 8 properties
- PySceneObject.cpp: 3 methods + 2 properties

All conversions build successfully. Enhanced descriptions with implementation details.

Note: PyScene.cpp has no exposed methods/properties, so no conversion needed.

Progress: Phase 1 (4 files) + Phase 2 (3 files) = 7 new classes complete

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 17:03:28 -04:00
67aba5ef1f docs: convert Phase 1 classes to documentation macros (Color, Font, Texture, Timer)
Converted 4 files to use MCRF_* documentation macros:
- PyColor.cpp: 3 methods + 4 properties
- PyFont.cpp: 2 properties (read-only)
- PyTexture.cpp: 6 properties (read-only)
- PyTimer.cpp: 4 methods + 7 properties

All conversions verified with test_phase1_docs.py - 0 placeholders.
Documentation regenerated with enhanced descriptions.

Progress: 11/12 class files complete (92%)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 16:59:40 -04:00
6aa4625b76 fix: correct module docstring newline escaping
Fixed module-level docstring in PyModuleDef where double-backslash newlines
(\\n) were appearing as literal "\n" text in help(mcrfpy) output.

Changed from escaped newlines (\\n) to actual newlines (\n) so the C compiler
interprets them correctly.

Before: help(mcrfpy) showed "McRogueFace Python API\\n\\nCore game..."
After:  help(mcrfpy) shows proper formatting with line breaks

The issue was in the PyDoc_STR() macro call - it doesn't interpret escape
sequences, so the string literal itself needs to have proper newlines.
2025-10-30 15:57:17 -04:00
4c61bee512 docs: update CLAUDE.md with MCRF_* macro documentation system
Updated documentation guidelines to reflect the new macro-based system:
- Documented MCRF_METHOD and MCRF_PROPERTY usage
- Listed all available macros (MCRF_SIG, MCRF_DESC, MCRF_ARG, etc.)
- Added prose guidelines (concise C++, verbose external docs)
- Updated regeneration workflow (removed references to deleted scripts)
- Emphasized single source of truth and zero-drift architecture

Removed references to obsolete hardcoded documentation scripts that were
deleted in previous commits.

Related: #92 (Inline C++ documentation system)
2025-10-30 12:37:04 -04:00
cc80964835 fix: update child class property overrides to use MCRF_PROPERTY macros
Fixes critical issue discovered in code review where PyDrawable property
docstrings were being overridden by child classes, making enhanced documentation
invisible to users.

Updated files:
- src/UIBase.h: UIDRAWABLE_GETSETTERS macro (visible, opacity)
- src/UIFrame.cpp: click and z_index properties
- src/UISprite.cpp: click and z_index properties
- src/UICaption.cpp: click and z_index properties
- src/UIGrid.cpp: click and z_index properties

All four UI class hierarchies (Frame, Sprite, Caption, Grid) now expose
consistent, enhanced property documentation to Python users.

Verification:
- tools/test_child_class_docstrings.py: All 16 property tests pass
- All 4 properties (click, z_index, visible, opacity) match across all 4 classes

Related: #92 (Inline C++ documentation system)
2025-10-30 12:33:27 -04:00
326b692908 feat: convert PyDrawable properties to documentation macros
All Drawable properties (click, z_index, visible, opacity) now
use MCRF_PROPERTY with enhanced descriptions.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 12:22:00 -04:00
dda5305256 feat: convert PyDrawable methods to documentation macros
Converts get_bounds, move, and resize to MCRF_METHOD.
These are inherited by all UI classes (Frame, Caption, Sprite, Grid).

Updated both PyDrawable.cpp and UIBase.h (UIDRAWABLE_METHODS macro).
All method docstrings now include complete Args, Returns, and Note sections.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 12:06:37 -04:00
1f6175bfa5 refactor: remove obsolete documentation generators
Removes ~3,979 lines of hardcoded Python documentation dictionaries.
Documentation is now generated from C++ docstrings via macros.

Deleted:
- generate_complete_api_docs.py (959 lines)
- generate_complete_markdown_docs.py (820 lines)
- generate_api_docs.py (481 lines)
- generate_api_docs_simple.py (118 lines)
- generate_api_docs_html.py (1,601 lines)

Addresses issue #97.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:51:21 -04:00
7f253da581 fix: escape HTML in descriptions before link transformation
Fixes HTML injection vulnerability in generate_dynamic_docs.py where
description text was not HTML-escaped before being inserted into HTML
output. Special characters like <, >, & could be interpreted as HTML.

Changes:
- Modified transform_doc_links() to escape all non-link text when
  format='html' or format='web'
- Link text and hrefs are also properly escaped
- Non-HTML formats (markdown, python) remain unchanged
- Added proper handling for descriptions with mixed plain text and links

The fix splits docstrings into link and non-link segments, escapes
non-link segments, and properly escapes content within link patterns.

Tested with comprehensive test suite covering:
- Basic HTML special characters
- Special chars with links
- Special chars in link text
- Multiple links with special chars

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:48:09 -04:00
fac6a9a457 feat: add link transformation to documentation generator
Adds transform_doc_links() function that converts MCRF_LINK patterns
to appropriate format (HTML links, Markdown links, or plain text).
Addresses issue #97.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:39:54 -04:00
a8a257eefc feat: convert PyVector properties to use macros
Properties x and y now use MCRF_PROPERTY for consistency.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:33:49 -04:00
07e8207a08 feat: complete PyVector documentation macro conversion
All Vector methods now use MCRF_METHOD macros with complete
documentation including Args, Returns, and Notes.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:27:50 -04:00
23d7882b93 fix: correct normalize() documentation to match implementation
The normalize() method's implementation returns a zero vector when
called on a zero-magnitude vector, rather than raising ValueError as
the documentation claimed. Updated the MCRF_RAISES to MCRF_NOTE to
accurately describe the actual behavior.

Also added test coverage in tools/test_vector_docs.py to verify the
normalize() docstring contains the correct Note section.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:25:43 -04:00
91461d0f87 feat: convert PyVector to use documentation macros
Converts magnitude, normalize, and dot methods to MCRF_METHOD macro.
Docstrings now include complete Args/Returns/Raises sections.
Addresses issue #92.
2025-10-30 11:20:48 -04:00
a08003bda4 feat: add documentation macro system header
Adds C++ preprocessor macros for consistent API documentation.
Addresses issue #92.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 11:16:44 -04:00
e41f83a5b3 docs: Complete wiki migration and issue labeling system
This commit completes a comprehensive documentation migration initiative
that transforms McRogueFace's documentation from scattered markdown files
into a structured, navigable wiki with systematic issue organization.

## Wiki Content Created (20 pages)

**Navigation & Indices:**
- Home page with 3 entry points (by system, use-case, workflow)
- Design Proposals index
- Issue Roadmap (46 issues organized by tier/system)

**System Documentation (5 pages):**
- Grid System (3-layer architecture: Visual/World/Perspective)
- Animation System (property-based, 24+ easing functions)
- Python Binding System (C++/Python integration patterns)
- UI Component Hierarchy (UIDrawable inheritance tree)
- Performance and Profiling (ScopedTimer, F3 overlay)

**Workflow Guides (3 pages):**
- Adding Python Bindings (step-by-step tutorial)
- Performance Optimization Workflow (profile → optimize → verify)
- Writing Tests (direct execution vs game loop tests)

**Use-Case Documentation (5 pages):**
- Entity Management
- Rendering
- AI and Pathfinding
- Input and Events
- Procedural Generation

**Grid System Deep Dives (3 pages):**
- Grid Rendering Pipeline (4-stage process)
- Grid-TCOD Integration (FOV, pathfinding)
- Grid Entity Lifecycle (5 states, memory management)

**Strategic Documentation (2 pages):**
- Proposal: Next-Gen Grid-Entity System (consolidated from 3 files)
- Strategic Direction (extracted from FINAL_RECOMMENDATIONS.md)

## Issue Organization System

Created 14 new labels across 3 orthogonal dimensions:

**System Labels (8):**
- system:grid, system:animation, system:python-binding
- system:ui-hierarchy, system:performance, system:rendering
- system:input, system:documentation

**Priority Labels (3):**
- priority:tier1-active (18 issues) - Critical path to v1.0
- priority:tier2-foundation (11 issues) - Important but not blocking
- priority:tier3-future (17 issues) - Deferred until after v1.0

**Workflow Labels (3):**
- workflow:blocked - Waiting on dependencies
- workflow:needs-benchmark - Needs performance testing
- workflow:needs-documentation - Needs docs before/after implementation

All 46 open issues now labeled with appropriate system/priority/workflow tags.

## Documentation Updates

**README.md:**
- Updated Documentation section to reference Gitea wiki
- Added key wiki page links (Home, Grid System, Python Binding, etc.)
- Updated Contributing section with issue tracking information
- Documented label taxonomy and Issue Roadmap

**Analysis Files:**
- Moved 17 completed analysis files to .archive/ directory:
  - EVAL_*.md (5 files) - Strategic analysis
  - TOPICS_*.md (4 files) - Task analysis
  - NEXT_GEN_GRIDS_ENTITIES_*.md (3 files) - Design proposals
  - FINAL_RECOMMENDATIONS.md, MASTER_TASK_SCHEDULE.md
  - PROJECT_THEMES_ANALYSIS.md, ANIMATION_FIX_IMPLEMENTATION.md
  - compass_artifact_*.md - Research artifacts

## Benefits

This migration provides:
1. **Agent-friendly documentation** - Structured for LLM context management
2. **Multiple navigation paths** - By system, use-case, or workflow
3. **Dense cross-referencing** - Wiki pages link to related content
4. **Systematic issue organization** - Filterable by system AND priority
5. **Living documentation** - Wiki can evolve with the codebase
6. **Clear development priorities** - Tier 1/2/3 system guides focus

Wiki URL: https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 20:54:55 -04:00
5205b5d7cd docs: Add Gitea-first workflow guidelines to project documentation
Establish Gitea as the single source of truth for issue tracking,
documentation, and project management to improve development efficiency.

CLAUDE.md changes:
- Add comprehensive "Gitea-First Workflow" section at top of file
- Document 5 core principles for using Gitea effectively
- Provide workflow pattern diagram for development process
- List available Gitea MCP tools for programmatic access
- Explain benefits: reduced context switching, better planning, living docs

ROADMAP.md changes:
- Add "Development Workflow" section referencing Gitea-first approach
- Include 5-step checklist for starting any work
- Link to detailed workflow guidelines in CLAUDE.md
- Emphasize Gitea as single source of truth

Workflow principles:
1. Always check Gitea issues/wiki before starting work
2. Create granular, focused issues for new features/problems
3. Document as you go - update related issues when work affects them
4. If docs mislead, create task to correct/expand them
5. Cross-reference everything - commits, issues, wiki pages

Benefits:
- Avoid re-reading entire codebase by consulting brief issue descriptions
- Reduce duplicate or contradictory work through better planning
- Maintain living documentation that stays current
- Capture historical context and decision rationale
- Improve efficiency using MCP tools for programmatic queries

This establishes best practices for keeping the project organized and
reducing cognitive load during development.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 00:56:21 -04:00
3c20a6be50 docs: Streamline ROADMAP.md and defer to Gitea issue tracking
Removed stale data and duplicate tracking from ROADMAP.md to establish
Gitea as the single source of truth for issue tracking.

Changes:
- Removed outdated urgent priorities from July 2025 (now October)
- Removed extensive checkbox task lists that duplicate Gitea issues
- Removed "Recent Achievements" changelog (use git log instead)
- Removed dated commentary and out-of-sync issue statuses
- Streamlined from 936 lines to 207 lines (~78% reduction)

Kept strategic content:
- Engine philosophy and architecture goals
- Three-layer grid architecture decisions
- Performance optimization patterns
- Development phase summaries with Gitea issue references
- Future vision: Pure Python extension architecture
- Resource links to Gitea issue tracker

The roadmap now focuses on strategic vision and architecture decisions,
while deferring all task tracking, bug reports, and current priorities
to the Gitea issue tracker.

Related: All issue status tracking moved to Gitea
See: https://gamedev.ffwf.net/gitea/john/McRogueFace/issues

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 00:46:17 -04:00
e9e9cd2f81 feat: Add comprehensive profiling system with F3 overlay
Add real-time performance profiling infrastructure to monitor frame times,
render performance, and identify bottlenecks.

Features:
- Profiler.h: ScopedTimer RAII helper for automatic timing measurements
- ProfilerOverlay: F3-togglable overlay displaying real-time metrics
- Detailed timing breakdowns: grid rendering, entity rendering, FOV,
  Python callbacks, and animation updates
- Per-frame counters: cells rendered, entities rendered, draw calls
- Performance color coding: green (<16ms), yellow (<33ms), red (>33ms)
- Benchmark suite: static grid and moving entities performance tests

Integration:
- GameEngine: Integrated profiler overlay with F3 toggle
- UIGrid: Added timing instrumentation for grid and entity rendering
- Metrics tracked in ProfilingMetrics struct with 60-frame averaging

Usage:
- Press F3 in-game to toggle profiler overlay
- Run benchmarks with tests/benchmark_*.py scripts
- ScopedTimer automatically measures code block execution time

This addresses issue #104 (Basic profiling/metrics).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 00:45:44 -04:00
8153fd2503 Merge branch 'rogueliketutorial25' - TCOD Tutorial Implementation
This merge brings in the complete TCOD-style tutorial implementation
for McRogueFace, demonstrating the engine as a viable alternative to
python-tcod for roguelike game development.

Key additions:
- Tutorial parts 0-6 with full documentation
- EntityCollection.remove() API improvement (object-based vs index-based)
- Development tooling scripts (test runner, issue tracking)
- Complete API reference documentation

Tutorial follows "forward-only" philosophy where each step builds
on previous work without requiring refactoring, making it more
accessible for beginners.

This work represents 2+ months of development (July-August 2025)
focused on validating McRogueFace's educational value and TCOD
compatibility.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 13:19:50 -04:00
8b7ea544dd docs: Add complete API reference documentation
Add comprehensive HTML API reference documentation covering
all McRogueFace Python API components, methods, and properties.

This documentation was generated from the C++ inline docstrings
and provides complete reference material for engine users.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 13:19:36 -04:00
3a9f76d850 feat: Add development tooling scripts
Add utility scripts for development workflow:
- tests/run_all_tests.sh: Test runner script for automated testing
- tools/gitea_issues.py: Issue tracking integration tool

These support the development and testing workflow for McRogueFace.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 13:19:25 -04:00
10610db86e feat: Add tutorial Python implementations
Add Python code for tutorial parts 0-6:
- part_0.py: Initial setup and character rendering
- part_1.py, part_1b.py: Movement systems
- part_2.py variants: Movement with naive, queued, and final implementations
- part_3.py: Dungeon generation with BSP
- part_4.py: Field of view implementation
- part_5.py: Enemy entities and basic interaction
- part_6.py: Combat mechanics
- _generated_part_5.py: Machine-generated draft for reference

These implementations demonstrate McRogueFace capabilities
and serve as foundation for tutorial documentation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 13:18:56 -04:00
a76ebcd05a feat: Add tutorial parts 0-6 with documentation
Add working tutorial implementations covering:
- Part 0: Basic setup and character display
- Part 1: Movement and grid interaction
- Part 2: Movement variations (naive, queued, final)
- Part 3: Dungeon generation
- Part 4: Field of View
- Part 5: Entities and interactions
- Part 6: Combat system

Each part includes corresponding README with explanations.
Implementation plan document included for parts 6-8.

Tutorial follows "forward-only" philosophy - each step builds
on previous without requiring refactoring.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 13:18:45 -04:00
327da3622a feat: Change EntityCollection.remove() to accept Entity objects
Previously, EntityCollection.remove() required an integer index, which was
inconsistent with Python's list.remove(item) behavior and the broader
Python ecosystem conventions.

Changes:
- remove() now accepts Entity object directly instead of index
- Searches collection by comparing C++ shared_ptr identity
- Raises ValueError if entity not found in collection
- More Pythonic API matching Python's list.remove() semantics

This aligns with Issue #73 and improves API consistency across the
collection system.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 13:17:45 -04:00
604 changed files with 1830749 additions and 26081 deletions

15
.gitignore vendored
View file

@ -8,26 +8,31 @@ PCbuild
obj
build
lib
obj
__pycache__
.cache/
7DRL2025 Release/
CMakeFiles/
Makefile
*.md
*.zip
__lib/
__lib_windows/
build-windows/
build_windows/
_oldscripts/
assets/
cellular_automata_fire/
*.txt
deps/
fetch_issues_txt.py
forest_fire_CA.py
mcrogueface.github.io
scripts/
test_*
tcod_reference
.archive
.mcp.json
dist/
# Keep important documentation and tests
!CLAUDE.md
!README.md
!tests/

7
.gitmodules vendored
View file

@ -10,6 +10,7 @@
[submodule "modules/SFML"]
path = modules/SFML
url = git@github.com:SFML/SFML.git
[submodule "modules/libtcod"]
path = modules/libtcod
url = git@github.com:libtcod/libtcod.git
[submodule "modules/libtcod-headless"]
path = modules/libtcod-headless
url = git@github.com:jmccardle/libtcod-headless.git
branch = 2.2.1-headless

306
BUILD_FROM_SOURCE.md Normal file
View file

@ -0,0 +1,306 @@
# 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 Normal file
View file

@ -0,0 +1,847 @@
# 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,53 +8,155 @@ project(McRogueFace)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# Add include directories
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux)
include_directories(${CMAKE_SOURCE_DIR}/deps)
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux/Python-3.11.1)
include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
# 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()
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
# Add include directories
include_directories(${CMAKE_SOURCE_DIR}/deps)
include_directories(SYSTEM ${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
)
# 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
set(LINK_LIBS
if(MCRF_CROSS_WINDOWS)
# MinGW cross-compilation: use full library names
set(LINK_LIBS
sfml-graphics
sfml-window
sfml-system
sfml-audio
tcod)
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
)
# On Windows, add any additional libs and include directories
if(WIN32)
# Windows-specific Python library name (no dots)
list(APPEND LINK_LIBS python312)
# 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)
else()
# Unix/Linux specific libraries
list(APPEND LINK_LIBS python3.12 m dl util pthread)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
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)
# 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()
# Define the executable target before linking libraries
add_executable(mcrogueface ${SOURCES})
# On Windows, set subsystem to WINDOWS to hide console
if(WIN32)
# 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
@ -76,20 +178,36 @@ add_custom_command(TARGET mcrogueface POST_BUILD
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
# On Windows, copy DLLs to executable directory
if(WIN32)
# Copy all DLL files from lib to the 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")
# Alternative: Copy specific DLLs if you want more control
# file(GLOB DLLS "${CMAKE_SOURCE_DIR}/__lib/*.dll")
# foreach(DLL ${DLLS})
# add_custom_command(TARGET mcrogueface POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E copy_if_different
# ${DLL} $<TARGET_FILE_DIR:mcrogueface>)
# endforeach()
endif()
# rpath for including shared libraries (Linux/Unix only)

View file

@ -1,54 +0,0 @@
# Convenience Makefile wrapper for McRogueFace
# This delegates to CMake build in the build directory
.PHONY: all build clean run test dist help
# Default target
all: build
# Build the project
build:
@./build.sh
# Clean build artifacts
clean:
@./clean.sh
# Run the game
run: build
@cd build && ./mcrogueface
# Run in Python mode
python: build
@cd build && ./mcrogueface -i
# Test basic functionality
test: build
@echo "Testing McRogueFace..."
@cd build && ./mcrogueface -V
@cd build && ./mcrogueface -c "print('Test passed')"
@cd build && ./mcrogueface --headless -c "import mcrfpy; print('mcrfpy imported successfully')"
# Create distribution archive
dist: build
@echo "Creating distribution archive..."
@cd build && zip -r ../McRogueFace-$$(date +%Y%m%d).zip . -x "*.o" "CMakeFiles/*" "Makefile" "*.cmake"
@echo "Distribution archive created: McRogueFace-$$(date +%Y%m%d).zip"
# Show help
help:
@echo "McRogueFace Build System"
@echo "======================="
@echo ""
@echo "Available targets:"
@echo " make - Build the project (default)"
@echo " make build - Build the project"
@echo " make clean - Remove all build artifacts"
@echo " make run - Build and run the game"
@echo " make python - Build and run in Python interactive mode"
@echo " make test - Run basic tests"
@echo " make dist - Create distribution archive"
@echo " make help - Show this help message"
@echo ""
@echo "Build output goes to: ./build/"
@echo "Distribution archives are created in project root"

View file

@ -35,13 +35,50 @@ 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
mcrfpy.createScene("intro")
intro = mcrfpy.Scene("intro")
# Add a text caption
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
@ -49,26 +86,36 @@ caption.size = 48
caption.fill_color = (255, 255, 255)
# Add to scene
mcrfpy.sceneUI("intro").append(caption)
intro.children.append(caption)
# Switch to the scene
mcrfpy.setScene("intro")
intro.activate()
```
## Documentation
### 📚 Full Documentation Site
### 📚 Developer Documentation
For comprehensive documentation, tutorials, and API reference, visit:
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
For comprehensive documentation about systems, architecture, and development workflows:
The documentation site includes:
**[Project Wiki](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki)**
- **[Quickstart Guide](https://mcrogueface.github.io/quickstart)** - Get running in 5 minutes
- **[McRogueFace Does The Entire Roguelike Tutorial](https://mcrogueface.github.io/tutorials)** - Step-by-step game building
- **[Complete API Reference](https://mcrogueface.github.io/api)** - Every function documented
- **[Cookbook](https://mcrogueface.github.io/cookbook)** - Ready-to-use code recipes
- **[C++ Extension Guide](https://mcrogueface.github.io/extending-cpp)** - For C++ developers: Add engine features
Key wiki pages:
- **[Home](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Home)** - Documentation hub with multiple entry points
- **[Grid System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Grid-System)** - Three-layer grid architecture
- **[Python Binding System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Python-Binding-System)** - C++/Python integration
- **[Performance and Profiling](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Performance-and-Profiling)** - Optimization tools
- **[Adding Python Bindings](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Adding-Python-Bindings)** - Step-by-step binding guide
- **[Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap)** - All open issues organized by system
### 📖 Development Guides
In the repository root:
- **[CLAUDE.md](CLAUDE.md)** - Build instructions, testing guidelines, common tasks
- **[ROADMAP.md](ROADMAP.md)** - Strategic vision and development phases
- **[roguelike_tutorial/](roguelike_tutorial/)** - Complete roguelike tutorial implementations
## Build Requirements
@ -114,7 +161,15 @@ If you are writing a game in Python using McRogueFace, you only need to rename a
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.
The project has a private roadmap and issue list. Reach out via email or social media if you have bugs or feature requests.
### Issue Tracking
The project uses [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for task tracking and bug reports. Issues are organized with labels:
- **System labels** (grid, animation, python-binding, etc.) - identify which codebase area
- **Priority labels** (tier1-active, tier2-foundation, tier3-future) - development timeline
- **Type labels** (Major Feature, Minor Feature, Bugfix, etc.) - effort and scope
See the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap) on the wiki for organized view of all open tasks.
## License

223
ROADMAP.md Normal file
View file

@ -0,0 +1,223 @@
# 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).*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,34 @@
# 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,12 +1,12 @@
#ifndef __PLATFORM
#define __PLATFORM
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 0
#include <Windows.h>
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
#include <windows.h>
std::wstring executable_path()
{
wchar_t buffer[MAX_PATH];
GetModuleFileName(NULL, buffer, MAX_PATH);
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
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];
GetModuleFileName(NULL, buffer, MAX_PATH);
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
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

View file

@ -0,0 +1,13 @@
"""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

@ -0,0 +1,12 @@
"""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

@ -0,0 +1,45 @@
"""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

@ -0,0 +1,11 @@
"""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

@ -0,0 +1,14 @@
"""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

@ -0,0 +1,82 @@
"""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

@ -0,0 +1,15 @@
"""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

@ -0,0 +1,14 @@
"""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

@ -0,0 +1,56 @@
"""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

@ -0,0 +1,16 @@
"""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

@ -0,0 +1,12 @@
"""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

@ -0,0 +1,45 @@
"""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

@ -0,0 +1,118 @@
"""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

@ -0,0 +1,61 @@
"""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

@ -0,0 +1,41 @@
"""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

@ -0,0 +1,85 @@
"""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

@ -0,0 +1,25 @@
"""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

@ -0,0 +1,42 @@
"""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

@ -0,0 +1,65 @@
"""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

@ -0,0 +1,166 @@
"""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

@ -0,0 +1,38 @@
"""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

@ -0,0 +1,58 @@
"""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

@ -0,0 +1,74 @@
"""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

@ -0,0 +1,74 @@
"""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

@ -0,0 +1,23 @@
"""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

@ -0,0 +1,31 @@
"""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

@ -0,0 +1,44 @@
"""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

@ -0,0 +1,125 @@
"""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

@ -0,0 +1,148 @@
"""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

@ -0,0 +1,20 @@
"""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

@ -0,0 +1,114 @@
"""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

@ -0,0 +1,38 @@
"""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

@ -0,0 +1,120 @@
"""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

@ -0,0 +1,43 @@
"""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

@ -0,0 +1,123 @@
"""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

@ -0,0 +1,108 @@
"""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

@ -0,0 +1,53 @@
"""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

@ -0,0 +1,159 @@
"""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

@ -0,0 +1,54 @@
"""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

@ -0,0 +1,27 @@
"""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

@ -0,0 +1,69 @@
"""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

@ -0,0 +1,78 @@
"""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

@ -0,0 +1,65 @@
"""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

@ -0,0 +1,80 @@
"""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)

1528
docs/mcrfpy.3 Normal file

File diff suppressed because it is too large Load diff

289
docs/templates/complete/ai.py vendored Normal file
View file

@ -0,0 +1,289 @@
"""
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

187
docs/templates/complete/combat.py vendored Normal file
View file

@ -0,0 +1,187 @@
"""
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

210
docs/templates/complete/constants.py vendored Normal file
View file

@ -0,0 +1,210 @@
"""
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"

298
docs/templates/complete/dungeon.py vendored Normal file
View file

@ -0,0 +1,298 @@
"""
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

319
docs/templates/complete/entities.py vendored Normal file
View file

@ -0,0 +1,319 @@
"""
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)

313
docs/templates/complete/game.py vendored Normal file
View file

@ -0,0 +1,313 @@
"""
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()

232
docs/templates/complete/turns.py vendored Normal file
View file

@ -0,0 +1,232 @@
"""
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))

330
docs/templates/complete/ui.py vendored Normal file
View file

@ -0,0 +1,330 @@
"""
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

176
docs/templates/minimal/game.py vendored Normal file
View file

@ -0,0 +1,176 @@
"""
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()

138
docs/templates/roguelike/constants.py vendored Normal file
View file

@ -0,0 +1,138 @@
"""
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,
},
}

340
docs/templates/roguelike/dungeon.py vendored Normal file
View file

@ -0,0 +1,340 @@
"""
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

364
docs/templates/roguelike/entities.py vendored Normal file
View file

@ -0,0 +1,364 @@
"""
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

290
docs/templates/roguelike/game.py vendored Normal file
View file

@ -0,0 +1,290 @@
"""
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

@ -0,0 +1,30 @@
"""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

@ -0,0 +1,119 @@
"""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

@ -0,0 +1,206 @@
"""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

@ -0,0 +1,356 @@
"""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

@ -0,0 +1,363 @@
"""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!")

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