From a1b692bb1fb642d73da87c6fe58843a13590f65b Mon Sep 17 00:00:00 2001 From: Frick Date: Thu, 15 Jan 2026 04:06:24 +0000 Subject: [PATCH] Add cookbook and tutorial showcase demos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/demo/: - cookbook_showcase.py: Interactive demo of cookbook recipes - tutorial_showcase.py: Visual walkthrough of tutorial content - tutorial_screenshots.py: Automated screenshot generation - new_features_showcase.py: Demo of modern API features - procgen_showcase.py: Procedural generation examples - simple_showcase.py: Minimal working examples Created during docs modernization to verify cookbook examples work. 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude --- tests/demo/cookbook_showcase.py | 495 ++++++++++++++++++++++++++++ tests/demo/new_features_showcase.py | 255 ++++++++++++++ tests/demo/procgen_showcase.py | 286 ++++++++++++++++ tests/demo/simple_showcase.py | 103 ++++++ tests/demo/tutorial_screenshots.py | 169 ++++++++++ tests/demo/tutorial_showcase.py | 426 ++++++++++++++++++++++++ 6 files changed, 1734 insertions(+) create mode 100644 tests/demo/cookbook_showcase.py create mode 100644 tests/demo/new_features_showcase.py create mode 100644 tests/demo/procgen_showcase.py create mode 100644 tests/demo/simple_showcase.py create mode 100644 tests/demo/tutorial_screenshots.py create mode 100644 tests/demo/tutorial_showcase.py diff --git a/tests/demo/cookbook_showcase.py b/tests/demo/cookbook_showcase.py new file mode 100644 index 0000000..9095414 --- /dev/null +++ b/tests/demo/cookbook_showcase.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 +""" +Cookbook Screenshot Showcase - Visual examples for cookbook recipes! + +Generates beautiful screenshots for cookbook pages. +Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/cookbook_showcase.py + +In headless mode, automation.screenshot() is SYNCHRONOUS - no timer dance needed! +""" +import mcrfpy +from mcrfpy import automation +import sys +import os + +# Output directory - in the docs site images folder +OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook" + +# Tile sprites from the labeled tileset +TILES = { + 'player_knight': 84, + 'player_mage': 85, + 'player_rogue': 86, + 'player_warrior': 87, + 'enemy_slime': 108, + 'enemy_orc': 120, + 'enemy_skeleton': 123, + 'floor_stone': 42, + 'wall_stone': 30, + 'wall_brick': 14, + 'torch': 72, + 'chest_closed': 89, + 'item_potion': 113, +} + + +def screenshot_health_bar(): + """Create a health bar showcase.""" + scene = mcrfpy.Scene("health_bar") + + # Dark background + bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) + bg.fill_color = mcrfpy.Color(20, 20, 30) + scene.children.append(bg) + + # Title + title = mcrfpy.Caption(text="Health Bar Recipe", pos=(50, 30)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.font_size = 28 + scene.children.append(title) + + subtitle = mcrfpy.Caption(text="Nested frames for dynamic UI elements", pos=(50, 60)) + subtitle.fill_color = mcrfpy.Color(180, 180, 200) + subtitle.font_size = 16 + scene.children.append(subtitle) + + # Example health bars at different levels + y_start = 120 + bar_configs = [ + ("Player - Full Health", 100, 100, mcrfpy.Color(50, 200, 50)), + ("Player - Damaged", 65, 100, mcrfpy.Color(200, 200, 50)), + ("Player - Critical", 20, 100, mcrfpy.Color(200, 50, 50)), + ("Boss - 3/4 Health", 750, 1000, mcrfpy.Color(150, 50, 150)), + ] + + for i, (label, current, maximum, color) in enumerate(bar_configs): + y = y_start + i * 100 + + # Label + lbl = mcrfpy.Caption(text=label, pos=(50, y)) + lbl.fill_color = mcrfpy.Color(220, 220, 220) + lbl.font_size = 18 + scene.children.append(lbl) + + # Background bar + bar_bg = mcrfpy.Frame(pos=(50, y + 30), size=(400, 30)) + bar_bg.fill_color = mcrfpy.Color(40, 40, 50) + bar_bg.outline = 2 + bar_bg.outline_color = mcrfpy.Color(80, 80, 100) + scene.children.append(bar_bg) + + # Fill bar (scaled to current/maximum) + fill_width = int(400 * (current / maximum)) + bar_fill = mcrfpy.Frame(pos=(50, y + 30), size=(fill_width, 30)) + bar_fill.fill_color = color + scene.children.append(bar_fill) + + # Text overlay + hp_text = mcrfpy.Caption(text=f"{current}/{maximum}", pos=(60, y + 35)) + hp_text.fill_color = mcrfpy.Color(255, 255, 255) + hp_text.font_size = 16 + scene.children.append(hp_text) + + scene.activate() + output_path = os.path.join(OUTPUT_DIR, "ui_health_bar.png") + automation.screenshot(output_path) + print(f" -> {output_path}") + + +def screenshot_fog_of_war(): + """Create a fog of war showcase.""" + scene = mcrfpy.Scene("fog_of_war") + + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + grid = mcrfpy.Grid( + pos=(50, 80), + size=(700, 480), + grid_size=(16, 12), + texture=texture, + zoom=2.8 + ) + grid.fill_color = mcrfpy.Color(0, 0, 0) # Black for unknown areas + scene.children.append(grid) + + # Title + title = mcrfpy.Caption(text="Fog of War Recipe", pos=(50, 20)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.font_size = 28 + scene.children.append(title) + + subtitle = mcrfpy.Caption(text="Visible, discovered, and unknown areas", pos=(50, 50)) + subtitle.fill_color = mcrfpy.Color(180, 180, 200) + subtitle.font_size = 16 + scene.children.append(subtitle) + + # Fill floor + for y in range(12): + for x in range(16): + grid.at(x, y).tilesprite = TILES['floor_stone'] + + # Add walls + for x in range(16): + grid.at(x, 0).tilesprite = TILES['wall_stone'] + grid.at(x, 11).tilesprite = TILES['wall_stone'] + for y in range(12): + grid.at(0, y).tilesprite = TILES['wall_stone'] + grid.at(15, y).tilesprite = TILES['wall_stone'] + + # Interior walls (to break LOS) + for y in range(3, 8): + grid.at(8, y).tilesprite = TILES['wall_brick'] + + # Player (mage with light) + player = mcrfpy.Entity(grid_pos=(4, 6), texture=texture, sprite_index=TILES['player_mage']) + grid.entities.append(player) + + # Hidden enemies on the other side + enemy1 = mcrfpy.Entity(grid_pos=(12, 4), texture=texture, sprite_index=TILES['enemy_orc']) + grid.entities.append(enemy1) + enemy2 = mcrfpy.Entity(grid_pos=(13, 8), texture=texture, sprite_index=TILES['enemy_skeleton']) + grid.entities.append(enemy2) + + # Torch in visible area + torch = mcrfpy.Entity(grid_pos=(2, 3), texture=texture, sprite_index=TILES['torch']) + grid.entities.append(torch) + + grid.center = (4 * 16 + 8, 6 * 16 + 8) + + scene.activate() + output_path = os.path.join(OUTPUT_DIR, "grid_fog_of_war.png") + automation.screenshot(output_path) + print(f" -> {output_path}") + + +def screenshot_combat_melee(): + """Create a melee combat showcase.""" + scene = mcrfpy.Scene("combat_melee") + + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + grid = mcrfpy.Grid( + pos=(50, 80), + size=(700, 480), + grid_size=(12, 9), + texture=texture, + zoom=3.5 + ) + grid.fill_color = mcrfpy.Color(20, 20, 30) + scene.children.append(grid) + + # Title + title = mcrfpy.Caption(text="Melee Combat Recipe", pos=(50, 20)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.font_size = 28 + scene.children.append(title) + + subtitle = mcrfpy.Caption(text="Bump-to-attack mechanics with damage calculation", pos=(50, 50)) + subtitle.fill_color = mcrfpy.Color(180, 180, 200) + subtitle.font_size = 16 + scene.children.append(subtitle) + + # Fill with dirt floor (battle arena feel) + for y in range(9): + for x in range(12): + grid.at(x, y).tilesprite = 50 # dirt + + # Brick walls + for x in range(12): + grid.at(x, 0).tilesprite = TILES['wall_brick'] + grid.at(x, 8).tilesprite = TILES['wall_brick'] + for y in range(9): + grid.at(0, y).tilesprite = TILES['wall_brick'] + grid.at(11, y).tilesprite = TILES['wall_brick'] + + # Player knight engaging orc! + player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_knight']) + grid.entities.append(player) + + enemy = mcrfpy.Entity(grid_pos=(6, 4), texture=texture, sprite_index=TILES['enemy_orc']) + grid.entities.append(enemy) + + # Fallen enemy (bones) + bones = mcrfpy.Entity(grid_pos=(8, 6), texture=texture, sprite_index=75) # bones + grid.entities.append(bones) + + # Potion for healing + potion = mcrfpy.Entity(grid_pos=(3, 2), texture=texture, sprite_index=TILES['item_potion']) + grid.entities.append(potion) + + grid.center = (5 * 16 + 8, 4 * 16 + 8) + + # Combat log UI + log_frame = mcrfpy.Frame(pos=(50, 520), size=(700, 60)) + log_frame.fill_color = mcrfpy.Color(30, 30, 40, 220) + log_frame.outline = 1 + log_frame.outline_color = mcrfpy.Color(60, 60, 80) + scene.children.append(log_frame) + + msg1 = mcrfpy.Caption(text="You hit the Orc for 8 damage!", pos=(10, 10)) + msg1.fill_color = mcrfpy.Color(255, 200, 100) + msg1.font_size = 14 + log_frame.children.append(msg1) + + msg2 = mcrfpy.Caption(text="The Orc hits you for 4 damage!", pos=(10, 30)) + msg2.fill_color = mcrfpy.Color(255, 100, 100) + msg2.font_size = 14 + log_frame.children.append(msg2) + + scene.activate() + output_path = os.path.join(OUTPUT_DIR, "combat_melee.png") + automation.screenshot(output_path) + print(f" -> {output_path}") + + +def screenshot_dungeon_generator(): + """Create a dungeon generator showcase.""" + scene = mcrfpy.Scene("dungeon_gen") + + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + grid = mcrfpy.Grid( + pos=(50, 80), + size=(700, 480), + grid_size=(24, 16), + texture=texture, + zoom=2.0 + ) + grid.fill_color = mcrfpy.Color(10, 10, 15) + scene.children.append(grid) + + # Title + title = mcrfpy.Caption(text="Dungeon Generator Recipe", pos=(50, 20)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.font_size = 28 + scene.children.append(title) + + subtitle = mcrfpy.Caption(text="Procedural rooms connected by corridors", pos=(50, 50)) + subtitle.fill_color = mcrfpy.Color(180, 180, 200) + subtitle.font_size = 16 + scene.children.append(subtitle) + + # Fill with walls + for y in range(16): + for x in range(24): + grid.at(x, y).tilesprite = TILES['wall_stone'] + + # Carve rooms + rooms = [ + (2, 2, 6, 5), # Room 1 + (10, 2, 7, 5), # Room 2 + (18, 3, 5, 4), # Room 3 + (2, 9, 5, 5), # Room 4 + (10, 10, 6, 5), # Room 5 + (18, 9, 5, 6), # Room 6 + ] + + for rx, ry, rw, rh in rooms: + for y in range(ry, ry + rh): + for x in range(rx, rx + rw): + if x < 24 and y < 16: + grid.at(x, y).tilesprite = TILES['floor_stone'] + + # Carve corridors (horizontal and vertical) + # Room 1 to Room 2 + for x in range(7, 11): + grid.at(x, 4).tilesprite = 50 # dirt corridor + # Room 2 to Room 3 + for x in range(16, 19): + grid.at(x, 4).tilesprite = 50 + # Room 1 to Room 4 + for y in range(6, 10): + grid.at(4, y).tilesprite = 50 + # Room 2 to Room 5 + for y in range(6, 11): + grid.at(13, y).tilesprite = 50 + # Room 3 to Room 6 + for y in range(6, 10): + grid.at(20, y).tilesprite = 50 + # Room 5 to Room 6 + for x in range(15, 19): + grid.at(x, 12).tilesprite = 50 + + # Add player in first room + player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_knight']) + grid.entities.append(player) + + # Add decorations + grid.entities.append(mcrfpy.Entity(grid_pos=(3, 3), texture=texture, sprite_index=TILES['torch'])) + grid.entities.append(mcrfpy.Entity(grid_pos=(12, 4), texture=texture, sprite_index=TILES['torch'])) + grid.entities.append(mcrfpy.Entity(grid_pos=(19, 11), texture=texture, sprite_index=TILES['chest_closed'])) + grid.entities.append(mcrfpy.Entity(grid_pos=(13, 12), texture=texture, sprite_index=TILES['enemy_slime'])) + grid.entities.append(mcrfpy.Entity(grid_pos=(20, 5), texture=texture, sprite_index=TILES['enemy_skeleton'])) + + grid.center = (12 * 16, 8 * 16) + + scene.activate() + output_path = os.path.join(OUTPUT_DIR, "grid_dungeon_generator.png") + automation.screenshot(output_path) + print(f" -> {output_path}") + + +def screenshot_floating_text(): + """Create a floating text/damage numbers showcase.""" + scene = mcrfpy.Scene("floating_text") + + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + grid = mcrfpy.Grid( + pos=(50, 100), + size=(700, 420), + grid_size=(12, 8), + texture=texture, + zoom=3.5 + ) + grid.fill_color = mcrfpy.Color(20, 20, 30) + scene.children.append(grid) + + # Title + title = mcrfpy.Caption(text="Floating Text Recipe", pos=(50, 20)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.font_size = 28 + scene.children.append(title) + + subtitle = mcrfpy.Caption(text="Animated damage numbers and status messages", pos=(50, 50)) + subtitle.fill_color = mcrfpy.Color(180, 180, 200) + subtitle.font_size = 16 + scene.children.append(subtitle) + + # Fill floor + for y in range(8): + for x in range(12): + grid.at(x, y).tilesprite = TILES['floor_stone'] + + # Walls + for x in range(12): + grid.at(x, 0).tilesprite = TILES['wall_stone'] + grid.at(x, 7).tilesprite = TILES['wall_stone'] + for y in range(8): + grid.at(0, y).tilesprite = TILES['wall_stone'] + grid.at(11, y).tilesprite = TILES['wall_stone'] + + # Player and enemy in combat + player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_warrior']) + grid.entities.append(player) + + enemy = mcrfpy.Entity(grid_pos=(7, 4), texture=texture, sprite_index=TILES['enemy_orc']) + grid.entities.append(enemy) + + grid.center = (5.5 * 16, 4 * 16) + + # Floating damage numbers (as captions positioned over entities) + # These would normally animate upward + dmg1 = mcrfpy.Caption(text="-12", pos=(330, 240)) + dmg1.fill_color = mcrfpy.Color(255, 80, 80) + dmg1.font_size = 24 + scene.children.append(dmg1) + + dmg2 = mcrfpy.Caption(text="-5", pos=(500, 260)) + dmg2.fill_color = mcrfpy.Color(255, 100, 100) + dmg2.font_size = 20 + scene.children.append(dmg2) + + crit = mcrfpy.Caption(text="CRITICAL!", pos=(280, 200)) + crit.fill_color = mcrfpy.Color(255, 200, 50) + crit.font_size = 18 + scene.children.append(crit) + + heal = mcrfpy.Caption(text="+8", pos=(320, 280)) + heal.fill_color = mcrfpy.Color(100, 255, 100) + heal.font_size = 20 + scene.children.append(heal) + + scene.activate() + output_path = os.path.join(OUTPUT_DIR, "effects_floating_text.png") + automation.screenshot(output_path) + print(f" -> {output_path}") + + +def screenshot_message_log(): + """Create a message log showcase.""" + scene = mcrfpy.Scene("message_log") + + # Dark background + bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) + bg.fill_color = mcrfpy.Color(20, 20, 30) + scene.children.append(bg) + + # Title + title = mcrfpy.Caption(text="Message Log Recipe", pos=(50, 30)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.font_size = 28 + scene.children.append(title) + + subtitle = mcrfpy.Caption(text="Scrollable combat and event messages", pos=(50, 60)) + subtitle.fill_color = mcrfpy.Color(180, 180, 200) + subtitle.font_size = 16 + scene.children.append(subtitle) + + # Message log frame + log_frame = mcrfpy.Frame(pos=(50, 100), size=(700, 400)) + log_frame.fill_color = mcrfpy.Color(30, 30, 40) + log_frame.outline = 2 + log_frame.outline_color = mcrfpy.Color(60, 60, 80) + scene.children.append(log_frame) + + # Sample messages with colors + messages = [ + ("Welcome to the dungeon!", mcrfpy.Color(200, 200, 255)), + ("You see a dark corridor ahead.", mcrfpy.Color(180, 180, 180)), + ("A goblin appears!", mcrfpy.Color(255, 200, 100)), + ("You hit the Goblin for 8 damage!", mcrfpy.Color(255, 255, 150)), + ("The Goblin hits you for 3 damage!", mcrfpy.Color(255, 100, 100)), + ("You hit the Goblin for 12 damage! Critical hit!", mcrfpy.Color(255, 200, 50)), + ("The Goblin dies!", mcrfpy.Color(150, 255, 150)), + ("You found a Healing Potion.", mcrfpy.Color(100, 200, 255)), + ("An Orc blocks your path!", mcrfpy.Color(255, 150, 100)), + ("You drink the Healing Potion. +15 HP", mcrfpy.Color(100, 255, 100)), + ("You hit the Orc for 6 damage!", mcrfpy.Color(255, 255, 150)), + ("The Orc hits you for 8 damage!", mcrfpy.Color(255, 100, 100)), + ] + + for i, (msg, color) in enumerate(messages): + caption = mcrfpy.Caption(text=msg, pos=(15, 15 + i * 30)) + caption.fill_color = color + caption.font_size = 16 + log_frame.children.append(caption) + + # Scroll indicator + scroll = mcrfpy.Caption(text="▼ More messages below", pos=(580, 370)) + scroll.fill_color = mcrfpy.Color(100, 100, 120) + scroll.font_size = 12 + log_frame.children.append(scroll) + + scene.activate() + output_path = os.path.join(OUTPUT_DIR, "ui_message_log.png") + automation.screenshot(output_path) + print(f" -> {output_path}") + + +def main(): + """Generate all cookbook screenshots!""" + os.makedirs(OUTPUT_DIR, exist_ok=True) + + print("=== Cookbook Screenshot Showcase ===") + print(f"Output: {OUTPUT_DIR}\n") + + showcases = [ + ('Health Bar UI', screenshot_health_bar), + ('Fog of War', screenshot_fog_of_war), + ('Melee Combat', screenshot_combat_melee), + ('Dungeon Generator', screenshot_dungeon_generator), + ('Floating Text', screenshot_floating_text), + ('Message Log', screenshot_message_log), + ] + + for name, func in showcases: + print(f"Generating {name}...") + try: + func() + except Exception as e: + print(f" ERROR: {e}") + import traceback + traceback.print_exc() + + print("\n=== All cookbook screenshots generated! ===") + sys.exit(0) + + +main() diff --git a/tests/demo/new_features_showcase.py b/tests/demo/new_features_showcase.py new file mode 100644 index 0000000..29cdee0 --- /dev/null +++ b/tests/demo/new_features_showcase.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +""" +New Features Screenshot Showcase - Alignment + Dijkstra-to-HeightMap + +Generates screenshots for the new API cookbook recipes. +Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/new_features_showcase.py +""" +import mcrfpy +from mcrfpy import automation +import sys +import os + +OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook" + + +def screenshot_alignment(): + """Create an alignment system showcase.""" + scene = mcrfpy.Scene("alignment") + + # Dark background + bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) + bg.fill_color = mcrfpy.Color(20, 20, 30) + scene.children.append(bg) + + # Title + title = mcrfpy.Caption(text="UI Alignment System", pos=(50, 20)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.font_size = 28 + scene.children.append(title) + + subtitle = mcrfpy.Caption(text="Auto-positioning with reactive resize", pos=(50, 50)) + subtitle.fill_color = mcrfpy.Color(180, 180, 200) + subtitle.font_size = 16 + scene.children.append(subtitle) + + # Demo container + container = mcrfpy.Frame(pos=(100, 100), size=(600, 400)) + container.fill_color = mcrfpy.Color(40, 40, 50) + container.outline = 2 + container.outline_color = mcrfpy.Color(80, 80, 100) + scene.children.append(container) + + # Container label + container_label = mcrfpy.Caption(text="Parent Container (600x400)", pos=(10, 10)) + container_label.fill_color = mcrfpy.Color(100, 100, 120) + container_label.font_size = 12 + container.children.append(container_label) + + # 9 alignment positions demo + alignments = [ + (mcrfpy.Alignment.TOP_LEFT, "TL", mcrfpy.Color(200, 80, 80)), + (mcrfpy.Alignment.TOP_CENTER, "TC", mcrfpy.Color(200, 150, 80)), + (mcrfpy.Alignment.TOP_RIGHT, "TR", mcrfpy.Color(200, 200, 80)), + (mcrfpy.Alignment.CENTER_LEFT, "CL", mcrfpy.Color(80, 200, 80)), + (mcrfpy.Alignment.CENTER, "C", mcrfpy.Color(80, 200, 200)), + (mcrfpy.Alignment.CENTER_RIGHT, "CR", mcrfpy.Color(80, 80, 200)), + (mcrfpy.Alignment.BOTTOM_LEFT, "BL", mcrfpy.Color(150, 80, 200)), + (mcrfpy.Alignment.BOTTOM_CENTER, "BC", mcrfpy.Color(200, 80, 200)), + (mcrfpy.Alignment.BOTTOM_RIGHT, "BR", mcrfpy.Color(200, 80, 150)), + ] + + for align, label, color in alignments: + box = mcrfpy.Frame(pos=(0, 0), size=(60, 40)) + box.fill_color = color + box.outline = 1 + box.outline_color = mcrfpy.Color(255, 255, 255) + box.align = align + if align != mcrfpy.Alignment.CENTER: + box.margin = 15.0 + + # Label inside box + text = mcrfpy.Caption(text=label, pos=(0, 0)) + text.fill_color = mcrfpy.Color(255, 255, 255) + text.font_size = 16 + text.align = mcrfpy.Alignment.CENTER + box.children.append(text) + + container.children.append(box) + + # Legend + legend = mcrfpy.Caption(text="TL=TOP_LEFT TC=TOP_CENTER TR=TOP_RIGHT etc.", pos=(100, 520)) + legend.fill_color = mcrfpy.Color(150, 150, 170) + legend.font_size = 14 + scene.children.append(legend) + + legend2 = mcrfpy.Caption(text="All boxes have margin=15 except CENTER", pos=(100, 545)) + legend2.fill_color = mcrfpy.Color(150, 150, 170) + legend2.font_size = 14 + scene.children.append(legend2) + + scene.activate() + output_path = os.path.join(OUTPUT_DIR, "ui_alignment.png") + automation.screenshot(output_path) + print(f" -> {output_path}") + + +def screenshot_dijkstra_heightmap(): + """Create a dijkstra-to-heightmap showcase.""" + scene = mcrfpy.Scene("dijkstra_hmap") + + # Title + title = mcrfpy.Caption(text="Dijkstra to HeightMap", pos=(50, 20)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.font_size = 28 + scene.children.append(title) + + subtitle = mcrfpy.Caption(text="Distance-based gradients for fog, difficulty, and visualization", pos=(50, 50)) + subtitle.fill_color = mcrfpy.Color(180, 180, 200) + subtitle.font_size = 16 + scene.children.append(subtitle) + + # Create grid for dijkstra visualization + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + grid = mcrfpy.Grid( + pos=(50, 90), + size=(350, 350), + grid_size=(16, 16), + texture=texture, + zoom=1.3 + ) + grid.fill_color = mcrfpy.Color(20, 20, 30) + scene.children.append(grid) + + # Initialize grid + for y in range(16): + for x in range(16): + grid.at((x, y)).walkable = True + grid.at((x, y)).tilesprite = 42 # floor + + # Add some walls + for i in range(5, 11): + grid.at((i, 5)).walkable = False + grid.at((i, 5)).tilesprite = 30 # wall + grid.at((5, i)).walkable = False + grid.at((5, i)).tilesprite = 30 + + # Player at center + player = mcrfpy.Entity(grid_pos=(8, 8), texture=texture, sprite_index=84) + grid.entities.append(player) + + # Get dijkstra and create color visualization + dijkstra = grid.get_dijkstra_map((8, 8)) + hmap = dijkstra.to_heightmap(unreachable=-1.0) + + # Find max for normalization + max_dist = 0 + for y in range(16): + for x in range(16): + d = hmap[(x, y)] + if d > max_dist and d >= 0: + max_dist = d + + # Second visualization panel - color gradient + viz_frame = mcrfpy.Frame(pos=(420, 90), size=(350, 350)) + viz_frame.fill_color = mcrfpy.Color(30, 30, 40) + viz_frame.outline = 2 + viz_frame.outline_color = mcrfpy.Color(60, 60, 80) + scene.children.append(viz_frame) + + viz_label = mcrfpy.Caption(text="Distance Visualization", pos=(80, 10)) + viz_label.fill_color = mcrfpy.Color(200, 200, 220) + viz_label.font_size = 16 + viz_frame.children.append(viz_label) + + # Draw colored squares for each cell + cell_size = 20 + offset_x = 15 + offset_y = 35 + + for y in range(16): + for x in range(16): + dist = hmap[(x, y)] + + if dist < 0: + # Unreachable - dark red + color = mcrfpy.Color(60, 0, 0) + elif dist == 0: + # Source - bright yellow + color = mcrfpy.Color(255, 255, 0) + else: + # Gradient: green (near) to blue (far) + t = min(1.0, dist / max_dist) + r = 0 + g = int(200 * (1 - t)) + b = int(200 * t) + color = mcrfpy.Color(r, g, b) + + cell = mcrfpy.Frame( + pos=(offset_x + x * cell_size, offset_y + y * cell_size), + size=(cell_size - 1, cell_size - 1) + ) + cell.fill_color = color + viz_frame.children.append(cell) + + # Legend + legend_frame = mcrfpy.Frame(pos=(50, 460), size=(720, 100)) + legend_frame.fill_color = mcrfpy.Color(30, 30, 40) + legend_frame.outline = 1 + legend_frame.outline_color = mcrfpy.Color(60, 60, 80) + scene.children.append(legend_frame) + + leg1 = mcrfpy.Caption(text="Use Cases:", pos=(15, 10)) + leg1.fill_color = mcrfpy.Color(255, 255, 255) + leg1.font_size = 16 + legend_frame.children.append(leg1) + + uses = [ + "Distance-based enemy difficulty", + "Fog intensity gradients", + "Pathfinding visualization", + "Influence maps for AI", + ] + for i, use in enumerate(uses): + txt = mcrfpy.Caption(text=f"- {use}", pos=(15 + (i // 2) * 350, 35 + (i % 2) * 25)) + txt.fill_color = mcrfpy.Color(180, 180, 200) + txt.font_size = 14 + legend_frame.children.append(txt) + + # Color key + key_label = mcrfpy.Caption(text="Yellow=Source Green=Near Blue=Far Red=Blocked", pos=(420, 450)) + key_label.fill_color = mcrfpy.Color(150, 150, 170) + key_label.font_size = 12 + scene.children.append(key_label) + + scene.activate() + output_path = os.path.join(OUTPUT_DIR, "grid_dijkstra_heightmap.png") + automation.screenshot(output_path) + print(f" -> {output_path}") + + +def main(): + os.makedirs(OUTPUT_DIR, exist_ok=True) + + print("=== New Features Screenshot Showcase ===") + print(f"Output: {OUTPUT_DIR}\n") + + showcases = [ + ('Alignment System', screenshot_alignment), + ('Dijkstra to HeightMap', screenshot_dijkstra_heightmap), + ] + + for name, func in showcases: + print(f"Generating {name}...") + try: + func() + except Exception as e: + print(f" ERROR: {e}") + import traceback + traceback.print_exc() + + print("\n=== New feature screenshots generated! ===") + sys.exit(0) + + +main() diff --git a/tests/demo/procgen_showcase.py b/tests/demo/procgen_showcase.py new file mode 100644 index 0000000..2cdba0a --- /dev/null +++ b/tests/demo/procgen_showcase.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +"""Generate screenshots for procgen cookbook recipes. + +Uses Frame-based visualization since Grid cell colors use ColorLayer API. +""" +import mcrfpy +from mcrfpy import automation +import sys + +OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook" + +# Simple PRNG +_seed = 42 + +def random(): + global _seed + _seed = (_seed * 1103515245 + 12345) & 0x7fffffff + return (_seed >> 16) / 32768.0 + +def seed(n): + global _seed + _seed = n + +def choice(lst): + return lst[int(random() * len(lst))] + + +def screenshot_cellular_caves(): + """Generate cellular automata caves visualization.""" + print("Generating cellular automata caves...") + + scene = mcrfpy.Scene("caves") + scene.activate() + mcrfpy.step(0.1) + + # Background + bg = mcrfpy.Frame(pos=(0, 0), size=(640, 500)) + bg.fill_color = mcrfpy.Color(15, 15, 25) + scene.children.append(bg) + + width, height = 50, 35 + cell_size = 12 + seed(42) + + # Store cell data + cells = [[False for _ in range(width)] for _ in range(height)] + + # Step 1: Random noise (45% walls) + for y in range(height): + for x in range(width): + if x == 0 or x == width-1 or y == 0 or y == height-1: + cells[y][x] = True # Border walls + else: + cells[y][x] = random() < 0.45 + + # Step 2: Smooth with cellular automata (5 iterations) + for _ in range(5): + new_cells = [[cells[y][x] for x in range(width)] for y in range(height)] + for y in range(1, height - 1): + for x in range(1, width - 1): + wall_count = sum( + 1 for dy in [-1, 0, 1] for dx in [-1, 0, 1] + if not (dx == 0 and dy == 0) and cells[y + dy][x + dx] + ) + if wall_count >= 5: + new_cells[y][x] = True + elif wall_count <= 3: + new_cells[y][x] = False + cells = new_cells + + # Find largest connected region + visited = set() + regions = [] + + def flood_fill(start_x, start_y): + result = [] + stack = [(start_x, start_y)] + while stack: + x, y = stack.pop() + if (x, y) in visited or x < 0 or x >= width or y < 0 or y >= height: + continue + if cells[y][x]: # Wall + continue + visited.add((x, y)) + result.append((x, y)) + stack.extend([(x+1, y), (x-1, y), (x, y+1), (x, y-1)]) + return result + + for y in range(height): + for x in range(width): + if (x, y) not in visited and not cells[y][x]: + region = flood_fill(x, y) + if region: + regions.append(region) + + largest = max(regions, key=len) if regions else [] + largest_set = set(largest) + + # Draw cells as colored frames + for y in range(height): + for x in range(width): + px = 20 + x * cell_size + py = 20 + y * cell_size + cell = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) + + if cells[y][x]: + cell.fill_color = mcrfpy.Color(60, 40, 30) # Wall + elif (x, y) in largest_set: + cell.fill_color = mcrfpy.Color(50, 90, 100) # Main cave + else: + cell.fill_color = mcrfpy.Color(45, 35, 30) # Filled region + + scene.children.append(cell) + + # Title + title = mcrfpy.Caption(text="Cellular Automata Caves", pos=(20, 445)) + title.fill_color = mcrfpy.Color(200, 200, 200) + title.font_size = 18 + scene.children.append(title) + + subtitle = mcrfpy.Caption(text="45% fill, 5 iterations, largest region preserved", pos=(20, 468)) + subtitle.fill_color = mcrfpy.Color(130, 130, 140) + subtitle.font_size = 12 + scene.children.append(subtitle) + + mcrfpy.step(0.1) + automation.screenshot(OUTPUT_DIR + "/procgen_cellular_caves.png") + print("Saved: procgen_cellular_caves.png") + + +def screenshot_wfc(): + """Generate WFC pattern visualization.""" + print("Generating WFC patterns...") + + scene = mcrfpy.Scene("wfc") + scene.activate() + mcrfpy.step(0.1) + + # Background + bg = mcrfpy.Frame(pos=(0, 0), size=(640, 500)) + bg.fill_color = mcrfpy.Color(15, 20, 15) + scene.children.append(bg) + + width, height = 40, 28 + cell_size = 15 + seed(123) + + GRASS, DIRT, WATER, SAND = 0, 1, 2, 3 + colors = { + GRASS: mcrfpy.Color(60, 120, 50), + DIRT: mcrfpy.Color(100, 70, 40), + WATER: mcrfpy.Color(40, 80, 140), + SAND: mcrfpy.Color(180, 160, 90) + } + + rules = { + GRASS: {'N': [GRASS, DIRT, SAND], 'S': [GRASS, DIRT, SAND], + 'E': [GRASS, DIRT, SAND], 'W': [GRASS, DIRT, SAND]}, + DIRT: {'N': [GRASS, DIRT], 'S': [GRASS, DIRT], + 'E': [GRASS, DIRT], 'W': [GRASS, DIRT]}, + WATER: {'N': [WATER, SAND], 'S': [WATER, SAND], + 'E': [WATER, SAND], 'W': [WATER, SAND]}, + SAND: {'N': [GRASS, WATER, SAND], 'S': [GRASS, WATER, SAND], + 'E': [GRASS, WATER, SAND], 'W': [GRASS, WATER, SAND]} + } + + tiles = set(rules.keys()) + possibilities = {(x, y): set(tiles) for y in range(height) for x in range(width)} + result = {} + + # Seed water lake + for x in range(22, 32): + for y in range(8, 18): + possibilities[(x, y)] = {WATER} + result[(x, y)] = WATER + + # Seed dirt path + for y in range(10, 18): + possibilities[(3, y)] = {DIRT} + result[(3, y)] = DIRT + + directions = {'N': (0, -1), 'S': (0, 1), 'E': (1, 0), 'W': (-1, 0)} + + def propagate(sx, sy): + stack = [(sx, sy)] + while stack: + x, y = stack.pop() + current = possibilities[(x, y)] + for dir_name, (dx, dy) in directions.items(): + nx, ny = x + dx, y + dy + if not (0 <= nx < width and 0 <= ny < height): + continue + neighbor = possibilities[(nx, ny)] + if len(neighbor) == 1: + continue + allowed = set() + for tile in current: + if dir_name in rules[tile]: + allowed.update(rules[tile][dir_name]) + new_opts = neighbor & allowed + if new_opts and new_opts != neighbor: + possibilities[(nx, ny)] = new_opts + stack.append((nx, ny)) + + # Propagate from seeds + for x in range(22, 32): + for y in range(8, 18): + propagate(x, y) + for y in range(10, 18): + propagate(3, y) + + # Collapse + for _ in range(width * height): + best, best_e = None, 1000.0 + for pos, opts in possibilities.items(): + if len(opts) > 1: + e = len(opts) + random() * 0.1 + if e < best_e: + best_e, best = e, pos + + if best is None: + break + + x, y = best + opts = list(possibilities[(x, y)]) + if not opts: + break + + weights = {GRASS: 5, DIRT: 2, WATER: 1, SAND: 2} + weighted = [] + for t in opts: + weighted.extend([t] * weights.get(t, 1)) + chosen = choice(weighted) if weighted else GRASS + + possibilities[(x, y)] = {chosen} + result[(x, y)] = chosen + propagate(x, y) + + # Fill remaining + for y in range(height): + for x in range(width): + if (x, y) not in result: + opts = list(possibilities[(x, y)]) + result[(x, y)] = choice(opts) if opts else GRASS + + # Draw + for y in range(height): + for x in range(width): + px = 20 + x * cell_size + py = 20 + y * cell_size + cell = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) + cell.fill_color = colors[result[(x, y)]] + scene.children.append(cell) + + # Title + title = mcrfpy.Caption(text="Wave Function Collapse", pos=(20, 445)) + title.fill_color = mcrfpy.Color(200, 200, 200) + title.font_size = 18 + scene.children.append(title) + + subtitle = mcrfpy.Caption(text="Constraint-based terrain (seeded lake + path)", pos=(20, 468)) + subtitle.fill_color = mcrfpy.Color(130, 140, 130) + subtitle.font_size = 12 + scene.children.append(subtitle) + + # Legend + for i, (name, tid) in enumerate([("Grass", GRASS), ("Dirt", DIRT), ("Sand", SAND), ("Water", WATER)]): + lx, ly = 480, 445 + i * 14 + swatch = mcrfpy.Frame(pos=(lx, ly), size=(12, 12)) + swatch.fill_color = colors[tid] + scene.children.append(swatch) + label = mcrfpy.Caption(text=name, pos=(lx + 16, ly)) + label.fill_color = mcrfpy.Color(150, 150, 150) + label.font_size = 11 + scene.children.append(label) + + mcrfpy.step(0.1) + automation.screenshot(OUTPUT_DIR + "/procgen_wfc.png") + print("Saved: procgen_wfc.png") + + +if __name__ == "__main__": + screenshot_cellular_caves() + screenshot_wfc() + print("\nDone!") + sys.exit(0) diff --git a/tests/demo/simple_showcase.py b/tests/demo/simple_showcase.py new file mode 100644 index 0000000..eeb63a1 --- /dev/null +++ b/tests/demo/simple_showcase.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Simple Tutorial Screenshot Generator + +This creates ONE screenshot - the part01 tutorial showcase. +Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/simple_showcase.py + +NOTE: In headless mode, automation.screenshot() is SYNCHRONOUS - it renders +and captures immediately. No timer dance needed! +""" +import mcrfpy +from mcrfpy import automation +import sys +import os + +# Output +OUTPUT_PATH = "/opt/goblincorps/repos/mcrogueface.github.io/images/tutorials/part_01_grid_movement.png" + +# Tile sprites from the labeled tileset +PLAYER_KNIGHT = 84 +FLOOR_STONE = 42 +WALL_STONE = 30 +TORCH = 72 +BARREL = 73 +SKULL = 74 + +def main(): + """Create the part01 showcase screenshot.""" + # Ensure output dir exists + os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True) + + # Create scene + scene = mcrfpy.Scene("showcase") + + # Load texture + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + + # Create grid - bigger zoom for visibility + grid = mcrfpy.Grid( + pos=(50, 80), + size=(700, 480), + grid_size=(12, 9), + texture=texture, + zoom=3.5 + ) + grid.fill_color = mcrfpy.Color(20, 20, 30) + scene.children.append(grid) + + # Fill with floor + for y in range(9): + for x in range(12): + grid.at(x, y).tilesprite = FLOOR_STONE + + # Add wall border + for x in range(12): + grid.at(x, 0).tilesprite = WALL_STONE + grid.at(x, 0).walkable = False + grid.at(x, 8).tilesprite = WALL_STONE + grid.at(x, 8).walkable = False + for y in range(9): + grid.at(0, y).tilesprite = WALL_STONE + grid.at(0, y).walkable = False + grid.at(11, y).tilesprite = WALL_STONE + grid.at(11, y).walkable = False + + # Add player entity - a knight! + player = mcrfpy.Entity( + grid_pos=(6, 4), + texture=texture, + sprite_index=PLAYER_KNIGHT + ) + grid.entities.append(player) + + # Add decorations + for pos, sprite in [((2, 2), TORCH), ((9, 2), TORCH), ((2, 6), BARREL), ((9, 6), SKULL)]: + entity = mcrfpy.Entity(grid_pos=pos, texture=texture, sprite_index=sprite) + grid.entities.append(entity) + + # Center camera on player + grid.center = (6 * 16 + 8, 4 * 16 + 8) + + # Add title + title = mcrfpy.Caption(text="Part 1: The '@' and the Dungeon Grid", pos=(50, 20)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.font_size = 28 + scene.children.append(title) + + subtitle = mcrfpy.Caption(text="Creating a grid, placing entities, handling input", pos=(50, 50)) + subtitle.fill_color = mcrfpy.Color(180, 180, 200) + subtitle.font_size = 16 + scene.children.append(subtitle) + + # Activate scene + scene.activate() + + # In headless mode, screenshot() is synchronous - renders then captures! + result = automation.screenshot(OUTPUT_PATH) + print(f"Screenshot saved: {OUTPUT_PATH} (result: {result})") + sys.exit(0) + + +# Run it +main() diff --git a/tests/demo/tutorial_screenshots.py b/tests/demo/tutorial_screenshots.py new file mode 100644 index 0000000..ee14e03 --- /dev/null +++ b/tests/demo/tutorial_screenshots.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +Tutorial Screenshot Generator + +Usage: + ./mcrogueface --headless --exec tests/demo/tutorial_screenshots.py + +Extracts code from tutorial markdown files and generates screenshots. +""" +import mcrfpy +from mcrfpy import automation +import sys +import os +import re + +# Paths +DOCS_REPO = "/opt/goblincorps/repos/mcrogueface.github.io" +TUTORIAL_DIR = os.path.join(DOCS_REPO, "tutorial") +OUTPUT_DIR = os.path.join(DOCS_REPO, "images", "tutorials") + +# Tutorials to process (in order) +TUTORIALS = [ + "part_01_grid_movement.md", + "part_02_tiles_collision.md", + "part_03_dungeon_generation.md", + "part_04_fov.md", + "part_05_enemies.md", + "part_06_combat.md", + "part_07_ui.md", +] + + +def extract_code_from_markdown(filepath): + """Extract the main Python code block from a tutorial markdown file.""" + with open(filepath, 'r') as f: + content = f.read() + + # Find code blocks after "## The Complete Code" header + # Look for the first python code block after that header + complete_code_match = re.search( + r'##\s+The Complete Code.*?```python\s*\n(.*?)```', + content, + re.DOTALL | re.IGNORECASE + ) + + if complete_code_match: + return complete_code_match.group(1) + + # Fallback: just get the first large python code block + code_blocks = re.findall(r'```python\s*\n(.*?)```', content, re.DOTALL) + if code_blocks: + # Return the largest code block (likely the main example) + return max(code_blocks, key=len) + + return None + + +def add_screenshot_hook(code, screenshot_path): + """Add screenshot capture code to the end of the script.""" + # Add code to take screenshot after a brief delay + hook_code = f''' + +# === Screenshot capture hook (added by tutorial_screenshots.py) === +import mcrfpy +from mcrfpy import automation +import sys + +_screenshot_taken = [False] + +def _take_screenshot(timer, runtime): + if not _screenshot_taken[0]: + _screenshot_taken[0] = True + automation.screenshot("{screenshot_path}") + print(f"Screenshot saved: {screenshot_path}") + sys.exit(0) + +# Wait a moment for scene to render, then capture +mcrfpy.Timer("_screenshot_hook", _take_screenshot, 200) +''' + return code + hook_code + + +class TutorialScreenshotter: + """Manages tutorial screenshot generation.""" + + def __init__(self): + self.tutorials = [] + self.current_index = 0 + + def load_tutorials(self): + """Load and parse all tutorial files.""" + for filename in TUTORIALS: + filepath = os.path.join(TUTORIAL_DIR, filename) + if not os.path.exists(filepath): + print(f"Warning: {filepath} not found, skipping") + continue + + code = extract_code_from_markdown(filepath) + if code: + # Generate output filename + base = os.path.splitext(filename)[0] + screenshot_name = f"{base}.png" + self.tutorials.append({ + 'name': filename, + 'code': code, + 'screenshot': screenshot_name, + 'filepath': filepath, + }) + print(f"Loaded: {filename}") + else: + print(f"Warning: No code found in {filename}") + + def run(self): + """Generate all screenshots.""" + # Ensure output directory exists + os.makedirs(OUTPUT_DIR, exist_ok=True) + + print(f"\nGenerating {len(self.tutorials)} tutorial screenshots...") + print(f"Output directory: {OUTPUT_DIR}\n") + + self.process_next() + + def process_next(self): + """Process the next tutorial.""" + if self.current_index >= len(self.tutorials): + print("\nAll screenshots generated!") + sys.exit(0) + return + + tutorial = self.tutorials[self.current_index] + print(f"[{self.current_index + 1}/{len(self.tutorials)}] Processing {tutorial['name']}...") + + # Add screenshot hook to the code + screenshot_path = os.path.join(OUTPUT_DIR, tutorial['screenshot']) + modified_code = add_screenshot_hook(tutorial['code'], screenshot_path) + + # Write to temp file and execute + temp_path = f"/tmp/tutorial_screenshot_{self.current_index}.py" + with open(temp_path, 'w') as f: + f.write(modified_code) + + try: + # Execute the code + exec(compile(modified_code, temp_path, 'exec'), {'__name__': '__main__'}) + except Exception as e: + print(f"Error processing {tutorial['name']}: {e}") + self.current_index += 1 + self.process_next() + finally: + try: + os.unlink(temp_path) + except: + pass + + +def main(): + """Main entry point.""" + screenshotter = TutorialScreenshotter() + screenshotter.load_tutorials() + + if not screenshotter.tutorials: + print("No tutorials found to process!") + sys.exit(1) + + screenshotter.run() + + +# Run when executed +main() diff --git a/tests/demo/tutorial_showcase.py b/tests/demo/tutorial_showcase.py new file mode 100644 index 0000000..6f22445 --- /dev/null +++ b/tests/demo/tutorial_showcase.py @@ -0,0 +1,426 @@ +#!/usr/bin/env python3 +""" +Tutorial Screenshot Showcase - ALL THE SCREENSHOTS! + +Generates beautiful screenshots for all tutorial parts. +Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/tutorial_showcase.py + +In headless mode, automation.screenshot() is SYNCHRONOUS - no timer dance needed! +""" +import mcrfpy +from mcrfpy import automation +import sys +import os + +# Output directory +OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/tutorials" + +# Tile meanings from the labeled tileset - the FUN sprites! +TILES = { + # Players - knights and heroes! + 'player_knight': 84, + 'player_mage': 85, + 'player_rogue': 86, + 'player_warrior': 87, + 'player_archer': 88, + 'player_alt1': 96, + 'player_alt2': 97, + 'player_alt3': 98, + + # Enemies - scary! + 'enemy_slime': 108, + 'enemy_bat': 109, + 'enemy_spider': 110, + 'enemy_rat': 111, + 'enemy_orc': 120, + 'enemy_troll': 121, + 'enemy_ghost': 122, + 'enemy_skeleton': 123, + 'enemy_demon': 124, + 'enemy_boss': 92, + + # Terrain + 'floor_stone': 42, + 'floor_wood': 49, + 'floor_grass': 48, + 'floor_dirt': 50, + 'wall_stone': 30, + 'wall_brick': 14, + 'wall_mossy': 28, + + # Items + 'item_potion': 113, + 'item_scroll': 114, + 'item_key': 115, + 'item_coin': 116, + + # Equipment + 'equip_sword': 101, + 'equip_shield': 102, + 'equip_helm': 103, + 'equip_armor': 104, + + # Chests and doors + 'chest_closed': 89, + 'chest_open': 90, + 'door_closed': 33, + 'door_open': 35, + + # Decorations + 'torch': 72, + 'barrel': 73, + 'skull': 74, + 'bones': 75, +} + + +class TutorialShowcase: + """Creates beautiful showcase screenshots for tutorials.""" + + def __init__(self, scene_name, output_name): + self.scene = mcrfpy.Scene(scene_name) + self.output_path = os.path.join(OUTPUT_DIR, output_name) + self.grid = None + + def setup_grid(self, width, height, zoom=3.0): + """Create a grid with nice defaults.""" + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + self.grid = mcrfpy.Grid( + pos=(50, 80), + size=(700, 500), + grid_size=(width, height), + texture=texture, + zoom=zoom + ) + self.grid.fill_color = mcrfpy.Color(20, 20, 30) + self.scene.children.append(self.grid) + self.texture = texture + return self.grid + + def add_title(self, text, subtitle=None): + """Add a title to the scene.""" + title = mcrfpy.Caption(text=text, pos=(50, 20)) + title.fill_color = mcrfpy.Color(255, 255, 255) + title.font_size = 28 + self.scene.children.append(title) + + if subtitle: + sub = mcrfpy.Caption(text=subtitle, pos=(50, 50)) + sub.fill_color = mcrfpy.Color(180, 180, 200) + sub.font_size = 16 + self.scene.children.append(sub) + + def fill_floor(self, tile=None): + """Fill grid with floor tiles.""" + if tile is None: + tile = TILES['floor_stone'] + w, h = int(self.grid.grid_size[0]), int(self.grid.grid_size[1]) + for y in range(h): + for x in range(w): + self.grid.at(x, y).tilesprite = tile + + def add_walls(self, tile=None): + """Add wall border.""" + if tile is None: + tile = TILES['wall_stone'] + w, h = int(self.grid.grid_size[0]), int(self.grid.grid_size[1]) + for x in range(w): + self.grid.at(x, 0).tilesprite = tile + self.grid.at(x, 0).walkable = False + self.grid.at(x, h-1).tilesprite = tile + self.grid.at(x, h-1).walkable = False + for y in range(h): + self.grid.at(0, y).tilesprite = tile + self.grid.at(0, y).walkable = False + self.grid.at(w-1, y).tilesprite = tile + self.grid.at(w-1, y).walkable = False + + def add_entity(self, x, y, sprite): + """Add an entity to the grid.""" + entity = mcrfpy.Entity( + grid_pos=(x, y), + texture=self.texture, + sprite_index=sprite + ) + self.grid.entities.append(entity) + return entity + + def center_on(self, x, y): + """Center camera on a position.""" + self.grid.center = (x * 16 + 8, y * 16 + 8) + + def screenshot(self): + """Take the screenshot - synchronous in headless mode!""" + self.scene.activate() + result = automation.screenshot(self.output_path) + print(f" -> {self.output_path} (result: {result})") + return result + + +def part01_grid_movement(): + """Part 1: Grid Movement - Knight in a dungeon room.""" + showcase = TutorialShowcase("part01", "part_01_grid_movement.png") + showcase.setup_grid(12, 9, zoom=3.5) + showcase.add_title("Part 1: The '@' and the Dungeon Grid", + "Creating a grid, placing entities, handling input") + + showcase.fill_floor(TILES['floor_stone']) + showcase.add_walls(TILES['wall_stone']) + + # Add the player (a cool knight, not boring @) + showcase.add_entity(6, 4, TILES['player_knight']) + + # Add some decorations to make it interesting + showcase.add_entity(2, 2, TILES['torch']) + showcase.add_entity(9, 2, TILES['torch']) + showcase.add_entity(2, 6, TILES['barrel']) + showcase.add_entity(9, 6, TILES['skull']) + + showcase.center_on(6, 4) + showcase.screenshot() + + +def part02_tiles_collision(): + """Part 2: Tiles and Collision - Walls and walkability.""" + showcase = TutorialShowcase("part02", "part_02_tiles_collision.png") + showcase.setup_grid(14, 10, zoom=3.0) + showcase.add_title("Part 2: Tiles, Collision, and Walkability", + "Different tile types and blocking movement") + + showcase.fill_floor(TILES['floor_stone']) + showcase.add_walls(TILES['wall_brick']) + + # Create some interior walls to show collision + for y in range(2, 5): + showcase.grid.at(5, y).tilesprite = TILES['wall_stone'] + showcase.grid.at(5, y).walkable = False + for y in range(5, 8): + showcase.grid.at(9, y).tilesprite = TILES['wall_stone'] + showcase.grid.at(9, y).walkable = False + + # Add a door + showcase.grid.at(5, 5).tilesprite = TILES['door_closed'] + showcase.grid.at(5, 5).walkable = False + + # Player navigating the maze + showcase.add_entity(3, 4, TILES['player_warrior']) + + # Chest as goal + showcase.add_entity(11, 5, TILES['chest_closed']) + + showcase.center_on(7, 5) + showcase.screenshot() + + +def part03_dungeon_generation(): + """Part 3: Dungeon Generation - Procedural rooms and corridors.""" + showcase = TutorialShowcase("part03", "part_03_dungeon_generation.png") + showcase.setup_grid(20, 14, zoom=2.5) + showcase.add_title("Part 3: Procedural Dungeon Generation", + "Random rooms connected by corridors") + + # Fill with walls first + for y in range(14): + for x in range(20): + showcase.grid.at(x, y).tilesprite = TILES['wall_stone'] + showcase.grid.at(x, y).walkable = False + + # Carve out two rooms + # Room 1 (left) + for y in range(3, 8): + for x in range(2, 8): + showcase.grid.at(x, y).tilesprite = TILES['floor_stone'] + showcase.grid.at(x, y).walkable = True + + # Room 2 (right) + for y in range(6, 12): + for x in range(12, 18): + showcase.grid.at(x, y).tilesprite = TILES['floor_stone'] + showcase.grid.at(x, y).walkable = True + + # Corridor connecting them + for x in range(7, 13): + showcase.grid.at(x, 6).tilesprite = TILES['floor_dirt'] + showcase.grid.at(x, 6).walkable = True + for y in range(6, 9): + showcase.grid.at(12, y).tilesprite = TILES['floor_dirt'] + showcase.grid.at(12, y).walkable = True + + # Player in first room + showcase.add_entity(4, 5, TILES['player_knight']) + + # Some loot in second room + showcase.add_entity(14, 9, TILES['chest_closed']) + showcase.add_entity(16, 8, TILES['item_potion']) + + # Torches + showcase.add_entity(3, 3, TILES['torch']) + showcase.add_entity(6, 3, TILES['torch']) + showcase.add_entity(13, 7, TILES['torch']) + + showcase.center_on(10, 7) + showcase.screenshot() + + +def part04_fov(): + """Part 4: Field of View - Showing explored vs visible areas.""" + showcase = TutorialShowcase("part04", "part_04_fov.png") + showcase.setup_grid(16, 12, zoom=2.8) + showcase.add_title("Part 4: Field of View and Fog of War", + "What the player can see vs. the unknown") + + showcase.fill_floor(TILES['floor_stone']) + showcase.add_walls(TILES['wall_brick']) + + # Some interior pillars to block sight + for pos in [(5, 4), (5, 7), (10, 5), (10, 8)]: + showcase.grid.at(pos[0], pos[1]).tilesprite = TILES['wall_mossy'] + showcase.grid.at(pos[0], pos[1]).walkable = False + + # Player with "light" + showcase.add_entity(8, 6, TILES['player_mage']) + + # Hidden enemy (player wouldn't see this!) + showcase.add_entity(12, 3, TILES['enemy_ghost']) + + # Visible enemies + showcase.add_entity(3, 5, TILES['enemy_bat']) + showcase.add_entity(6, 8, TILES['enemy_spider']) + + showcase.center_on(8, 6) + showcase.screenshot() + + +def part05_enemies(): + """Part 5: Enemies - A dungeon full of monsters.""" + showcase = TutorialShowcase("part05", "part_05_enemies.png") + showcase.setup_grid(18, 12, zoom=2.5) + showcase.add_title("Part 5: Adding Enemies", + "Different monster types with AI behavior") + + showcase.fill_floor(TILES['floor_stone']) + showcase.add_walls(TILES['wall_stone']) + + # The hero + showcase.add_entity(3, 5, TILES['player_warrior']) + + # A variety of enemies + showcase.add_entity(7, 3, TILES['enemy_slime']) + showcase.add_entity(10, 6, TILES['enemy_bat']) + showcase.add_entity(8, 8, TILES['enemy_spider']) + showcase.add_entity(14, 4, TILES['enemy_orc']) + showcase.add_entity(15, 8, TILES['enemy_skeleton']) + showcase.add_entity(12, 5, TILES['enemy_rat']) + + # Boss at the end + showcase.add_entity(15, 6, TILES['enemy_boss']) + + # Some decorations + showcase.add_entity(5, 2, TILES['bones']) + showcase.add_entity(13, 9, TILES['skull']) + showcase.add_entity(2, 8, TILES['torch']) + showcase.add_entity(16, 2, TILES['torch']) + + showcase.center_on(9, 5) + showcase.screenshot() + + +def part06_combat(): + """Part 6: Combat - Battle in progress!""" + showcase = TutorialShowcase("part06", "part_06_combat.png") + showcase.setup_grid(14, 10, zoom=3.0) + showcase.add_title("Part 6: Combat System", + "HP, attack, defense, and turn-based fighting") + + showcase.fill_floor(TILES['floor_dirt']) + showcase.add_walls(TILES['wall_brick']) + + # Battle scene - player vs enemy + showcase.add_entity(5, 5, TILES['player_knight']) + showcase.add_entity(8, 5, TILES['enemy_orc']) + + # Fallen enemies (show combat has happened) + showcase.add_entity(4, 3, TILES['bones']) + showcase.add_entity(9, 7, TILES['skull']) + + # Equipment the player has + showcase.add_entity(3, 6, TILES['equip_shield']) + showcase.add_entity(10, 4, TILES['item_potion']) + + showcase.center_on(6, 5) + showcase.screenshot() + + +def part07_ui(): + """Part 7: User Interface - Health bars and menus.""" + showcase = TutorialShowcase("part07", "part_07_ui.png") + showcase.setup_grid(12, 8, zoom=3.0) + showcase.add_title("Part 7: User Interface", + "Health bars, message logs, and menus") + + showcase.fill_floor(TILES['floor_wood']) + showcase.add_walls(TILES['wall_brick']) + + # Player + showcase.add_entity(6, 4, TILES['player_rogue']) + + # Some items to interact with + showcase.add_entity(4, 3, TILES['chest_open']) + showcase.add_entity(8, 5, TILES['item_coin']) + + # Add UI overlay example - health bar frame + ui_frame = mcrfpy.Frame(pos=(50, 520), size=(200, 40)) + ui_frame.fill_color = mcrfpy.Color(40, 40, 50, 200) + ui_frame.outline = 2 + ui_frame.outline_color = mcrfpy.Color(80, 80, 100) + showcase.scene.children.append(ui_frame) + + # Health label + hp_label = mcrfpy.Caption(text="HP: 45/50", pos=(10, 10)) + hp_label.fill_color = mcrfpy.Color(255, 100, 100) + hp_label.font_size = 18 + ui_frame.children.append(hp_label) + + # Health bar background + hp_bg = mcrfpy.Frame(pos=(90, 12), size=(100, 16)) + hp_bg.fill_color = mcrfpy.Color(60, 20, 20) + ui_frame.children.append(hp_bg) + + # Health bar fill + hp_fill = mcrfpy.Frame(pos=(90, 12), size=(90, 16)) # 90% health + hp_fill.fill_color = mcrfpy.Color(200, 50, 50) + ui_frame.children.append(hp_fill) + + showcase.center_on(6, 4) + showcase.screenshot() + + +def main(): + """Generate all showcase screenshots!""" + os.makedirs(OUTPUT_DIR, exist_ok=True) + + print("=== Tutorial Screenshot Showcase ===") + print(f"Output: {OUTPUT_DIR}\n") + + showcases = [ + ('Part 1: Grid Movement', part01_grid_movement), + ('Part 2: Tiles & Collision', part02_tiles_collision), + ('Part 3: Dungeon Generation', part03_dungeon_generation), + ('Part 4: Field of View', part04_fov), + ('Part 5: Enemies', part05_enemies), + ('Part 6: Combat', part06_combat), + ('Part 7: UI', part07_ui), + ] + + for name, func in showcases: + print(f"Generating {name}...") + try: + func() + except Exception as e: + print(f" ERROR: {e}") + + print("\n=== All screenshots generated! ===") + sys.exit(0) + + +main()