diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..539cecd65ed811e06e8a98fe319da37d242ffb97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.pyo +.DS_Store +*.log +sd_output_*.png diff --git a/TECHNICAL_DEBT_PLAN_2026-01-21.md b/TECHNICAL_DEBT_PLAN_2026-01-21.md new file mode 100644 index 0000000000000000000000000000000000000000..ce7c8a78f31e35637ee3418a847bfb83bdcaa2e0 --- /dev/null +++ b/TECHNICAL_DEBT_PLAN_2026-01-21.md @@ -0,0 +1,136 @@ +# Technical Debt & Refactoring Plan +**Created**: 2026-01-21 +**Status**: Pending - UX improvements in progress first +**Last Updated**: 2026-01-21 + +--- + +## Executive Summary + +GameConfigIdeaEditBrainstorm is a sophisticated AI-powered game design platform with ~24,343 lines of Python across 79 files. While feature-rich, it has accumulated technical debt that impacts maintainability, testability, and extensibility. + +**Decision**: Focus on UX improvements first to better understand real usage patterns, then return to this restructuring plan with concrete insights. + +--- + +## Current Architecture Overview + +``` +├── app.py (4,104 lines) # Monolithic main file - PRIMARY CONCERN +├── Core Engine +│ ├── my_text_game_engine_attempt.py +│ ├── game_state.py +│ ├── condition_evaluator.py +│ └── game_configs.py +├── AI/ML Integration +│ ├── leveraging_machine_learning.py +│ └── llm_playtester.py +├── UI Tabs (ui_tabs/) # Already modularized - GOOD +├── Exporters (exporters/) # 14 platforms - GOOD separation +├── External Ports +│ ├── narrativeengine_hfport/ +│ ├── storygenattempt_hfport/ +│ └── dnd_game_master_hfport/ +└── Scenario Templates (*_scenarios.py) +``` + +--- + +## Issues by Priority + +### P0 - Critical (Blocks scaling) + +| Issue | Impact | Effort | Notes | +|-------|--------|--------|-------| +| Monolithic `app.py` (4,104 lines) | Hard to maintain, test, or onboard contributors | High | Break into feature modules | +| No automated tests | Can't refactor safely | Medium | Add pytest suite | +| Tight UI-engine coupling | Can't unit test game logic | High | Extract pure engine layer | + +### P1 - High (Impacts development velocity) + +| Issue | Impact | Effort | Notes | +|-------|--------|--------|-------| +| Mixed state management (Player + GameState) | Confusing, potential bugs | Medium | Complete migration to GameState | +| 60+ hardcoded LLM list | Hard to maintain/extend | Low | Create ModelRegistry class | +| Sparse error handling | Silent failures confuse users | Medium | Add structured logging | +| Lambda consequences + declarative effects coexisting | Inconsistent, harder to validate | Medium | Migrate all to declarative | + +### P2 - Medium (Quality of life) + +| Issue | Impact | Effort | Notes | +|-------|--------|--------|-------| +| Code duplication in exporters | Maintenance burden | Medium | Extract base exporter class | +| Missing type hints | IDE support, bugs | Low | Add progressively | +| Inconsistent naming | Cognitive load | Low | Establish conventions | +| Magic strings/numbers | Bugs, hard to refactor | Low | Create enums/constants | + +### P3 - Low (Nice to have) + +| Issue | Impact | Effort | Notes | +|-------|--------|--------|-------| +| Exporter quality variance | Some exports may fail | Medium | Add capability metadata | +| No caching for LLM inferences | Repeated work | Medium | Add caching layer | +| Sparse docstrings | Onboarding difficulty | Low | Document as we go | + +--- + +## Proposed Refactoring Phases + +### Phase 1: Foundation (After UX work) +- [ ] Extract `app.py` into logical modules: + - `app_core.py` - Gradio app setup, shared state + - `app_generation.py` - Content generation handlers + - `app_playtest.py` - Playtest/preview handlers + - `app_export.py` - Export handlers + - `app_media.py` - Media generation handlers +- [ ] Add basic pytest infrastructure +- [ ] Create constants/enums for magic strings + +### Phase 2: Engine Isolation +- [ ] Extract pure game engine (no Gradio dependencies) +- [ ] Complete Player → GameState migration +- [ ] Migrate lambda consequences to declarative effects +- [ ] Add engine unit tests + +### Phase 3: ML Infrastructure +- [ ] Create ModelRegistry class with metadata +- [ ] Add structured error handling + logging +- [ ] Implement inference caching + +### Phase 4: Polish +- [ ] Extract shared exporter base class +- [ ] Add type hints throughout +- [ ] Comprehensive documentation pass +- [ ] Add integration tests + +--- + +## Metrics to Track + +- Lines in `app.py` (target: <500) +- Test coverage % (target: >60%) +- Average function length (target: <50 lines) +- Number of untyped functions (target: 0) + +--- + +## UX Insights to Gather First + +Before restructuring, document insights from UX work: + +- [ ] Which tabs/features are actually used most? +- [ ] What are common user workflows? +- [ ] Where do users get confused or stuck? +- [ ] Which exporters are production-quality vs experimental? +- [ ] What error messages do users encounter? + +These insights will inform which modules to prioritize and how to structure the codebase for real usage patterns. + +--- + +## Notes / Updates + +*Add notes here as UX work progresses* + +- 2026-01-21: Plan created. Starting UX improvements first. + diff --git a/TODO_MOVINGDOTSPACE.md b/TODO_MOVINGDOTSPACE.md new file mode 100644 index 0000000000000000000000000000000000000000..07b66ba486a94b3851045db6adf581e6158a6154 --- /dev/null +++ b/TODO_MOVINGDOTSPACE.md @@ -0,0 +1,91 @@ +# MovingDotSpace Export - Known Issues + +## Status: Deferred for later fix + +Last updated: 2025-01-11 + +--- + +## Issues to Fix + +### 1. Stats Panel Not Updating +- **Money stat** doesn't update in the player stats display +- **Inventory section** remains empty despite items being in config +- **Skills section** not populating + +**Debug info:** Console should log `Stats loaded: {...}` with counts. Check if: +- Data is in the exported config (`exportedConfig.themes['Exported Game']`) +- `gameState.inventory`, `gameState.skills`, `gameState.playerStats.money` are set +- `renderStats()` is being called and executing + +**Possible causes:** +- Timing issue with `loadTheme()` overwriting values after we set them +- `renderStats()` function not finding the DOM elements +- Data format mismatch between config and what `renderStats()` expects + +### 2. Format Verification Needed +Check that extracted data matches MovingDotSpace expected format: + +**Inventory items:** +```javascript +{ type: "weapon", name: "Iron Sword", description: "A sturdy blade" } +``` + +**Skills:** +```javascript +{ branch: "Combat", name: "One-Handed", learned: false } +``` + +**Objectives:** +```javascript +{ id: "quest_id", name: "Quest Name", complete: false } +``` + +--- + +## What Works + +- Autotravel toggle (checkbox in top-right) +- Location locking/unlocking system +- Cross-location transitions (only when location actually changes) +- Modal state machine navigation within locations +- Special locations filtered out (game_start, inventory, quest_journal) + +--- + +## Files Involved + +- `exporters/html_exporters.py` - Main MovingDotSpace exporter +- `ui_tabs/big_rpg_scale_tab.py` - Adds `_movingdotspace_meta` with items/skills +- `MovingDotSpaceExport/MovingDotSpaceStandalone.html` - Template with `renderStats()` + +--- + +## Testing Steps + +1. Load Big RPG Scale demo data +2. Export to Game Config +3. Use Platform Export > MovingDotSpace +4. Open browser DevTools (F12) > Console +5. Check for `Stats loaded: {...}` log +6. Inspect `gameState.inventory`, `gameState.skills`, `gameState.playerStats` + +--- + +## Potential Fixes to Try + +1. **Move stats loading after a delay:** + ```javascript + setTimeout(() => { + gameState.inventory = themeConfig.inventory; + renderStats(); + }, 500); + ``` + +2. **Check if loadTheme overwrites:** + - In MovingDotSpaceStandalone.html, `loadTheme()` sets `gameState.inventory = theme.inventory || []` + - Our explicit setting happens after, so should work + +3. **Verify DOM elements exist:** + - `elements.inventoryList` must exist before `renderStats()` runs + - Check for null element references diff --git a/app.py b/app.py index d1d4391c252820d8f2932d22bd32585cfe052b48..b17b796baf1f529cc0e774f4aa37cf437a252b80 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,12 @@ import gradio as gr +import spaces import json import textwrap from gradio_client import Client -import spaces +import os +import requests +from huggingface_hub import InferenceClient +from openai import OpenAI #Imported Long Variables - comment for each move to search from relatively_constant_variables import * @@ -13,8 +17,322 @@ from my_text_game_engine_attempt import * from file_explorer_and_upload import * from state_prompt_fileman_UI_functions import * from ui_gr_media_management import * - -LinPEWFprevious_messages = [] +from timeline_narrative_construction_tools import * +from bible_as_inspiration_source import * +from config_linting import ( + validate_branching, generate_story_flags, generate_mermaid_diagram, + get_config_health_summary, get_llm_cohesion_prompts, + get_validation_with_fixes, apply_all_quick_fixes, format_issues_for_display, + get_state_issues_map +) +from timeline_and_UI_generation_functions import ( + NARRATIVE_TEMPLATES, get_narrative_templates_list, + generate_config_from_template, generate_config_from_prompt +) +from my_text_game_engine_attempt import ( + get_all_states_from_config, jump_to_state, hot_reload_config, + get_current_state_id, make_choice_with_state_display, export_playthrough_log, + get_initgameinfo, get_path_errors +) +from ui_tabs.big_rpg_scale_tab import EntityDatabase + +# LinPEWFprevious_messages = [] + +# FILM_SCENARIOS and generate_scenario_sequence moved to film_scenarios.py + +# ==================== DEMO CONFIGS ==================== +SANDWICH_DEMO_CONFIG = { + "game_start": { + "intro": { + "description": "**Sandwich Quest**\n\nYou're hungry! Time to get a sandwich from Tony's shop.", + "choices": ["Start adventure"], + "transitions": {"Start adventure": "home/start"}, + "on_enter": {"set_money": 20, "start_mission": "get_lunch"} + } + }, + "home": { + "start": { + "description": "**Your Home**\n\nYour stomach growls. The fridge is empty.\n\nYou have 20 gold.", + "choices": ["Go outside", "Check wallet"], + "transitions": {"Go outside": "street/arrive", "Check wallet": "home/wallet"} + }, + "wallet": { + "description": "You check your wallet. **20 gold** - enough for a sandwich!", + "choices": ["Go outside"], + "transitions": {"Go outside": "street/arrive"} + } + }, + "street": { + "arrive": { + "description": "**Main Street**\n\nPeople walk by. You see Tony's Sandwich Shop ahead.\n\nMrs. Jenkins waves at you.", + "choices": ["Enter sandwich shop", "Talk to Mrs. Jenkins", "Go home"], + "transitions": { + "Enter sandwich shop": "sandwich_shop/arrive", + "Talk to Mrs. Jenkins": "street/jenkins", + "Go home": "home/start" + } + }, + "jenkins": { + "description": "**Mrs. Jenkins** smiles.\n\n\"Hello dear! Getting lunch? Tony makes the best sandwiches!\"", + "choices": ["Thanks for the tip!", "Goodbye"], + "transitions": {"Thanks for the tip!": "street/arrive", "Goodbye": "street/arrive"} + } + }, + "sandwich_shop": { + "arrive": { + "description": "**Tony's Sandwich Shop**\n\nThe smell of fresh bread fills the air.\n\n**Tony** stands behind the counter.\n\n*Quest objective reached!*", + "choices": ["Talk to Tony", "Look at menu", "Leave"], + "transitions": { + "Talk to Tony": "sandwich_shop/tony", + "Look at menu": "sandwich_shop/menu", + "Leave": "street/arrive" + }, + "on_enter": {"set_flag": "reached_sandwich_shop"} + }, + "menu": { + "description": "**Menu:**\n- Classic Sandwich: 5 gold\n- Deluxe Sandwich: 10 gold\n- Super Sandwich: 15 gold", + "choices": ["Talk to Tony", "Leave"], + "transitions": {"Talk to Tony": "sandwich_shop/tony", "Leave": "street/arrive"} + }, + "tony": { + "description": "**Tony** grins.\n\n\"What can I get you today?\"", + "choices": ["Buy Classic (5g)", "Buy Deluxe (10g)", "Just looking"], + "transitions": { + "Buy Classic (5g)": "sandwich_shop/buy_classic", + "Buy Deluxe (10g)": "sandwich_shop/buy_deluxe", + "Just looking": "sandwich_shop/arrive" + } + }, + "buy_classic": { + "description": "You hand over 5 gold.\n\nTony gives you a **Classic Sandwich**!\n\n*Quest complete!*", + "choices": ["Eat it!", "Save for later"], + "transitions": {"Eat it!": "sandwich_shop/eat", "Save for later": "street/arrive"}, + "on_enter": {"remove_money": 5, "add_item": "Classic Sandwich", "complete_mission": "get_lunch"} + }, + "buy_deluxe": { + "description": "You hand over 10 gold.\n\nTony gives you a **Deluxe Sandwich**!\n\n*Quest complete!*", + "choices": ["Eat it!", "Save for later"], + "transitions": {"Eat it!": "sandwich_shop/eat", "Save for later": "street/arrive"}, + "on_enter": {"remove_money": 10, "add_item": "Deluxe Sandwich", "complete_mission": "get_lunch"} + }, + "eat": { + "description": "Mmm! Delicious!\n\n**THE END**\n\nThanks for playing!", + "choices": ["Play again"], + "transitions": {"Play again": "game_start/intro"} + } + } +} + +SKYRIM_DEMO_CONFIG = { + "game_start": { + "intro": { + "description": "**Welcome, Dragonborn!**\n\nYour journey begins. You have 100 gold.\n\n*Main quest: Unbound has started.*", + "choices": ["Begin adventure"], + "transitions": {"Begin adventure": "whiterun/arrive"}, + "on_enter": {"set_money": 100, "start_mission": ["mq101", "mq102"]} + } + }, + "whiterun": { + "arrive": { + "description": "**Whiterun** (Whiterun Hold)\n\nA bustling city of 200 souls. The streets are alive with activity.\n\nYou see: Belethor the Merchant.", + "choices": ["Speak with Belethor", "Browse Belethor's wares", "Travel to Riverwood", "Travel to Dragonsreach", "Check quest journal"], + "transitions": { + "Speak with Belethor": "whiterun/talk_belethor", + "Browse Belethor's wares": "whiterun/shop", + "Travel to Riverwood": "riverwood/arrive", + "Travel to Dragonsreach": "dragonsreach/arrive", + "Check quest journal": "quest_journal/journal" + } + }, + "talk_belethor": { + "description": "**Belethor** greets you.\n\n\"Everything's for sale, my friend. Everything. If I had a sister, I'd sell her in a second.\"", + "choices": ["What do you have?", "Goodbye"], + "transitions": {"What do you have?": "whiterun/shop", "Goodbye": "whiterun/arrive"} + }, + "shop": { + "description": "**Belethor's General Goods**\n\n- Iron Sword (25g)\n- Health Potion (25g)\n- Leather Strips (3g)", + "choices": ["Buy Iron Sword", "Buy Health Potion", "Leave"], + "transitions": { + "Buy Iron Sword": "whiterun/buy_sword", + "Buy Health Potion": "whiterun/buy_potion", + "Leave": "whiterun/arrive" + } + }, + "buy_sword": { + "description": "You purchase an **Iron Sword** for 25 gold.", + "choices": ["Continue shopping", "Leave"], + "transitions": {"Continue shopping": "whiterun/shop", "Leave": "whiterun/arrive"}, + "on_enter": {"remove_money": 25, "add_item": "Iron Sword"} + }, + "buy_potion": { + "description": "You purchase a **Health Potion** for 25 gold.", + "choices": ["Continue shopping", "Leave"], + "transitions": {"Continue shopping": "whiterun/shop", "Leave": "whiterun/arrive"}, + "on_enter": {"remove_money": 25, "add_item": "Health Potion"} + } + }, + "riverwood": { + "arrive": { + "description": "**Riverwood** (Whiterun Hold)\n\nA quiet village of 50 people. Life here is simple.\n\nYou see: Alvor the Blacksmith.\n\n*Quest objective: Escape to Riverwood - Complete!*", + "choices": ["Speak with Alvor", "Travel to Whiterun", "Enter Bleak Falls Barrow", "Check quest journal"], + "transitions": { + "Speak with Alvor": "riverwood/talk_alvor", + "Travel to Whiterun": "whiterun/arrive", + "Enter Bleak Falls Barrow": "bleak_falls/arrive", + "Check quest journal": "quest_journal/journal" + }, + "on_enter": {"set_flag": "reached_riverwood", "set_knowledge": {"last_location": "riverwood"}} + }, + "talk_alvor": { + "description": "**Alvor** looks up from his forge.\n\n\"You're the one who escaped Helgen? By the Nine! Come, tell me what happened.\"\n\n*Quest updated: Spoke with Alvor*", + "choices": ["Tell him about the dragon", "Ask about Bleak Falls", "Goodbye"], + "transitions": { + "Tell him about the dragon": "riverwood/dragon_talk", + "Ask about Bleak Falls": "riverwood/bleak_falls_info", + "Goodbye": "riverwood/arrive" + }, + "on_enter": {"set_flag": "talked_alvor"} + }, + "dragon_talk": { + "description": "Alvor's face pales.\n\n\"A dragon? Are you sure? We need to warn Jarl Balgruuf in Dragonsreach!\"", + "choices": ["I'll go warn him", "Maybe later"], + "transitions": {"I'll go warn him": "whiterun/arrive", "Maybe later": "riverwood/arrive"} + }, + "bleak_falls_info": { + "description": "\"Bleak Falls Barrow? That old ruin? Nothing but draugr and bandits up there. But I've heard the court wizard is looking for something from that place.\"", + "choices": ["Thanks", "Goodbye"], + "transitions": {"Thanks": "riverwood/arrive", "Goodbye": "riverwood/arrive"} + } + }, + "dragonsreach": { + "arrive": { + "description": "**Dragonsreach** (Whiterun Hold)\n\nThe great hall of the Jarl. Guards watch your every move.\n\nYou see: Jarl Balgruuf on his throne.", + "choices": ["Speak with Jarl Balgruuf", "Leave"], + "transitions": { + "Speak with Jarl Balgruuf": "dragonsreach/talk_balgruuf", + "Leave": "whiterun/arrive" + } + }, + "talk_balgruuf": { + "description": "**Jarl Balgruuf** regards you from his throne.\n\n\"What brings you to Dragonsreach?\"\n\n*Quest updated: Spoke with the Jarl*", + "choices": ["I bring news of the dragon attack", "I seek work", "Farewell"], + "transitions": { + "I bring news of the dragon attack": "dragonsreach/dragon_news", + "I seek work": "dragonsreach/work", + "Farewell": "dragonsreach/arrive" + }, + "on_enter": {"set_flag": "talked_balgruuf"} + }, + "dragon_news": { + "description": "The Jarl leans forward.\n\n\"A dragon? This is grave news indeed. Speak to my court wizard, Farengar. He has been researching dragons.\"\n\n*Quest: Before the Storm progressed*", + "choices": ["I'll speak to Farengar", "Where is Farengar?"], + "transitions": { + "I'll speak to Farengar": "dragonsreach/farengar", + "Where is Farengar?": "dragonsreach/farengar" + } + }, + "work": { + "description": "\"Work? Perhaps. The Companions are always looking for strong arms. Speak to Kodlak at Jorrvaskr.\"", + "choices": ["Thank you"], + "transitions": {"Thank you": "dragonsreach/arrive"} + }, + "farengar": { + "description": "**Farengar Secret-Fire** looks up from his books.\n\n\"Ah, the Jarl's new errand person. I need you to fetch something from Bleak Falls Barrow - a stone tablet called the Dragonstone.\"\n\n*New quest: Bleak Falls Barrow*", + "choices": ["I'll get it", "Tell me more about the Dragonstone"], + "transitions": { + "I'll get it": "dragonsreach/arrive", + "Tell me more about the Dragonstone": "dragonsreach/dragonstone_info" + }, + "on_enter": {"start_mission": "mq103"} + }, + "dragonstone_info": { + "description": "\"It's an ancient map of dragon burial sites. If the dragons are truly returning, we need to know where they might appear.\"", + "choices": ["I'll retrieve it"], + "transitions": {"I'll retrieve it": "dragonsreach/arrive"} + } + }, + "bleak_falls": { + "arrive": { + "description": "**Bleak Falls Barrow** (Whiterun Hold)\n\nAncient stone corridors stretch into darkness. Danger lurks in every shadow.\n\n*This is the destination of your quest!*", + "choices": ["Explore deeper", "Leave"], + "transitions": { + "Explore deeper": "bleak_falls/explore", + "Leave": "riverwood/arrive" + }, + "on_enter": {"set_flag": "reached_bleak_falls"} + }, + "explore": { + "description": "You venture into the barrow. Cobwebs and ancient dust fill the air.\n\nYou hear shuffling in the darkness...\n\n*A draugr emerges!*", + "choices": ["Fight!", "Retreat!"], + "transitions": { + "Fight!": "bleak_falls/combat", + "Retreat!": "bleak_falls/arrive" + } + }, + "combat": { + "description": "You strike down the draugr!\n\nDeeper in, you find a chamber with an ancient wall... and the **Dragonstone**!", + "choices": ["Take the Dragonstone", "Examine the wall"], + "transitions": { + "Take the Dragonstone": "bleak_falls/get_stone", + "Examine the wall": "bleak_falls/word_wall" + } + }, + "word_wall": { + "description": "Strange words glow on the wall... You feel power flowing into you.\n\n*You learned a Word of Power: FUS (Force)*", + "choices": ["Take the Dragonstone"], + "transitions": {"Take the Dragonstone": "bleak_falls/get_stone"}, + "on_enter": {"set_flag": "learned_fus"} + }, + "get_stone": { + "description": "You take the **Dragonstone**!\n\n*Quest updated: Return to Farengar*", + "choices": ["Return to Dragonsreach"], + "transitions": {"Return to Dragonsreach": "dragonsreach/arrive"}, + "on_enter": {"add_item": "Dragonstone", "set_flag": "has_dragonstone"} + } + }, + "quest_journal": { + "journal": { + "description": "**Quest Journal**\n\n**Main Quests:**\n- Unbound: Escape to Riverwood\n- Before the Storm: Speak to the Jarl\n- Bleak Falls Barrow: Retrieve the Dragonstone", + "choices": ["Travel to Riverwood", "Travel to Dragonsreach", "Close journal"], + "transitions": { + "Travel to Riverwood": "riverwood/arrive", + "Travel to Dragonsreach": "dragonsreach/arrive", + "Close journal": "whiterun/arrive" + } + } + } +} + +def add_scenario_prompts_to_queue(prompts_text, media_type="image"): + """Add generated prompts to the generation queue. + + Args: + prompts_text: Text containing prompts (one per line starting with 'Cinematic shot:') + media_type: Type of media to generate ('image', 'audio', '3d', 'tts') + """ + global generation_queue + if not prompts_text or not prompts_text.strip(): + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", "No prompts to add" + + lines = prompts_text.strip().split("\n") + added = 0 + for line in lines: + line = line.strip() + # Skip header lines and empty lines + if not line or line.startswith("##") or line.startswith("#"): + continue + # Accept lines starting with "Cinematic shot:" or any non-empty line + if line.startswith("Cinematic shot:") or (len(line) > 10 and not line.startswith("Video Prompts")): + item = { + "id": len(generation_queue) + 1, + "type": media_type, # Use image type since video isn't supported yet + "prompt": line.strip(), + "status": "pending" + } + generation_queue.append(item) + added += 1 + + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", f"Added {added} {media_type} prompts to queue" def LinPEWFformat_prompt(current_prompt, prev_messages): formatted_prompt = textwrap.dedent(""" @@ -63,678 +381,3633 @@ def TestGradioClientrandommodel(text): #-----=------------------=--------------------=--------------------=----------------------=---- -ConfigConversionforExporttoPlatformselector = gr.Dropdown(choices=["playcanvas", "unreal", "gamemaker", "flutter", "2d map related space", "existing game"]) - -def ConfigConversionforExporttoPlatform(platform, config): - FinalOutput = "" - FinalOutputExplanation = "" - - if platform == "playcanvas": - FinalOutput = "" - FinalOutputExplanation = "as the config is for 1d? and the demo has teleport, the conversion to 3d is based location as backdrops for 2d interfaces to essentially use the config in the exact same way for now" - elif platform == "unreal": - FinalOutput = "" - FinalOutputExplanation = "" - elif platform == "gamemaker": - FinalOutput = "" - FinalOutputExplanation = "" - elif platform == "flutter": - FinalOutput = "" - FinalOutputExplanation = "" - elif platform == "2d map related space": - FinalOutput = "" - FinalOutputExplanation = "" - elif platform == "existing game": - FinalOutput = "Mods / Custom creator already in the game" - FinalOutputExplanation = "" - - FinalOutput = "" - return FinalOutputExplanation, FinalOutput +ConfigConversionforExporttoPlatformselector = gr.Dropdown(choices=["playcanvas", "godot", "unreal", "gamemaker", "flutter", "2d map related space", "existing game"]) -#--------------------------------------- +# Import platform exporters +from playcanvas_exporter import export_to_playcanvas +from godot_exporter import export_to_godot +from unreal_exporter import export_to_unreal + +# Import LLM playtester +from llm_playtester import run_llm_playtest + +# Import platform exporters package +from exporters import ConfigConversionforExporttoPlatform + +# Import film scenarios and JSON utilities +from film_scenarios import FILM_SCENARIOS, generate_scenario_sequence +from beat_scenarios import STORY_BEATS, generate_beat_sequence +from dnd_scenarios import DND_ENCOUNTERS, generate_dnd_sequence +from world_scenarios import WORLD_LOCATIONS, generate_world_sequence +from timeline_scenarios import TIMELINE_EVENTS, generate_timeline_sequence +from song_scenarios import EMOTIONAL_STATES, generate_song_sequence +from mystery_scenarios import MYSTERY_LAYERS, generate_mystery_sequence +from json_utils import find_and_visualize_json_errors, join_lines_after_correct_json_errors -def find_and_visualize_json_errors(json_string): - error_positions = [] - start = 0 - while start < len(json_string): - try: - json.loads(json_string[start:]) - break # If no error, we're done - except json.JSONDecodeError as e: - position = start + e.pos - if not error_positions or position > error_positions[-1] + 10: - error_positions.append(position) - start = position + 1 - - # Insert newlines at error positions - result = "" - last_pos = 0 - for pos in error_positions: - result += json_string[last_pos:pos] + "\n" - last_pos = pos - result += json_string[last_pos:] - - return result - -def join_lines_after_correct_json_errors(lines, separator='\n'): - return separator.join(lines) +# Import UI tabs +from ui_tabs import create_story_graph_tab, create_3d_study_tab, create_resources_hub_tab, create_mechanic_translation_tab, create_llm_playtest_tab, create_big_rpg_scale_tab, create_dnd_gm_tab, create_story_architect_tab, create_narrative_engine_tab, create_config_analysis_tab +from ui_tabs.dnd_gm_tab import search_monsters, search_spells, search_magicitems, search_conditions, search_classes, get_monster_details, get_spell_details, get_magicitem_details +# ConfigConversionforExporttoPlatform moved to exporters/router.py +# find_and_visualize_json_errors, join_lines_after_correct_json_errors moved to json_utils.py + +#--------------------------------------- def testgroupingUIremotely(): - with gr.Accordion("Mini-Tutorial for this space", open=False) as testui: - gr.HTML("https://huggingface.co/spaces/fffiloni/tts-hallo-talking-portrait, https://huggingface.co/spaces/fudan-generative-ai/hallo") - gr.HTML("Cant figure how to make a live version yet so will use video") - gr.HTML("Live portrait for the explanations and slide show / screen record for the tutorial (when UI is changed have to remake so need stable workflow / order independent video)") - gr.HTML("""
Main End Goal is Rapid prototypes for education and training purposes eg. Degree of the future is game score with asecondary goal of boilerplate for context to plan stateful game mechanics (eg. power up or down in sunlight etc.)
A way to prototype basic non-3D parts of a game while trying to understand where models can streamline workflow - Doubles up a game developer quest giver (every decision point can be turned into minigame)
Game Studio 'Studio' = Config -- Assets -- Weak Playtest -- Port
""") + with gr.Accordion("Mini-Tutorial & Resources", open=False) as testui: + with gr.Tabs(): + # ============================================================ + # TAB 1: TUTORIAL + # ============================================================ + with gr.Tab("Tutorial"): + gr.HTML("""
+ Main Goal: Rapid prototypes for education/training (game-based learning)
+ Secondary: Plan stateful game mechanics with AI assistance
+ Workflow: Config → Assets → Playtest → Port to Real Engine +
""") + + # SECTION 1: Config Shape + gr.Markdown("---") + gr.Markdown("## 1. Config Shape - The Core Concept") + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("### Video Explanation") + tutorial_video_1 = gr.Video( + value="testmedia/Hallo - My Current Understanding of a game is.mp4", + label="Config Shape Explained" + ) + gr.HTML("🎬 Talking head video placeholder - generate via Media Studio → Video Generation") + + with gr.Column(scale=1): + gr.Markdown("""### Config Structure + +```json +{ + "masterlocation1": { + "sublocation1": { + "description": "What the player sees", + "choices": ["option A", "option B"], + "transitions": { + "option A": "masterlocation1_sublocation2", + "option B": "masterlocation2_start" + }, + "consequences": { + "option A": "lambda player: player.add_item('key')" + }, + "media": ["background.png", "ambient.mp3"] + } + } +} +``` + +**Key Points:** +- **Masterlocations** = Major areas (village, forest) +- **Sublocations** = States within areas +- **Transitions** = Where choices lead +- **Consequences** = State changes +- **Media** = Images, audio, video, 3D + """) + + # SECTION 2: What This Space Can Do + gr.Markdown("---") + gr.Markdown("## 2. What This Space Can Do") + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("### Video Overview") + tutorial_video_2 = gr.Video( + value=None, + label="Capabilities Overview" + ) + gr.HTML("🎬 Talking head video placeholder - generate via Media Studio → Video Generation") + + with gr.Column(scale=1): + gr.Markdown("""### Capabilities + +**Create Games From:** +- Your ideas → LLM generates JSON config +- Textbooks, songs, memes → Interactive games +- Existing designs → Import and modify + +**Test & Improve:** +- Play in-browser with state tracking +- LLM automated playtesting +- Config validation & Mermaid diagrams + +**Export To:** +- PlayCanvas, Godot 4.x, GameMaker +- Flutter, Unreal Engine 5 +- 2D Map visualizations (SVG) + +**Design Tools:** +- Mechanic Translation (1D ↔ 2D ↔ 3D) +- State system with logic gates +- D&D 5e SRD integration + """) + + # Generate Tutorial Videos Section + gr.Markdown("---") + with gr.Accordion("🎬 Generate Tutorial Videos", open=False): + gr.Markdown("""### How to Create Tutorial Videos + +1. **Go to Media Studio → Video Generation** +2. **Upload a portrait image** (or generate one in Media Studio → HF Spaces → Images) +3. **Enter the script text** - the AI will generate TTS and animate the portrait + +**Suggested Scripts:** + +**Video 1 - Config Shape:** +> "The config is a nested JSON structure that defines your game. At the top level, you have masterlocations like village or forest. Inside each are sublocations - the actual game states. Each state has a description players see, choices they can make, transitions to other states, and media like images or audio." + +**Video 2 - Capabilities:** +> "This space helps you prototype games quickly. Generate configs from your ideas using AI, test them right in your browser with live state tracking, then export to real game engines like Godot or Unreal. You can also generate images, audio, 3D models, and even talking head videos like this one." + """) + + with gr.Accordion("Legacy Video Tutorial", open=False): + gr.Markdown("Original prototype videos using Hallo") + gr.Video(value="testmedia/Hallo - My Current Understanding of a game is.mp4") + gr.HTML("Talking portrait spaces: Hallo TTS | SadTalker") + + # ============================================================ + # TAB 2: IMPROVEMENT PROCESS + # ============================================================ + with gr.Tab("Improvement Process"): + gr.Markdown("""## The Improvement Process - Core Abstraction + +The key insight: **systematic improvement with access to the original enables real analysis.** + +This isn't about UI buttons - it's about understanding HOW configs get better.""") + + gr.Markdown("---") + gr.Markdown("### The 4 Focus Areas") + gr.Markdown(""" +Each round of improvement focuses on ONE aspect. This prevents overwhelming changes and lets you see what each focus contributes. + +| Round | Focus | What Changes | Feel Impact | +|-------|-------|--------------|-------------| +| **R1** | Environment | Descriptions gain sensory details, clues planted | World feels *real*, *explorable* | +| **R2** | Characters | NPCs gain motivations, dialogue reveals personality | World feels *inhabited*, *alive* | +| **R3** | Choices | More options per state, different approaches | Player feels *agency*, *freedom* | +| **R4** | Tension | Time pressure, red herrings, stakes raised | Player feels *urgency*, *engagement* | + +**The order matters.** Environment first gives characters a world to inhabit. Characters give meaning to choices. Choices create opportunities for tension. +""") + + gr.Markdown("---") + gr.Markdown("### Why Compare to Original?") + gr.Markdown(""" +Without the original, you can't answer: +- "Did this round actually improve things?" +- "What specifically changed?" +- "Did I lose something important?" + +**The 5-round comparison view** (in Config Analysis) shows the same state across R0→R4, so you can see the journey, not just the destination. +""") + + gr.Markdown("---") + gr.Markdown("### Derivative Changes - The RPG Scale Challenge") + gr.Markdown(""" +At small scale (7-30 states), you can hold the whole config in your head. + +At RPG scale (100+ states), changing one thing affects many others: + +| You Change... | Consider Also Updating... | +|---------------|---------------------------| +| Location name | NPCs who mention it, quests there, travel descriptions | +| NPC role | Their dialogue, faction rank, what they sell | +| Faction relationship | Cross-faction dialogue, quest availability | +| Item properties | Recipes using it, quest objectives, merchant prices | + +The **Derivative Analyzer** (in Big RPG Scale → Validation) shows these connections before you make changes. +""") + + gr.Markdown("---") + gr.Markdown("### The Workflow Pattern") + gr.Markdown(""" +``` +┌─────────────────────────────────────────────────────────┐ +│ 1. START │ +│ Load a config (starter, demo, or your own) │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ 2. ANALYZE │ +│ Current state: metrics, issues, graph visualization │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ 3. IMPROVE (one focus at a time) │ +│ Environment → Characters → Choices → Tension │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ 4. COMPARE │ +│ Side-by-side with original - what changed? │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ 5. TEST │ +│ Playtest (manual or AI personas) │ +│ Did the changes work? Any new issues? │ +└─────────────────────────────────────────────────────────┘ + ↓ + ┌───────────────────────┐ + │ Happy with result? │ + └───────────────────────┘ + ↓ ↓ + NO YES + ↓ ↓ + Back to step 3 EXPORT to game engine +``` + +**The loop is the process.** Each cycle makes the config better. You always have the original to compare against. +""") + + gr.Markdown("---") + gr.Markdown("### Coming Full Circle") + gr.Markdown(""" +This space's purpose: +1. **Idea exposure** - See what's possible with game configs +2. **Abstract the improvement process** - Learn the pattern, not just the tools + +Once you internalize the 4-focus improvement cycle, you can apply it anywhere - with or without this UI. +""") + + gr.Markdown("---") + with gr.Accordion("🎬 Tutorial Video Scripts", open=False): + gr.Markdown("""### Generate These Videos in Media Studio → Talking Head + +**How to use:** Copy a script below, go to Media Studio → Video Generation → Talking Head tab, +upload a portrait image, paste the script text, and generate. + +--- + +**Video 1: The 4 Focus Areas** (~45 seconds) +``` +When improving a game config, we focus on ONE aspect at a time. + +Round one: Environment. Descriptions gain sensory details - what you see, hear, smell. +The world starts feeling real and explorable. + +Round two: Characters. NPCs gain motivations and personality. Their dialogue reveals who they are. +The world feels inhabited. + +Round three: Choices. More options per state. Different approaches to problems. +The player feels agency. + +Round four: Tension. Time pressure, red herrings, raised stakes. +The player feels urgency. + +The order matters. Environment gives characters a world. Characters give meaning to choices. +Choices create opportunities for tension. +``` + +--- + +**Video 2: Why Compare to Original** (~30 seconds) +``` +Why do we keep the original config alongside our improvements? + +Because without it, you can't answer basic questions: +Did this round actually make things better? +What specifically changed? +Did we accidentally lose something important? + +The five-round comparison view shows the same state across all rounds. +You see the journey, not just the destination. +That's how you learn what each focus area contributes. +``` + +--- + +**Video 3: The Workflow Loop** (~40 seconds) +``` +The improvement process is a loop, not a straight line. + +Start with a config - a starter, a demo, or your own. +Analyze it. What's the current state? Any issues? +Improve it - one focus at a time. +Compare to the original. What changed? +Test it. Play through. Did the changes work? + +If you're happy, export to your game engine. +If not, go back to improve. Each cycle makes the config better. + +You always have the original to compare against. +That's the key insight: systematic improvement with access to the original enables real analysis. +``` + +--- + +**Tips:** +- Use a clear, front-facing portrait image +- Each video takes 1-5 minutes to generate depending on text length +- SadTalker works best with natural-sounding scripts +""") + + # ============================================================ + # TAB 3: RESOURCES (moved from Media Studio) + # ============================================================ + with gr.Tab("Resources"): + gr.Markdown("## Inspiration, Learning & Licensing") + + with gr.Accordion("Chat UIs - Free Tier LLMs", open=False): + gr.HTML("""Hugging Face Chat | AnyChat (Latest Releases)""") + gr.HTML("You can turn any space into a tool for huggingchat and the default image tool can do 5-10 secs per image - paste the current description and ask for images") + gr.HTML("Huggingface chat supports - State Management (Threads), Image Generation and editing, Websearch, Document parsing (PDF?), Assistants and larger models than zero gpu can support in July 2024 (Unquantised 30B and above)") + gr.HTML("""Lambda Chat - beta and text only - no login so chats expire (October 2024)""") + gr.HTML("ChatGPT is good for multiple images in one image - eg game maps and for this space all decisions in one picture") + gr.HTML("Gemini") + gr.HTML("Claude - Coding") + gr.HTML("Existing Assistants to use and planning custom assistants placeholder") + + with gr.Accordion("Idea / Inspiration Sources", open=False): + with gr.Row(): + gr.HTML("""Licenses for the spaces still to be evaluated - June 2024
Users to follow with cool spaces:
+ osanseviero - TheMLGame
+ jbilcke-hf
+ dylanebert
+ fffiloni
+ artificialguybr
+ radames
+ multimodalart""") + gr.HTML("""Some Youtube Channels to keep up with updates:

+ @lev-selector
+ @fahdmirza""") + gr.HTML("""Social media that shows possibilities:

+ r/aivideo | + r/StableDiffusion | + r/midjourney | + @blizaine | + r/singularity""") + + with gr.Accordion("Examples of Generated Media on Reddit", open=False): + gr.HTML("""FaceCam AI - convert image into video
+ Tripo v2.0 - stunning 3D""") + + with gr.Accordion("Licensing Notes", open=False): + gr.HTML("""To be continued.... Need to find the press release to see license eg. Black Forest Labs announcement""") + + with gr.Accordion("Future Developments / Links to Integrate", open=False): + gr.HTML("""Video games made entirely by o1-preview""") + gr.HTML("""Artificial Analysis | lmarena.ai | web.lmarena.ai""") + gr.HTML(""" + Apache 2.0 models to aim for:
+ HF Text-to-Image (Apache 2.0)

+ + Inspiration and food for thought:
+ tost.ai | + GaussianAnything-AIGC3D | + HF Image-to-3D
+ List of MCP Servers - awesome-mcp-servers
+ Llama Mesh Blender addon | + Clone Robotics Torso 2
+ Moondream 0.5B VLM | + DepthFlow
+ AI-generated CSGO on RTX 3090
+ thuwzy.github.io
+ Teleoperating robots | + Tesla Optimus | + Police robots in China
+ r/aivideo | + Driver 2 PS1 reimagined by AI
+ Llama3 codebase experiments
+ Llama 3.2 all versions - cpu application considerations
+ HF Forum Dashboard - how can we make a dashboard for the game
+ CogVideoX-Fun-5b - Video Gen
+ open-notebooklm | + PDF2Audio | + Roblox 3D Assets Generator
+ HF Video Datasets
+ Midjourney Kling AI Motion Brush | + DART human motions
+ Genie 2 from Google | + SAMURAI vs SAM 2
+ TRELLIS | + Apple DepthPro
+ Pika 2.0 | + Pika Labs update
+ ExBody2 whole-body tracking | + Meta FAIR updates
+ Parking systems analysis | + Joust trailer
+ Moondream 2B gaze detection | + Text to CAD
+ Gorgeous AI technique | + AI standup comedy
+ Llama Mesh blog post | + Research Tracker
+ Scaling Test-Time Compute | + Chess Openings | + AnyChat

+ + MIT filter + Modality:
+ HF Audio Datasets (MIT) | + HF Video Datasets (MIT)
+ VideoChatGPT Dataset - GPU poverty workaround
+ Llama 3.2 Taiwan Legal LoRA - How LoRAs work
+ StoryMaker - Consistency | + Fish Speech 1.4 | + mini-omni
+ Alibaba MiMo controllable character | + ChatGPT Jamaican vibes
+ Whisper v3 Turbo | + OpenMusic | + Ovis1.6-Gemma2-9B
+ Harvard AR glasses | + reasoning-base-20k
+ NVIDIA EdgeRunner | + MeshAnything
+ r/3Dprinting | + Color lithopanes
+ SAM 2.1 Hiera Base Plus
+ MONST3R geometry estimation | + Apple hand gesture
+ Zamba2-7B | + Paper 2410.10306
+ Unitree offroad | + Titans learning
+ MiniMax01 deepdive | + Killed by LLM
+ Polymathic AI datasets | + FrontierMath
+ Robotic firearm OpenAI integration | + Why grokking happens
+ Fictional characters | + DynamicCity
+ Meta brain-to-AI models
+ CGDream pricing | + Tensor.art
+ OpenR1 from HuggingFace | + Build DeepSeek from scratch (MIT)
+ Claude 3.7 plays Pokemon | + Claude Sonnet 3.7 is insane
+ Claude 3.7 Manim code | + Claude 3.7 coded game
+ Flappy Bird Claude 3.7 vs o3-mini | + Google Veo 2 pricing
+ Boids simulation Claude 3.7 | + AI + real footage
+ 3D Unicorn Claude 3.7 Blender | + Veo 2 videogames
+ More Google Veo videos | + CAST 3D scene recovery
+ Shanghai robot factory | + Claude trapped in Mt Moon
+ Godot Docker image | + HF Game-AI Spaces
+ Robot that can see, hear, talk, dance | + LLaDA diffusion model
+ Gemini Code Assist free
+ Kimi.ai | + Inception Labs Chat
+ VACE all-in-one video | + Gemini visual stories
+ One-shot character consistency solved
+ Phone video to animation | + Claude inside Blender
+ New model for animations | + Forcing GPT-4o image gen
+ MathOverflow (Google) | + MathOverflow
+ Maze generation Kruskal's algorithm
+ Amputee with new prosthetics | + ChatGPT memory improved
+ Google AI Co-Scientist
+ Mario game by Gemini Pro 2.5 | + Multiview 3D by Stability AI
+ Meta lightning-fast 3D | + Feeling the AGI strong
+ OptimusAlpha MCBench builds | + Berkeley Humanoid Lite $5k
+ 2-minute AI short film | + ByteDance UI-TARS-1.5
+ 3DTown project

+ + HF Games - Spaces:
+ FarmingGame | + Godot 3D Trucks | + Godot 2D
+ Tetris Game | + SimPhysics-HTML5 + """) + + with gr.Accordion("Current Offline Ideas/Developments", open=False): + gr.HTML("""1.58-bit LLM Extreme Quantization""") + + # Resources Hub moved inside Mini-Tutorial & Resources + create_resources_hub_tab() + return testui #----------------------------------------------------------------------------------------------------------------------------------- with gr.Blocks() as demo: + gr.HTML("WIP - Create Config / Improve existing config / Create Media / Plan Story Direction / Learn Some of the current tools available / Playtest") + gr.HTML("This space (Q1 2024) original objective is obsolete as new 2025+ LLMs can make the UI reliably (and also struggle less with the syntax) so now is an idea exposure tool and to abstract the improvement process (which naturally means full workflow as well)") + + # ========== SHARED STATE FOR CROSS-TAB COMMUNICATION ========== + # Stores paths of generated media (images, audio, video) for use in game config + generated_media_paths = gr.State(value=[]) + # Stores the edited config from the render UI (for syncing back to config textbox) + edited_config_state = gr.State(value="") + testgroupingUIremotely() - ui_gr_media_management_acc() + ui_gr_media_management_acc(generated_media_paths=generated_media_paths) with gr.Tab("Test"): - with gr.Accordion("New Config Errors", open=False): - gr.HTML("Currently the fastest option I know for config = https://huggingface.co/spaces/KwabsHug/SimpleGameConfigIdea | End goal is to use NLP to suggest points to base intertaction on so that text stories can be supported as input as well") - with gr.Row(): - with gr.Column(): - gr.Interface(find_and_visualize_json_errors, inputs=["text"], outputs=["text"], description="Splits the JSON where suppected errors are - Remove extra double quotes and non-pair symbols") - with gr.Column(): - gr.Interface(join_lines_after_correct_json_errors, inputs=["text"], outputs=["text"], description="needs one line to load in the game section") + with gr.Tab("Playtest & Edit"): + gr.Markdown("## Playtest & Edit Workflow") + gr.Markdown("Play the game on the left, edit config on the right. Changes sync back to config for reload.") - with gr.Row(): - with gr.Column(scale=1): - NewConfErrorinput = gr.Textbox(label="Enter a new config here") - NewConfErrorinputbtn = gr.Button("Find Error") - NewConfErroroutput = gr.Code(language="json") - - with gr.Column(scale=2): - NewConfErrorEditedSplitinput = gr.TextArea() - NewConfErrorEditedSplitinputbtn = gr.Button("Remove seperate lines after edit") - - NewConfErrorinputbtn.click(find_and_visualize_json_errors, inputs=[NewConfErrorinput], outputs=[NewConfErrorEditedSplitinput]) - NewConfErrorEditedSplitinputbtn.click(join_lines_after_correct_json_errors, inputs=[NewConfErrorEditedSplitinput], outputs=[NewConfErroroutput]) - - with gr.Accordion("My Previous Quick Config Attempts", open=False): - with gr.Tab("Gemini Examples"): - for experimetal_config_name, experimetal_config in ExampleGameConfigs.items(): - with gr.Tab(experimetal_config_name): - gr.Code(json.dumps(experimetal_config, default=lambda o: o.__dict__, indent=2), label=experimetal_config_name) #str(experimetal_config) - - with gr.Tab("Demo of 'Finished Product Example'"): - gr.HTML("Incomplete") - with gr.Tab("Original with media"): - with gr.Row(): - with gr.Column(scale=2): - fpewadescription = gr.Textbox(label="Current Situation", lines=4, value=fpeinitgameinfo[0]) - fpewamediabool = gr.State(value=True) - fpewamedia = gr.State([]) #"testmedia/Stable Audio - Raindrops, output.wav"]) + # Load Demo Configs - prominently placed at top + with gr.Accordion("Load a Demo Config", open=False): + with gr.Tabs(): + with gr.Tab("Traditional (No State)"): + gr.Markdown("*Classic configs with deterministic transitions and lambda consequences*") + with gr.Row(): + ewpwa_demo_btn_village = gr.Button("Village Adventure (Basic)") + ewpwa_demo_btn_racing = gr.Button("Racing Game") + ewpwa_demo_btn_event = gr.Button("Event Planning") + with gr.Row(): + ewpwa_demo_btn_timeline = gr.Button("Timeline Structure") + ewpwa_demo_btn_memory = gr.Button("Memory Fragments (Advanced)") + with gr.Tab("Stateful (With Logic Gates)"): + gr.Markdown("*Configs using conditions, effects, dynamic transitions, and state tracking*") + with gr.Row(): + ewpwa_demo_btn_logic = gr.Button("Logic Gates Demo (Tavern)") + ewpwa_demo_btn_reference = gr.Button("State System Reference (Comprehensive)") + + # ============================================================ + # ONE-CLICK TO GAMEPLAY - Multiple Scenario Types + # ============================================================ + with gr.Accordion("One-Click to Gameplay", open=False): + gr.Markdown("### Generate Complete Game from Scenarios") + gr.Markdown("Choose a scenario type, configure settings, generate assets, and auto-load into playtest.") + + # Shared generation settings (at top, used by all tabs) + with gr.Accordion("Shared Generation Settings", open=False): + otg_gen_mode = gr.Radio( + ["Local (ZeroGPU)", "API"], + value="Local (ZeroGPU)", + label="Generation Mode" + ) + with gr.Row(): + otg_text_model = gr.Dropdown( + choices=modelnames, + value=modelnames[1] if len(modelnames) > 1 else modelnames[0], + label="Text Model" + ) + otg_image_model = gr.Dropdown( + choices=imagemodelnames, + value=imagemodelnames[0], + label="Image Model" + ) + with gr.Row(): + otg_media_type = gr.Dropdown( + choices=["image", "audio", "3d", "tts"], + value="image", + label="Media Type", + scale=1 + ) - @gr.render(inputs=fpewamedia) - def dynamic_with_media(media_items): - print(media_items) - with gr.Group() as fpewamediagrouping: - gr.HTML("Placeholder to load all media tests - still need to test clearing media on ") - if media_items == []: - gr.Markdown("No media items to display.") - else: - for item in media_items: - render = create_media_component(item) + # Tabs for different scenario types + with gr.Tabs(): + # ==================== TAB 1: FILM SCENARIOS (Original Videographer) ==================== + with gr.Tab("Film Scenarios"): + gr.Markdown("**Videographer's perspective** - Generate from film scene types") - return fpewamediagrouping - - fpewagame_session = gr.State(value=fpeinitgameinfo[3]) - - with gr.Column(scale=1): - fpewachoices = gr.Radio(label="Your Choices", choices=fpeinitgameinfo[1]) - fpewasubmit_btn = gr.Button("Make Choice") - fpewagame_log = gr.Textbox(label="Game Log", lines=20, value=fpeinitgameinfo[2]) - fpewagme_restart = gr.Button('Restart Game') - fpewacustom_configstate = gr.State(value=json.dumps(finished_product_demo, default=lambda o: o.__dict__, indent=2)) - - #Placeholders - #fpewacustom_config = gr.State() - fpewaerror_box = gr.State() - - fpewasubmit_btn.click( - make_choice, - inputs=[fpewachoices, fpewagame_session, fpewamediabool], - outputs=[fpewadescription, fpewachoices, fpewagame_log, fpewagame_session, fpewamedia] - ) + with gr.Row(): + otg_count = gr.Slider(minimum=3, maximum=10, value=5, step=1, label="Number of Scenes") + + with gr.Row(): + otg_opening = gr.Checkbox(label="Openings", value=True) + otg_tension = gr.Checkbox(label="Tension", value=True) + otg_action = gr.Checkbox(label="Action", value=True) + otg_emotional = gr.Checkbox(label="Emotional", value=True) - fpewagme_restart.click(load_game, inputs=[fpewacustom_configstate, fpewamediabool], outputs=[fpewaerror_box, fpewagame_log, fpewadescription, fpewachoices, fpewacustom_configstate, fpewagame_session, fpewamedia]) + with gr.Row(): + otg_discovery = gr.Checkbox(label="Discovery", value=True) + otg_social = gr.Checkbox(label="Social", value=True) + otg_ending = gr.Checkbox(label="Endings", value=True) - with gr.Tab("After Extention / Enhancement Attempt"): - gr.HTML("") + with gr.Row(): + otg_force_opening = gr.Checkbox(label="Force Opening First", value=True) + otg_force_ending = gr.Checkbox(label="Force Ending Last", value=True) - with gr.Tab("Manual - Paste Config"): - gr.HTML("Placeholder as not complete yet (3D not supported, and time (esp need for audio)") - with gr.Row(): - with gr.Column(scale=2): - gr.Markdown("# Text-based Adventure Game") - - wamediabool = gr.State(value=True) - wamedia = gr.State(["testmedia/Stable Audio - Raindrops, output.wav"]) - - @gr.render(inputs=wamedia) - def dynamic_with_media(media_items): - print(media_items) - with gr.Group() as wamediagrouping: - gr.HTML("Placeholder to load all media tests - still need to test clearing media on ") - if media_items == []: - gr.Markdown("No media items to display.") - else: - for item in media_items: - render = create_media_component(item) - - return wamediagrouping - - wadescription = gr.Textbox(label="Current Situation", lines=4, value=initgameinfo[0]) - wachoices = gr.Radio(label="Your Choices", choices=initgameinfo[1]) - wasubmit_btn = gr.Button("Make Choice") - wagame_log = gr.Textbox(label="Game Log", lines=20, value=initgameinfo[2]) - wagame_session = gr.State(value=initgameinfo[3]) - wasubmit_btn.click( - make_choice, - inputs=[wachoices, wagame_session, wamediabool], - outputs=[wadescription, wachoices, wagame_log, wagame_session, wamedia] + with gr.Row(): + otg_generate_btn = gr.Button("Generate Film Scenario", variant="primary", scale=3) + otg_stop_btn = gr.Button("Stop", variant="stop", scale=1, visible=False) + + # ==================== TAB 2: STORY BEATS (Writer's Perspective) ==================== + with gr.Tab("Story Beats"): + gr.Markdown("**Writer's perspective** - Generate from narrative beat structures") + + with gr.Row(): + otg_beat_format = gr.Dropdown( + choices=[ + ("90-Minute Feature Film", "film_90min"), + ("30-Minute TV Episode", "tv_30min"), + ("9-Minute YouTube Video", "youtube_9min"), + ("3-Minute Short", "short_3min"), + ], + value="film_90min", + label="Format" + ) + otg_beat_genre = gr.Dropdown( + choices=[ + ("Action", "action"), + ("Drama", "drama"), + ("Comedy", "comedy"), + ("Thriller", "thriller"), + ("Romance", "romance"), + ("Sci-Fi", "scifi"), + ("Fantasy", "fantasy"), + ("Horror", "horror"), + ], + value="thriller", + label="Genre" + ) + + with gr.Row(): + otg_beat_count = gr.Slider(minimum=3, maximum=15, value=6, step=1, label="Number of Beats") + + with gr.Row(): + otg_beat_generate_btn = gr.Button("Generate Story Beats", variant="primary", scale=3) + + # ==================== TAB 3: D&D ADVENTURE ==================== + with gr.Tab("D&D Adventure"): + gr.Markdown("**Fantasy RPG** - Generate from D&D encounter types") + + with gr.Row(): + otg_dnd_difficulty = gr.Dropdown( + choices=[ + ("Easy", "easy"), + ("Medium", "medium"), + ("Hard", "hard"), + ("Deadly", "deadly"), + ], + value="medium", + label="Difficulty" + ) + otg_dnd_count = gr.Slider(minimum=3, maximum=10, value=5, step=1, label="Encounter Count") + + with gr.Row(): + otg_dnd_combat = gr.Checkbox(label="Combat", value=True) + otg_dnd_social = gr.Checkbox(label="Social", value=True) + otg_dnd_exploration = gr.Checkbox(label="Exploration", value=True) + otg_dnd_puzzle = gr.Checkbox(label="Puzzle", value=True) + + with gr.Row(): + otg_dnd_boss = gr.Checkbox(label="Include Boss Fight", value=True) + otg_dnd_force_boss_end = gr.Checkbox(label="Boss at End", value=True) + + with gr.Row(): + otg_dnd_generate_btn = gr.Button("Generate D&D Adventure", variant="primary", scale=3) + + # ==================== TAB 4: WORLD EXPLORER ==================== + with gr.Tab("World Explorer"): + gr.Markdown("**Geography perspective** - Generate explorable world locations") + + with gr.Row(): + otg_world_count = gr.Slider(minimum=3, maximum=12, value=6, step=1, label="Number of Locations") + + with gr.Row(): + otg_world_forest = gr.Checkbox(label="Forest", value=True) + otg_world_mountain = gr.Checkbox(label="Mountain", value=True) + otg_world_desert = gr.Checkbox(label="Desert", value=False) + otg_world_coastal = gr.Checkbox(label="Coastal", value=True) + + with gr.Row(): + otg_world_urban = gr.Checkbox(label="Urban", value=True) + otg_world_swamp = gr.Checkbox(label="Swamp", value=False) + otg_world_tundra = gr.Checkbox(label="Tundra", value=False) + otg_world_urban_start = gr.Checkbox(label="Start in City", value=True) + + with gr.Row(): + otg_world_generate_btn = gr.Button("Generate World Map", variant="primary", scale=3) + + # ==================== TAB 5: TIMELINE ==================== + with gr.Tab("Timeline"): + gr.Markdown("**Chronological perspective** - Generate time-based narrative") + + with gr.Row(): + otg_timeline_count = gr.Slider(minimum=3, maximum=12, value=6, step=1, label="Number of Events") + + with gr.Row(): + otg_timeline_morning = gr.Checkbox(label="Morning", value=True) + otg_timeline_midday = gr.Checkbox(label="Midday", value=True) + otg_timeline_afternoon = gr.Checkbox(label="Afternoon", value=True) + otg_timeline_evening = gr.Checkbox(label="Evening", value=True) + + with gr.Row(): + otg_timeline_night = gr.Checkbox(label="Night", value=True) + otg_timeline_past = gr.Checkbox(label="Flashbacks", value=False) + otg_timeline_future = gr.Checkbox(label="Flash-forwards", value=False) + otg_timeline_chrono = gr.Checkbox(label="Chronological Order", value=True) + + with gr.Row(): + otg_timeline_generate_btn = gr.Button("Generate Timeline", variant="primary", scale=3) + + # ==================== TAB 6: EMOTIONAL JOURNEY ==================== + with gr.Tab("Emotional Journey"): + gr.Markdown("**Song/Lyrics perspective** - Generate emotional arc narrative") + + with gr.Row(): + otg_song_count = gr.Slider(minimum=3, maximum=10, value=6, step=1, label="Number of Emotional Beats") + otg_song_arc = gr.Dropdown( + choices=[ + ("Varied Journey", "journey"), + ("Building Crescendo", "crescendo"), + ("Tension to Peace", "resolution"), + ], + value="journey", + label="Emotional Arc" + ) + + with gr.Row(): + otg_song_joy = gr.Checkbox(label="Joy", value=True) + otg_song_sorrow = gr.Checkbox(label="Sorrow", value=True) + otg_song_anger = gr.Checkbox(label="Anger", value=False) + otg_song_fear = gr.Checkbox(label="Fear", value=False) + + with gr.Row(): + otg_song_hope = gr.Checkbox(label="Hope", value=True) + otg_song_nostalgia = gr.Checkbox(label="Nostalgia", value=True) + otg_song_peace = gr.Checkbox(label="Peace", value=True) + + with gr.Row(): + otg_song_generate_btn = gr.Button("Generate Emotional Journey", variant="primary", scale=3) + + # ==================== TAB 7: MYSTERY ==================== + with gr.Tab("Mystery"): + gr.Markdown("**Investigation perspective** - Generate layered mystery (iceberg model)") + + with gr.Row(): + otg_mystery_count = gr.Slider(minimum=3, maximum=10, value=5, step=1, label="Number of Clues/Reveals") + + with gr.Row(): + otg_mystery_surface = gr.Checkbox(label="Surface Clues", value=True) + otg_mystery_shallow = gr.Checkbox(label="Shallow Secrets", value=True) + otg_mystery_mid = gr.Checkbox(label="Mid-Depth", value=True) + + with gr.Row(): + otg_mystery_deep = gr.Checkbox(label="Deep Truths", value=True) + otg_mystery_abyss = gr.Checkbox(label="Core Truth (Abyss)", value=True) + otg_mystery_gradual = gr.Checkbox(label="Gradual Reveal", value=True) + + with gr.Row(): + otg_mystery_generate_btn = gr.Button("Generate Mystery", variant="primary", scale=3) + + # Progress display + otg_progress_md = gr.Markdown("**Status:** Ready - Configure settings and click Generate") + otg_current_item = gr.Textbox(label="Current Item", interactive=False, visible=False) + + # Preview outputs (collapsed by default) + with gr.Accordion("Generation Preview", open=False): + otg_sequence_preview = gr.Markdown(label="Generated Sequence") + otg_config_preview = gr.Code(label="Generated Config", language="json", lines=10) + otg_queue_display = gr.Dataframe( + headers=["#", "Type", "Prompt", "Status"], + label="Queue Status", + interactive=False ) - with gr.Column(scale=1): - gr.Markdown("# Debugging") - waerror_box = gr.Textbox(label="Path Errors - Incomplete debug process", lines=4, value=path_errors) - with gr.Accordion("Config (Game Spoiler and Example for llm to remix)", open=False): - wacustom_config = gr.Textbox(label="Custom Configuration (JSON)", value=json.dumps(all_states, default=lambda o: o.__dict__, indent=2), lines=8) - wacustom_configbtn = gr.Button("Load Custom Config") - - wacustom_configbtn.click( - load_game, - inputs=[wacustom_config, wamediabool], - outputs=[waerror_box, wagame_log, wadescription, wachoices, wacustom_config, wagame_session, wamedia] + + # AI Assistance Prompt section + with gr.Accordion("AI Assistance Prompt (Copy to Chat)", open=False): + gr.Markdown("Generate a prompt to paste into ChatGPT/Claude for expanding and improving your config.") + with gr.Row(): + otg_prompt_type = gr.Dropdown( + choices=[ + ("Auto-detect from config", "auto"), + ("World/Geography Exploration", "world"), + ("Timeline/Chronological Story", "timeline"), + ("Emotional Journey/Song", "song"), + ("Mystery/Investigation", "mystery"), + ("D&D/Fantasy Adventure", "dnd"), + ("Film/Video Scenes", "film"), + ("Story Beats", "beats") + ], + value="auto", + label="Config Type", + scale=2 ) - with gr.Tab("Manual - With Asset generation and state change options"): - gr.HTML("Edit Complexity - Story Cohesion + Valid JSON formating + Correct branching to further match the story") - with gr.Row(): - with gr.Column(scale=2): - gr.Markdown("# Text-based Adventure Game") - - edwamediabool = gr.State(value=True) - edwamedia = gr.State(["testmedia/Stable Audio - Raindrops, output.wav"]) - - @gr.render(inputs=edwamedia) - def dynamic_with_media(media_items): - print(media_items) - with gr.Group() as edwamediagrouping: - gr.HTML("Placeholder to load all media tests - still need to test clearing media on ") - if media_items == []: - gr.Markdown("No media items to display.") - else: - for item in media_items: - render = create_media_component(item) - - return edwamediagrouping - - edwadescription = gr.Textbox(label="Current Situation", lines=4, value=initgameinfo[0]) - edwachoices = gr.Radio(label="Your Choices", choices=initgameinfo[1]) - edwasubmit_btn = gr.Button("Make Choice") - edwagame_log = gr.Textbox(label="Game Log", lines=20, value=initgameinfo[2]) - edwagame_session = gr.State(value=initgameinfo[3]) - edwasubmit_btn.click( - make_choice, - inputs=[edwachoices, edwagame_session, edwamediabool], - outputs=[edwadescription, edwachoices, edwagame_log, edwagame_session, edwamedia] - ) - with gr.Column(scale=1): - gr.Markdown("# Debugging") - edwaerror_box = gr.Textbox(label="Path Errors - Incomplete debug process", lines=4, value=path_errors) - with gr.Accordion("Config - Game Spoiler - skip around here", open=False): - edwacustom_config = gr.Textbox(label="Custom Configuration (JSON)", value=json.dumps(all_states, default=lambda o: o.__dict__, indent=2), lines=8) - with gr.Row(): - edwacustom_location = gr.Textbox(label="starting location") - edwacustom_start_state = gr.Textbox(label="starting state") - edwacustom_configbtn = gr.Button("Load Custom Config") - - edwacustom_configbtn.click( - load_game_edit_version, - inputs=[edwacustom_config, edwamediabool, edwacustom_location, edwacustom_start_state], - outputs=[edwaerror_box, edwagame_log, edwadescription, edwachoices, edwacustom_config, edwagame_session, edwamedia] + otg_prompt_focus = gr.Dropdown( + choices=[ + ("Balanced improvements", "balanced"), + ("Add more detail/descriptions", "detail"), + ("Add more choices/branches", "choices"), + ("Deepen character motivations", "characters"), + ("Add environmental storytelling", "environment"), + ("Increase dramatic tension", "tension") + ], + value="balanced", + label="Focus Area", + scale=2 ) - gr.HTML("Export currently loaded config") - with gr.Row(): - edexportstatus = gr.Textbox("", ) - gr.Button(value="Export loaded Config").click(export_config_with_media, inputs=[edwacustom_config], outputs=[edexportstatus]) - with gr.Accordion("Edit with Format Assist - Game Spoiler"): - @gr.render(inputs=edwacustom_config) #ewpgame_structure_output_text_with_media - def update(edwacustom_config): - return show_elements_json_input_play_and_edit_version(edwacustom_config) - - with gr.Tab("Semi-Auto - Edit config while playing game"): - gr.HTML("-- Incomplete -- Current problem is passing values from rendered items to the config box
Need a way have dropdowns for the filelist and transitions eg. changing transitions must auto update choices
Config to components has hardcoded variables based on the auto gen so changes break it") - with gr.Column(scale=1): - gr.Markdown("# Debugging") + otg_generate_prompt_btn = gr.Button("Generate AI Prompt", variant="secondary", scale=1) + otg_ai_prompt_output = gr.Code( + label="AI Prompt (click copy button to copy to your chat)", + language=None, + lines=12 + ) + + # Export section + with gr.Accordion("Export Config + Media Files", open=False): + gr.Markdown("Download a ZIP containing the config JSON and all generated media files.") with gr.Row(): - with gr.Column(): - ewpwaerror_box = gr.Textbox(label="Path Errors - Incomplete debug process", lines=4, value=path_errors) - - - with gr.Accordion("Generate a new config"): - with gr.Accordion("Lists for config - only linear path for now", open=False): - gr.Markdown("# Story and Timeline Generator") - gr.Markdown("Click the button to generate a random timeline and story based on UI elements and story events.
Ask an LLM to use this to write a story around") - #with gr.Row(): - #ewpgame_structure_output_text_with_media = gr.Code(language="json") - #ewpgame_structure_output_text = gr.Code(language="json") - with gr.Row(): - ewptimeline_output_with_assets = gr.Textbox(label="Timeline with Assets Considered", lines=20) - #ewptimeline_output = gr.Textbox(label="Timeline (Order might be different for now)", lines=20) - with gr.Column(): - ewptimeline_output_text = gr.Textbox(label="Random Suggestions", lines=10) - ewptimeline_selected_lists_text = gr.Textbox(label="Selected Idea Lists for Inspiration", lines=2) - ewpstory_output = gr.Textbox(label="Generated Story (Order might be different for now)", lines=20) - with gr.Row(): - ewpgenerate_no_story_timeline_points = gr.Slider(minimum=1, value=10, step=1, maximum=30, label="Choose the amount of story timeline points") - ewpgenerate_no_ui_timeline_points = gr.Slider(minimum=1, value=10, step=1, maximum=30, label="Choose the amount of ui timeline points") - #ewpgenerate_no_media_timeline_points = gr.Slider(minimum=1, value=5, step=1, maximum=30, label="Choose the amount of media timeline points") - #ewpgenerate_with_media_check = gr.Checkbox(label="Generate with media", value=True) - with gr.Row(): - ewptimeline_num_lists_slider = gr.Slider(minimum=1, maximum=len(all_idea_lists), step=1, label="Number of Lists to Consider", value=3) - ewptimeline_items_per_list_slider = gr.Slider(minimum=1, maximum=10, step=1, label="Items per List", value=3) - ewptimeline_include_existing_games = gr.Checkbox(label="Include Existing Game Inspirations", value=True) - ewptimeline_include_multiplayer = gr.Checkbox(label="Include Multiplayer Features", value=True) - - ewpgenerate_button = gr.Button("Generate Story and Timeline") - - ewpwacustom_config = gr.Textbox(label="Custom Configuration (JSON)", lines=4) #value=json.dumps(all_states, default=lambda o: o.__dict__, indent=2), lines=4) #Commented out due to initial load issues - ewpwacustom_configbtn = gr.Button("Load Custom Config") - + otg_export_btn = gr.Button("Export as ZIP", variant="secondary") + otg_export_file = gr.File(label="Download ZIP", interactive=False) + otg_export_status = gr.Textbox(label="Export Status", interactive=False, lines=3) + + # Hidden state variables + otg_generated_config = gr.State(value="") + otg_prompts_text = gr.State(value="") + with gr.Row(): + # ========== LEFT COLUMN: GAME PLAYTEST ========== with gr.Column(scale=1): with gr.Group(): - gr.Markdown("# Text-based Adventure Game") + gr.Markdown("### Game Preview") + + # Current state display + ewpwa_current_state_display = gr.Textbox( + label="Current State ID", + value="No game loaded", + interactive=False, + max_lines=1 + ) - ewpwadescription = gr.Textbox(label="Current Situation", lines=4, value=initgameinfo[0]) + # Use lazy getter to avoid import-time game initialization + _init_info = get_initgameinfo() + ewpwadescription = gr.Textbox(label="Current Situation", lines=3, value=_init_info[0]) ewpwamediabool = gr.State(value=True) ewpwamedia = gr.State(["testmedia/Stable Audio - Raindrops, output.wav"]) @gr.render(inputs=ewpwamedia) def dynamic_with_media(media_items): - print(media_items) - with gr.Group() as ewpwamediagrouping: - gr.HTML("Placeholder to load all media tests - still need to test clearing media on ") - if media_items == []: - gr.Markdown("No media items to display.") - else: - for item in media_items: - render = create_media_component(item) - - return ewpwamediagrouping - - ewpwachoices = gr.Radio(label="Your Choices", choices=initgameinfo[1]) - ewpwasubmit_btn = gr.Button("Make Choice") - ewpwagame_log = gr.Textbox(label="Game Log", lines=20, value=initgameinfo[2]) - ewpwagame_session = gr.State(value=initgameinfo[3]) - ewpwasubmit_btn.click( - make_choice, - inputs=[ewpwachoices, ewpwagame_session, ewpwamediabool], - outputs=[ewpwadescription, ewpwachoices, ewpwagame_log, ewpwagame_session, ewpwamedia] + if media_items and len(media_items) > 0: + for item in media_items: + create_media_component(item) + else: + gr.Markdown("*No media for this scene*") + + ewpwachoices = gr.Radio(label="Your Choices", choices=_init_info[1]) + ewpwasubmit_btn = gr.Button("Make Choice", variant="primary") + ewpwagame_log = gr.Textbox(label="Game Log", lines=10, value=_init_info[2]) + ewpwagame_session = gr.State(value=_init_info[3]) + + # Jump to State & Hot Reload controls + with gr.Accordion("Quick Navigation", open=False): + gr.Markdown("**Jump to any state** for testing, or **hot reload** after edits") + ewpwa_state_dropdown = gr.Dropdown( + label="Jump to State", + choices=[], + interactive=True + ) + with gr.Row(): + ewpwa_jump_btn = gr.Button("Jump to State") + ewpwa_hot_reload_btn = gr.Button("Hot Reload Config", variant="secondary") + ewpwa_export_log_btn = gr.Button("Export Playthrough Log") + ewpwa_exported_log = gr.Textbox(label="Exported Log", lines=5, visible=False) + + with gr.Accordion("Config & Debug", open=False): + ewpwaerror_box = gr.Textbox(label="Path Errors", lines=2, value=get_path_errors()) + gr.Markdown("**Quick Load Demo:**") + with gr.Row(): + ewpwa_load_sandwich_btn = gr.Button("Sandwich Shop", variant="secondary", size="sm") + ewpwa_load_skyrim_btn = gr.Button("Skyrim RPG", variant="secondary", size="sm") + ewpwa_load_big_rpg_btn = gr.Button("Big RPG Scale", variant="secondary", size="sm") + ewpwacustom_config = gr.Textbox(label="Game Config (JSON)", lines=6) + with gr.Row(): + ewpwacustom_configbtn = gr.Button("Load Config", variant="primary") + ewpwasync_from_edit_btn = gr.Button("Re-apply Last Edit") + + # NEW: Template-based generation + with gr.Accordion("Generate from Template", open=False): + gr.Markdown("**Generate a config** from narrative templates or a text prompt") + ewpwa_template_dropdown = gr.Dropdown( + label="Narrative Template", + choices=[(v["name"], k) for k, v in NARRATIVE_TEMPLATES.items()], + value="heros_journey" + ) + ewpwa_template_desc = gr.Textbox( + label="Template Description", + value=NARRATIVE_TEMPLATES["heros_journey"]["description"], + interactive=False, + lines=2 + ) + with gr.Row(): + ewpwa_theme_dropdown = gr.Dropdown( + label="Theme", + choices=["fantasy", "scifi", "modern", "horror"], + value="fantasy" + ) + ewpwa_endings_slider = gr.Slider(1, 5, value=3, step=1, label="Number of Endings") + ewpwa_generate_template_btn = gr.Button("Generate from Template", variant="primary") + + gr.Markdown("---") + gr.Markdown("**Or generate from a text prompt:**") + ewpwa_prompt_input = gr.Textbox( + label="Describe your game", + placeholder="e.g., A mystery game with 4 endings where the player investigates a haunted mansion...", + lines=2 + ) + ewpwa_structure_dropdown = gr.Dropdown( + label="Structure Type", + choices=[("Linear", "linear"), ("Branching", "branching"), ("Hub & Spoke", "hub")], + value="branching" + ) + ewpwa_generate_prompt_btn = gr.Button("Generate from Prompt") + + with gr.Accordion("Generate Random (Legacy)", open=False): + with gr.Row(): + ewptimeline_output_with_assets = gr.Textbox(label="Timeline", lines=8) + ewpstory_output = gr.Textbox(label="Story", lines=8) + with gr.Row(): + ewptimeline_output_text = gr.Textbox(label="Suggestions", lines=4) + ewptimeline_selected_lists_text = gr.Textbox(label="Source Lists", lines=2) + with gr.Row(): + ewpgenerate_no_story_timeline_points = gr.Slider(1, 30, value=10, step=1, label="Story Points") + ewpgenerate_no_ui_timeline_points = gr.Slider(1, 30, value=10, step=1, label="UI Points") + with gr.Row(): + ewptimeline_num_lists_slider = gr.Slider(1, len(all_idea_lists), value=3, step=1, label="Idea Lists") + ewptimeline_items_per_list_slider = gr.Slider(1, 10, value=3, step=1, label="Items/List") + with gr.Row(): + ewptimeline_include_existing_games = gr.Checkbox(label="Include Game Inspirations", value=True) + ewptimeline_include_multiplayer = gr.Checkbox(label="Include Multiplayer", value=True) + ewpgenerate_button = gr.Button("Generate Story and Timeline") + + # ========== RIGHT COLUMN: CONFIG EDITOR ========== + with gr.Column(scale=1): + gr.Markdown("### Edit Config While Playing") + + # State to hold edited config for syncing + ewpwa_edited_config = gr.State(value="") + + # Display available media from saved_media folder (uploads + generations) + with gr.Accordion("Available Media Files (uploads + generated)", open=False): + ewpwa_media_paths_display = gr.Textbox( + label="All media files in saved_media/ - copy filenames into media fields below", + lines=5, + interactive=False ) + ewpwa_refresh_media_btn = gr.Button("Refresh Media List") + + def get_media_paths_display(gen_paths): + # Get all files from saved_media folder + all_files = get_all_media_files() + if all_files: + return "\n".join(all_files) + return "No media files yet. Upload files in Media Studio → Library or generate in Media Studio → Generate." + + ewpwa_refresh_media_btn.click( + fn=get_media_paths_display, + inputs=[generated_media_paths], + outputs=[ewpwa_media_paths_display] + ) + + # Config Validation / Linting - ENHANCED with actionable fixes + with gr.Accordion("Validate Config (Story Linting)", open=False): + gr.Markdown("**Validation with actionable suggestions and auto-fix**") + with gr.Row(): + ewpwa_validate_btn = gr.Button("Run Validation", variant="primary") + ewpwa_autofix_btn = gr.Button("Auto-Fix All Issues", variant="secondary") + ewpwa_mermaid_btn = gr.Button("Generate Diagram") + + # Stats display + ewpwa_validation_stats = gr.Markdown(value="*Run validation to see stats*") + + # Detailed issues with fix suggestions + ewpwa_validation_output = gr.Markdown(label="Validation Results") + + # Auto-fix log + ewpwa_autofix_log = gr.Textbox(label="Auto-Fix Log", lines=4, visible=False) + + ewpwa_mermaid_output = gr.Code(label="Mermaid Diagram (copy to mermaid.live)", language=None, lines=10) + + def run_enhanced_validation(config): + """Run validation and return formatted results with stats.""" + if not config: + return "No config loaded", "*Load a config first*" + + validation = get_validation_with_fixes(config) + issues = validation["issues"] + stats = validation["stats"] - ewpwacustom_configbtn.click( - load_game, - inputs=[ewpwacustom_config, ewpwamediabool], - outputs=[ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, ewpwacustom_config, ewpwagame_session, ewpwamedia] + # Format stats + stats_md = f"""**Config Stats:** {stats.get('total_states', 0)} states | """ + stats_md += f"""{stats.get('total_issues', 0)} issues ({stats.get('fixable_issues', 0)} auto-fixable)""" + + if stats.get('issue_types'): + types_str = ", ".join([f"{k}: {v}" for k, v in stats['issue_types'].items()]) + stats_md += f"\n*Issue types: {types_str}*" + + # Format issues + issues_md = format_issues_for_display(issues) + + return issues_md, stats_md + + def run_autofix(config): + """Apply all auto-fixes and return updated config.""" + if not config: + return config, "No config loaded", gr.update(visible=False) + + fixed_config, count, log = apply_all_quick_fixes(config) + + log_text = f"Applied {count} fixes:\n" + "\n".join(log) + return fixed_config, log_text, gr.update(visible=True) + + ewpwa_validate_btn.click( + fn=run_enhanced_validation, + inputs=[ewpwacustom_config], + outputs=[ewpwa_validation_output, ewpwa_validation_stats] + ) + + ewpwa_autofix_btn.click( + fn=run_autofix, + inputs=[ewpwacustom_config], + outputs=[ewpwacustom_config, ewpwa_autofix_log, ewpwa_autofix_log] ) + + ewpwa_mermaid_btn.click( + fn=generate_mermaid_diagram, + inputs=[ewpwacustom_config], + outputs=[ewpwa_mermaid_output] + ) + + # LLM Structure Advice + with gr.Accordion("Ask LLM for Structure Advice", open=False): + gr.Markdown("**Model:** Gemma-2-9b-it via Gradio Client (huggingface-projects/gemma-2-9b-it)") + + # Preset prompts dropdown + cohesion_prompts = get_llm_cohesion_prompts() + ewpwa_preset_dropdown = gr.Dropdown( + choices=["Custom question"] + list(cohesion_prompts.keys()), + value="Custom question", + label="Preset Analysis" + ) + ewpwa_llm_question = gr.Textbox( + label="Question about your config", + placeholder="e.g., How can I add branching paths? What transitions am I missing?", + lines=2 + ) + ewpwa_llm_advice_btn = gr.Button("Get Advice") + ewpwa_llm_response = gr.Textbox(label="LLM Response", lines=8) + + def get_structure_advice(preset, question, config): + if preset != "Custom question" and preset in cohesion_prompts: + # Use preset prompt + prompt = cohesion_prompts[preset].format(config=config[:3000] if config else "No config loaded") + elif question: + prompt = f"""Analyze this game config JSON and answer the question. + +Config: +{config[:2000] if config else "No config loaded"} + +Question: {question} + +Provide specific, actionable advice for improving the game structure.""" + else: + return "Please enter a question or select a preset analysis." + + try: + result = TestGradioClientrandommodel(prompt) + return result if result else "No response received." + except Exception as e: + return f"Error: {str(e)}" + + ewpwa_llm_advice_btn.click( + fn=get_structure_advice, + inputs=[ewpwa_preset_dropdown, ewpwa_llm_question, ewpwacustom_config], + outputs=[ewpwa_llm_response] + ) + + # Dynamic config editor with sync capability + ewpwa_edit_output = gr.Textbox(label="Last Built Config (auto-applied to main)", lines=8, visible=True) + + @gr.render(inputs=[ewpwacustom_config, generated_media_paths]) + def render_config_editor(config_json, media_paths): + if not config_json: + gr.Markdown("*Enter JSON config on left and click 'Load Config' to start editing*") + return + + try: + data = json.loads(config_json) + except json.JSONDecodeError as e: + gr.Markdown(f"**JSON Error:** {str(e)}") + return + + # Get ALL media files from saved_media folder (uploads + generated) + all_media_files = get_all_media_files() + + # Determine structure + if 'masterlocation1' in data: + locations_data = data['masterlocation1'] + wrapper_key = 'masterlocation1' + else: + locations_data = data + wrapper_key = None + + outputs = [] + location_keys = [] + + for location, details in locations_data.items(): + if location == 'end': + continue + + location_keys.append(location) + desc_preview = (details.get('description', '')[:40] + '...') if len(details.get('description', '')) > 40 else details.get('description', '') + + with gr.Accordion(f"{location}: {desc_preview}", open=False): + desc = gr.Textbox(label="Description", value=details.get('description', ''), lines=2, interactive=True) + outputs.append(desc) + + events = gr.Textbox(label="Events", value=json.dumps(details.get('events', [])), interactive=True) + outputs.append(events) + + choices = gr.Textbox(label="Choices", value=json.dumps(details.get('choices', [])), interactive=True) + outputs.append(choices) + + transitions = gr.Textbox(label="Transitions", value=json.dumps(details.get('transitions', {})), interactive=True) + outputs.append(transitions) + + with gr.Row(): + media = gr.Textbox(label="Media", value=json.dumps(details.get('media', [])), scale=2, interactive=True) + if all_media_files: + media_dd = gr.Dropdown(choices=all_media_files, label="Add path", scale=1, interactive=True) + outputs.append(media) + + devnotes = gr.Textbox(label="Notes", value=json.dumps(details.get('developernotes', [])) if isinstance(details.get('developernotes'), list) else str(details.get('developernotes', '')), interactive=True) + outputs.append(devnotes) + + if 'end' in locations_data: + with gr.Accordion("End State", open=False): + gr.JSON(value=locations_data['end']) + + num_fields = 6 + + def build_json(*values): + result = {} + for i, loc in enumerate(location_keys): + try: + result[loc] = { + "description": values[i * num_fields], + "events": json.loads(values[i * num_fields + 1]) if values[i * num_fields + 1] else [], + "choices": json.loads(values[i * num_fields + 2]) if values[i * num_fields + 2] else [], + "transitions": json.loads(values[i * num_fields + 3]) if values[i * num_fields + 3] else {}, + "media": json.loads(values[i * num_fields + 4]) if values[i * num_fields + 4] else [], + "developernotes": json.loads(values[i * num_fields + 5]) if values[i * num_fields + 5].startswith('[') else values[i * num_fields + 5] + } + except: + result[loc] = {"description": values[i * num_fields], "events": [], "choices": [], "transitions": {}, "media": [], "developernotes": "parse error"} + + if 'end' in locations_data: + result['end'] = locations_data['end'] + + if wrapper_key: + return json.dumps({wrapper_key: result}, indent=2) + return json.dumps(result, indent=2) + + def build_and_sync(*values): + result = build_json(*values) + return result, result # Return to both edit output and main config + + sync_btn = gr.Button("Build & Apply Config", variant="primary") + sync_btn.click(fn=build_and_sync, inputs=outputs, outputs=[ewpwa_edit_output, ewpwacustom_config]) + + # ========== EVENT HANDLERS ========== + # Note: ewpwasubmit_btn handler moved below with enhanced state display + + ewpwacustom_configbtn.click( + load_game, + inputs=[ewpwacustom_config, ewpwamediabool], + outputs=[ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, ewpwacustom_config, ewpwagame_session, ewpwamedia] + ) + + # Demo config loaders + def load_sandwich_demo(): + config_json = json.dumps(SANDWICH_DEMO_CONFIG, indent=2) + return config_json + + def load_skyrim_demo(): + config_json = json.dumps(SKYRIM_DEMO_CONFIG, indent=2) + return config_json + + def load_big_rpg_demo(): + """Load the Big RPG Scale demo data and export it as a game config.""" + db = EntityDatabase() + db.load_demo_data() + return db.export_to_game_config() + + ewpwa_load_sandwich_btn.click( + fn=load_sandwich_demo, + outputs=[ewpwacustom_config] + ) + ewpwa_load_skyrim_btn.click( + fn=load_skyrim_demo, + outputs=[ewpwacustom_config] + ) + ewpwa_load_big_rpg_btn.click( + fn=load_big_rpg_demo, + outputs=[ewpwacustom_config] + ) + + # Sync edited config back to main config textbox + ewpwasync_from_edit_btn.click( + fn=lambda x: x, + inputs=[ewpwa_edit_output], + outputs=[ewpwacustom_config] + ) + + ewpgenerate_button.click( + generate_story_and_timeline, + inputs=[ewpgenerate_no_story_timeline_points, ewpgenerate_no_ui_timeline_points, ewptimeline_num_lists_slider, ewptimeline_items_per_list_slider, ewptimeline_include_existing_games, ewptimeline_include_multiplayer], + outputs=[ewptimeline_output_with_assets, ewpstory_output, ewpwacustom_config, ewptimeline_output_text, ewptimeline_selected_lists_text] + ) + + # ========== NEW EVENT HANDLERS FOR ENHANCED FEATURES ========== + + # Update state dropdown when config is loaded + def update_state_dropdown(config_json): + """Update the jump-to-state dropdown with states from the config.""" + states, display_map = get_all_states_from_config(config_json) + if states: + choices = [(display_map.get(s, s), s) for s in states] + return gr.update(choices=choices, value=states[0] if states else None) + return gr.update(choices=[], value=None) + + ewpwacustom_configbtn.click( + update_state_dropdown, + inputs=[ewpwacustom_config], + outputs=[ewpwa_state_dropdown] + ) + + # Jump to state handler + def handle_jump_to_state(config_json, state_id): + """Handle jumping to a specific state.""" + result = jump_to_state(config_json, state_id, with_media=True) + # result is: (error_box, game_log, description, choices, config, session, media, state_display) + return result + + ewpwa_jump_btn.click( + handle_jump_to_state, + inputs=[ewpwacustom_config, ewpwa_state_dropdown], + outputs=[ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, ewpwacustom_config, ewpwagame_session, ewpwamedia, ewpwa_current_state_display] + ) + + # Hot reload handler + def handle_hot_reload(config_json, game_session): + """Handle hot reloading config while preserving game state.""" + result = hot_reload_config(config_json, game_session, with_media=True) + # result is: (error_box, game_log, description, choices, config, session, media, state_display) + return result + + ewpwa_hot_reload_btn.click( + handle_hot_reload, + inputs=[ewpwacustom_config, ewpwagame_session], + outputs=[ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, ewpwacustom_config, ewpwagame_session, ewpwamedia, ewpwa_current_state_display] + ) + + # Update current state display when making choices + def make_choice_enhanced(choice, game_session, with_media): + """Enhanced make_choice that updates state display.""" + result = make_choice_with_state_display(choice, game_session, with_media) + # result is: (description, choices, game_log, session, media, state_display) + return result + + ewpwasubmit_btn.click( + make_choice_enhanced, + inputs=[ewpwachoices, ewpwagame_session, ewpwamediabool], + outputs=[ewpwadescription, ewpwachoices, ewpwagame_log, ewpwagame_session, ewpwamedia, ewpwa_current_state_display] + ) + + # Export playthrough log + def handle_export_log(game_session): + """Export the current playthrough as markdown.""" + log = export_playthrough_log(game_session) + return log, gr.update(visible=True) + + ewpwa_export_log_btn.click( + handle_export_log, + inputs=[ewpwagame_session], + outputs=[ewpwa_exported_log, ewpwa_exported_log] + ) + + # Load demo config handler + def create_demo_loader(demo_key): + """Create a loader function for a specific demo.""" + def load_specific_demo(): + # Get the config based on the key + if demo_key == "finished_product_demo": + config_data = finished_product_demo + elif demo_key in ExampleGameConfigs: + config_data = ExampleGameConfigs[demo_key] + else: + return ( + gr.update(value=f"Demo '{demo_key}' not found"), + None, None, gr.update(choices=[]), None, None, None, + "Demo not found", gr.update(choices=[], value=None) + ) + + config_json = json.dumps(config_data, default=lambda o: o.__dict__, indent=2) + + # Load the game + result = load_game(config_json, with_media=True) + + # Also update state dropdown + states, display_map = get_all_states_from_config(config_json) + state_choices = [(display_map.get(s, s), s) for s in states] if states else [] + + # Get current state for display + current_state = states[0] if states else "No states" + + return ( + result[0], # error_box + result[1], # game_log + result[2], # description + result[3], # choices + gr.update(value=config_json), # config textbox + result[5], # game_session + result[6], # media + current_state, # current_state_display + gr.update(choices=state_choices, value=states[0] if states else None) # state_dropdown + ) + return load_specific_demo + + demo_outputs = [ + ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, + ewpwacustom_config, ewpwagame_session, ewpwamedia, + ewpwa_current_state_display, ewpwa_state_dropdown + ] + + ewpwa_demo_btn_village.click(create_demo_loader("finished_product_demo"), outputs=demo_outputs) + ewpwa_demo_btn_racing.click(create_demo_loader("Racinggametest"), outputs=demo_outputs) + ewpwa_demo_btn_event.click(create_demo_loader("Eventplanningstortytest"), outputs=demo_outputs) + ewpwa_demo_btn_timeline.click(create_demo_loader("TimelineStructurePlanningAttempt"), outputs=demo_outputs) + ewpwa_demo_btn_memory.click(create_demo_loader("MemoryFragments"), outputs=demo_outputs) + # Stateful configs (with logic gates) + ewpwa_demo_btn_logic.click(create_demo_loader("LogicGatesDemo"), outputs=demo_outputs) + ewpwa_demo_btn_reference.click(create_demo_loader("StateSystemReference"), outputs=demo_outputs) + + # Template-based config generation + def update_template_description(template_key): + """Update template description when selection changes.""" + if template_key in NARRATIVE_TEMPLATES: + return NARRATIVE_TEMPLATES[template_key]["description"] + return "" + + ewpwa_template_dropdown.change( + update_template_description, + inputs=[ewpwa_template_dropdown], + outputs=[ewpwa_template_desc] + ) + + def handle_generate_from_template(template_key, theme, num_endings): + """Generate config from selected template.""" + config = generate_config_from_template(template_key, theme, num_endings) + # Also update the state dropdown + states, display_map = get_all_states_from_config(config) + choices = [(display_map.get(s, s), s) for s in states] if states else [] + return config, gr.update(choices=choices, value=states[0] if states else None) + + ewpwa_generate_template_btn.click( + handle_generate_from_template, + inputs=[ewpwa_template_dropdown, ewpwa_theme_dropdown, ewpwa_endings_slider], + outputs=[ewpwacustom_config, ewpwa_state_dropdown] + ) + + # Prompt-based config generation + def handle_generate_from_prompt(prompt, structure_type): + """Generate config from text prompt.""" + if not prompt: + return "", gr.update(choices=[], value=None) + config = generate_config_from_prompt(prompt, structure_type) + # Also update the state dropdown + states, display_map = get_all_states_from_config(config) + choices = [(display_map.get(s, s), s) for s in states] if states else [] + return config, gr.update(choices=choices, value=states[0] if states else None) + + ewpwa_generate_prompt_btn.click( + handle_generate_from_prompt, + inputs=[ewpwa_prompt_input, ewpwa_structure_dropdown], + outputs=[ewpwacustom_config, ewpwa_state_dropdown] + ) + + # ============================================================ + # ONE-CLICK TO GAMEPLAY EVENT HANDLERS + # ============================================================ + + def otg_start_workflow(count, inc_open, inc_tens, inc_act, inc_emo, + inc_disc, inc_soc, inc_end, force_open, force_end, + media_type, gen_mode, text_model, image_model): + """Step 1: Generate sequence, queue prompts, return initial state.""" + # Clear queue and prepare + otg_clear_and_prepare_queue() + + # Generate sequence + list_out, json_out, prompts_out = generate_scenario_sequence( + count, inc_open, inc_tens, inc_act, inc_emo, + inc_disc, inc_soc, inc_end, force_open, force_end + ) + + if "Select at least one category" in list_out: + return ( + list_out, json_out, [], "**Error:** Select at least one category", + gr.update(visible=True), gr.update(visible=False), + json_out, prompts_out + ) + + # Add prompts to queue with section tracking + queue_df, queue_info, status = otg_add_prompts_with_section_tracking( + prompts_out, json_out, media_type + ) + + return ( + list_out, json_out, queue_df, + f"**Queued {len(generation_queue)} items** - Starting generation...", + gr.update(visible=False), # Hide generate button + gr.update(visible=True), # Show stop button + json_out, prompts_out + ) + + def otg_start_beat_workflow(format_type, genre, beat_count, media_type, gen_mode, text_model, image_model): + """Story Beats workflow: Generate sequence, queue prompts, return initial state.""" + otg_clear_and_prepare_queue() + + # Generate story beat sequence + list_out, json_out, prompts_out = generate_beat_sequence(format_type, genre, int(beat_count)) + + if "Select a valid" in list_out or not json_out or json_out == "{}": + return ( + list_out, json_out, [], "**Error:** Invalid format or genre selection", + gr.update(visible=True), gr.update(visible=False), + json_out, prompts_out + ) + + # Add prompts to queue with section tracking + queue_df, queue_info, status = otg_add_prompts_with_section_tracking( + prompts_out, json_out, media_type + ) + + return ( + list_out, json_out, queue_df, + f"**Queued {len(generation_queue)} items** - Starting {genre} story generation...", + gr.update(visible=False), + gr.update(visible=True), + json_out, prompts_out + ) + + def otg_start_dnd_workflow(difficulty, encounter_count, inc_combat, inc_social, + inc_exploration, inc_puzzle, inc_boss, force_boss_end, + media_type, gen_mode, text_model, image_model): + """D&D Adventure workflow: Generate sequence, queue prompts, return initial state.""" + otg_clear_and_prepare_queue() + + # Generate D&D encounter sequence + list_out, json_out, prompts_out = generate_dnd_sequence( + int(encounter_count), difficulty, + inc_combat, inc_social, inc_exploration, inc_puzzle, inc_boss, + force_boss_end + ) + + if "Select at least one" in list_out or not json_out or json_out == "{}": + return ( + list_out, json_out, [], "**Error:** Select at least one encounter type", + gr.update(visible=True), gr.update(visible=False), + json_out, prompts_out + ) + + # Add prompts to queue with section tracking + queue_df, queue_info, status = otg_add_prompts_with_section_tracking( + prompts_out, json_out, media_type + ) + + return ( + list_out, json_out, queue_df, + f"**Queued {len(generation_queue)} items** - Starting D&D adventure generation...", + gr.update(visible=False), + gr.update(visible=True), + json_out, prompts_out + ) + + def otg_start_world_workflow(location_count, inc_forest, inc_mountain, inc_desert, + inc_coastal, inc_urban, inc_swamp, inc_tundra, urban_start, + media_type, gen_mode, text_model, image_model): + """World Explorer workflow: Generate sequence, queue prompts, return initial state.""" + otg_clear_and_prepare_queue() + + # Generate world exploration sequence + list_out, json_out, prompts_out = generate_world_sequence( + int(location_count), + inc_forest, inc_mountain, inc_desert, inc_coastal, + inc_urban, inc_swamp, inc_tundra, urban_start + ) + + if "Select at least one" in list_out or not json_out or json_out == "{}": + return ( + list_out, json_out, [], "**Error:** Select at least one terrain type", + gr.update(visible=True), gr.update(visible=False), + json_out, prompts_out + ) + + # Add prompts to queue with section tracking + queue_df, queue_info, status = otg_add_prompts_with_section_tracking( + prompts_out, json_out, media_type + ) + + return ( + list_out, json_out, queue_df, + f"**Queued {len(generation_queue)} items** - Starting world exploration generation...", + gr.update(visible=False), + gr.update(visible=True), + json_out, prompts_out + ) + + def otg_start_timeline_workflow(event_count, inc_morning, inc_midday, inc_afternoon, + inc_evening, inc_night, inc_past, inc_future, chrono_order, + media_type, gen_mode, text_model, image_model): + """Timeline workflow: Generate sequence, queue prompts, return initial state.""" + otg_clear_and_prepare_queue() + + # Generate timeline sequence + list_out, json_out, prompts_out = generate_timeline_sequence( + int(event_count), + inc_morning, inc_midday, inc_afternoon, inc_evening, + inc_night, inc_past, inc_future, chrono_order + ) + + if "Select at least one" in list_out or not json_out or json_out == "{}": + return ( + list_out, json_out, [], "**Error:** Select at least one time period", + gr.update(visible=True), gr.update(visible=False), + json_out, prompts_out + ) + + # Add prompts to queue with section tracking + queue_df, queue_info, status = otg_add_prompts_with_section_tracking( + prompts_out, json_out, media_type + ) + + return ( + list_out, json_out, queue_df, + f"**Queued {len(generation_queue)} items** - Starting timeline generation...", + gr.update(visible=False), + gr.update(visible=True), + json_out, prompts_out + ) + + def otg_start_song_workflow(verse_count, emotional_arc, inc_joy, inc_sorrow, inc_anger, + inc_fear, inc_hope, inc_nostalgia, inc_peace, + media_type, gen_mode, text_model, image_model): + """Emotional Journey workflow: Generate sequence, queue prompts, return initial state.""" + otg_clear_and_prepare_queue() + + # Generate emotional journey sequence + list_out, json_out, prompts_out = generate_song_sequence( + int(verse_count), + inc_joy, inc_sorrow, inc_anger, inc_fear, + inc_hope, inc_nostalgia, inc_peace, emotional_arc + ) + + if "Select at least one" in list_out or not json_out or json_out == "{}": + return ( + list_out, json_out, [], "**Error:** Select at least one emotional category", + gr.update(visible=True), gr.update(visible=False), + json_out, prompts_out + ) + + # Add prompts to queue with section tracking + queue_df, queue_info, status = otg_add_prompts_with_section_tracking( + prompts_out, json_out, media_type + ) + + return ( + list_out, json_out, queue_df, + f"**Queued {len(generation_queue)} items** - Starting emotional journey generation...", + gr.update(visible=False), + gr.update(visible=True), + json_out, prompts_out + ) + + def otg_start_mystery_workflow(clue_count, inc_surface, inc_shallow, inc_mid, + inc_deep, inc_abyss, gradual_reveal, + media_type, gen_mode, text_model, image_model): + """Mystery workflow: Generate sequence, queue prompts, return initial state.""" + otg_clear_and_prepare_queue() + + # Generate mystery investigation sequence + list_out, json_out, prompts_out = generate_mystery_sequence( + int(clue_count), + inc_surface, inc_shallow, inc_mid, inc_deep, inc_abyss, + gradual_reveal + ) + + if "Select at least one" in list_out or not json_out or json_out == "{}": + return ( + list_out, json_out, [], "**Error:** Select at least one mystery layer", + gr.update(visible=True), gr.update(visible=False), + json_out, prompts_out + ) + + # Add prompts to queue with section tracking + queue_df, queue_info, status = otg_add_prompts_with_section_tracking( + prompts_out, json_out, media_type + ) + + return ( + list_out, json_out, queue_df, + f"**Queued {len(generation_queue)} items** - Starting mystery investigation generation...", + gr.update(visible=False), + gr.update(visible=True), + json_out, prompts_out + ) + + def otg_process_and_load(gen_mode, text_model, image_model, config_json): + """Step 2: Process queue items and auto-load when complete.""" + # Use the generator to process items + for progress_text, current_item, queue_df, is_complete, final_config, error_details in otg_process_queue_generator( + gen_mode, text_model, image_model, + use_rag=False, use_streaming=False, + api_source="HF Inference", hf_model_id="", replicate_model="" + ): + if is_complete: + # All done - load config into playtest (even with partial results) + if final_config and final_config.strip(): + try: + # Load into playtest with updated media paths + result = load_game(final_config, True) + states, display_map = get_all_states_from_config(final_config) + state_choices = [(display_map.get(s, s), s) for s in states] if states else [] + + # Add error details to progress if any + display_progress = progress_text + if error_details: + display_progress += f"\n\nErrors:\n{error_details}" + + yield ( + display_progress, queue_df, + gr.update(visible=True), # Show generate button + gr.update(visible=False), # Hide stop button + final_config, # Update config preview with new file paths + # Playtest outputs + result[0], result[1], result[2], result[3], + final_config, result[5], result[6], + states[0] if states else "No game loaded", + gr.update(choices=state_choices, value=states[0] if states else None) + ) + except Exception as e: + # Error loading config - still show the updated config + error_msg = f"{progress_text}\n\n**Error loading game:** {str(e)}" + yield ( + error_msg, queue_df, + gr.update(visible=True), + gr.update(visible=False), + final_config, # Show updated config even on error + gr.update(value=f"Error: {str(e)}"), gr.update(), gr.update(), gr.update(), + final_config, gr.update(), gr.update(), + gr.update(), gr.update() + ) + else: + # No config generated (all items failed) + error_msg = progress_text + if error_details: + error_msg += f"\n\nErrors:\n{error_details}" + yield ( + error_msg, queue_df, + gr.update(visible=True), + gr.update(visible=False), + gr.update(), # No config to show + gr.update(value="Generation failed - no assets created"), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), + gr.update(), gr.update() + ) + return + else: + # Still processing - yield progress update + yield ( + progress_text, queue_df, + gr.update(), gr.update(), + gr.update(), # Config preview unchanged during processing + gr.update(), gr.update(), gr.update(), gr.update(), + gr.update(), gr.update(), gr.update(), + gr.update(), gr.update() + ) + + def otg_handle_stop(): + """Stop the workflow and show partial results with updated config.""" + progress_text, queue_df, partial_config = otg_stop_workflow() + return ( + progress_text, queue_df, + gr.update(visible=True), # Show generate button + gr.update(visible=False), # Hide stop button + partial_config if partial_config else gr.update() # Update config preview with partial results + ) + + # Wire up the main workflow + otg_generate_btn.click( + otg_start_workflow, + inputs=[ + otg_count, otg_opening, otg_tension, otg_action, otg_emotional, + otg_discovery, otg_social, otg_ending, otg_force_opening, otg_force_ending, + otg_media_type, otg_gen_mode, otg_text_model, otg_image_model + ], + outputs=[ + otg_sequence_preview, otg_config_preview, otg_queue_display, otg_progress_md, + otg_generate_btn, otg_stop_btn, + otg_generated_config, otg_prompts_text + ] + ).then( + otg_process_and_load, + inputs=[otg_gen_mode, otg_text_model, otg_image_model, otg_generated_config], + outputs=[ + otg_progress_md, otg_queue_display, + otg_generate_btn, otg_stop_btn, + otg_config_preview, # Update config preview with new file paths + # Playtest outputs for auto-load + ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, + ewpwacustom_config, ewpwagame_session, ewpwamedia, + ewpwa_current_state_display, ewpwa_state_dropdown + ] + ) + + otg_stop_btn.click( + otg_handle_stop, + outputs=[otg_progress_md, otg_queue_display, otg_generate_btn, otg_stop_btn, otg_config_preview] + ) + + # Story Beats generate button + otg_beat_generate_btn.click( + otg_start_beat_workflow, + inputs=[ + otg_beat_format, otg_beat_genre, otg_beat_count, + otg_media_type, otg_gen_mode, otg_text_model, otg_image_model + ], + outputs=[ + otg_sequence_preview, otg_config_preview, otg_queue_display, otg_progress_md, + otg_beat_generate_btn, otg_stop_btn, + otg_generated_config, otg_prompts_text + ] + ).then( + otg_process_and_load, + inputs=[otg_gen_mode, otg_text_model, otg_image_model, otg_generated_config], + outputs=[ + otg_progress_md, otg_queue_display, + otg_beat_generate_btn, otg_stop_btn, + otg_config_preview, + ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, + ewpwacustom_config, ewpwagame_session, ewpwamedia, + ewpwa_current_state_display, ewpwa_state_dropdown + ] + ) + + # D&D Adventure generate button + otg_dnd_generate_btn.click( + otg_start_dnd_workflow, + inputs=[ + otg_dnd_difficulty, otg_dnd_count, + otg_dnd_combat, otg_dnd_social, otg_dnd_exploration, otg_dnd_puzzle, + otg_dnd_boss, otg_dnd_force_boss_end, + otg_media_type, otg_gen_mode, otg_text_model, otg_image_model + ], + outputs=[ + otg_sequence_preview, otg_config_preview, otg_queue_display, otg_progress_md, + otg_dnd_generate_btn, otg_stop_btn, + otg_generated_config, otg_prompts_text + ] + ).then( + otg_process_and_load, + inputs=[otg_gen_mode, otg_text_model, otg_image_model, otg_generated_config], + outputs=[ + otg_progress_md, otg_queue_display, + otg_dnd_generate_btn, otg_stop_btn, + otg_config_preview, + ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, + ewpwacustom_config, ewpwagame_session, ewpwamedia, + ewpwa_current_state_display, ewpwa_state_dropdown + ] + ) + + # World Explorer generate button + otg_world_generate_btn.click( + otg_start_world_workflow, + inputs=[ + otg_world_count, + otg_world_forest, otg_world_mountain, otg_world_desert, otg_world_coastal, + otg_world_urban, otg_world_swamp, otg_world_tundra, otg_world_urban_start, + otg_media_type, otg_gen_mode, otg_text_model, otg_image_model + ], + outputs=[ + otg_sequence_preview, otg_config_preview, otg_queue_display, otg_progress_md, + otg_world_generate_btn, otg_stop_btn, + otg_generated_config, otg_prompts_text + ] + ).then( + otg_process_and_load, + inputs=[otg_gen_mode, otg_text_model, otg_image_model, otg_generated_config], + outputs=[ + otg_progress_md, otg_queue_display, + otg_world_generate_btn, otg_stop_btn, + otg_config_preview, + ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, + ewpwacustom_config, ewpwagame_session, ewpwamedia, + ewpwa_current_state_display, ewpwa_state_dropdown + ] + ) + + # Timeline generate button + otg_timeline_generate_btn.click( + otg_start_timeline_workflow, + inputs=[ + otg_timeline_count, + otg_timeline_morning, otg_timeline_midday, otg_timeline_afternoon, + otg_timeline_evening, otg_timeline_night, otg_timeline_past, otg_timeline_future, + otg_timeline_chrono, + otg_media_type, otg_gen_mode, otg_text_model, otg_image_model + ], + outputs=[ + otg_sequence_preview, otg_config_preview, otg_queue_display, otg_progress_md, + otg_timeline_generate_btn, otg_stop_btn, + otg_generated_config, otg_prompts_text + ] + ).then( + otg_process_and_load, + inputs=[otg_gen_mode, otg_text_model, otg_image_model, otg_generated_config], + outputs=[ + otg_progress_md, otg_queue_display, + otg_timeline_generate_btn, otg_stop_btn, + otg_config_preview, + ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, + ewpwacustom_config, ewpwagame_session, ewpwamedia, + ewpwa_current_state_display, ewpwa_state_dropdown + ] + ) + + # Emotional Journey generate button + otg_song_generate_btn.click( + otg_start_song_workflow, + inputs=[ + otg_song_count, otg_song_arc, + otg_song_joy, otg_song_sorrow, otg_song_anger, otg_song_fear, + otg_song_hope, otg_song_nostalgia, otg_song_peace, + otg_media_type, otg_gen_mode, otg_text_model, otg_image_model + ], + outputs=[ + otg_sequence_preview, otg_config_preview, otg_queue_display, otg_progress_md, + otg_song_generate_btn, otg_stop_btn, + otg_generated_config, otg_prompts_text + ] + ).then( + otg_process_and_load, + inputs=[otg_gen_mode, otg_text_model, otg_image_model, otg_generated_config], + outputs=[ + otg_progress_md, otg_queue_display, + otg_song_generate_btn, otg_stop_btn, + otg_config_preview, + ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, + ewpwacustom_config, ewpwagame_session, ewpwamedia, + ewpwa_current_state_display, ewpwa_state_dropdown + ] + ) + + # Mystery generate button + otg_mystery_generate_btn.click( + otg_start_mystery_workflow, + inputs=[ + otg_mystery_count, + otg_mystery_surface, otg_mystery_shallow, otg_mystery_mid, + otg_mystery_deep, otg_mystery_abyss, otg_mystery_gradual, + otg_media_type, otg_gen_mode, otg_text_model, otg_image_model + ], + outputs=[ + otg_sequence_preview, otg_config_preview, otg_queue_display, otg_progress_md, + otg_mystery_generate_btn, otg_stop_btn, + otg_generated_config, otg_prompts_text + ] + ).then( + otg_process_and_load, + inputs=[otg_gen_mode, otg_text_model, otg_image_model, otg_generated_config], + outputs=[ + otg_progress_md, otg_queue_display, + otg_mystery_generate_btn, otg_stop_btn, + otg_config_preview, + ewpwaerror_box, ewpwagame_log, ewpwadescription, ewpwachoices, + ewpwacustom_config, ewpwagame_session, ewpwamedia, + ewpwa_current_state_display, ewpwa_state_dropdown + ] + ) + + # Export config with media files as zip + def otg_export_config_with_media(config_json): + """Export the generated config and all media files as a zip.""" + if not config_json or not config_json.strip(): + return None, "No config to export. Generate a sequence first." + + try: + import tempfile + import zipfile + from datetime import datetime + + # Parse config to find media files + config = json.loads(config_json) + media_files = [] + + # Extract all media paths from config + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict) and 'media' in state_data: + media_list = state_data.get('media', []) + if isinstance(media_list, list): + media_files.extend(media_list) + + # Create zip file + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + zip_filename = f"game_export_{timestamp}.zip" + zip_path = os.path.join(tempfile.gettempdir(), zip_filename) + + files_included = 0 + files_missing = [] + + with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + # Add config JSON (named "config.json" for import compatibility) + zipf.writestr("config.json", config_json) + + # Add media files to root (not in media/ subfolder) for import compatibility + for media_path in media_files: + if media_path and isinstance(media_path, str): + # Check if file exists + if os.path.exists(media_path): + # Add to zip root with just filename + arcname = os.path.basename(media_path) + zipf.write(media_path, arcname) + files_included += 1 + else: + files_missing.append(media_path) + + # Build status message + status = f"Exported: {zip_filename}\n" + status += f"- Config: config.json\n" + status += f"- Media files: {files_included} included" + if files_missing: + status += f"\n- Missing files: {len(files_missing)} ({', '.join(files_missing[:3])}{'...' if len(files_missing) > 3 else ''})" + + return zip_path, status + + except json.JSONDecodeError as e: + return None, f"Invalid JSON: {str(e)}" + except Exception as e: + return None, f"Export error: {str(e)}" + + otg_export_btn.click( + otg_export_config_with_media, + inputs=[otg_config_preview], + outputs=[otg_export_file, otg_export_status] + ) + + # AI Prompt Generation Function + def generate_ai_assistance_prompt(config_json, prompt_type, focus_area): + """Generate a context-aware prompt for AI chat assistance.""" + if not config_json or not config_json.strip() or config_json == "{}": + return "Please generate a config first before creating an AI assistance prompt." + + # Auto-detect config type from content + detected_type = prompt_type + if prompt_type == "auto": + config_lower = config_json.lower() + if "world" in config_lower or "location_" in config_lower or "terrain" in config_lower: + detected_type = "world" + elif "timeline" in config_lower or "event_" in config_lower or "morning" in config_lower or "evening" in config_lower: + detected_type = "timeline" + elif "emotional_journey" in config_lower or "moment_" in config_lower or "feeling" in config_lower: + detected_type = "song" + elif "investigation" in config_lower or "clue_" in config_lower or "mystery" in config_lower: + detected_type = "mystery" + elif "adventure" in config_lower or "encounter_" in config_lower or "combat" in config_lower or "dungeon" in config_lower: + detected_type = "dnd" + elif "scene_" in config_lower or "film" in config_lower or "camera" in config_lower: + detected_type = "film" + elif "beat_" in config_lower or "act_" in config_lower: + detected_type = "beats" + else: + detected_type = "film" # Default fallback + + # Type-specific instructions + type_instructions = { + "world": """This is a WORLD/GEOGRAPHY exploration game config. The story unfolds through location discovery. + +Key aspects to enhance: +- Each location should have distinct atmosphere and secrets +- Connections between locations should feel natural (paths, rivers, roads) +- Hidden areas reward thorough exploration +- NPCs should have reasons to be in specific locations +- Environmental storytelling through descriptions""", + + "timeline": """This is a TIMELINE/CHRONOLOGICAL narrative config. Events unfold in time sequence. + +Key aspects to enhance: +- Each time period should have distinct mood (morning hope, night danger) +- Cause and effect between events should be clear +- Flashbacks/flash-forwards should reveal crucial information +- Pacing should alternate between action and reflection +- Time pressure can create tension""", + + "song": """This is an EMOTIONAL JOURNEY config based on musical/lyrical structure. + +Key aspects to enhance: +- Emotional transitions should feel earned, not jarring +- Verses build detail, choruses reinforce themes +- The bridge should offer a perspective shift +- Sensory details anchor abstract emotions +- The arc should have a satisfying resolution""", + + "mystery": """This is a MYSTERY/INVESTIGATION config using the iceberg model (surface to deep truths). + +Key aspects to enhance: +- Surface clues should be obvious but misleading +- Each layer should recontextualize previous discoveries +- Red herrings should be plausible but distinguishable +- The core truth should be surprising yet inevitable +- Character motivations drive the mystery""", + + "dnd": """This is a D&D/FANTASY ADVENTURE config with encounters and exploration. + +Key aspects to enhance: +- Combat encounters need tactical variety +- Social encounters should have multiple resolution paths +- Treasure and rewards should feel earned +- Boss encounters should be memorable set pieces +- Rest points allow strategic resource management""", + + "film": """This is a FILM/VIDEO SCENES config structured around visual storytelling. + +Key aspects to enhance: +- Each scene needs a clear visual hook +- Camera direction implies emotional tone +- Transitions between scenes should be motivated +- Dialogue should reveal character through subtext +- Visual motifs can recur for thematic resonance""", + + "beats": """This is a STORY BEATS config following professional screenplay structure. + +Key aspects to enhance: +- Each beat should turn the story in a new direction +- Character decisions should drive plot, not coincidence +- Stakes should escalate through the midpoint +- The dark night of the soul needs emotional weight +- The climax should pay off earlier setups""" + } + + # Focus area instructions + focus_instructions = { + "balanced": """Improve the config with balanced attention to: +- Richer descriptions (2-3 sentences per state) +- 3-4 meaningful choices per state where appropriate +- Clear character motivations +- Environmental details that support the mood""", + + "detail": """Focus on ADDING DETAIL to descriptions: +- Expand each description to 3-4 vivid sentences +- Include sensory details (sight, sound, smell, touch) +- Add atmospheric elements (weather, lighting, ambient sounds) +- Include small environmental storytelling details +- Make locations feel lived-in and real""", + + "choices": """Focus on ADDING MORE CHOICES and branches: +- Ensure each state has 3-4 distinct choices +- Add choices that reflect different playstyles (cautious, bold, clever) +- Include hidden or conditional choices +- Create meaningful branches that reconverge later +- Add optional side paths that reward exploration""", + + "characters": """Focus on DEEPENING CHARACTER MOTIVATIONS: +- Give NPCs clear wants, fears, and secrets +- Add dialogue that reveals personality +- Create relationship dynamics between characters +- Include character-specific choices and reactions +- Show character growth through the narrative""", + + "environment": """Focus on ENVIRONMENTAL STORYTELLING: +- Add details that imply history without exposition +- Include objects that tell stories (old letters, worn paths, abandoned items) +- Use weather and time of day to set mood +- Create spaces that feel connected to events +- Add discoverable lore through exploration""", + + "tension": """Focus on INCREASING DRAMATIC TENSION: +- Add time pressure or urgency where appropriate +- Include moments of difficult moral choice +- Create setbacks that raise stakes +- Add foreshadowing of dangers +- Include moments of false safety before reveals""" + } + + # Count states in config + try: + config = json.loads(config_json) + state_count = 0 + for section in config.values(): + if isinstance(section, dict): + state_count += len(section) + except: + state_count = "unknown" + + # Build the prompt + prompt = f"""I have a game config JSON for an interactive narrative game. Please help me improve and expand it. + +**CONFIG TYPE:** {detected_type.upper()} +**CURRENT SIZE:** {state_count} states +**FOCUS:** {focus_area.replace('_', ' ').title()} + +{type_instructions.get(detected_type, type_instructions['film'])} + +{focus_instructions.get(focus_area, focus_instructions['balanced'])} + +**IMPORTANT RULES:** +1. Return ONLY valid JSON - no explanations before or after +2. Keep the exact same structure (location > state > properties) +3. Every state MUST have: "description", "choices" (list), "transitions" (dict mapping choices to state IDs) +4. Do NOT rename existing state IDs - only add new ones +5. Ensure all transitions point to valid state IDs +6. Keep "media" and "media_prompt" fields if present + +**HERE IS MY CURRENT CONFIG:** +```json +{config_json} +``` + +Please return an improved version of this config with the enhancements described above. Remember: ONLY return valid JSON, nothing else.""" + + return prompt + + otg_generate_prompt_btn.click( + generate_ai_assistance_prompt, + inputs=[otg_config_preview, otg_prompt_type, otg_prompt_focus], + outputs=[otg_ai_prompt_output] + ) + + # ==================== GAME PREVIEW TABS (Moved from Portability) ==================== + with gr.Tab("Game Preview (2D)"): + gr.Markdown("### 2D MovingDotSpace Preview") + gr.Markdown("Generate a 2D top-down exploration game from your config.") + + with gr.Row(): + preview_2d_config = gr.Textbox( + label="Game Config JSON", + lines=8, + placeholder='{"village": {"start": {"description": "You arrive.", "choices": ["Enter tavern"], "transitions": {"Enter tavern": "tavern"}}}}', + scale=3 + ) with gr.Column(scale=1): - @gr.render(inputs=ewpwacustom_config) #ewpgame_structure_output_text_with_media - def update(ewpwacustom_config): - return show_elements_json_input(ewpwacustom_config) - - ewpgenerate_button.click(generate_story_and_timeline, inputs=[ewpgenerate_no_story_timeline_points, ewpgenerate_no_ui_timeline_points, ewptimeline_num_lists_slider, ewptimeline_items_per_list_slider, ewptimeline_include_existing_games, ewptimeline_include_multiplayer], outputs=[ewptimeline_output_with_assets, ewpstory_output, ewpwacustom_config, ewptimeline_output_text, ewptimeline_selected_lists_text]) #ewptimeline_output_with_assets, ewptimeline_output, ewpstory_output, ewpwacustom_config, ewpgame_structure_output_text]) #ewpgame_structure_output_text_with_media, ewpgame_structure_output_text]) - - with gr.Tab("Config Development Assistance"): #, open=False): - gr.HTML("Jonas Tyroller (https://youtu.be/o5K0uqhxgsE)- This problem changes your perspective on game dev - minimise the cost of exploration so you can explore more (17:00) | dont make the same game again but worse (:)
How can we measure worse?") - gr.Markdown("# Current [Timeline] Workflow = [Precreation] Mermaid Diagram to (1) Story to (2) [Creation] Initial JSON (through LLM and fix JSON by hand) to (3) [Postcreation] JSON Corrections (through LLM and fix JSON by hand) to (4) Media prompts to (5) Asset Generation to (6) JSON Media field population") - with gr.Tab("Precreation"): - gr.Markdown() - gr.HTML("Need to learn how rules (our preferences) affect the config creation process
eg. core game loop - Write a story where the plot only moves based on - [insert core game loop]") - with gr.Tab("Main parts of stories to put into structures"): - with gr.Tab("Purpose (The types of decisions you want the user to make) (Main Theme)"): - gr.HTML("Unpredictable Nuances (Any movie) / Simulation (eg. Work - mixing rare industry crossover) (eg. A phrasebook into interactive game) / Time capsule / Tangible Metaphor / Song Concept as game / Advert as a game") - gr.HTML(" https://en.wikipedia.org/wiki/Philosophy https://en.wikipedia.org/wiki/Moral_injury") - with gr.Tab("Structure - (Non/)Linear Storylines"): - gr.HTML("Trying to abstract the process into one worflow is beyond me so multiple paths to goal (config) is the aim now") - with gr.Tab("Branching - Decisions / Timeline Creation to Story to Config Conversation"): - gr.HTML("Structures for interesting timeline progression") - gr.HTML("Claude Artifacts to illustrate nested structure brainstorms -
https://claude.site/artifacts/4a910d81-1541-49f4-8531-4f27fe56cd1e
https://claude.site/artifacts/265e9242-2093-46e1-9011-ed6ad938be90?fullscreen=false
") - gr.HTML("Placeholder - Considerations - Story from the perspective of Main character or NPC in the LLM genereated story") - mermaideditoriframebtn = gr.Button("Load Mermaid Editor") - mermaideditoriframe = gr.HTML("") - mermaideditoriframebtn.click(fn=lambda x: "", outputs=mermaideditoriframe) - with gr.Accordion("Mermaid Structures - click to open", open=False): - for key, item in mermaidstorystructures.items(): - with gr.Accordion(key, open=False): - gr.Code(item, label=key) - - with gr.Tab("Linear - Player List to Empty Config with Edit support (Narrative based)"): - with gr.Accordion("Can copy in the Test Example State Machine tab - only linear path for now", open=False): - gr.Markdown("# Story and Timeline Generator") - gr.Markdown("Click the button to generate a random timeline and story based on UI elements and story events.
Ask an LLM to use this to write a story around") - with gr.Row(): - game_structure_output_text_with_media = gr.Code(language="json") - #game_structure_output_text = gr.Code(language="json") - with gr.Accordion("JSON with no edits"): - gr.HTML("A long game is a bunch of short games") - with gr.Row(): - timeline_output_with_assets = gr.Textbox(label="Timeline with Assets Considered (gaps = side quests)", lines=25) - #timeline_output = gr.Textbox(label="Timeline (Order might be different for now)", lines=20) - with gr.Column(): - timeline_output_text = gr.Textbox(label="Random Suggestions", lines=10) - timeline_selected_lists_text = gr.Textbox(label="Selected Idea Lists for Inspiration", lines=2) - story_output = gr.Textbox(label="Generated Story (Order might be different for now)", lines=20) - with gr.Row(): - generate_no_story_timeline_points = gr.Slider(minimum=1, value=10, step=1, maximum=30, label="Choose the amount of story timeline points") - generate_no_ui_timeline_points = gr.Slider(minimum=1, value=10, step=1, maximum=30, label="Choose the amount of ui timeline points") - #generate_no_media_timeline_points = gr.Slider(minimum=1, value=5, step=1, maximum=30, label="Choose the amount of media timeline points") - #generate_with_media_check = gr.Checkbox(label="Generate with media", value=True) - with gr.Row(): - timeline_num_lists_slider = gr.Slider(minimum=1, maximum=len(all_idea_lists), step=1, label="Number of Lists to Consider", value=3) - timeline_items_per_list_slider = gr.Slider(minimum=1, maximum=10, step=1, label="Items per List", value=3) - timeline_include_existing_games = gr.Checkbox(label="Include Existing Game Inspirations", value=True) - timeline_include_multiplayer = gr.Checkbox(label="Include Multiplayer Features", value=True) - # timeline_generate_button = gr.Button("Generate Random Suggestions").click( - # timeline_get_random_suggestions, - # inputs=[timeline_num_lists_slider, timeline_items_per_list_slider, timeline_include_existing_games, timeline_include_multiplayer], - # outputs=[timeline_output_text, timeline_selected_lists_text] - # ) - generate_button = gr.Button("Generate Story and Timeline (Click to get UI that will assist with JSON formatting)") - - @gr.render(inputs=game_structure_output_text_with_media) - def update(game_structure_output_text_with_media): - return show_elements_json_input(game_structure_output_text_with_media) - - generate_button.click(generate_story_and_timeline, inputs=[generate_no_story_timeline_points, generate_no_ui_timeline_points, timeline_num_lists_slider, timeline_items_per_list_slider, timeline_include_existing_games, timeline_include_multiplayer], outputs=[timeline_output_with_assets, story_output, game_structure_output_text_with_media, timeline_output_text, timeline_selected_lists_text]) #, generate_no_media_timeline_points, generate_with_media_check], outputs=[timeline_output_with_assets, timeline_output, story_output, game_structure_output_text_with_media, game_structure_output_text]) - - with gr.Tab("Linear - Existing Media eg. Songs and Screenshots"): - gr.HTML("Media position in the story part beginning, during or end") - gr.HTML("Create media first and then ask llm to make join points in the config.
For images need to add the location to every prompt or images will be in random lo") - - with gr.Tab("Linear - Machine Leaning Architectures as game maps"): - gr.HTML("Transformers, SSMs, Image and Video Generation Architectures, GANs, RNNS, etc.") - - with gr.Tab("Linear - Prompt Engineering as basis for ideation process"): - gr.HTML("Current Assited workflow idea - Story timeline events suggestions (LLM / Premade List) | Merging events with premade mermaid structures (LLM + Story Text + Mermaid Text) | Edit mermaid till satisfied (LLM + Story Text) | Ask LLM to convert to config (LLM + JSON Text) | Edit config (LLM / User with format assistance or not) | Playtest and go back to mermaaid or config if there are problems") - gr.HTML("Interactive movie (UI interaction or no progress) vs Branching Paths (Maze)") - gr.HTML("Things that can change the workflow - Asset First (Make Asset and make the transitions using LLM), Export First (Custom JS config, Playcanvas, Unreal Engine reverse engineered to this spaces config?) Game Mechanics First (eg. Player Stats, Inventory and NPCS not implemented yet, so traversal type games best aka graph like structures)") - gr.HTML("Config writing = Remix old one, Ask LLM to make one, Endless combination testing using the prompt engineering above or writing by hand (prompt engineering on yourself)") - gr.HTML("Can use song lyrics as thematic source") - gr.HTML("Placeholder for each below prompt getting a Textbox") - # for item in Storycraftprompts: - # input = gr.State(item) - # output = gr.Textbox("", label=item) - # outputbtn = gr.Button(item).click(fn=llmguide_generate_response, inputs=input, outputs=output) - # for i, item in enumerate(Storycraftprompts, 1): - # input = gr.State(item) - # previous_input = gr.State(lambda: LinPEWFprevious_messages) - # output = gr.Textbox("", label=f"Output {i}") - - # def LinPEWF_update_and_generate(prompt, prev_msgs): - # prev_msgs.append(prompt) - # formatted_prompt = LinPEWFformat_prompt(prompt, prev_msgs) - # response = llmguide_generate_response(formatted_prompt) - # full_response = "" - # for chunk in response: - # full_response += chunk - # prev_msgs.append(f"Response: {full_response}") - # return full_response - - # outputbtn = gr.Button(f"Generate {i}").click( - # fn=LinPEWF_update_and_generate, - # inputs=[input, previous_input], - # outputs=output - # ) - - # LinPEWFprevious_messages.append(item) - - #with gr.Accordion("Decisions / Timeline Creation to Story to Config Conversation", open=False): - with gr.Tab("Branching - Network analysis to Game config"): - gr.HTML("Placeholder for analysing multiple stories for their network structures and creating general rules for a strucutre generator based of named entity recognition and bias to locations or people - The extreme long way") - - with gr.Tab("Linear - Chess PNG to Game config"): - gr.HTML("Any Chess match can serve as end of game final battle") - - with gr.Tab("Conflicts"): - gr.HTML("Most stories are based around") - with gr.Tab("Setting"): - gr.HTML("https://en.wikipedia.org/wiki/History#Periods") - gr.HTML("") - with gr.Tab("Locations"): - gr.HTML("Jungle, Sea, Desert, Snow, City, Village, Space") - with gr.Tab("Character Relations"): - gr.HTML("Friend or foe") - with gr.Tab("Character Archetypes"): - gr.HTML("") - with gr.Tab("Additional Themes"): - gr.HTML("") - with gr.Tab("Generalized Inspiration from existing games"): - gr.HTML("Consequences of buying into Hype quickly (I was going to find a way to fit 70B into zerogpu) - https://www.reddit.com/r/LocalLLaMA/comments/1fc98fu/confirmed_reflection_70bs_official_api_is_sonnet/") - with gr.Tab("Old Ideas to merge"): - gr.HTML("Random Scenario / Song to 'full game' manual or auto is end goal ") - gr.HTML("Componets (outside Code Support for Config): Decisions (and context explanation), Nested Sections, Media (Especially to affect decisions), Replayability (GTA and Tekken type mechanics in text form), Theme integration (Modified Varibles that affect UI or config order)") - gr.HTML("Existing Games eg. GTA Heists - Same Map with overlapping branching narratives, Battlefront - Elites amongst Commoners, Tekken Casino (one mistake = 1/2 or 1/3 of your Resources) and Turn based: 'Tactics' type nintendo games, Chess (and any other tile based game) ") - gr.HTML("Existing Game Rules for text - Cyberpunk RED, ") - gr.HTML("Community playthrough = Tally of players choices, Random item placed in a random location - first person to get it wins, Survival by location or characters met") - gr.HTML("Some Kinds of game skeletons ideas - Timelines, Graph as State machine paths, Economy ecosystem") - gr.HTML("One prompt to be used to test models -
Please make 10 python lists for the types of media files and their purposes in a game and then use those lists to random generate a timeline of 20 items when the function is called
Great next suggest ways to improve this function to create better timelines") - with gr.Tab("Structural Inspirations"): - gr.HTML("GTA Heists - Replayability and stakes, Tekken - 2/3 mistakes = lost round ") - gr.HTML("Elden Ring - Story telling by traversal of map") - gr.HTML("Sports Scores, ") - - with gr.Tab("Worldbuilding tools"): - gr.HTML("Good wordbuilding makes new scenarios automatic") - gr.HTML("In a game awareness = directly affect one of the characters - Any time fights are involved, paralympics categorisations can be inspiration (watched paralympics and wanted to learn the categories eg. s10)") - - with gr.Tab("Concept combination brainstorm"): - gr.HTML("The story and the gameplay dont have to occur at the same time - eg. ") - gr.Markdown("## Prompts / Mermaid diagrams to be made from the ideas for workflow") - with gr.Tab("Using Time as a proxy for all conepts?"): - gr.HTML("A timeline is the most important part of the story - once that is set you can do anything?") - with gr.Tab("Concept Bashing? Ideas"): - with gr.Row(): - gr.Textbox(TimeRelatedConceptsForIdeaGeneration, lines=30) - gr.Textbox(Nonlinearprogressionideas, lines=30) - gr.Textbox(Adjectivebasedcombinationideatextsv2, lines=30) - gr.Textbox(Adjectivebasedcombinationideatexts, lines=30) - gr.HTML("Media Critiques (eg. Youtube Rants) as Prompts to whole games as interactive explanation") - with gr.Tab("Mermaid Diagram to (1) Story"): - gr.HTML("Below 70B seem to struggle here") - gr.Code(WFStage1prompt , label="Prompt Used") + preview_2d_demo_btn = gr.Button("Load Demo Config", variant="secondary") + preview_2d_generate_btn = gr.Button("Generate & Test", variant="primary") + + preview_2d_status = gr.Markdown("") + gr.Markdown("### Game Preview (click inside to focus, use arrow keys to move)") + preview_2d_html = gr.HTML( + value='
Click "Generate & Test" to preview your game here
', + label="Game Preview" + ) + + with gr.Accordion("Generated HTML Code (copy to save)", open=False): + preview_2d_code = gr.Code(label="HTML Source", language="html", lines=15) + + def load_2d_demo(): + import os + try: + demo_path = os.path.join(os.path.dirname(__file__), "sandwich_quest_demo.json") + with open(demo_path, 'r', encoding='utf-8') as f: + return f.read() + except: + return '{"village": {"start": {"description": "Demo village.", "choices": ["Explore"], "transitions": {"Explore": "village_explore"}}}}' + + def generate_2d_preview(config_json): + if not config_json or not config_json.strip(): + return "Please provide config JSON", '
Please provide a game config JSON
', "" + try: + from exporters import export_to_movingdotspace + explanation, html_code = export_to_movingdotspace(config_json) + if html_code: + import base64 + html_b64 = base64.b64encode(html_code.encode('utf-8')).decode('utf-8') + iframe_html = f'' + return f"Generated! {explanation}", iframe_html, html_code + return f"Failed: {explanation}", '
Generation failed
', "" + except Exception as e: + return f"Error: {str(e)}", f'
Error: {str(e)}
', "" + + preview_2d_demo_btn.click(fn=load_2d_demo, outputs=[preview_2d_config]) + preview_2d_generate_btn.click( + fn=generate_2d_preview, + inputs=[preview_2d_config], + outputs=[preview_2d_status, preview_2d_html, preview_2d_code] + ) + + with gr.Tab("Game Preview (3D)"): + gr.Markdown("### 3D PlayCanvas Preview") + gr.Markdown("Preview your game config as a 3D WebGL scene.") + + with gr.Row(): + preview_3d_config = gr.Textbox( + label="Game Config JSON", + lines=8, + placeholder="Paste your game config JSON here...", + scale=3 + ) + with gr.Column(scale=1): + preview_3d_demo_btn = gr.Button("Load Demo Config", variant="secondary") + preview_3d_generate_btn = gr.Button("Generate 3D Preview", variant="primary") + + preview_3d_status = gr.Markdown("") + preview_3d_html = gr.HTML( + value='
Click "Generate 3D Preview" to see your game
', + label="3D Preview" + ) + + with gr.Accordion("Generated HTML Code", open=False): + preview_3d_code = gr.Code(label="HTML Source", language="html", lines=15) + + def load_3d_demo(): + return '{"castle": {"entrance": {"description": "A grand castle entrance.", "choices": ["Enter", "Leave"], "transitions": {"Enter": "castle_hall", "Leave": "forest"}}}}' + + def generate_3d_preview(config_json): + if not config_json or not config_json.strip(): + return "Please provide config JSON", '
Please provide a game config JSON
', "" + try: + from exporters import export_to_playcanvas_html + explanation, html_code = export_to_playcanvas_html(config_json) + if html_code: + import base64 + html_b64 = base64.b64encode(html_code.encode('utf-8')).decode('utf-8') + iframe_html = f'' + return f"Generated! {explanation}", iframe_html, html_code + return f"Failed: {explanation}", '
Generation failed
', "" + except Exception as e: + return f"Error: {str(e)}", f'
Error: {str(e)}
', "" + + preview_3d_demo_btn.click(fn=load_3d_demo, outputs=[preview_3d_config]) + preview_3d_generate_btn.click( + fn=generate_3d_preview, + inputs=[preview_3d_config], + outputs=[preview_3d_status, preview_3d_html, preview_3d_code] + ) + + # ==================== D&D GAME MASTER (Moved from Config Dev) ==================== + create_dnd_gm_tab() + + # ==================== CONFIG ANALYSIS & IMPROVEMENT (Merged Tab) ==================== + create_config_analysis_tab() + + create_llm_playtest_tab(modelnames) + + with gr.Tab("Config Development Assistance"): + gr.Markdown("## Config Development Assistance") + gr.Markdown("Tools and references for creating game configs. Full design theory available in the Design Reference Guide below.") + + # ==================== STARTING CONSIDERATIONS ==================== + gr.Markdown(""" +### Starting Considerations - Where to Begin? +Every game config can begin from different creative angles. Pick the approach that matches your inspiration: + """) + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown(""" +**🌍 World Facts / Geography** + +Start with locations, regions, and how they connect. Build the world first, then populate it. + +→ **Big RPG Scale** tab → Locations + """) + with gr.Column(scale=1): + gr.Markdown(""" +**🎬 Video/Film Ideas (Writer)** + +Structure your story using professional beat templates for 90-min films, 30-min TV episodes, or 9-min YouTube videos. + +→ **Story Architect** tab + """) + with gr.Column(scale=1): + gr.Markdown(""" +**📹 Video/Film Ideas (Videographer)** + +Start from camera angles, shot composition, visual grammar, and direction notes. Think visually first. + +→ **Story Architect** tab → Visual Grammar + """) + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown(""" +**🎵 Song / Lyric Ideas** + +Transform emotional music or poetry into journey-based games. Each verse becomes a location or mood. + +→ **Source Content to Games** tab + """) + with gr.Column(scale=1): + gr.Markdown(""" +**⏱️ Timeline Events** + +Build narrative beats positioned on a visual timeline. Great for story-driven games with clear progression. + +→ **Story Graph** tab or **Generate Config** → Timeline Generator + """) + with gr.Column(scale=1): + gr.Markdown(""" +**📚 Existing Structures** + +Use D&D monsters/items, story templates, or classic narrative structures as building blocks. + +→ **D&D 5e SRD** tab, **Mermaid Diagrams** tab + """) + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown(""" +**🧠 Character Psychology** + +Create deeply motivated characters that drive the story. Define wants, flaws, and psychological profiles. + +→ **Narrative Engine** tab → Character Generator + """) + with gr.Column(scale=1): + gr.Markdown(""" +**🔮 Mystery / Hidden Depth** + +Layer information with surface events hiding deeper truths. Use the 5-layer iceberg model. + +→ **Narrative Engine** tab → Mystery Layers + """) + with gr.Column(scale=1): + gr.Markdown(""" +**⚔️ Faction Politics** + +Design allegiances, hierarchies, and reputation systems. Build conflict through competing groups. + +→ **Big RPG Scale** tab → Factions + """) + + # ==================== DESIGN REFERENCE GUIDE ==================== + # Random tips data for randomiser + DESIGN_TIPS = { + "core": [ + "Make story-based games using mechanics-based games (Chess, Poker, Sudoku) as a conduit", + "Sometimes a fun game includes unfairness - Elden Ring uses controls and movement restrictions", + "Minimise the cost of exploration so players can explore more (Jonas Tyroller)", + "Every theme can be turned into a battle story (resource accumulation across a map)", + "Story structure: Ending Conditions → Villain → Travel/Politics/Combat → Characters", + "A game is alive when the same decisions won't work over and over", + "Boss fights create memorable moments through controlled unfairness", + "The best games teach mechanics through play, not tutorials", + "Stakes make choices matter - what can the player lose?", + "Pacing is about alternating tension and release" + ], + "blocks": [ + "Locations: Jungle, Sea, Desert, Snow, City, Village, Space - each has unique mood", + "Character Relations: Friend or foe - but the best stories blur this line", + "Conflict Origins: Betrayal, lack of sympathy, unfair expectations, misinterpretation", + "GTA Heists: Replayability through overlapping branching narratives", + "Tekken: 2/3 mistakes = lost round (casino-style stakes)", + "Elden Ring: Storytelling by traversal of map, environmental narrative", + "Battlefront: Elites amongst Commoners creates power fantasy variation", + "Purpose: Unpredictable Nuances, Simulation, Time capsule, Tangible Metaphor", + "Every location should have a secret or hidden element", + "NPCs should have their own goals, not just serve the player" + ], + "workflow": [ + "Timeline: Mermaid Diagram → Story → Initial JSON → Corrections → Media → Assets", + "Remix an existing config to learn the format quickly", + "Ask LLM to generate, then manually fix edge cases", + "Endless combination testing reveals unexpected interactions", + "Write by hand with format assistance for full control", + "Test early, test often - broken configs are harder to fix later", + "Keep a library of working configs as templates", + "Version control your configs - git is your friend", + "Document your design decisions for future reference", + "Playtest with fresh eyes after a break" + ] + } + + def get_random_design_tip(): + """Get random tips from each category.""" + import random + core = random.choice(DESIGN_TIPS["core"]) + blocks = random.choice(DESIGN_TIPS["blocks"]) + workflow = random.choice(DESIGN_TIPS["workflow"]) + return f"**🎲 Random Design Tips:**\n\n**Core:** {core}\n\n**Building Block:** {blocks}\n\n**Workflow:** {workflow}" + + with gr.Accordion("Design Reference Guide (Theory & Ideas)", open=False): + gr.Markdown("*Full design theory, game fundamentals, and workflow documentation.*") + + with gr.Row(): + random_tip_btn = gr.Button("🎲 Random Tips", variant="secondary", size="sm") + random_tip_output = gr.Markdown("") + + random_tip_btn.click(fn=get_random_design_tip, outputs=[random_tip_output]) + + with gr.Accordion("Core Principles", open=False): + gr.Markdown(""" +**End Goal:** Make story-based games using mechanics-based games (Chess, Poker, Sudoku, Rubiks Cube, etc.) as a conduit. + +**Key Ideas:** +- Sometimes a fun game includes unfairness - Elden Ring (Controls and movement restrictions), Boss fights +- Minimise the cost of exploration so you can explore more ([Jonas Tyroller](https://youtu.be/o5K0uqhxgsE)) +- Every theme can be turned into a battle story (resource accumulation across a map) +- Story structure: Ending Conditions → Main Villain/Antagonist → Travel, Politics, Combat → Characters +- A game is alive when the same decisions won't work over and over + """) + + with gr.Accordion("Story Building Blocks", open=False): + gr.Markdown(""" +**Locations:** Jungle, Sea, Desert, Snow, City, Village, Space + +**Character Relations:** Friend or foe + +**Conflict Origins:** Betrayal, lack of sympathy, unfair expectations, misinterpretation of actions + +**Purpose Types:** Unpredictable Nuances, Simulation, Time capsule, Tangible Metaphor, Song Concept, Advert as game + +**Structural Inspirations:** +- GTA Heists - Replayability and stakes, overlapping branching narratives +- Tekken - 2/3 mistakes = lost round (casino-style stakes) +- Elden Ring - Storytelling by traversal of map +- Battlefront - Elites amongst Commoners + """) + + with gr.Accordion("Workflow Overview", open=False): + gr.Markdown(""" +**Timeline Workflow:** +1. [Pre-creation] Mermaid Diagram → Story +2. [Creation] Initial JSON (LLM + manual fixes) +3. [Post-creation] JSON Corrections → Media prompts → Asset Generation → JSON Media population + +**Config Writing Approaches:** +- Remix an existing config +- Ask LLM to generate one +- Endless combination testing +- Write by hand with format assistance + """) + + # ==================== GENERATE CONFIG TAB ==================== + with gr.Tab("Generate Config"): + gr.Markdown("### Config Generation Tools") + + with gr.Accordion("Timeline Generator", open=True): + gr.Markdown("Generate a random timeline and story based on UI elements and story events. Use this as a starting point for LLM refinement.") with gr.Row(): - gr.Textbox(TimeRelatedMermaidStoryAttempttoRefinefrom[0], lines=30) - gr.Textbox(TimeRelatedMermaidStoryAttempttoRefinefrom[1], lines=30) - gr.Textbox(TimeRelatedMermaidStoryAttempttoRefinefrom[2], lines=30) - gr.Textbox(TimeRelatedMermaidStoryAttempttoRefinefrom[3], lines=30) - gr.Textbox(TimeRelatedMermaidStoryAttempttoRefinefrom[4], lines=30) - with gr.Tab("Mermaid Diagrams"): - with gr.Accordion("Mermaid Structures - click to open", open=False): - for key, item in examplemermaidconceptblendingstrutures.items(): - gr.Code(item, label=key) - with gr.Tab("HF datasets as scenario inspiration"): - gr.HTML("eg. https://huggingface.co/datasets/thesven/gsm8k-reasoning | https://huggingface.co/datasets/HuggingFaceFW/fineweb | HuggingFaceFW/fineweb-edu | https://huggingface.co/datasets/HuggingFaceFV/finevideo - https://huggingface.co/spaces/HuggingFaceFV/FineVideo-Explorer | even translations - https://huggingface.co/datasets/opencsg/chinese-fineweb-edu") - with gr.Tab("Creation"): - with gr.Tab("AI generates scaffolds"): - gr.HTML("8b size models make Format mistakes. For extention purposes this step specifically might have to be function call only if you explain the tool can take as many blocks as neccesary
Prompts without extra considerations") + cda_timeline_output = gr.Textbox(label="Generated Timeline", lines=15) + cda_story_output = gr.Textbox(label="Generated Story", lines=15) with gr.Row(): - gr.Code(WFStage2prompt , label="Prompt Used (working on 70B and above)") - gr.Code(WFStage2prompt2 , label="Alternate prompt to use (working on 70B and above)") + cda_config_output = gr.Code(label="Generated Config JSON", language="json", lines=10) with gr.Row(): - gr.HTML("The theme is waking up on the day AGI is created and how life will change") - gr.HTML("") - with gr.Tab("Extension ideas"): - gr.HTML("The basic idea is just make a new story and fight the JSON issues to make them fit and then rinse and repeat") - with gr.Tab("Follow-up prompts ideas"): - gr.HTML("Extension = any description in original JSON as the theme using the prompt template") - with gr.Tab("Change flow of existing config"): - gr.HTML("Ask for incorporation of mermaid structure into JSON or ask for how the mermaid structure would change the JSON / story") - - with gr.Tab("Post-creation"): - gr.HTML("Assumed story is complete is complete at this stage / will be continued in seperate config") - with gr.Tab("Initial Media Generation"): - gr.HTML("Placeholder for verb extraction from description and choices - to help guide initial media population") - gr.HTML("To do a full test need media - Fast and quality - https://huggingface.co/spaces/black-forest-labs/FLUX.1-schnell") - - with gr.Tab("Initial JSON (through LLM and fix JSON by hand) to (3) JSON Corrections (through LLM and fix JSON by hand)"): - gr.Code("Lets a critique this JSON to find areas fix", label="prompt used") - with gr.Tab("JSON Corrections (through LLM and fix JSON by hand) to (4) Media prompts"): - gr.HTML("This Step specifically has to be function call only") - gr.HTML("Gemma-9b and Mistral 8x7b is better at this prompt than llama 3.1 8b and 70b
Can add (each media field must get an entry) and (in python list of list format for plug and play) but they affect final output") - gr.Code("Lets a make a list for the prompts we will use to make media objects in this JSON. Make one for a person to interpret and one for direct media generators that focus on keywords: ", label="prompt used") - with gr.Tab("Media prompts to (5) Asset Generation to (6) JSON Media field population"): - gr.HTML("This Step specifically has to be function call only") - gr.Code("For each Media item described classify it by media type and comment if in a story setting it would need timing ", label="prompt used") - gr.HTML("This Step can be merged with the next if we can make a editor like in the semi-Auto space in test and edit tailored to just accepting the JSON and exposing only media part for editing") - gr.Code("Here is a list of file names - assume they are in the order of the empty media sections of the JSON and rewrite the JSON", label="prompt used") - with gr.Tab("Existing Config Crafting Progression"): - with gr.Accordion("Test config format assist - to merge with the one in the test area", open=False ): - gr.HTML("Placeholder for media field uploading / dropdowns for all files that have been uploaded") - gr.HTML("Splits by new line and is looking something like this 1: UI/Story/Media - content/type - Need to adjust it to current config format ") - input_text = gr.Textbox(label="Input Text", lines=10) - output_group = gr.Group() - - @gr.render(inputs=input_text) - def update(text): - return show_elements(text) - -# import originalconfigatbeinningofthisspace, claude3_5_06072024configtips, tipsupdatedconfigatbeinningofthisspace from relatively_constant_variables - with gr.Tab("Quick Ways to evaluate current config"): - gr.HTML("Ask SOTA LLMs This prompt:
This config is for a basic text based game engine. I dont have any structural metrics to assess the quality of the config. What JSON things can we look at to see if it may be too bland for a person testing the game?
Then Paste the Config with the prompt") - gr.HTML("""Original Claude 3.5 Sonnet Response snippets:
-Limited state variety: With only 13 states across 5 locations, the game might feel short and lacking in diversity. Consider adding more locations or states within existing locations. -Low average choices: An average of 1.92 choices per state might make the game feel linear. Increasing the number of choices in more states could improve player engagement. -Limited consequences: Only 3 states have consequences, which might make player choices feel less impactful. Adding more consequences could increase the sense of agency. -Short descriptions: The average description length of 13.15 words might not provide enough detail to immerse players. Consider expanding descriptions to create a richer narrative. -Lack of media: No states currently use media elements, which could make the game feel less engaging. Adding images, sound effects, or other media could enhance the player experience. -Limited narrative branching: While there are some loops and choices, the overall structure is relatively linear. Adding more branching paths could increase replayability and player interest. - -To make the game less bland, consider: - -Adding more states and locations -Increasing the number of choices in each state -Implementing more consequences for player actions -Expanding descriptions to create a richer narrative -Incorporating media elements -Creating more diverse paths through the game""") - with gr.Tab("Improvement of the default config"): - gr.HTML("Example of how to advance a game config with LLM - end goal is to have automatic worflow that takes these considerations into account
Things missing from the game engine - Economics and Basic Politics (NPC affiliation)") - gr.HTML("Suggestions from claude 3.5 on how to change config") - display_originalconfigatbeinningofthisspace = originalconfigatbeinningofthisspace.replace(' ', ' ').replace('\n', '
') - display_claude3_5_06072024configtips = claude3_5_06072024configtips.replace(' ', ' ').replace('\n', '
') - display_tipsupdatedconfigatbeinningofthisspace = tipsupdatedconfigatbeinningofthisspace.replace(' ', ' ').replace('\n', '
') - gr.HTML("""
-
- """ + display_originalconfigatbeinningofthisspace + """ -
-
- """ + display_claude3_5_06072024configtips + """ -
-
- """ + display_tipsupdatedconfigatbeinningofthisspace + """ -
-
""") + cda_story_points = gr.Slider(minimum=1, value=10, step=1, maximum=30, label="Story Points") + cda_ui_points = gr.Slider(minimum=1, value=10, step=1, maximum=30, label="UI Points") + with gr.Row(): + cda_num_lists = gr.Slider(minimum=1, maximum=len(all_idea_lists), step=1, label="Idea Lists to Use", value=3) + cda_items_per_list = gr.Slider(minimum=1, maximum=10, step=1, label="Items per List", value=3) + with gr.Row(): + cda_include_games = gr.Checkbox(label="Include Game Inspirations", value=True) + cda_include_multiplayer = gr.Checkbox(label="Include Multiplayer Features", value=True) + with gr.Row(): + cda_randomise_btn = gr.Button("🎲 Randomise All", variant="secondary") + cda_generate_btn = gr.Button("Generate Story and Timeline", variant="primary") + + def randomise_timeline_settings(): + """Randomise all Timeline Generator settings.""" + import random + return ( + random.randint(5, 25), # story_points + random.randint(5, 25), # ui_points + random.randint(1, min(5, len(all_idea_lists))), # num_lists + random.randint(2, 7), # items_per_list + random.choice([True, False]), # include_games + random.choice([True, False]) # include_multiplayer + ) + + cda_randomise_btn.click( + fn=randomise_timeline_settings, + outputs=[cda_story_points, cda_ui_points, cda_num_lists, cda_items_per_list, cda_include_games, cda_include_multiplayer] + ) + + with gr.Row(): + cda_suggestions = gr.Textbox(label="Random Suggestions", lines=5) + cda_selected_lists = gr.Textbox(label="Selected Idea Lists", lines=2) + + cda_generate_btn.click( + generate_story_and_timeline, + inputs=[cda_story_points, cda_ui_points, cda_num_lists, cda_items_per_list, cda_include_games, cda_include_multiplayer], + outputs=[cda_timeline_output, cda_story_output, cda_config_output, cda_suggestions, cda_selected_lists] + ) + + with gr.Accordion("Narrative Construction Tool (tnct)", open=False): + gr.Markdown("Build narratives using character relationships and interpersonal events.") + tnct_ui() + + with gr.Accordion("Worldbuilding - Bible Stories as Templates", open=False): + bible_story_elements() + + # ==================== STORY GRAPH TAB (Config Development Device) ==================== + create_story_graph_tab() + + # ==================== BIG RPG SCALE TAB (Config Development Device) ==================== + create_big_rpg_scale_tab() + + # ==================== MERMAID DIAGRAMS TAB ==================== + with gr.Tab("Mermaid Diagrams"): + gr.Markdown("### Visual Story Structure Planning") + gr.Markdown("Use Mermaid diagrams to plan your story structure before converting to JSON config.") + + def get_random_mermaid_template(): + """Get a random mermaid template from either story structures or concept blending.""" + import random + all_templates = {**mermaidstorystructures, **examplemermaidconceptblendingstrutures} + name = random.choice(list(all_templates.keys())) + return f"**{name}**", all_templates[name] + + with gr.Row(): + cda_mermaid_random_btn = gr.Button("🎲 Random Template", variant="secondary") + cda_mermaid_load_btn = gr.Button("Load Mermaid.live Editor", variant="primary") + + cda_random_template_name = gr.Markdown("*Click 'Random Template' to see a random story structure*") + cda_random_template_code = gr.Code(label="Random Template", language=None, lines=10) + + cda_mermaid_random_btn.click( + fn=get_random_mermaid_template, + outputs=[cda_random_template_name, cda_random_template_code] + ) + + cda_mermaid_iframe = gr.HTML("") + cda_mermaid_load_btn.click( + fn=lambda: "", + outputs=cda_mermaid_iframe + ) + + with gr.Accordion("Story Structure Templates", open=False): + for key, item in mermaidstorystructures.items(): + with gr.Accordion(key, open=False): + gr.Code(item, label=key, language=None) + + with gr.Accordion("Concept Blending Examples", open=False): + for key, item in examplemermaidconceptblendingstrutures.items(): + gr.Code(item, label=key, language=None) + + # ==================== D&D 5E SRD REFERENCE TAB ==================== + with gr.Tab("D&D 5e SRD"): + gr.Markdown("### Dungeons & Dragons 5th Edition - System Reference Document") + + with gr.Accordion("What is D&D 5e?", open=True): + gr.Markdown(""" +**Dungeons & Dragons 5th Edition** is a tabletop role-playing game (TTRPG) published by Wizards of the Coast. + +**Core Concepts:** +- **Players** create characters with races (Elf, Dwarf, Human, etc.) and classes (Fighter, Wizard, Rogue, etc.) +- **Dungeon Master (DM)** narrates the story, controls NPCs/monsters, and adjudicates rules +- **Dice-based resolution** - primarily the d20 (20-sided die) for skill checks, attacks, and saves +- **Combat** uses turn-based initiative with movement, actions, and reactions +- **Character progression** through levels 1-20 with increasing abilities and spells + +**Why use D&D for game config inspiration?** +- Rich ecosystem of monsters, items, spells, and locations +- Well-balanced mechanics tested over decades +- Familiar framework for many players +- Excellent source for fantasy game content + """) + + with gr.Accordion("License Information (CC-BY-4.0)", open=False): + gr.Markdown(""" +**System Reference Document 5.1 (SRD 5.1)** is available under **Creative Commons Attribution 4.0 International (CC-BY-4.0)**. + +**What this means:** +- ✅ You CAN use SRD 5.1 content in your games (commercial or non-commercial) +- ✅ You CAN modify and adapt the content +- ✅ You CAN share and redistribute +- ⚠️ You MUST give appropriate credit to Wizards of the Coast +- ❌ You CANNOT use D&D trademarks (like "Dungeons & Dragons" in your product name) + +**Attribution Example:** +> "This work includes material from the System Reference Document 5.1 ("SRD 5.1") by Wizards of the Coast LLC, available at https://dnd.wizards.com/resources/systems-reference-document. The SRD 5.1 is licensed under the Creative Commons Attribution 4.0 International License." + +**Official Sources:** +- [SRD 5.1 PDF](https://dnd.wizards.com/resources/systems-reference-document) +- [CC-BY-4.0 License](https://creativecommons.org/licenses/by/4.0/) + """) + + with gr.Accordion("Random Idea Generator (SRD Content)", open=True): + gr.Markdown("Generate random D&D elements for game inspiration. All content from SRD 5.1.") + + # SRD Monster list (subset for demo) + DND_MONSTERS = [ + "Aboleth", "Animated Armor", "Banshee", "Basilisk", "Behir", "Beholder", "Bugbear", + "Bulette", "Centaur", "Chimera", "Cockatrice", "Couatl", "Cyclops", "Death Knight", + "Doppelganger", "Dragon (Red)", "Dragon (Gold)", "Dragon (Black)", "Drider", "Dryad", + "Duergar", "Elemental (Fire)", "Elemental (Water)", "Elemental (Earth)", "Elemental (Air)", + "Ettercap", "Ettin", "Gargoyle", "Genie (Djinni)", "Genie (Efreeti)", "Ghost", "Ghoul", + "Giant (Hill)", "Giant (Stone)", "Giant (Frost)", "Giant (Fire)", "Giant (Cloud)", "Giant (Storm)", + "Gibbering Mouther", "Gnoll", "Goblin", "Golem (Clay)", "Golem (Stone)", "Golem (Iron)", + "Gorgon", "Green Hag", "Griffon", "Harpy", "Hell Hound", "Hippogriff", "Hobgoblin", + "Hydra", "Intellect Devourer", "Invisible Stalker", "Kobold", "Kraken", "Lamia", "Lich", + "Lizardfolk", "Manticore", "Medusa", "Merfolk", "Mimic", "Mind Flayer", "Minotaur", + "Mummy", "Naga (Guardian)", "Nightmare", "Ogre", "Oni", "Orc", "Otyugh", "Owlbear", + "Pegasus", "Phase Spider", "Piercer", "Pixie", "Pseudodragon", "Purple Worm", "Rakshasa", + "Remorhaz", "Roc", "Roper", "Rust Monster", "Sahuagin", "Salamander", "Satyr", + "Shambling Mound", "Shield Guardian", "Skeleton", "Specter", "Sphinx", "Sprite", + "Stirge", "Succubus/Incubus", "Tarrasque", "Treant", "Troll", "Unicorn", "Vampire", + "Wight", "Will-o'-Wisp", "Wraith", "Wyvern", "Xorn", "Zombie" + ] + + DND_SPELLS = [ + "Fireball", "Lightning Bolt", "Magic Missile", "Shield", "Mage Armor", "Detect Magic", + "Dispel Magic", "Counterspell", "Fly", "Invisibility", "Polymorph", "Teleport", + "Wish", "Power Word Kill", "Meteor Swarm", "Time Stop", "Gate", "True Resurrection", + "Heal", "Cure Wounds", "Revivify", "Raise Dead", "Greater Restoration", "Mass Heal", + "Bless", "Spirit Guardians", "Guiding Bolt", "Sacred Flame", "Banishment", "Hold Person", + "Charm Person", "Suggestion", "Dominate Person", "Fear", "Sleep", "Hypnotic Pattern", + "Wall of Fire", "Wall of Force", "Cloudkill", "Cone of Cold", "Chain Lightning", + "Disintegrate", "Finger of Death", "Prismatic Spray", "Sunburst", "Earthquake" + ] + + DND_ITEMS = [ + "Bag of Holding", "Cloak of Elvenkind", "Boots of Speed", "Ring of Protection", + "Wand of Fireballs", "Staff of Power", "Vorpal Sword", "Holy Avenger", "Flame Tongue", + "Frost Brand", "Dancing Sword", "Luck Blade", "Rod of Lordly Might", "Sphere of Annihilation", + "Deck of Many Things", "Portable Hole", "Immovable Rod", "Rope of Climbing", + "Amulet of Proof Against Detection", "Belt of Giant Strength", "Bracers of Defense", + "Carpet of Flying", "Crystal Ball", "Eversmoking Bottle", "Figurine of Wondrous Power", + "Gauntlets of Ogre Power", "Headband of Intellect", "Helm of Brilliance", "Horn of Blasting", + "Ioun Stone", "Lantern of Revealing", "Medallion of Thoughts", "Mirror of Life Trapping", + "Necklace of Fireballs", "Periapt of Health", "Robe of Eyes", "Scarab of Protection", + "Talisman of Pure Good", "Wings of Flying" + ] + + DND_CLASSES = [ + "Barbarian - Primal warriors fueled by rage", + "Bard - Magical performers and jack-of-all-trades", + "Cleric - Divine spellcasters serving a deity", + "Druid - Nature-based spellcasters who shapeshift", + "Fighter - Masters of martial combat", + "Monk - Martial artists harnessing ki energy", + "Paladin - Holy warriors bound by an oath", + "Ranger - Wilderness experts and hunters", + "Rogue - Stealthy specialists and skill experts", + "Sorcerer - Innate magic users with metamagic", + "Warlock - Spellcasters with otherworldly patrons", + "Wizard - Scholarly magic users with spellbooks" + ] + + DND_LOCATIONS = [ + "Ancient Dragon Lair", "Abandoned Dwarven Mine", "Cursed Graveyard", "Floating Castle", + "Underground City (Underdark)", "Haunted Manor", "Wizard's Tower", "Sunken Temple", + "Frost Giant Stronghold", "Volcanic Forge", "Feywild Glade", "Shadowfell Crossing", + "Planar Portal Chamber", "Lich's Phylactery Vault", "Beholder's Lair", "Mind Flayer Colony", + "Pirate Cove", "Desert Pyramid", "Jungle Ziggurat", "Arctic Glacier Dungeon" + ] + + import random + + def generate_dnd_ideas(num_monsters, num_spells, num_items, include_class, include_location): + result = [] + + if num_monsters > 0: + monsters = random.sample(DND_MONSTERS, min(num_monsters, len(DND_MONSTERS))) + result.append(f"**Monsters ({num_monsters}):**\n" + "\n".join(f"• {m}" for m in monsters)) + + if num_spells > 0: + spells = random.sample(DND_SPELLS, min(num_spells, len(DND_SPELLS))) + result.append(f"**Spells ({num_spells}):**\n" + "\n".join(f"• {s}" for s in spells)) + + if num_items > 0: + items = random.sample(DND_ITEMS, min(num_items, len(DND_ITEMS))) + result.append(f"**Magic Items ({num_items}):**\n" + "\n".join(f"• {i}" for i in items)) + + if include_class: + char_class = random.choice(DND_CLASSES) + result.append(f"**Suggested Class:**\n• {char_class}") + + if include_location: + location = random.choice(DND_LOCATIONS) + result.append(f"**Location Idea:**\n• {location}") + + return "\n\n".join(result) if result else "Select at least one category!" + + with gr.Row(): + dnd_num_monsters = gr.Slider(minimum=0, maximum=10, value=3, step=1, label="Number of Monsters") + dnd_num_spells = gr.Slider(minimum=0, maximum=10, value=3, step=1, label="Number of Spells") + dnd_num_items = gr.Slider(minimum=0, maximum=10, value=2, step=1, label="Number of Magic Items") + with gr.Row(): + dnd_include_class = gr.Checkbox(label="Include Random Class", value=True) + dnd_include_location = gr.Checkbox(label="Include Random Location", value=True) + + dnd_generate_btn = gr.Button("Generate Random D&D Ideas", variant="primary") + dnd_output = gr.Markdown(label="Generated Ideas") + + dnd_generate_btn.click( + generate_dnd_ideas, + inputs=[dnd_num_monsters, dnd_num_spells, dnd_num_items, dnd_include_class, dnd_include_location], + outputs=dnd_output + ) + + with gr.Accordion("Quick Reference - Combat Basics", open=False): + gr.Markdown(""" +**Combat Round Structure:** +1. **Roll Initiative** (d20 + Dexterity modifier) - determines turn order +2. **Take Turns** - each creature gets: Movement + Action + Bonus Action (if available) +3. **Repeat** until combat ends + +**Action Types:** +- **Attack** - Make a weapon or spell attack +- **Cast a Spell** - Use a spell (some are bonus actions) +- **Dash** - Double your movement +- **Disengage** - Move without provoking opportunity attacks +- **Dodge** - Attacks against you have disadvantage +- **Help** - Give an ally advantage on their next check +- **Hide** - Attempt to become hidden +- **Ready** - Prepare an action for a trigger + +**Advantage/Disadvantage:** +- **Advantage** = Roll 2d20, take the higher +- **Disadvantage** = Roll 2d20, take the lower + +**For Text Games:** Consider simplifying to choice-based combat with stat checks rather than full dice simulation. + """) + + with gr.Accordion("SRD Lookup (Open5e API)", open=True): + gr.Markdown("*Search the full D&D 5e SRD database via Open5e API for detailed monster stats, spell descriptions, and more.*") + + # Random search terms for "I'm Feeling Lucky" + SRD_RANDOM_SEARCHES = { + "monsters": ["dragon", "goblin", "orc", "zombie", "skeleton", "wolf", "giant", "demon", "troll", "ogre", "vampire", "ghost", "spider", "bear", "elemental"], + "spells": ["fire", "ice", "lightning", "heal", "shield", "teleport", "invisibility", "charm", "sleep", "detect", "summon", "cure", "protection", "dispel", "light"], + "magicitems": ["sword", "ring", "staff", "wand", "cloak", "boots", "belt", "amulet", "potion", "bag", "helm", "gauntlets", "armor", "shield", "rod"] + } + + def srd_feeling_lucky(): + """Pick a random category and search term, then search.""" + import random + category = random.choice(["monsters", "spells", "magicitems"]) + search_term = random.choice(SRD_RANDOM_SEARCHES[category]) + # Return the category, search term (to update inputs) + return category, search_term + + with gr.Row(): + srd_api_category = gr.Dropdown( + label="Category", + choices=[ + ("Monsters", "monsters"), + ("Spells", "spells"), + ("Magic Items", "magicitems"), + ("Conditions", "conditions"), + ("Classes", "classes") + ], + value="monsters" + ) + srd_api_search = gr.Textbox( + label="Search", + placeholder="goblin, fireball, vorpal...", + scale=2 + ) + srd_api_lucky_btn = gr.Button("🎲 Lucky", variant="secondary") + srd_api_search_btn = gr.Button("Search", variant="primary") + + srd_api_lucky_btn.click( + fn=srd_feeling_lucky, + outputs=[srd_api_category, srd_api_search] + ) + + srd_api_results = gr.Markdown("*Enter a search term and click Search to query the Open5e API*") + + with gr.Row(): + srd_api_detail_slug = gr.Textbox( + label="Get Full Details (slug)", + placeholder="goblin, fireball, vorpal-sword...", + info="Enter the item's slug (lowercase, hyphenated name) from search results" + ) + srd_api_detail_btn = gr.Button("Get Full Stats") + + srd_api_detail_output = gr.Markdown("") + + def srd_api_search_handler(category, query): + """Handle SRD API search.""" + if not query.strip(): + return "Please enter a search term" + if category == "monsters": + return search_monsters(query) + elif category == "spells": + return search_spells(query) + elif category == "magicitems": + return search_magicitems(query) + elif category == "conditions": + return search_conditions(query) + elif category == "classes": + return search_classes(query) + return "Select a category" + + def srd_api_detail_handler(category, slug): + """Handle SRD API detail lookup.""" + if not slug: + return "Enter a slug to get details" + slug = slug.lower().strip().replace(" ", "-") + if category == "monsters": + return get_monster_details(slug) + elif category == "spells": + return get_spell_details(slug) + elif category == "magicitems": + return get_magicitem_details(slug) + return "Detail view available for Monsters, Spells, and Magic Items" + + srd_api_search_btn.click( + fn=srd_api_search_handler, + inputs=[srd_api_category, srd_api_search], + outputs=[srd_api_results] + ) + + srd_api_detail_btn.click( + fn=srd_api_detail_handler, + inputs=[srd_api_category, srd_api_detail_slug], + outputs=[srd_api_detail_output] + ) + + gr.Markdown(""" +--- +*Live data from [Open5e API](https://open5e.com/) - search returns up to 10 results. Use slugs (e.g., "ancient-red-dragon") for full stat blocks.* + """) + + # ==================== STORY ARCHITECT TAB ==================== + create_story_architect_tab() + + # ==================== NARRATIVE ENGINE TAB ==================== + create_narrative_engine_tab() + + # ==================== SOURCE CONTENT TO GAMES TAB ==================== + with gr.Tab("Source Content to Games"): + gr.Markdown("""## Convert Any Text to Game Configs + +**Input Sources:** +- Textbooks / Educational materials → Interactive learning games +- Memes / Sayings / Quotes → Decision-based humor games +- Song lyrics / Poetry → Emotional journey games +- Books / Stories / Scripts → Narrative adventures + +**NLP-Guided Process:** +1. Extract key concepts, themes, characters +2. Identify decision points and consequences +3. Map to location → state → choices structure +4. Generate valid JSON config + """) + + with gr.Accordion("Prompts for Each Source Type", open=True): + gr.Markdown(""" +### Textbook/Educational Material +``` +Convert this educational content into a text adventure game config: +[PASTE CONTENT] + +Create locations for each major concept. Each location should: +- Have a description explaining the concept +- Offer choices that test understanding +- Include consequences that reinforce correct/incorrect answers +Output as JSON with this structure: {"location": {"state": {"description": "...", "choices": [...]}}} +``` + +### Song Lyrics/Poetry +``` +Transform these lyrics into an emotional journey game: +[PASTE LYRICS] + +Each verse = a location. Choices reflect the emotional themes. +The chorus can be a recurring location or state change. +Output as JSON game config. +``` + +### Meme/Saying/Quote +``` +Create a short decision game based on this saying: +[PASTE QUOTE] + +The game should explore the meaning through 3-5 choices. +Each choice demonstrates understanding or misunderstanding of the wisdom. +Output as JSON game config. +``` + +### Book/Story Chapter +``` +Convert this story excerpt into an interactive adventure: +[PASTE TEXT] + +Identify: characters, locations, key decisions, consequences. +Create branching paths where reader choices affect outcomes. +Output as JSON game config. +``` + """) + + # ==================== GEMMA SCOPE ANALYSIS TAB ==================== + with gr.Tab("Gemma Scope Analysis"): + gr.Markdown("""## Analyze Prompt Features with Gemma Scope 2 + +See which internal features activate when generating game configs. +Use this to understand what patterns lead to **interesting** vs **predictable** outputs. + """) + + gr.HTML(''' + + ''') + gr.Markdown("[Open SAE Analyzer in new tab](https://huggingface.co/spaces/KwabsHug/TestSAEGemmaScope)") + + with gr.Accordion("Understanding Feature IDs", open=False): + gr.Markdown(""" +## Gemma Scope Naming Convention + +A feature ID like `gemma-2-2b_12-gemmascope-res-16k_4667` breaks down as: + +| Component | Meaning | +|-----------|---------| +| `gemma-2-2b` | The base language model being analyzed | +| `12` | Layer 12 - middle layer where abstract concepts form | +| `gemmascope` | The Gemma Scope SAE project | +| `res` | **Residual stream** - the main "highway" of information | +| `16k` | SAE has 16,384 learned features | +| `4667` | This specific feature's ID | + +### What Different Layers Capture + +| Layer Range | What It Captures | +|-------------|------------------| +| 0-5 | Basic syntax, token patterns, grammar | +| 6-12 | Concepts, word meanings, simple relationships | +| 13-18 | Abstract reasoning, complex semantics | +| 19-25 | Output formatting, final predictions | + +**Layer 12** (default) is good for capturing meaningful concepts without being too low-level or too output-focused. + """) + + with gr.Accordion("About Gemma Scope 2", open=False): + gr.Markdown(""" +**What is Gemma Scope 2?** +Sparse Autoencoders (SAEs) that decompose model activations into interpretable features. +Think of it as a "microscope" for looking inside the AI model. + +**Use Cases for Game Config Development:** +- **Plan interesting configs**: Identify which features lead to creative outputs +- **Plan predictable configs**: Find patterns for reliable/consistent results +- Analyze feature activations for different game mechanics (combat, exploration, dialogue) +- Debug unexpected LLM outputs by seeing which features activated + +**How to Use:** +1. Enter a prompt you'd use for game config generation +2. Click "Analyze Features" to see which internal features activate +3. Look up specific features to understand what they represent +4. Compare different prompts to understand what patterns the model uses + +**Resources:** +- [Neuronpedia Demo](https://www.neuronpedia.org/gemma-scope-2) +- [Technical Paper](https://storage.googleapis.com/deepmind-media/DeepMind.com/Blog/gemma-scope-2-helping-the-ai-safety-community-deepen-understanding-of-complex-language-model-behavior/Gemma_Scope_2_Technical_Paper.pdf) +- [SAELens Library](https://github.com/jbloomAus/SAELens) + """) + + # ==================== VIDEOGRAPHER PERSPECTIVE TAB ==================== + # NOTE: T5Gemma2 moved to Media Studio → Generate tab + with gr.Tab("Videographer Perspective"): + gr.Markdown("""## Videographer Perspective for Game Configs + +Think about your game config like a **film director** or **cinematographer**. +Video is now a supported media type - use these concepts to enhance storytelling. + """) + + with gr.Accordion("Shot Types & When to Use Them", open=True): + gr.Markdown(""" +**Establishing Shot** - Wide view showing the environment +- Use for: Location introductions, setting the scene +- Config tip: First state in a new location should have establishing imagery/video + +**Close-Up** - Tight focus on face/object +- Use for: Emotional moments, important items, decision points +- Config tip: Key choices or reveals deserve close-up media + +**Medium Shot** - Waist-up framing +- Use for: Dialogue, character interactions +- Config tip: Standard conversation states + +**Over-the-Shoulder** - Camera behind one character looking at another +- Use for: Conversations, confrontations +- Config tip: States where player is observing/listening to NPCs + +**POV (Point of View)** - What the character sees +- Use for: Exploration, discovery, immersion +- Config tip: Investigation states, finding clues + +**Dutch Angle** - Tilted camera +- Use for: Unease, disorientation, something wrong +- Config tip: Horror/thriller moments, betrayals + """) + + with gr.Accordion("Pacing & Rhythm", open=False): + gr.Markdown(""" +**Fast Cutting** - Quick transitions between shots +- Creates: Tension, action, urgency +- Config tip: Short descriptions, many quick choices, time pressure + +**Long Takes** - Extended continuous shots +- Creates: Immersion, realism, building tension +- Config tip: Detailed descriptions, fewer choices, let moments breathe + +**Montage** - Series of shots showing passage of time/progress +- Creates: Progression, training, travel +- Config tip: Summary states, "time passes" transitions + +**Parallel Editing** - Cutting between simultaneous events +- Creates: Suspense, showing cause/effect +- Config tip: State variables tracking multiple storylines + """) + + with gr.Accordion("Lighting & Mood", open=False): + gr.Markdown(""" +**High Key Lighting** - Bright, even, minimal shadows +- Mood: Happy, safe, comedic, optimistic +- Config tip: Describe bright environments, daylight scenes + +**Low Key Lighting** - Dark, strong shadows, contrast +- Mood: Mysterious, dangerous, dramatic, noir +- Config tip: Describe shadows, dim lighting, hidden corners + +**Practical Lighting** - Light sources visible in scene +- Mood: Realistic, immersive +- Config tip: Mention candles, lamps, screens as light sources + +**Color Temperature** +- Warm (orange/yellow): Comfort, nostalgia, intimacy +- Cool (blue/green): Isolation, technology, unease +- Config tip: Include color descriptions in media prompts + """) + + with gr.Accordion("Camera Movement Concepts", open=False): + gr.Markdown(""" +**Static Camera** - No movement +- Effect: Stability, observation, documentation +- Config tip: Standard descriptive states + +**Pan** - Camera rotates left/right +- Effect: Revealing space, following action +- Config tip: Descriptions that scan across a scene + +**Tilt** - Camera rotates up/down +- Effect: Showing scale, power dynamics +- Config tip: Looking up at imposing structures, down at fallen enemies + +**Dolly/Tracking** - Camera moves through space +- Effect: Following subject, exploration +- Config tip: Descriptions of moving through corridors, chasing + +**Zoom** - Lens adjustment (not camera movement) +- Effect: Focusing attention, psychological shift +- Config tip: Transitioning from wide context to specific detail + """) + + with gr.Accordion("Video Generation Prompts", open=True): + gr.Markdown(""" +### Prompt Templates for Video Media + +**Establishing Shot:** +``` +Wide cinematic shot of [LOCATION], [TIME OF DAY], [WEATHER]. +Camera slowly pans across [KEY FEATURES]. [LIGHTING DESCRIPTION]. +``` + +**Character Introduction:** +``` +Medium shot of [CHARACTER], [CLOTHING/APPEARANCE]. +[ACTION/POSE]. [EXPRESSION]. [BACKGROUND ELEMENTS]. +``` + +**Action Sequence:** +``` +Dynamic tracking shot following [CHARACTER] as they [ACTION]. +Fast-paced movement, [ENVIRONMENT DETAILS]. Urgent atmosphere. +``` + +**Emotional Moment:** +``` +Close-up on [CHARACTER]'s face showing [EMOTION]. +Soft lighting, shallow depth of field. [SUBTLE MOVEMENT]. +``` + +**Suspense/Horror:** +``` +POV shot moving slowly through [DARK LOCATION]. +Minimal lighting, shadows moving. Something glimpsed in periphery. +``` + """) + + with gr.Accordion("Story Structure Through Video Lens", open=False): + gr.Markdown(""" +**Three-Act Structure for Videos:** + +**Act 1 - Setup (25%)** +- Establishing shots of world +- Character introductions (medium shots) +- Normal world before conflict + +**Act 2 - Confrontation (50%)** +- Increasing close-ups as tension rises +- More dynamic camera movements +- Parallel editing for multiple storylines +- Low key lighting as stakes increase + +**Act 3 - Resolution (25%)** +- Climax: Rapid cutting, close-ups, dynamic movement +- Resolution: Return to longer takes, breathing room +- Final shot: Often mirrors opening but transformed + +**Apply to Configs:** +- Early locations: Establishing shots, bright lighting +- Middle locations: More variety, building tension +- Late game: Intimate shots, dramatic lighting +- Endings: Reflect the journey visually + """) + + with gr.Accordion("65 Classic Film Scenarios", open=False): + gr.Markdown(""" +### Frequently Reused Film Scenarios +These scenarios appear across countless films. Use them as templates for game states. + +**OPENINGS & INTRODUCTIONS** +1. **Dawn Patrol** - Character wakes up, morning routine reveals personality/situation +2. **The Arrival** - Protagonist enters new location (city, building, world) for first time +3. **In Medias Res** - Opens mid-action, audience catches up +4. **The Funeral** - Character death sets story in motion, mourners gathered +5. **Voiceover Reflection** - Older character narrates past events we're about to see + +**TENSION & SUSPENSE** +6. **The Ticking Clock** - Countdown visible/mentioned, deadline approaching +7. **Hiding in Plain Sight** - Character conceals identity among enemies +8. **The Stakeout** - Characters wait and watch from vehicle/building +9. **Parallel Editing Chase** - Cut between pursuer and pursued +10. **The Interrogation** - Character questioned under pressure (police, villain, etc.) +11. **Eavesdropping** - Character overhears crucial conversation +12. **The Setup/Double-Cross** - Trusted ally reveals betrayal +13. **Trapped in Enclosed Space** - Elevator, room, vehicle - no escape +14. **The Hostage Situation** - Loved one held, demands made +15. **Walking Into a Trap** - Audience knows danger character doesn't + +**ACTION & CONFLICT** +16. **The Standoff** - Multiple parties aim weapons, no one shoots first +17. **The Bar Fight** - Violence erupts in drinking establishment +18. **Car Chase Through City** - Vehicles weave through traffic, obstacles +19. **Rooftop Confrontation** - Final battle on building top, city below +20. **The Heist Execution** - Plan unfolds step by step (with complications) +21. **Training Montage** - Character improves skills over compressed time +22. **The Last Stand** - Outnumbered defenders hold position +23. **One vs Many** - Single fighter takes on multiple opponents +24. **The Duel** - Two opponents face off, formal or informal +25. **Escape Sequence** - Character flees captivity/danger + +**EMOTIONAL MOMENTS** +26. **The Confession** - Character admits truth (love, crime, secret) +27. **Deathbed Scene** - Dying character's final words/wishes +28. **The Reunion** - Long-separated characters meet again +29. **Breaking the News** - Character learns devastating information +30. **The Sacrifice** - Character gives up something precious for others +31. **Saying Goodbye** - Characters part ways, possibly forever +32. **The Breakdown** - Character's composure finally cracks +33. **Reconciliation** - Estranged characters make peace +34. **The Proposal** - Marriage or significant commitment offered +35. **Visiting the Grave** - Character speaks to deceased at burial site + +**DISCOVERY & REVELATION** +36. **Finding the Body** - Character discovers corpse +37. **The Evidence Room** - Character finds proof of conspiracy/truth +38. **Flashback Reveal** - Past event recontextualizes everything +39. **The Twist Revealed** - Major plot revelation changes understanding +40. **Reading the Letter/Document** - Written words deliver crucial info +41. **Surveillance Footage** - Character watches recording of key event +42. **The Photograph** - Image reveals connection or identity +43. **Decoding the Message** - Cipher/puzzle solved, meaning clear + +**SOCIAL SCENARIOS** +44. **The Dinner Party** - Tension beneath polite social gathering +45. **The Job Interview** - Character proves worth or fails to +46. **Meeting the Parents** - Romantic partner meets family +47. **The Courtroom** - Legal proceedings, testimony, verdict +48. **Press Conference** - Public statement, reporters' questions +49. **The Gala/Ball** - Formal event, everyone dressed up, intrigue beneath +50. **The Wake/Reception** - Social gathering after significant event + +**ENDINGS & CONCLUSIONS** +51. **Walking Into the Sunset** - Hero departs, back to camera, horizon ahead +52. **The Circular Return** - Final scene mirrors opening, showing change +53. **Freeze Frame** - Action stops, often with voiceover or text overlay +54. **The Bittersweet Victory** - Won but at great cost, hollow celebration +55. **New Dawn** - Sun rises on changed world/character, hope restored +56. **The Cliffhanger** - Unresolved tension, question left hanging +57. **Where Are They Now** - Text/montage showing characters' futures +58. **The Twist Ending** - Final revelation reframes entire story +59. **Full Circle Reunion** - Characters gather one last time +60. **The Long Walk Away** - Character leaves location, camera lingers +61. **Passing the Torch** - Legacy/knowledge transferred to next generation +62. **The Final Confrontation** - Last face-to-face with antagonist +63. **Quiet Moment After Storm** - Calm after climax, processing events +64. **The Sacrifice Payoff** - Earlier sacrifice proven worthwhile +65. **Open Road/New Beginning** - Character sets off on next journey + +--- +**Using These in Configs:** +- Each scenario = potential game state template +- Combine scenarios for complex scenes (Dinner Party + The Setup) +- Consider which shot types fit each (Interrogation = close-ups, Chase = tracking) +- Match lighting to mood (Funeral = low key, Gala = high key) + """) + + with gr.Accordion("Scenario Sequence Generator", open=True): + gr.Markdown("### Generate Story Sequences from Film Scenarios") + gr.Markdown("Create 3-10 scene sequences for your game config. Output as list, JSON config, or send prompts to generation queue.") + + with gr.Row(): + seq_count = gr.Slider(minimum=3, maximum=10, value=5, step=1, label="Number of Scenes") + + with gr.Row(): + seq_opening = gr.Checkbox(label="Openings", value=True) + seq_tension = gr.Checkbox(label="Tension", value=True) + seq_action = gr.Checkbox(label="Action", value=True) + seq_emotional = gr.Checkbox(label="Emotional", value=True) + + with gr.Row(): + seq_discovery = gr.Checkbox(label="Discovery", value=True) + seq_social = gr.Checkbox(label="Social", value=True) + seq_ending = gr.Checkbox(label="Endings", value=True) + + with gr.Row(): + seq_force_opening = gr.Checkbox(label="Force Opening First", value=True) + seq_force_ending = gr.Checkbox(label="Force Ending Last", value=True) + + with gr.Row(): + seq_media_type = gr.Dropdown( + choices=["image", "audio", "3d", "tts"], + value="image", + label="Media Type", + scale=1 + ) + seq_auto_queue = gr.Checkbox(label="Auto-add to Queue", value=False, scale=1) + seq_generate_btn = gr.Button("Generate Sequence", variant="primary", scale=2) - - with gr.Tab("Leaveraging Machine Learning"): # , open=False): - gr.HTML("https://www.reddit.com/r/singularity/comments/1fhbipv/some_video_games_made_entirely_by_o1preview_and/") - gr.HTML("""Artificial Analysis""") - with gr.Tab("Chat UIs - Free Tier"): - gr.HTML("""Hugging Face Chat""") - gr.HTML("You can turn any space into a tool for huggingchat and the default image tool can do 5-10 secs per image - paste the current description and ask for images") - gr.HTML("Huggingface chat supports - State Management (Threads), Image Generation and editing, Websearch, Document parsing (PDF?), Assistants and larger models than zero gpu can support in July 2024 (Unquantised 30B and above)") - gr.HTML("ChatGPT is good for multiple images in one image - eg game maps and for this space all decisions in one picture") - gr.HTML("Gemini") - gr.HTML("Claude - Coding") - gr.HTML("Existing Assistants to use and planning custom assistants placeholder") - with gr.Tab("Leveraging ZeroGPU Attempt"): - gr.HTML("https://huggingface.co/posts/cbensimon/747180194960645
https://github.com/NexaAI/Awesome-LLMs-on-device/
13092024 - https://www.reddit.com/r/LocalLLaMA/comments/1fg3bin/6_months_out_of_date_what_has_changed/") - - with gr.Accordion("Seperate interfaces I want to mix and space restraint management"): - gr.HTML("Text generation (RAG, Front End prompt engineering, Streaming output, Access to contextual generation accross the other interfaces)
Image generation (prompt only, context guided generation)
") - gr.HTML("Tracking models to build with first to save money and privacy - https://github.com/NexaAI/Awesome-LLMs-on-device/, https://huggingface.co/gpt-omni/mini-omni https://www.reddit.com/r/LocalLLaMA/comments/1f84p1g/an_opensource_voicetovoice_llm_miniomni/, ") - - model_name = gr.State(modelname) - gr.Markdown(f"# Language Model with RAG and Model Switching, and FLUX Integration") - gr.Markdown("This demo allows you to switch between different Qwen models and use Retrieval-Augmented Generation (RAG).") - gr.Markdown("**Note:** Model switching is intended for testing output quality. Due to GPU limitations, speed differences may not be apparent. Models requiring over 50GB to load will likely not work.") - gr.Markdown("Need to add support for switching models and loading GGUF and GPTQ and BNB") - gr.Markdown("57b MOE takes 6min to load and gets workload evicted - storage limit over 100G") - - with gr.Tab("Text Generation"): with gr.Row(): with gr.Column(): - model_dropdown = gr.Dropdown(choices=modelnames, value=modelname, label="Select Model") - current_model_info = gr.Markdown(f"Current model: {modelname}") - current_model_info2 = gr.Interface(lambda x: f"Current model: {lastmodelnameinloadfunction[0]} ({lastmodelnameinloadfunction[1]}) (tokeniser = {lastmodelnameinloadfunction[2]})", inputs=None, outputs=["markdown"], description="Check what was last loaded (As the space has memory and I havent figured how spaces work enough eg. how does multiple users affect this)") # gr.Markdown(f"Current model: {lastmodelnameinloadfunction}") - gr.HTML("Need to figure out my test function calling for groq-8b as it doesnt seem to answer chat properly - will need a seperate space - eg. letter counting, plural couting, using a function as form for llm to fill (like choosing which model and input parameters for media in game)?") - prompt = gr.Textbox(lines=2, placeholder="Enter your prompt here...") - stream_checkbox = gr.Checkbox(label="Enable streaming") - rag_checkbox = gr.Checkbox(label="Enable RAG") - submit_button = gr.Button("Generate") - clear_models_button = gr.Button("Clear All Models") - loaded_models_button = gr.Button("Load Model List") - model_list_input = gr.Textbox(lines=2, placeholder="Enter model names separated by commas") - + seq_list_output = gr.Markdown(label="Sequence List") with gr.Column(): - with gr.Tab("Current Response"): - output = gr.Textbox(lines=10, label="Generated Response") - tokens_per_second = gr.Textbox(label="Tokens per Second") - ram_usage = gr.Textbox(label="RAM Usage") - doc_references = gr.Textbox(label="Document References") - with gr.Tab("All Responses So far"): - gr.Markdown("As we want a iterative process all old responses are saved for now - will figure how to make per user solution -- need some kind of hook onto the loading a space to assign random usercount with timestamp") - gr.Interface(format_output_dict, inputs=None, outputs=["textbox"]) - with gr.Tab("Image Generation"): - gr.HTML("Real Time Image Gen in this space is next aim like sd versions and https://huggingface.co/spaces/KingNish/Realtime-FLUX") - gr.Interface(generate_image, inputs=["text"], outputs=["text", "text", "image"], description="Current image") - #image_output = gr.Image(label="Generated Image (FLUX)") - - - model_dropdown.change( - model_change_handler, - inputs=[model_dropdown], - outputs=[model_name, current_model_info, output] + seq_json_output = gr.Code(label="Config JSON", language="json", lines=15) + + with gr.Row(): + seq_prompts_output = gr.Textbox(label="Media Prompts (for queue)", lines=8, interactive=False) + + with gr.Row(): + seq_add_to_queue_btn = gr.Button("Add Prompts to Queue", variant="secondary", scale=2) + seq_queue_status = gr.Textbox(label="Status", interactive=False, scale=2) + seq_queue_count = gr.Textbox(label="Queue", value="0 items", interactive=False, scale=1) + + def generate_with_optional_queue(count, inc_open, inc_tens, inc_act, inc_emo, + inc_disc, inc_soc, inc_end, force_open, force_end, + media_type, auto_queue): + """Generate sequence and optionally add to queue.""" + list_out, json_out, prompts_out = generate_scenario_sequence( + count, inc_open, inc_tens, inc_act, inc_emo, + inc_disc, inc_soc, inc_end, force_open, force_end + ) + + if auto_queue and prompts_out: + _, queue_info, status = add_scenario_prompts_to_queue(prompts_out, media_type) + return list_out, json_out, prompts_out, status, queue_info + else: + return list_out, json_out, prompts_out, "", f"{len(generation_queue)} items" + + seq_generate_btn.click( + generate_with_optional_queue, + inputs=[seq_count, seq_opening, seq_tension, seq_action, seq_emotional, + seq_discovery, seq_social, seq_ending, seq_force_opening, seq_force_ending, + seq_media_type, seq_auto_queue], + outputs=[seq_list_output, seq_json_output, seq_prompts_output, seq_queue_status, seq_queue_count] + ) + + def add_prompts_with_type(prompts_text, media_type): + """Add prompts to queue with specified media type.""" + _, queue_info, status = add_scenario_prompts_to_queue(prompts_text, media_type) + return status, queue_info + + seq_add_to_queue_btn.click( + add_prompts_with_type, + inputs=[seq_prompts_output, seq_media_type], + outputs=[seq_queue_status, seq_queue_count] + ) + + with gr.Tab("Portability & Design"): + with gr.Tab("Platform Export"): + gr.Markdown("### Export to Game Engines") + gr.Markdown("Select a platform, paste your game config JSON, then click Export.") + + # Platform test info - shows where to test selected platform + PLATFORM_TEST_INFO = { + "playcanvas": ("PlayCanvas Editor", "https://playcanvas.com/", "Paste JavaScript into a new script component"), + "godot": ("GDScript Online", "https://gdscript-online.github.io/", "Test GDScript snippets online, or use Godot Editor"), + "unreal": ("Unreal Engine", "https://www.unrealengine.com/", "Requires Unreal Engine 5.x with Python scripting enabled"), + "gamemaker": ("GameMaker", "https://gamemaker.io/", "Create new project, add script resource"), + "flutter": ("DartPad", "https://dartpad.dev/", "Paste Dart code to test UI logic"), + "twine": ("Twinery.org", "https://twinery.org/", "Import HTML or paste into new story"), + "renpy": ("Ren'Py", "https://www.renpy.org/", "Download SDK, replace script.rpy"), + "ink": ("Inky Editor", "https://www.inklestudios.com/ink/", "Download Inky or use web playground"), + "yarn": ("Try Yarn Spinner", "https://try.yarnspinner.dev/", "Paste .yarn code and click Run"), + "roblox": ("Roblox Studio", "https://www.roblox.com/create", "Create ModuleScript in ReplicatedStorage"), + "rpgmaker": ("RPG Maker MZ", "https://www.rpgmakerweb.com/", "Save as js/plugins/StoryAdventure.js"), + "minecraft": ("Minecraft Java", "https://www.minecraft.net/", "Create datapack in world/datapacks folder"), + "papyrus": ("Creation Kit", "https://www.creationkit.com/", "Compile .psc with Papyrus compiler"), + "vrchat": ("VRChat SDK", "https://hello.vrchat.com/", "Unity 2019.4 + VRChat SDK3 + UdonSharp"), + "lensstudio": ("Lens Studio", "https://lensstudio.snapchat.com/", "Free download, create new lens project"), + "metaspark": ("Meta Spark Studio", "https://spark.meta.com/", "Free download, create AR effect"), + "8thwall": ("8th Wall", "https://www.8thwall.com/", "WebAR - works in mobile browser"), + "tiktok": ("Effect House", "https://effecthouse.tiktok.com/", "Free download, create TikTok effect"), + "reality": ("Xcode", "https://developer.apple.com/xcode/", "Requires Xcode 15+ for visionOS/iOS"), + "inform7": ("Inform 7 / Borogove", "https://borogove.app/", "Online IDE or download Inform 7"), + "godot_dialogue": ("Godot + Dialogue Manager", "https://github.com/nathanhoad/godot_dialogue_manager", "Install addon from AssetLib"), + "movingdotspace": ("Browser", "", "Self-contained HTML - open in any browser, use arrow keys"), + "terminal": ("Python", "https://replit.com/", "Run with 'python game.py' - works on Replit, Pydroid, any Python env"), + "existing game": ("Various", "", "JSON format for manual conversion to your target"), + } + + def get_platform_info(platform): + """Get test info for selected platform.""" + if not platform or platform == "": + return "" + info = PLATFORM_TEST_INFO.get(platform, ("Unknown", "", "")) + name, url, tip = info + if url: + return f"**Test at:** [{name}]({url}) \n**Tip:** {tip}" + elif name: + return f"**Test at:** {name} \n**Tip:** {tip}" + return "" + + platform_info_display = gr.Markdown( + value=get_platform_info("playcanvas"), + elem_classes=["platform-info-box"] ) - submit_button.click( - generate_response, - inputs=[prompt, rag_checkbox, stream_checkbox], - outputs=[output, tokens_per_second, ram_usage, doc_references], + # Platform dropdown with all options grouped + platform_choices = [ + ("── Game Engines ──", ""), + ("PlayCanvas", "playcanvas"), + ("Godot 4.x", "godot"), + ("Unreal 5.x", "unreal"), + ("GameMaker", "gamemaker"), + ("Flutter", "flutter"), + ("── Narrative Engines ──", ""), + ("Twine", "twine"), + ("Ren'Py", "renpy"), + ("Ink", "ink"), + ("Yarn Spinner", "yarn"), + ("── Game Platforms ──", ""), + ("Roblox", "roblox"), + ("RPG Maker MZ", "rpgmaker"), + ("Minecraft", "minecraft"), + ("Skyrim/Fallout (Papyrus)", "papyrus"), + ("VRChat", "vrchat"), + ("── AR/XR Platforms ──", ""), + ("Lens Studio", "lensstudio"), + ("Meta Spark", "metaspark"), + ("8th Wall", "8thwall"), + ("TikTok Effect House", "tiktok"), + ("Apple Reality", "reality"), + ("── Interactive Fiction ──", ""), + ("Inform 7", "inform7"), + ("Godot Dialogue Manager", "godot_dialogue"), + ("── Playable HTML ──", ""), + ("MovingDotSpace", "movingdotspace"), + ("── Terminal ──", ""), + ("Python Terminal Game", "terminal"), + ("Existing Game (JSON)", "existing game"), + ] + + with gr.Row(): + platform_selector = gr.Dropdown( + choices=platform_choices, + value="playcanvas", + label="Target Platform", + scale=2 + ) + export_btn = gr.Button("Export", variant="primary", scale=1) + export_zip_btn = gr.Button("Download ZIP", variant="secondary", scale=1) + + with gr.Row(): + config_input_export = gr.Textbox(label="Paste Game Config JSON", lines=6, placeholder='{"village": {"start": {"description": "...", "choices": [...]}}}', scale=4) + load_sandwich_btn = gr.Button("📥 Load Sandwich Quest Demo", scale=1) + + # Load sandwich quest demo + def load_sandwich_demo(): + try: + import os + demo_path = os.path.join(os.path.dirname(__file__), "sandwich_quest_demo.json") + with open(demo_path, 'r', encoding='utf-8') as f: + return f.read() + except Exception as e: + return f'{{"error": "Could not load demo: {str(e)}"}}' + + load_sandwich_btn.click(fn=load_sandwich_demo, outputs=[config_input_export]) + + # Update platform info when selection changes + platform_selector.change( + fn=get_platform_info, + inputs=[platform_selector], + outputs=[platform_info_display] ) - clear_models_button.click( - clear_all_models, - inputs=[], - outputs=[output] + with gr.Row(): + export_explanation = gr.Textbox(label="Instructions", lines=8, interactive=False) + export_code = gr.Code(label="Generated Code", language="javascript") + + # ZIP download components + with gr.Row(): + export_zip_file = gr.File(label="Download ZIP", interactive=False) + export_zip_status = gr.Textbox(label="ZIP Status", interactive=False, lines=2) + + export_btn.click( + fn=ConfigConversionforExporttoPlatform, + inputs=[platform_selector, config_input_export], + outputs=[export_explanation, export_code] ) - loaded_models_button.click( - loaded_model_list, - outputs=[output] + # ZIP export function + def export_platform_as_zip(platform, config_json): + """Export platform code as a ZIP file.""" + if not config_json or not config_json.strip(): + return None, "No config provided" + + try: + import tempfile + import zipfile + from datetime import datetime + + # Get the export output + explanation, code = ConfigConversionforExporttoPlatform(platform, config_json) + + if not code: + return None, "Export failed - no code generated" + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + zip_filename = f"{platform}_export_{timestamp}.zip" + zip_path = os.path.join(tempfile.gettempdir(), zip_filename) + + with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + # Add setup instructions + zipf.writestr("README_SETUP.txt", explanation) + + # Add code file(s) based on platform + if platform == "godot": + # Godot outputs multiple files separated by markers + # Parse the combined output into separate files + files = parse_godot_output(code) + for filename, content in files.items(): + zipf.writestr(filename, content) + elif platform == "playcanvas": + zipf.writestr("game.js", code) + elif platform == "unreal": + zipf.writestr("game_controller.py", code) + elif platform == "gamemaker": + zipf.writestr("game_controller.gml", code) + elif platform == "flutter": + zipf.writestr("main.dart", code) + elif platform == "twine": + zipf.writestr("story.html", code) + elif platform == "renpy": + zipf.writestr("script.rpy", code) + elif platform == "ink": + zipf.writestr("story.ink", code) + elif platform == "yarn": + zipf.writestr("dialogue.yarn", code) + elif platform == "roblox": + zipf.writestr("StoryModule.lua", code) + elif platform == "rpgmaker": + zipf.writestr("js/plugins/StoryAdventure.js", code) + elif platform == "lensstudio": + zipf.writestr("StoryExperience.js", code) + elif platform == "metaspark": + zipf.writestr("StoryEffect.js", code) + elif platform == "8thwall": + zipf.writestr("index.html", code) + elif platform == "minecraft": + zipf.writestr("datapack_instructions.txt", code) + elif platform == "papyrus": + zipf.writestr("StoryAdventure.psc", code) + elif platform == "vrchat": + zipf.writestr("StoryAdventure.cs", code) + elif platform == "tiktok": + zipf.writestr("StoryEffect.js", code) + elif platform == "reality": + zipf.writestr("StoryView.swift", code) + elif platform == "inform7": + zipf.writestr("story.ni", code) + elif platform == "godot_dialogue": + zipf.writestr("story.dialogue", code) + elif platform == "movingdotspace" or platform == "2d map related space": + zipf.writestr("game.html", code) + elif platform == "terminal": + zipf.writestr("game.py", code) + else: + zipf.writestr("export.txt", code) + + # Add the original config + zipf.writestr("config.json", config_json) + + return zip_path, f"Exported: {zip_filename}" + except Exception as e: + return None, f"ZIP error: {str(e)}" + + def parse_godot_output(combined_code): + """Parse Godot's combined output into separate files.""" + files = {} + current_file = None + current_content = [] + + for line in combined_code.split('\n'): + # Check for file markers like "# === game_state.gd ===" or "### FILE: game_state.gd ###" + if line.startswith('# ===') and '===' in line[5:]: + if current_file and current_content: + files[current_file] = '\n'.join(current_content) + # Extract filename + filename = line.replace('# ===', '').replace('===', '').strip() + current_file = filename + current_content = [] + elif line.startswith('### FILE:') and '###' in line[9:]: + if current_file and current_content: + files[current_file] = '\n'.join(current_content) + filename = line.replace('### FILE:', '').replace('###', '').strip() + current_file = filename + current_content = [] + else: + current_content.append(line) + + # Don't forget the last file + if current_file and current_content: + files[current_file] = '\n'.join(current_content) + + # If no files were parsed, return as single file + if not files: + files["export.gd"] = combined_code + + return files + + export_zip_btn.click( + fn=export_platform_as_zip, + inputs=[platform_selector, config_input_export], + outputs=[export_zip_file, export_zip_status] ) - with gr.Tab("Preview APIs / Gradio Client"): - gr.HTML("July 2024 - Gemini, Cohere and Groq rate limit free APIs") - gr.HTML("https://modal.com/pricing ($30 a month?), https://fireworks.ai/pricing ($1 free trial), https://sambanova.ai/fast-api?api_ref=577164 (free rate limitied api - llama 405B 114 toks/s)") - gr.Interface(fn=TestGradioClientrandommodel, inputs="text", outputs="markdown", description="Single response test of gradio client - Gemma 2 9b for test as api, Use for testing like using a space and duplicate for heavy testing") + gr.Markdown(""" +| Engine | Status | Output | Test Online | +|--------|--------|--------|-------------| +| **Game Engines** |||| +| PlayCanvas | Ready | JavaScript | [PlayCanvas Editor](https://playcanvas.com/) | +| Godot 4.x | Ready | .tscn + GDScript (ZIP) | [GDScript Online](https://gdscript-online.github.io/) | +| Unreal 5.x | Ready | Python script | Desktop only | +| GameMaker | Ready | GML script | [GameMaker](https://gamemaker.io/) | +| Flutter | Ready | Dart code | [DartPad](https://dartpad.dev/) | +| **Narrative Engines** |||| +| Twine | Ready | HTML story | [Twinery.org](https://twinery.org/) | +| Ren'Py | Ready | .rpy script | [Ren'Py](https://www.renpy.org/) | +| Ink | Ready | .ink story | [Inky Editor](https://www.inklestudios.com/ink/) | +| Yarn Spinner | Ready | .yarn dialogue | [Try Yarn](https://try.yarnspinner.dev/) | +| **Game Platforms** |||| +| Roblox | Ready | Lua ModuleScript | [Roblox Studio](https://www.roblox.com/create) | +| RPG Maker MZ | Ready | .js plugin | [RPG Maker](https://www.rpgmakerweb.com/) | +| Minecraft | Ready | Datapack (mcfunction) | In-game testing | +| Skyrim/Fallout | Ready | Papyrus .psc | [Creation Kit](https://www.creationkit.com/) | +| VRChat | Ready | UdonSharp .cs | [VRChat SDK](https://hello.vrchat.com/) | +| **AR/XR Platforms** |||| +| Lens Studio | Ready | JavaScript | [Lens Studio](https://lensstudio.snapchat.com/) | +| Meta Spark | Ready | JavaScript | [Meta Spark](https://spark.meta.com/) | +| 8th Wall | Ready | HTML + A-Frame | [8th Wall](https://www.8thwall.com/) | +| TikTok Effect House | Ready | JavaScript | [Effect House](https://effecthouse.tiktok.com/) | +| Apple Reality | Ready | Swift/RealityKit | [Xcode](https://developer.apple.com/xcode/) | +| **Interactive Fiction** |||| +| Inform 7 | Ready | Natural language | [Inform 7](http://inform7.com/) | +| Godot Dialogue | Ready | .dialogue file | [Dialogue Manager](https://github.com/nathanhoad/godot_dialogue_manager) | +| **Playable HTML** |||| +| MovingDotSpace | Ready | Self-contained HTML | Browser (arrow keys + modals) | +| **Terminal** |||| +| Python Terminal | Ready | Python script | [Replit](https://replit.com/) / Pydroid / Any Python | + """) + create_mechanic_translation_tab() - with gr.Tab("Everything Else"): - #with gr.Accordion("Game Customisation Tools", open=False): + with gr.Tab("Theory & Expansion Ideas"): + # with gr.Row(): + # with gr.Column(): + with gr.Accordion("'1d' vs 2d vs 3d", open=False): + gr.Markdown("""**Test your config in different dimensions:** +- **1D (Text):** Test tab → Playtest & Edit +- **2D (Top-down):** Test tab → Game Preview (2D) (MovingDotSpace) +- **3D (WebGL):** Test tab → Game Preview (3D) (PlayCanvas) - with gr.Tab("Portability"): - with gr.Tab("Export"): - with gr.Row(): - with gr.Column(): - gr.Markdown("""# Suggestions from Llama 405b through Lambdachat +--- + +# Suggestions from Llama 405b through Lambdachat To create 2D and 3D versions of your 1D text-based game, you'll need to expand your game engine to support additional features. Here's a high-level overview of the steps you can follow: @@ -839,202 +4112,169 @@ Remember that these ideas can be mixed and matched to create unique gameplay exp While some of these ideas can be adapted to 2D and 3D games, they are particularly well-suited for 1D text-based games, where the focus is on narrative, imagination, and text-driven gameplay. + """) + # with gr.Column(): + gr.HTML("Main idea for this tab is ways to reuse the config in same/different way in other platforms - Need a conversion management interface") + gr.HTML("Side Quests can be to build with a real world 3D printer - eg. Map or speculating how uncharted territories are") + with gr.Accordion("Basic planning for conversions", open=False): + gr.Markdown("**Use the Platform Export tool in the 'Platform Export' tab above for actual conversion.**") + gr.HTML("MikeTheTech Tutorials (He has many more playlists so check the rest of his channel) -
playcanvas https://www.youtube.com/playlist?list=PLJKGfra8Jl0B845PNo9cMxAQfghJrYH1Y,
unreal engine - https://www.youtube.com/watch?v=STHdIdWO0tk&list=PLJKGfra8Jl0CSjCzk1r0peTp0orIleqdG,
gamemaker - https://www.youtube.com/watch?v=ka_fVrGMOwo&list=PLJKGfra8Jl0AW3rNuwcmi_aq_PdsjQtbu ") + gr.HTML("Reskinng and extending the Tutorials / smallest examples = fastest way to gain intuition for whats important") + gr.HTML("For 3D with regard to this space - see '3D Study and Tutorials' → '3D Background with UI' tab") + with gr.Tab("Twine - Open-source"): + gr.Markdown("### Twine Export") + gr.HTML("twinery.org - Story based games | Twee File = easy import | Local Install = Actual Save Files instead of local storage in browser") + + with gr.Row(): + twine_config_input = gr.Textbox( + label="Game Config JSON", + lines=8, + placeholder='{"village": {"start": {"description": "You arrive at the village.", "choices": ["Enter tavern", "Talk to guard"], "transitions": {"Enter tavern": "tavern", "Talk to guard": "guard"}}}}', + scale=3 + ) + with gr.Column(scale=1): + twine_load_demo_btn = gr.Button("📥 Load Demo Config", variant="secondary") + twine_export_btn = gr.Button("🎮 Export to Twine", variant="primary") + + twine_output = gr.Code(label="Twine/Twee Output", language="html", lines=12) + twine_status = gr.Markdown("") + + def load_twine_demo(): + try: + import os + demo_path = os.path.join(os.path.dirname(__file__), "sandwich_quest_demo.json") + with open(demo_path, 'r', encoding='utf-8') as f: + return f.read() + except Exception as e: + return f'{{"error": "Could not load demo: {str(e)}"}}' + + def export_to_twine_ui(config_json): + if not config_json or not config_json.strip(): + return "", "⚠️ Please provide a game config JSON" + try: + from exporters import export_to_twine + explanation, code = export_to_twine(config_json) + if code: + return code, f"✅ Twine export generated!\n\n{explanation}" + else: + return "", f"❌ Export failed: {explanation}" + except Exception as e: + return "", f"❌ Error: {str(e)}" + + twine_load_demo_btn.click(fn=load_twine_demo, outputs=[twine_config_input]) + twine_export_btn.click( + fn=export_to_twine_ui, + inputs=[twine_config_input], + outputs=[twine_output, twine_status] + ) + + with gr.Accordion("Example Twine/Twee File Structure", open=False): + gr.Code(ExampleTwineFileStructureasInitialPrompt, language="html") + with gr.Tab("3D Engine Resources"): + gr.Markdown("""### 3D Game Engine Export Resources +Use the **Platform Export** tab to generate code, then use these resources to build your 3D game. + """) + with gr.Accordion("PlayCanvas", open=True): + gr.Markdown("""**Select 'playcanvas' in Platform Export to generate JavaScript code.** + +Generated code includes: GameState, ConditionEvaluator, EffectApplicator, TransitionResolver, scene entities, and click handling. + +**Live Preview:** Use the **Game Preview (3D)** tab for interactive 3D preview. + """) + with gr.Accordion("UI in PlayCanvas", open=False): + gr.HTML("Basic - Add 2 Entities to the root of the hierachy (Left top - displays items in the scene) -
one will load the story script and the will hold the UI items and load the UI script (Both Below)
UI entitiy needs to have a screen and script items added under its properties (left hand side)
UI Entity also needs three items added to it. text (need a font as well to load), a container for buttons, and the button that gets turned into a template (hierachy menu options)") + with gr.Column(): + gr.Code(playcanvasstorymanager) + gr.Code(playcanvasuimanager) + with gr.Accordion("Tutorials & Resources", open=False): + gr.HTML("Playcanvas Tutorials - https://developer.playcanvas.com/tutorials/webxr-hello-world/, Wow - https://developer.playcanvas.com/user-manual/assets/types/wasm/
https://github.com/playcanvas/playcanvas.github.io - https://playcanvas.vercel.app/#/misc/hello-world ") + gr.HTML(""" + https://playcanvas.vercel.app/#/physics/vehicle + https://forum.playcanvas.com/t/assets-free-3d-models-sprites-icons-and-sounds-for-your-games/19199 + https://github.com/playcanvas/playcanvas.github.io/tree/main + https://github.com/playcanvas/engine + https://github.com/playcanvas/engine/blob/main/examples/src/examples/physics/vehicle.example.mjs + https://github.com/playcanvas/engine/blob/main/examples/src/examples/misc/editor.example.mjs + https://github.com/playcanvas/engine/blob/main/examples/src/examples/input/keyboard.example.mjs + https://github.com/playcanvas/engine/blob/main/examples/src/examples/input/mouse.example.mjs + https://github.com/playcanvas/engine/blob/main/examples/src/examples/camera/first-person.example.mjs + """) + gr.HTML("https://youtu.be/a3pJiZ86XgA (Learn.Nimagin) - Youtube video showing user interface in playcanvas") + with gr.Accordion("Notes", open=False): + gr.HTML("How can add 100 shapes in playcanvas very quickly? - Seems one at a time during editor phase") + gr.HTML("Each location is new scene?") + gr.HTML("Repurpose the ball demo - ball replaced with a custom 3D object
platform can used as map floor by changing dimensions, islands unlock = hidden platforms unhidden by a trigger (verticality as location access control)
Teleport can be loader for UI ") + with gr.Accordion("Godot 4.x", open=False): + gr.Markdown("""**Select 'godot' in Platform Export to generate GDScript + Scene files.** + +Generated files: +- `main.tscn` - Scene with all locations and choices +- `game_state.gd` - AutoLoad singleton (add in Project Settings) +- `game_controller.gd` - Navigation and state management +- `condition_evaluator.gd`, `effect_applicator.gd`, `transition_resolver.gd` +- `choice.gd` - Script for clickable choice boxes + +**Setup:** Add `game_state.gd` as AutoLoad named "GameState" in Project Settings. + """) + with gr.Accordion("Tutorials & Resources", open=False): + gr.HTML("Godot Docs - https://docs.godotengine.org/en/stable/") + gr.HTML("GDQuest - https://www.gdquest.com/") + gr.HTML("https://github.com/godotengine/godot-demo-projects") + with gr.Accordion("Unreal Engine 5", open=False): + gr.Markdown("""**Select 'unreal' in Platform Export to generate a Python script.** + +**Requirements:** Enable "Python Editor Script Plugin" in Edit -> Plugins + +**Usage:** +1. Open Python Console: Window -> Developer Tools -> Output Log +2. Paste the generated script +3. Run `start_game()` to create the scene +4. Use `list_choices()` and `select_choice('id')` to play + +Note: This is for prototyping in-editor. For a shipped game, convert to Blueprints. + """) + with gr.Accordion("Tutorials & Resources", open=False): + gr.HTML("Learning Centre - https://dev.epicgames.com/community/unreal-engine/learning") + gr.HTML("Python in Unreal - https://docs.unrealengine.com/5.0/en-US/scripting-the-unreal-editor-using-python/") + with gr.Accordion("Existing Games as Engines", open=False): + gr.HTML("Unreal Engine for Fortnite https://dev.epicgames.com/community/fortnite/getting-started/uefn") + gr.HTML("https://www.reddit.com/r/singularity/comments/1fd4oo1/roblox_announces_ai_tool_for_generating_3d_game/ - roblox and genai") + gr.HTML("Modding Tools? eg. Skyrim, GTA, sims 4 custom scenarios") + gr.HTML("GTA (Rockstar Editor) - UI or mission next objective system") + gr.HTML("SIMS - Buildings as Paths - Like a maze path is your decisions") + gr.HTML("Racing Game - Next turn = choice") + gr.HTML("Other games as Iframes and MLM as the judge (visual confirmation) aka the quest completion algorithm") + + with gr.Tab("AR/XR Integration (Future)"): + gr.Markdown("""## AR/XR Integration Notes + +**Full AR/XR documentation has been moved to a dedicated section.** + +For comprehensive AR/XR resources including: +- Platform comparison tables (Lens Studio, Meta Spark, Effect House, Adobe Aero, 8th Wall) +- File types, export formats, and testing devices +- Detailed workflows for each platform +- Integration ideas for game configs +- WebXR and browser-based AR +- Publishing and distribution guides +- Animation resources (Mixamo alternatives) + +**See: 3D Study and Tutorials → AR/XR Development tab** + +This keeps AR/XR content consolidated with other 3D resources for easier reference. """) - with gr.Column(): - gr.HTML("Main idea for this tab is ways to reuse the config in same/different way in other platforms - Need a conversion management interface") - with gr.Accordion("Basic planning for conversions", open=False): - gr.HTML("MikeTheTech Tutorials (He has many more playlists so check the rest of his channel) -
playcanvas https://www.youtube.com/playlist?list=PLJKGfra8Jl0B845PNo9cMxAQfghJrYH1Y,
unreal engine - https://www.youtube.com/watch?v=STHdIdWO0tk&list=PLJKGfra8Jl0CSjCzk1r0peTp0orIleqdG,
gamemaker - https://www.youtube.com/watch?v=ka_fVrGMOwo&list=PLJKGfra8Jl0AW3rNuwcmi_aq_PdsjQtbu ") - gr.Interface(ConfigConversionforExporttoPlatform, inputs=[ConfigConversionforExporttoPlatformselector, "text"], outputs=["text", "code"], description="Helper for Exporting to other platforms") - gr.HTML("Reskinng and extending the Tutorials / smallest examples = fastest way to gain intuition for whats important") - gr.HTML("For 3D with regard to this space 3 is backdrop for the UI until major modifications are decided by the user") - with gr.Tab("Play Canvas"): - gr.HTML("Playcanvas Tutorials - https://developer.playcanvas.com/tutorials/webxr-hello-world/, Wow - https://developer.playcanvas.com/user-manual/assets/types/wasm/
https://github.com/playcanvas/playcanvas.github.io - https://playcanvas.vercel.app/#/misc/hello-world ") - gr.HTML("https://youtu.be/a3pJiZ86XgA (Learn.Nimagin) - Youtube video showing user interface in playcanvas (My observation is you can have 3d items moving behind the interface like how 2d fighter use 3d space)") - gr.HTML("How can add 100 shapes in playcanvas very quickly? - Seems one at a time during editor phase") - gr.HTML("Each location is new scene?") - gr.HTML("Repurpose the ball deno - ball replaced with a custom 3D object
platform can used as map floor by changing dimensions, islands unlock = hidden platforms unhidden by a trigger (verticality as location access control)
Teleport can be loader for UI ") - gr.HTML("") - with gr.Tab("Unreal Engine"): - gr.HTML("Learning Centre - https://dev.epicgames.com/community/unreal-engine/learning") - gr.HTML("How can add 100 shapes in playcanvas very quickly? - Can run python scripts and possibly c++") - gr.HTML("") - gr.HTML("") - gr.HTML("") - with gr.Tab("Existing Game as game engine"): - gr.HTML("Unreal Engine for Fortnite https://dev.epicgames.com/community/fortnite/getting-started/uefn") - gr.HTML("https://www.reddit.com/r/singularity/comments/1fd4oo1/roblox_announces_ai_tool_for_generating_3d_game/ - roblox and genai") - gr.HTML("Modding Tools? eg. Skyrim, GTA, sims 4 custom scenarios") - gr.HTML("Other games as Iframes and MLM as the judge (visual confirmation) aka the quest completion algorithm") - with gr.Tab("In-browser / mobile IDE to Mobile (Mainly Android)"): - gr.Interface(fn=lambda inputtoconvert: inputtoconvert + " + placeholder code", inputs=["text"], outputs=["text"], description="placeholder code for conversion from JSON to basic text UI (eg. terminal = nested while loop + input + if in python)" ) - gr.HTML("'Host' Dart app on github - create hyperlink like the page suggests: https://codewithandrea.com/tips/create-dartpad-from-github-gist/") - gr.HTML("https://docs.flutter.dev/ui, https://docs.flutter.dev/data-and-backend/state-mgmt/intro
https://developer.android.com/ai/aicore") - gr.HTML("Some ideas - https://github.com/divyanshub024/awesome-dart-pad, Dart Website - https://dart.dev/tools/dartpad") - gr.HTML("Flutter / Pydroid / Kivy / Colab") - gr.HTML("Flutter - Dartpad, Project IDX, Replit?") - - with gr.Tab("Import"): - gr.HTML("Textbooks/Memes/Sayings/Songs/Books to Games. NLP as a way to guide the generation process") - with gr.Tab("Basic Game Engine Mechanics"): - gr.HTML("There is an issue with the loading of the choices above 4 - only 3 load max it seems") - gr.HTML("Placeholder for explanations of Player and Game Session") - with gr.Tab("Endless Commerce support"): - gr.HTML("Need to be able to support this type of code for upgrades - https://www.decisionproblem.com/paperclips/index2.html - https://www.reddit.com/r/incremental_games/comments/rc7ks7/the_unique_storytelling_of_universal_paperclips/
https://huggingface.co/spaces/osanseviero/TheMLGame/blob/main/main.js") - with gr.Tab("Current '1D Engine' Defects"): - gr.HTML("To test the config idea I (with llm assistance) had to make an 'engine' that was based around the config - so there are many potholes ") - gr.HTML("All realtime events - Text still needs realtime as well") - with gr.Tab("Inventory and Skill Support"): - gr.HTML("Each decision affects Skills or inventory") - with gr.Tab("NPC Support"): - gr.HTML("Shared timeline that the player interfere with") - with gr.Tab("Economics Support"): - gr.HTML("Style Idea for a Basic Idea - Endless Economy (Tiny Tower as well) - Paperclip maximiser and inspirations - https://huggingface.co/spaces/osanseviero/TheMLGame") - with gr.Tab("Time Support"): - gr.HTML("No urgency / patience mechanics") - with gr.Tab("LLM play testing"): - gr.HTML("LLM can read the contents in full and give critiques but they can also play the game if you make a api interface - gradio allows this in the form of gradio client but you can also reroute the user inputs to function calling") - with gr.Tab("Custom JS Config Creator"): - gr.HTML("-- Incomplete -- Companion Space for zerogpu / client api workflow planning for a way to send a zip to the Basic Game Engine at the bottom of https://huggingface.co/spaces/KwabsHug/TestSvelteStatic (Also to test how much can be done majority on cpu)") - with gr.Tab("Simple Config Creator"): - inventory_items = gr.State([]) - skills_items = gr.State([]) - objectives_items = gr.State([]) - targets_items = gr.State([]) - with gr.Tabs(): - with gr.TabItem("Inventory"): - inventory_type = gr.Textbox(label="Type") - inventory_name = gr.Textbox(label="Name") - inventory_description = gr.Textbox(label="Description") - add_inventory = gr.Button("Add Inventory Item") - inventory_textbox = gr.JSON(label="Inventory Items", value=[]) - - with gr.TabItem("Skills"): - skills_branch = gr.Textbox(label="Branch") - skills_name = gr.Textbox(label="Name") - skills_learned = gr.Dropdown(choices=["True", "False"], label="Learned") - add_skill_button = gr.Button("Add Skill") - skills_textbox = gr.JSON(label="Skills", value=[]) - - with gr.TabItem("Objectives"): - objectives_id = gr.Textbox(label="ID") - objectives_name = gr.Textbox(label="Name") - objectives_complete = gr.Dropdown(choices=["True", "False"], label="Complete") - add_objective_button = gr.Button("Add Objective") - objectives_textbox = gr.JSON(label="Objectives", value=[]) - - with gr.TabItem("Targets"): - targets_name = gr.Textbox(label="Name") - targets_x = gr.Textbox(label="X Coordinate") - targets_y = gr.Textbox(label="Y Coordinate") - targets_collisionType = gr.Textbox(label="Collision Type") - targets_collisiontext = gr.Textbox(label="Collision Text") - add_target_button = gr.Button("Add Target") - targets_textbox = gr.JSON(label="Targets", value=[]) - - with gr.TabItem("Placeholders for Modal Target"): - gr.HTML("Placeholder") - - with gr.TabItem("Placeholders for State Machine Modal Target"): - gr.HTML("Placeholder") - - with gr.TabItem("Placeholders for Background"): - gr.HTML("Placeholder") - - config_output = gr.JSON(label="Updated Configuration") - - @gr.render(inputs=[inventory_items, skills_items, objectives_items, targets_items]) #, outputs=config_output) - def aggregate_config(inventory, skills, objectives, targets): - config = default_config.copy() - config['inventory'] = inventory - config['skills'] = skills - config['objectives'] = objectives - config['targets'] = targets - return config - - add_inventory.click(add_inventory_item, inputs=[inventory_items, inventory_type, inventory_name, inventory_description], outputs=inventory_textbox) - add_inventory.click(aggregate_config, inputs=[inventory_items, skills_items, objectives_items, targets_items], outputs=config_output) - - add_skill_button.click(add_skill, inputs=[skills_items, skills_branch, skills_name, skills_learned], outputs=skills_textbox) - add_skill_button.click(aggregate_config, inputs=[inventory_items, skills_items, objectives_items, targets_items], outputs=config_output) - - add_objective_button.click(add_objective, inputs=[objectives_items, objectives_id, objectives_name, objectives_complete], outputs=objectives_textbox) - add_objective_button.click(aggregate_config, inputs=[inventory_items, skills_items, objectives_items, targets_items], outputs=config_output) - - add_target_button.click(add_target, inputs=[targets_items, targets_name, targets_x, targets_y, targets_collisionType, targets_collisiontext], outputs=targets_textbox) - add_target_button.click(aggregate_config, inputs=[inventory_items, skills_items, objectives_items, targets_items], outputs=config_output) - - with gr.Tab("Advanced Config Creator"): - gr.HTML("Config with More than text and images") - - with gr.Tab("Real World reach of the game"): - gr.HTML("3D printed trophies or game items as real items") - gr.HTML("Smart Watch and Phone - Notifications to watch and eg. character messages, authorisation requests, in game internet - Cloud computing / SBC or controller project ideas") - with gr.Tab("LLM/Robotics as custom controllers"): - gr.HTML("https://www.reddit.com/r/singularity/comments/1fm7fup/ihmc_and_boardwalk_robotics_show_their_humanoid/") - gr.HTML("Controls changed the scope of the game eg. mouse vs keyboard vs console controller vs remote vs touch screen
LLM can be vision/surveilance based controler (eg. MGS/GTA camera gauged by an actual camera in real life) or it can be a companion (offline/off console game progrssion ideas)") - gr.HTML("https://github.com/Shaka-Labs/ACT $250 imitation learning/teleoperation - eg. a win loss result alert / NPC 'scout' telling you go or stay") - gr.HTML("https://huggingface.co/posts/thomwolf/809364796644704") - gr.HTML("Robotics - https://www.nvidia.com/en-us/autonomous-machines/embedded-systems/jetson-orin/ https://huggingface.co/lerobot https://github.com/tonyzhaozh/aloha https://github.com/Shaka-Labs/ACT https://github.com/OpenTeleVision/TeleVision https://www.stereolabs.com/ ") - gr.HTML("https://www.reddit.com/r/singularity/comments/1f88z58/the_first_ever_agent_civilization_1000_truly/") - with gr.Tab("Existing Game Developemnt Resources"): - gr.HTML("https://enginesdatabase.com/") - gr.HTML("https://develop.games/#nav-tools-engine ") - with gr.Tab("Other Considerations"): - with gr.Tab("General"): - gr.HTML("https://www.reddit.com/r/singularity/comments/1fiugew/wonderworld_a_novel_framework_for_interactive_3d/") - - gr.HTML("https://huggingface.co/docs/hub/api - daily papers is an endpoint so you can turn paper abstract into games with the help of LLM") - - gr.HTML("Experiment for https://huggingface.co/spaces/ysharma/open-interpreter/blob/main/app.py inplementation with gradio client api") - - gr.HTML("https://huggingface.co/spaces/HuggingFaceTB/SmolLM-135M-Instruct-WebGPU") - - gr.HTML("Useful Spaces and links: https://huggingface.co/spaces/artificialguybr/Stable-Audio-Open-Zero https://huggingface.co/spaces/stabilityai/TripoSR https://huggingface.co/spaces/wangfuyun/AnimateLCM-SVD https://huggingface.co/spaces/multimodalart/face-to-all https://huggingface.co/spaces/facebook/MusicGen https://huggingface.co/spaces/Doubiiu/tooncrafter") - - gr.HTML("langchain docs as awareness for alot of the integration use cases and providers that are possible - https://python.langchain.com/v0.2/docs/integrations/tools/") - - gr.HTML("https://huggingface.co/spaces/linoyts/scribble-sdxl-flash as map planner") - - gr.HTML("---------------------------------------Gameplay Ideas-------------------------------") - gr.HTML("https://huggingface.co/spaces/Lin-Chen/ShareCaptioner-Video - game use example police questions a event with multiple eye witnesses needs to give as close to the caption description to win") - with gr.Tab("State management through huggingface?"): - gr.HTML("Huggingface as the login provider? - https://huggingface.co/spaces/Wauplin/gradio-user-history/tree/main https://huggingface.co/spaces/AP123/IllusionDiffusion https://huggingface.co/docs/hub/en/spaces-oauth https://huggingface.co/docs/hub/en/oauth, persistent storage - https://huggingface.co/docs/hub/en/spaces-storage") - with gr.Tab("Finetuning options"): - gr.HTML("https://docs.mistral.ai/guides/finetuning/
https://openpipe.ai/blog/fine-tuning-best-practices-chapter-2-models") - gr.HTML("Unsloth and Colab? - https://github.com/unslothai/unsloth https://huggingface.co/unsloth
Mistral Nemo Base - https://huggingface.co/unsloth/Mistral-Nemo-Base-2407 - https://colab.research.google.com/drive/17d3U-CAIwzmbDRqbZ9NnpHxCkmXB6LZ0?usp=sharing
Llama 3 8B https://huggingface.co/unsloth/llama-3-8b-Instruct-bnb-4bit") - gr.HTML("Price - https://openpipe.ai/pricing") - with gr.Tab("Backend and/or Hosting?"): - gr.HTML("Deployemnt options - https://huggingface.co/SpacesExamples", "https://huggingface.co/templates") - gr.HTML("Prototyping and freemium
free api
HF Pro subscription") - gr.HTML("GPU (Data privacy) = No Rate limits? - https://replicate.com/pricing, https://lambdalabs.com/service/gpu-cloud https://huggingface.co/pricing#endpoints https://tensordock.com/cloud-gpus", "https://massedcompute.com/home/pricing/" ) - gr.HTML("Speed - Groq, SambaNova, https://www.etched.com/announcing-etched ") - gr.HTML("Price - Coding - https://aider.chat/docs/leaderboards/ - https://www.deepseek.com/ 0.3 per million - is this per token or chinese character as that means converting code to chinese if possible can save api cost?") - gr.HTML("Llama 3.1 405B - https://ai.meta.com/blog/meta-llama-3-1/ https://replicate.com/meta/meta-llama-3.1-405b-instruct https://fireworks.ai/pricing https://www.ibm.com/products/watsonx-ai/foundation-models") - gr.HTML("Covered by Anythingllm - https://github.com/Mintplex-Labs/anything-llm : https://repocloud.io/details/?app_id=276, https://render.com/pricing, https://docs.railway.app/reference/pricing/free-trial, https://repocloud.io/pricing, https://elest.io/pricing ") - with gr.Tab("Some Interesting Git Repos"): - gr.HTML("https://github.com/NVIDIA/Megatron-LM https://github.com/OpenGVLab/EfficientQAT https://github.com/evintunador/minLlama3/blob/main/model.py https://github.com/evintunador/micro-GPT-sandbox") - with gr.Tab("Old Ideas"): - gr.HTML("""
Main ideas for this space is (June 2024) (Custom component planning?):
-
-
We can generate almost any media data and more
-
A program exist around data
-
Time moves in a straight so all considerations are flattend by the nature of time
-
llms good at short questions
-
HF + Gradio allows for api use so this my prototype tool for tool use test
-
""") - with gr.Tab("Licensing"): - gr.HTML("Need to find the press release to see license eg. https://blackforestlabs.ai/announcing-black-forest-labs/") - with gr.Tabs("Links to go over when free"): - gr.HTML("https://www.reddit.com/r/singularity/comments/1ecuu8j/you_can_now_use_ai_for_3d_model_creation/ | ") + gr.Markdown("---") + gr.Markdown("*Navigate to '3D Study and Tutorials' → 'AR/XR Development' for full documentation.*") - with gr.Tab("Asset loading test"): - gr.HTML("SDXL (linoyts/scribble-sdxl-flash), SVD and Stable Audio used for the test assets (For commercial use need a licence)
testmedia/") - with gr.Row(): - gr.Image(value="testmedia/Flash scribble SDXL - random squiggles as roads.webp") - gr.Video(value="testmedia/SVD - random squiggles as roads video 004484.mp4") - gr.Audio(value="testmedia/Stable Audio - Raindrops, output.wav") - gr.HTML(TestmedialoadinHTML) # imported from relatively_constant_variables - + # NOTE: "Leveraging Machine Learning" tab has been merged into "Media Studio" (ui_gr_media_management.py) + # All content preserved in: Media Studio → Generate → Local (ZeroGPU), API, Resources + + # NOTE: Resources Hub tab moved inside Mini-Tutorial & Resources accordion (testgroupingUIremotely) + + # 3D Study tab (extracted to ui_tabs/study_3d_tab.py) + create_3d_study_tab() + + # Story Graph and Big RPG Scale moved to Config Development Assistance tab -demo.queue().launch() +demo.queue().launch(share=True) diff --git a/beat_scenarios.py b/beat_scenarios.py new file mode 100644 index 0000000000000000000000000000000000000000..cebd0894e117c1c259454d609f73a77173ed6555 --- /dev/null +++ b/beat_scenarios.py @@ -0,0 +1,237 @@ +"""Story beat data and sequence generation. + +This module provides: +- STORY_BEATS: Dictionary of story beats organized by format and genre +- generate_beat_sequence: Function to generate story sequences based on format/genre +""" + +import json +import random + +# Story beats organized by narrative function +STORY_BEATS = { + "setup": [ + ("Ordinary World", "Establish the protagonist's normal life before the adventure"), + ("Meet the Hero", "Introduction to protagonist with defining character moment"), + ("Establish Stakes", "Show what the protagonist stands to lose"), + ("The Want", "Protagonist expresses a desire or goal"), + ("The Flaw", "Hint at protagonist's weakness that must be overcome"), + ("Supporting Cast", "Introduce key allies and their relationships"), + ], + "catalyst": [ + ("Call to Adventure", "External event disrupts the ordinary world"), + ("Inciting Incident", "Something happens that demands a response"), + ("The Invitation", "Opportunity presents itself to the protagonist"), + ("The Challenge", "Direct challenge forces protagonist to act"), + ("Bad News", "Devastating information changes everything"), + ("The Stranger", "New character brings change or information"), + ], + "debate": [ + ("Refusal of Call", "Protagonist hesitates or refuses the challenge"), + ("The Doubt", "Protagonist questions their ability"), + ("Seeking Advice", "Protagonist consults mentor or wise figure"), + ("Weighing Options", "Protagonist considers paths forward"), + ("The Warning", "Someone cautions against the journey"), + ("Internal Conflict", "Protagonist struggles with competing desires"), + ], + "rising_action": [ + ("Crossing Threshold", "Protagonist commits to the journey"), + ("New World Rules", "Protagonist learns how this new world works"), + ("Tests and Allies", "Series of challenges, gaining companions"), + ("Fun and Games", "Promise of the premise fulfilled"), + ("Skill Building", "Protagonist develops abilities needed for climax"), + ("B-Story Develops", "Secondary plot line advances"), + ("Approaching Cave", "Preparing for the major challenge"), + ], + "midpoint": [ + ("False Victory", "Apparent success that will prove hollow"), + ("False Defeat", "Apparent failure that motivates comeback"), + ("Major Revelation", "Game-changing information revealed"), + ("Point of No Return", "Commitment that cannot be undone"), + ("Raised Stakes", "The cost of failure increases dramatically"), + ("New Goal", "Original objective shifts to something bigger"), + ], + "complications": [ + ("Bad Guys Close In", "Opposition intensifies, allies scatter"), + ("Betrayal", "Trusted ally reveals true colors"), + ("All Is Lost", "Lowest point - everything seems hopeless"), + ("Death Moment", "Literal or symbolic death experience"), + ("Dark Night", "Protagonist faces their deepest fears"), + ("Whiff of Death", "Mortality or failure becomes very real"), + ], + "climax": [ + ("Gathering the Team", "Final assembly before the battle"), + ("The Plan", "Strategy for the final confrontation revealed"), + ("Storming the Castle", "Direct assault on the antagonist"), + ("Final Battle", "Climactic confrontation begins"), + ("High Tower Surprise", "Unexpected complication in the climax"), + ("The Dig Deep", "Protagonist must use everything they've learned"), + ("Victory/Defeat", "The outcome of the main conflict"), + ], + "resolution": [ + ("New Equilibrium", "World rebalanced after the adventure"), + ("Character Changed", "Demonstrate protagonist's transformation"), + ("Reward", "Protagonist receives what they've earned"), + ("Return Home", "Protagonist returns to ordinary world, changed"), + ("Open Loop", "Hint at future adventures"), + ("Final Image", "Mirror of opening that shows change"), + ], +} + +# Genre-specific flavor modifiers +GENRE_FLAVORS = { + "action": { + "prefix": "PULSE-POUNDING: ", + "descriptors": ["explosive", "high-octane", "adrenaline-fueled", "relentless"], + }, + "drama": { + "prefix": "EMOTIONALLY CHARGED: ", + "descriptors": ["poignant", "raw", "intimate", "devastating"], + }, + "comedy": { + "prefix": "HILARIOUS: ", + "descriptors": ["absurd", "witty", "chaotic", "perfectly timed"], + }, + "thriller": { + "prefix": "HEART-STOPPING: ", + "descriptors": ["tense", "paranoid", "claustrophobic", "nerve-wracking"], + }, + "romance": { + "prefix": "SWOON-WORTHY: ", + "descriptors": ["tender", "passionate", "yearning", "chemistry-filled"], + }, + "scifi": { + "prefix": "MIND-BENDING: ", + "descriptors": ["futuristic", "technological", "alien", "conceptual"], + }, + "fantasy": { + "prefix": "EPIC: ", + "descriptors": ["mystical", "legendary", "enchanted", "otherworldly"], + }, + "horror": { + "prefix": "TERRIFYING: ", + "descriptors": ["dread-filled", "nightmarish", "unsettling", "visceral"], + }, +} + +# Format-specific beat counts (simplified for game flow) +FORMAT_BEATS = { + "film_90min": { + "name": "90-Minute Feature Film", + "structure": ["setup", "catalyst", "debate", "rising_action", "midpoint", + "complications", "climax", "resolution"], + "typical_count": 8, + }, + "tv_30min": { + "name": "30-Minute TV Episode", + "structure": ["setup", "catalyst", "rising_action", "midpoint", + "complications", "climax", "resolution"], + "typical_count": 6, + }, + "youtube_9min": { + "name": "9-Minute YouTube Video", + "structure": ["setup", "catalyst", "rising_action", "climax", "resolution"], + "typical_count": 5, + }, + "short_3min": { + "name": "3-Minute Short", + "structure": ["setup", "catalyst", "climax", "resolution"], + "typical_count": 4, + }, +} + + +def generate_beat_sequence(format_type, genre, beat_count): + """Generate a story beat sequence based on format and genre. + + Args: + format_type: One of 'film_90min', 'tv_30min', 'youtube_9min', 'short_3min' + genre: One of 'action', 'drama', 'comedy', 'thriller', 'romance', 'scifi', 'fantasy', 'horror' + beat_count: Number of beats to generate (3-15) + + Returns: + Tuple of (list_output, json_output, prompts_output) + """ + format_info = FORMAT_BEATS.get(format_type, FORMAT_BEATS["film_90min"]) + genre_info = GENRE_FLAVORS.get(genre, GENRE_FLAVORS["drama"]) + + structure = format_info["structure"] + + # Build sequence following structure + sequence = [] + beats_per_section = max(1, beat_count // len(structure)) + remaining = beat_count - (beats_per_section * len(structure)) + + for section in structure: + section_beats = STORY_BEATS.get(section, STORY_BEATS["setup"]) + # Pick random beats from this section + count_for_section = beats_per_section + (1 if remaining > 0 else 0) + if remaining > 0: + remaining -= 1 + + available = list(section_beats) + random.shuffle(available) + for beat in available[:count_for_section]: + descriptor = random.choice(genre_info["descriptors"]) + sequence.append((beat[0], beat[1], section, descriptor)) + + # Trim or pad to exact count + if len(sequence) > beat_count: + sequence = sequence[:beat_count] + + if not sequence: + return "Select a valid format and genre!", "{}", "" + + # Format as list + list_output = f"## {format_info['name']} - {genre.title()} Genre\n\n" + for i, (name, desc, section, descriptor) in enumerate(sequence, 1): + list_output += f"**{i}. {name}** [{section}]\n{descriptor.title()}: {desc}\n\n" + + # Format as config JSON + config = {"story_location": {}} + for i, (name, desc, section, descriptor) in enumerate(sequence, 1): + state_name = name.lower().replace(" ", "_").replace("/", "_").replace("-", "_") + current_state_id = f"beat_{i}_{state_name}" + + # Determine next state + if i < len(sequence): + next_name = sequence[i][0] + next_state_name = next_name.lower().replace(" ", "_").replace("/", "_").replace("-", "_") + next_state_id = f"beat_{i+1}_{next_state_name}" + else: + next_state_id = "story_end" + + config["story_location"][current_state_id] = { + "description": f"[{section.upper()}] {genre_info['prefix']}{desc}", + "media_prompt": f"{genre.title()} {format_info['name']} scene: {name} - {descriptor} {desc}", + "choices": ["Continue"], + "transitions": { + "Continue": next_state_id + } + } + + # Add ending state + first_beat = sequence[0][0].lower().replace(" ", "_").replace("/", "_").replace("-", "_") + config["story_location"]["story_end"] = { + "description": f"The {genre} story concludes. The journey has changed everything.", + "choices": ["Experience Again"], + "transitions": { + "Experience Again": f"beat_1_{first_beat}" + } + } + + json_output = json.dumps(config, indent=2) + + # Format prompts + prompts_output = f"## {genre.title()} Story Prompts\n\n" + for i, (name, desc, section, descriptor) in enumerate(sequence, 1): + prompts_output += f"{genre.title()} {format_info['name']} scene: {name} - {descriptor} {desc}\n" + + return list_output, json_output, prompts_output + + +# Quick test +if __name__ == "__main__": + list_out, json_out, prompts_out = generate_beat_sequence("film_90min", "thriller", 5) + print(list_out) + print(json_out) diff --git a/bible_as_inspiration_source.py b/bible_as_inspiration_source.py new file mode 100644 index 0000000000000000000000000000000000000000..3fc690f531c940ec8ec887479e68120247c760c8 --- /dev/null +++ b/bible_as_inspiration_source.py @@ -0,0 +1,502 @@ +import gradio as gr + +# ============================================================================= +# BIBLE STORY TEMPLATES - Structured Data for Game Config Generation +# Priority: Locations, People, Politics/Laws/Moral Rules +# ============================================================================= + +BIBLE_TEMPLATES = { + "matthew": { + "book_name": "Gospel of Matthew", + "youtube_link": "https://www.youtube.com/watch?v=mgUWg3ZY_C8", + "chapters": { + "1": { + "title": "Jesus' Lineage", + "summary": "Genealogy from Abraham to Jesus through David's line", + "locations": [ + {"name": "Bethlehem", "type": "city", "significance": "Birthplace of Jesus, City of David"}, + {"name": "Judea", "type": "region", "significance": "Southern kingdom, seat of Jewish religious authority"} + ], + "people": [ + {"name": "Abraham", "role": "ancestor", "faction": "Patriarchs", "description": "Father of the Jewish nation"}, + {"name": "David", "role": "ancestor", "faction": "Kings of Israel", "description": "Greatest king, promised eternal dynasty"}, + {"name": "Joseph", "role": "protagonist_support", "faction": "House of David", "description": "Carpenter, legal father of Jesus"}, + {"name": "Mary", "role": "protagonist_support", "faction": "House of David", "description": "Virgin mother of Jesus"}, + {"name": "Jesus", "role": "protagonist", "faction": "Divine", "description": "The Messiah, Son of God"} + ], + "politics_laws_morals": [ + {"type": "law", "name": "Mosaic Law", "description": "Jewish religious and civil law governing all aspects of life"}, + {"type": "moral", "name": "Righteousness through lineage", "description": "Importance of family line and covenant promises"}, + {"type": "political", "name": "Davidic Covenant", "description": "God's promise of eternal kingship to David's descendants"} + ] + }, + "2": { + "title": "King Herod's Order", + "summary": "Magi visit, flight to Egypt, massacre of innocents", + "locations": [ + {"name": "Bethlehem", "type": "city", "significance": "Where Jesus was born, target of Herod's massacre"}, + {"name": "Jerusalem", "type": "capital", "significance": "Herod's seat of power, religious center"}, + {"name": "Egypt", "type": "foreign_land", "significance": "Place of refuge, echoes of Exodus"}, + {"name": "Nazareth", "type": "town", "significance": "Final settlement of the holy family"} + ], + "people": [ + {"name": "Herod the Great", "role": "antagonist", "faction": "Roman Client Kings", "description": "Paranoid king who orders massacre of infants"}, + {"name": "The Magi", "role": "ally", "faction": "Eastern Wise Men", "description": "Foreign astrologers who seek the newborn king"}, + {"name": "Joseph", "role": "protagonist_support", "faction": "House of David", "description": "Protector who flees with family to Egypt"}, + {"name": "Mary", "role": "protagonist_support", "faction": "House of David", "description": "Mother protecting her child"}, + {"name": "Jesus", "role": "protagonist", "faction": "Divine", "description": "Infant king, target of Herod's jealousy"} + ], + "politics_laws_morals": [ + {"type": "political", "name": "Roman Occupation", "description": "Judea under Roman rule through client kings"}, + {"type": "political", "name": "Herodian Paranoia", "description": "Herod kills anyone threatening his throne, including family"}, + {"type": "moral", "name": "Protection of the innocent", "description": "Joseph acts on divine warning to save his family"}, + {"type": "moral", "name": "Sacrifice of innocents", "description": "Tragedy of children killed for one man's fear"}, + {"type": "prophecy", "name": "Out of Egypt", "description": "Fulfillment of 'Out of Egypt I called my son'"} + ] + }, + "3": { + "title": "John the Baptist", + "summary": "John prepares the way, Jesus is baptized", + "locations": [ + {"name": "Judean Wilderness", "type": "wilderness", "significance": "Place of repentance and preparation"}, + {"name": "Jordan River", "type": "river", "significance": "Site of baptism, boundary crossing symbolism"} + ], + "people": [ + {"name": "John the Baptist", "role": "herald", "faction": "Prophets", "description": "Voice crying in wilderness, prepares way for Messiah"}, + {"name": "Jesus", "role": "protagonist", "faction": "Divine", "description": "Receives baptism to fulfill righteousness"}, + {"name": "Pharisees", "role": "antagonist", "faction": "Religious Elite", "description": "Religious leaders challenged by John"}, + {"name": "Sadducees", "role": "antagonist", "faction": "Religious Elite", "description": "Temple aristocracy, political collaborators"} + ], + "politics_laws_morals": [ + {"type": "moral", "name": "Repentance", "description": "Turn from sin, change of heart and direction"}, + {"type": "ritual", "name": "Baptism", "description": "Symbolic cleansing and public commitment"}, + {"type": "moral", "name": "Bearing fruit", "description": "True repentance shown through changed actions"}, + {"type": "warning", "name": "Judgment coming", "description": "Axe at root of trees, winnowing fork ready"} + ] + }, + "4": { + "title": "Temptation and First Disciples", + "summary": "Jesus tempted by Satan, begins ministry, calls first disciples", + "locations": [ + {"name": "Judean Wilderness", "type": "wilderness", "significance": "40 days of fasting and temptation"}, + {"name": "Temple Pinnacle", "type": "landmark", "significance": "Site of second temptation"}, + {"name": "High Mountain", "type": "landmark", "significance": "Where Satan shows all kingdoms"}, + {"name": "Galilee", "type": "region", "significance": "Jesus' ministry headquarters"}, + {"name": "Capernaum", "type": "city", "significance": "Jesus' home base for ministry"}, + {"name": "Sea of Galilee", "type": "body_of_water", "significance": "Where fishermen disciples are called"}, + {"name": "Syria", "type": "region", "significance": "Fame spreads, sick brought from here"} + ], + "people": [ + {"name": "Jesus", "role": "protagonist", "faction": "Divine", "description": "Overcomes temptation, begins public ministry"}, + {"name": "Satan", "role": "antagonist", "faction": "Demonic", "description": "The tempter, offers worldly power"}, + {"name": "Peter (Simon)", "role": "ally", "faction": "Disciples", "description": "Fisherman, first called disciple"}, + {"name": "Andrew", "role": "ally", "faction": "Disciples", "description": "Peter's brother, also a fisherman"}, + {"name": "James son of Zebedee", "role": "ally", "faction": "Disciples", "description": "Fisherman, one of inner circle"}, + {"name": "John son of Zebedee", "role": "ally", "faction": "Disciples", "description": "James' brother, beloved disciple"} + ], + "politics_laws_morals": [ + {"type": "moral", "name": "Scripture as defense", "description": "Jesus counters temptation with 'It is written'"}, + {"type": "moral", "name": "Man shall not live by bread alone", "description": "Spiritual sustenance over physical"}, + {"type": "moral", "name": "Do not test God", "description": "Faith doesn't demand proof through stunts"}, + {"type": "moral", "name": "Worship God alone", "description": "Reject all other powers and kingdoms"}, + {"type": "calling", "name": "Fishers of men", "description": "New purpose for disciples - gathering people"} + ] + }, + "5": { + "title": "Sermon on the Mount - Part 1", + "summary": "Beatitudes, salt and light, fulfilling the law", + "locations": [ + {"name": "Mountain in Galilee", "type": "landmark", "significance": "Site of greatest sermon, new Sinai"} + ], + "people": [ + {"name": "Jesus", "role": "protagonist", "faction": "Divine", "description": "Teacher giving new interpretation of law"}, + {"name": "Disciples", "role": "ally", "faction": "Disciples", "description": "Inner circle receiving teaching"}, + {"name": "Crowds", "role": "neutral", "faction": "Common People", "description": "Masses drawn to Jesus' teaching"}, + {"name": "Pharisees", "role": "antagonist", "faction": "Religious Elite", "description": "Referenced as example of insufficient righteousness"} + ], + "politics_laws_morals": [ + {"type": "moral", "name": "Beatitudes", "description": "Blessed are the poor in spirit, mourning, meek, hungry for righteousness, merciful, pure, peacemakers, persecuted"}, + {"type": "moral", "name": "Salt and Light", "description": "Believers must preserve and illuminate the world"}, + {"type": "law", "name": "Law fulfilled not abolished", "description": "Jesus completes the law's true meaning"}, + {"type": "moral", "name": "Righteousness exceeding Pharisees", "description": "External compliance insufficient, heart matters"}, + {"type": "moral", "name": "Anger equals murder", "description": "Heart attitude as serious as the act"}, + {"type": "moral", "name": "Lust equals adultery", "description": "Internal sin as serious as external"}, + {"type": "moral", "name": "Cut off what causes sin", "description": "Radical measures against temptation"}, + {"type": "law", "name": "Divorce only for unfaithfulness", "description": "Stricter standard than Moses allowed"}, + {"type": "moral", "name": "Let yes be yes", "description": "No oaths needed for honest people"}, + {"type": "moral", "name": "Turn the other cheek", "description": "Non-retaliation, absorb evil"}, + {"type": "moral", "name": "Love your enemies", "description": "Radical love extending to persecutors"} + ] + }, + "6": { + "title": "Sermon on the Mount - Part 2", + "summary": "Secret righteousness, Lord's Prayer, treasures in heaven", + "locations": [ + {"name": "Mountain in Galilee", "type": "landmark", "significance": "Continuation of sermon"} + ], + "people": [ + {"name": "Jesus", "role": "protagonist", "faction": "Divine", "description": "Teaching on authentic spirituality"}, + {"name": "Disciples", "role": "ally", "faction": "Disciples", "description": "Learning to pray and trust"}, + {"name": "Hypocrites", "role": "antagonist", "faction": "Religious Elite", "description": "Those who practice piety for show"} + ], + "politics_laws_morals": [ + {"type": "moral", "name": "Secret giving", "description": "Give to needy without announcement or recognition"}, + {"type": "moral", "name": "Secret prayer", "description": "Pray in private, not for public display"}, + {"type": "prayer", "name": "Lord's Prayer", "description": "Model prayer: hallowed name, kingdom come, daily bread, forgiveness, deliverance"}, + {"type": "moral", "name": "Forgive to be forgiven", "description": "Receiving forgiveness linked to giving it"}, + {"type": "moral", "name": "Secret fasting", "description": "Fast without showing it to others"}, + {"type": "moral", "name": "Treasures in heaven", "description": "Store up eternal rewards, not earthly wealth"}, + {"type": "moral", "name": "Eye as lamp", "description": "What you focus on determines your inner state"}, + {"type": "moral", "name": "Cannot serve God and money", "description": "Divided loyalty impossible"}, + {"type": "moral", "name": "Do not worry", "description": "Trust God for provision like birds and flowers"}, + {"type": "moral", "name": "Seek first the kingdom", "description": "Priority of God's kingdom, other needs follow"} + ] + }, + "7": { + "title": "Sermon on the Mount - Part 3", + "summary": "Judging others, ask-seek-knock, narrow gate, wise builder", + "locations": [ + {"name": "Mountain in Galilee", "type": "landmark", "significance": "Conclusion of sermon"} + ], + "people": [ + {"name": "Jesus", "role": "protagonist", "faction": "Divine", "description": "Concluding with warnings and wisdom"}, + {"name": "Disciples", "role": "ally", "faction": "Disciples", "description": "Called to build on solid foundation"}, + {"name": "False prophets", "role": "antagonist", "faction": "Deceivers", "description": "Wolves in sheep's clothing"}, + {"name": "Crowds", "role": "neutral", "faction": "Common People", "description": "Amazed at teaching with authority"} + ], + "politics_laws_morals": [ + {"type": "moral", "name": "Do not judge hypocritically", "description": "Remove own plank before addressing others' speck"}, + {"type": "moral", "name": "Pearls before swine", "description": "Discernment about sharing sacred things"}, + {"type": "moral", "name": "Ask, seek, knock", "description": "Persistent prayer receives answer"}, + {"type": "moral", "name": "Golden Rule", "description": "Do to others what you want done to you"}, + {"type": "moral", "name": "Narrow gate", "description": "Few find the difficult path to life"}, + {"type": "warning", "name": "False prophets known by fruit", "description": "Actions reveal true nature"}, + {"type": "warning", "name": "Not everyone saying Lord Lord", "description": "Relationship over religious activity"}, + {"type": "moral", "name": "Wise and foolish builders", "description": "Hearing and doing vs hearing only"} + ] + }, + "8": { + "title": "Healings and Authority", + "summary": "Jesus heals many, calms storm, casts out demons", + "locations": [ + {"name": "Capernaum", "type": "city", "significance": "Base of ministry, centurion's servant healed"}, + {"name": "Peter's House", "type": "building", "significance": "Peter's mother-in-law healed"}, + {"name": "Sea of Galilee", "type": "body_of_water", "significance": "Storm calmed, disciples' faith tested"}, + {"name": "Region of Gadarenes", "type": "region", "significance": "Two demon-possessed men healed, pigs drowned"} + ], + "people": [ + {"name": "Jesus", "role": "protagonist", "faction": "Divine", "description": "Demonstrates authority over sickness, nature, demons"}, + {"name": "Man with leprosy", "role": "supplicant", "faction": "Outcast", "description": "Cleansed by Jesus' touch"}, + {"name": "Roman Centurion", "role": "ally", "faction": "Roman Military", "description": "Gentile with great faith for servant's healing"}, + {"name": "Peter's mother-in-law", "role": "recipient", "faction": "Disciples' Family", "description": "Healed of fever, immediately serves"}, + {"name": "Disciples", "role": "ally", "faction": "Disciples", "description": "Fearful in storm, amazed at Jesus' power"}, + {"name": "Two demon-possessed men", "role": "recipient", "faction": "Outcast", "description": "Violent men freed from Legion of demons"}, + {"name": "Abraham, Isaac, Jacob", "role": "reference", "faction": "Patriarchs", "description": "Gentiles will feast with them in kingdom"} + ], + "politics_laws_morals": [ + {"type": "law", "name": "Show yourself to priest", "description": "Jesus respects Mosaic law for cleansing verification"}, + {"type": "moral", "name": "Faith over nationality", "description": "Centurion's faith exceeds Israel's"}, + {"type": "warning", "name": "Sons of kingdom thrown out", "description": "Jewish privilege not automatic salvation"}, + {"type": "cost", "name": "Foxes have holes", "description": "Following Jesus means homelessness"}, + {"type": "cost", "name": "Let dead bury dead", "description": "Following Jesus takes absolute priority"}, + {"type": "authority", "name": "Even winds and waves obey", "description": "Jesus has divine authority over creation"}, + {"type": "spiritual", "name": "Demons recognize Jesus", "description": "Evil spirits know who Jesus is"} + ] + }, + "9": { + "title": "More Healings and Calling of Matthew", + "summary": "Paralytic forgiven and healed, Matthew called, various healings", + "locations": [ + {"name": "Capernaum", "type": "city", "significance": "Jesus' own town, paralytic healed"}, + {"name": "Tax collector's booth", "type": "workplace", "significance": "Matthew's place of work"}, + {"name": "Matthew's house", "type": "building", "significance": "Dinner with tax collectors and sinners"}, + {"name": "Ruler's house", "type": "building", "significance": "Where dead girl is raised"}, + {"name": "Towns and villages", "type": "region", "significance": "Jesus' teaching circuit"} + ], + "people": [ + {"name": "Jesus", "role": "protagonist", "faction": "Divine", "description": "Claims authority to forgive sins"}, + {"name": "Paralytic man", "role": "recipient", "faction": "Common People", "description": "Lowered through roof, sins forgiven, then healed"}, + {"name": "Matthew/Levi", "role": "ally", "faction": "Tax Collectors", "description": "Tax collector who becomes disciple and gospel writer"}, + {"name": "Pharisees", "role": "antagonist", "faction": "Religious Elite", "description": "Question Jesus eating with sinners"}, + {"name": "John's disciples", "role": "questioner", "faction": "John the Baptist", "description": "Ask about fasting practices"}, + {"name": "Synagogue ruler", "role": "supplicant", "faction": "Religious Leaders", "description": "Daughter dies, raised by Jesus"}, + {"name": "Woman with bleeding", "role": "recipient", "faction": "Outcast", "description": "12 years suffering, healed by touching cloak"}, + {"name": "Two blind men", "role": "recipient", "faction": "Disabled", "description": "Healed according to their faith"}, + {"name": "Mute demon-possessed man", "role": "recipient", "faction": "Possessed", "description": "Speaks after demon driven out"} + ], + "politics_laws_morals": [ + {"type": "authority", "name": "Son of Man forgives sins", "description": "Jesus claims divine prerogative"}, + {"type": "moral", "name": "Healthy don't need doctor", "description": "Jesus comes for sinners, not self-righteous"}, + {"type": "moral", "name": "Mercy not sacrifice", "description": "God desires compassion over ritual"}, + {"type": "teaching", "name": "New wine in new wineskins", "description": "Jesus brings new covenant, not patching old"}, + {"type": "faith", "name": "Your faith has healed you", "description": "Faith as conduit for healing power"}, + {"type": "warning", "name": "Harvest plentiful, workers few", "description": "Need for more disciples to reach people"}, + {"type": "accusation", "name": "By prince of demons", "description": "Pharisees accuse Jesus of satanic power"} + ] + }, + "10": { + "title": "Sending Out the Twelve", + "summary": "Twelve apostles commissioned with authority and warnings", + "locations": [ + {"name": "Israel", "type": "nation", "significance": "Mission field limited to lost sheep of Israel"}, + {"name": "Towns and villages", "type": "region", "significance": "Where disciples are sent"}, + {"name": "Sodom and Gomorrah", "type": "reference", "significance": "Judgment comparison for rejecting towns"} + ], + "people": [ + {"name": "Jesus", "role": "protagonist", "faction": "Divine", "description": "Commissions and instructs the Twelve"}, + {"name": "Peter", "role": "ally", "faction": "Twelve Apostles", "description": "Listed first among apostles"}, + {"name": "Andrew", "role": "ally", "faction": "Twelve Apostles", "description": "Peter's brother"}, + {"name": "James son of Zebedee", "role": "ally", "faction": "Twelve Apostles", "description": "One of inner three"}, + {"name": "John", "role": "ally", "faction": "Twelve Apostles", "description": "Brother of James"}, + {"name": "Philip", "role": "ally", "faction": "Twelve Apostles", "description": "From Bethsaida"}, + {"name": "Bartholomew", "role": "ally", "faction": "Twelve Apostles", "description": "Also called Nathanael"}, + {"name": "Thomas", "role": "ally", "faction": "Twelve Apostles", "description": "Later known as doubter"}, + {"name": "Matthew", "role": "ally", "faction": "Twelve Apostles", "description": "The tax collector"}, + {"name": "James son of Alphaeus", "role": "ally", "faction": "Twelve Apostles", "description": "The lesser James"}, + {"name": "Thaddaeus", "role": "ally", "faction": "Twelve Apostles", "description": "Also called Judas son of James"}, + {"name": "Simon the Zealot", "role": "ally", "faction": "Twelve Apostles", "description": "Former political revolutionary"}, + {"name": "Judas Iscariot", "role": "traitor", "faction": "Twelve Apostles", "description": "The one who would betray Jesus"} + ], + "politics_laws_morals": [ + {"type": "mission", "name": "Go to lost sheep of Israel", "description": "Initial mission limited to Jews, not Gentiles or Samaritans"}, + {"type": "provision", "name": "Freely received, freely give", "description": "No payment for healing ministry"}, + {"type": "provision", "name": "Worker deserves his keep", "description": "Depend on hospitality, travel light"}, + {"type": "judgment", "name": "Shake dust off feet", "description": "Symbolic rejection of rejecting towns"}, + {"type": "warning", "name": "Sheep among wolves", "description": "Danger awaits, be shrewd as snakes"}, + {"type": "warning", "name": "Handed over to councils", "description": "Persecution by religious and civil authorities"}, + {"type": "promise", "name": "Spirit will speak through you", "description": "Divine help in persecution"}, + {"type": "warning", "name": "Family betrayal", "description": "Even family members will turn against believers"}, + {"type": "cost", "name": "Take up cross", "description": "Following Jesus means death to self"}, + {"type": "promise", "name": "Whoever receives you receives me", "description": "Disciples represent Jesus himself"} + ] + }, + "11": { + "title": "Jesus and John the Baptist", + "summary": "John's question from prison, Jesus' testimony about John, woes on unrepentant cities", + "locations": [ + {"name": "Prison", "type": "building", "significance": "Where John is held by Herod Antipas"}, + {"name": "Chorazin", "type": "city", "significance": "Unrepentant city despite miracles"}, + {"name": "Bethsaida", "type": "city", "significance": "Unrepentant city despite miracles"}, + {"name": "Capernaum", "type": "city", "significance": "Jesus' base, will be brought down to Hades"}, + {"name": "Tyre", "type": "city", "significance": "Gentile city that would have repented"}, + {"name": "Sidon", "type": "city", "significance": "Gentile city that would have repented"}, + {"name": "Sodom", "type": "reference", "significance": "Would have remained if it saw Jesus' miracles"} + ], + "people": [ + {"name": "Jesus", "role": "protagonist", "faction": "Divine", "description": "Praises John, condemns unrepentant cities"}, + {"name": "John the Baptist", "role": "ally", "faction": "Prophets", "description": "In prison, sends disciples to ask if Jesus is the one"}, + {"name": "John's disciples", "role": "messenger", "faction": "John the Baptist", "description": "Carry question to Jesus and answer back"}, + {"name": "Elijah", "role": "reference", "faction": "Prophets", "description": "John is Elijah who was to come"} + ], + "politics_laws_morals": [ + {"type": "proof", "name": "Deeds prove identity", "description": "Blind see, lame walk, dead raised - proof Jesus is Messiah"}, + {"type": "honor", "name": "Greatest born of women", "description": "John's unique role in salvation history"}, + {"type": "paradox", "name": "Least in kingdom is greater", "description": "New covenant position exceeds old"}, + {"type": "warning", "name": "Kingdom suffers violence", "description": "Forceful opposition since John's ministry"}, + {"type": "criticism", "name": "This generation like children", "description": "Neither John's asceticism nor Jesus' fellowship satisfied critics"}, + {"type": "judgment", "name": "Woe to unrepentant cities", "description": "Greater light means greater accountability"}, + {"type": "revelation", "name": "Hidden from wise, revealed to children", "description": "God's reversal of expected recipients"}, + {"type": "invitation", "name": "Come to me, all weary", "description": "Jesus offers rest from religious burden"}, + {"type": "promise", "name": "My yoke is easy", "description": "Jesus' way lighter than Pharisaic law"} + ] + } + } + } +} + +# ============================================================================= +# HELPER FUNCTIONS +# ============================================================================= + +def get_all_locations(book="matthew"): + """Extract all unique locations from a book""" + locations = [] + if book in BIBLE_TEMPLATES: + for chapter_num, chapter in BIBLE_TEMPLATES[book]["chapters"].items(): + for loc in chapter.get("locations", []): + if loc not in locations: + locations.append(loc) + return locations + +def get_all_people(book="matthew"): + """Extract all unique people from a book""" + people = {} + if book in BIBLE_TEMPLATES: + for chapter_num, chapter in BIBLE_TEMPLATES[book]["chapters"].items(): + for person in chapter.get("people", []): + if person["name"] not in people: + people[person["name"]] = person + return list(people.values()) + +def get_all_politics_laws_morals(book="matthew"): + """Extract all unique politics/laws/morals from a book""" + plm = [] + seen = set() + if book in BIBLE_TEMPLATES: + for chapter_num, chapter in BIBLE_TEMPLATES[book]["chapters"].items(): + for item in chapter.get("politics_laws_morals", []): + key = (item["type"], item["name"]) + if key not in seen: + seen.add(key) + plm.append(item) + return plm + +def get_chapter_data(book="matthew", chapter="1"): + """Get all data for a specific chapter""" + if book in BIBLE_TEMPLATES and chapter in BIBLE_TEMPLATES[book]["chapters"]: + return BIBLE_TEMPLATES[book]["chapters"][chapter] + return None + +def format_locations_for_display(locations): + """Format locations list as markdown""" + if not locations: + return "No locations defined" + lines = ["| Location | Type | Significance |", "|----------|------|--------------|"] + for loc in locations: + lines.append(f"| {loc['name']} | {loc['type']} | {loc['significance']} |") + return "\n".join(lines) + +def format_people_for_display(people): + """Format people list as markdown""" + if not people: + return "No people defined" + lines = ["| Name | Role | Faction | Description |", "|------|------|---------|-------------|"] + for person in people: + lines.append(f"| {person['name']} | {person['role']} | {person['faction']} | {person['description']} |") + return "\n".join(lines) + +def format_politics_for_display(plm): + """Format politics/laws/morals as markdown""" + if not plm: + return "No rules defined" + lines = ["| Type | Name | Description |", "|------|------|-------------|"] + for item in plm: + lines.append(f"| {item['type']} | {item['name']} | {item['description']} |") + return "\n".join(lines) + +def get_chapter_summary_md(book="matthew", chapter="1"): + """Get formatted markdown summary for a chapter""" + data = get_chapter_data(book, chapter) + if not data: + return "Chapter not found" + + md = f"## Chapter {chapter}: {data['title']}\n\n" + md += f"**Summary:** {data['summary']}\n\n" + md += "### Locations\n" + format_locations_for_display(data.get("locations", [])) + "\n\n" + md += "### People\n" + format_people_for_display(data.get("people", [])) + "\n\n" + md += "### Politics, Laws & Moral Rules\n" + format_politics_for_display(data.get("politics_laws_morals", [])) + return md + +def generate_game_elements_from_chapter(book="matthew", chapter="1"): + """Generate game config elements from a chapter""" + data = get_chapter_data(book, chapter) + if not data: + return {} + + # Convert to game config format + config_elements = { + "setting": { + "title": data["title"], + "description": data["summary"], + "locations": {loc["name"]: {"type": loc["type"], "description": loc["significance"]} + for loc in data.get("locations", [])} + }, + "characters": { + person["name"]: { + "role": person["role"], + "faction": person["faction"], + "description": person["description"] + } for person in data.get("people", []) + }, + "rules": [ + {"type": item["type"], "name": item["name"], "effect": item["description"]} + for item in data.get("politics_laws_morals", []) + ] + } + return config_elements + +# ============================================================================= +# GRADIO UI +# ============================================================================= + +def bible_story_elements(): + with gr.Tab("Using Bible as Inspiration") as story_inspiration: + gr.Markdown(""" + # Bible Story Templates for Game Config Generation + + Use these structured templates to generate game configs based on Biblical narratives. + Each chapter is broken down into **Locations**, **People**, and **Politics/Laws/Moral Rules**. + """) + + with gr.Tab("Matthew Overview"): + gr.Markdown(f"**Source:** [{BIBLE_TEMPLATES['matthew']['book_name']}]({BIBLE_TEMPLATES['matthew']['youtube_link']})") + + # Summary stats + all_locs = get_all_locations("matthew") + all_people = get_all_people("matthew") + all_plm = get_all_politics_laws_morals("matthew") + + gr.Markdown(f""" + ### Template Statistics (Chapters 1-11) + - **Locations:** {len(all_locs)} unique places + - **People:** {len(all_people)} unique characters + - **Rules/Morals:** {len(all_plm)} unique laws, morals, and political elements + """) + + with gr.Accordion("All Locations", open=False): + gr.Markdown(format_locations_for_display(all_locs)) + + with gr.Accordion("All People", open=False): + gr.Markdown(format_people_for_display(all_people)) + + with gr.Accordion("All Politics/Laws/Morals", open=False): + gr.Markdown(format_politics_for_display(all_plm)) + + with gr.Tab("Chapter Browser"): + chapter_dropdown = gr.Dropdown( + choices=[(f"Chapter {k}: {v['title']}", k) for k, v in BIBLE_TEMPLATES["matthew"]["chapters"].items()], + label="Select Chapter", + value="1" + ) + chapter_display = gr.Markdown(get_chapter_summary_md("matthew", "1")) + + chapter_dropdown.change( + fn=lambda ch: get_chapter_summary_md("matthew", ch), + inputs=[chapter_dropdown], + outputs=[chapter_display] + ) + + with gr.Tab("Generate Game Elements"): + gr.Markdown("Select a chapter to generate game config elements from its narrative structure.") + + gen_chapter = gr.Dropdown( + choices=[(f"Chapter {k}: {v['title']}", k) for k, v in BIBLE_TEMPLATES["matthew"]["chapters"].items()], + label="Select Chapter", + value="1" + ) + + def random_bible_chapter(): + """Select a random chapter from available chapters.""" + import random + chapters = list(BIBLE_TEMPLATES["matthew"]["chapters"].keys()) + return random.choice(chapters) + + with gr.Row(): + random_chapter_btn = gr.Button("🎲 Random Chapter", variant="secondary") + generate_btn = gr.Button("Generate Game Elements", variant="primary") + + output_json = gr.Code(language="json", label="Generated Game Elements") + + random_chapter_btn.click( + fn=random_bible_chapter, + outputs=[gen_chapter] + ) + + import json + generate_btn.click( + fn=lambda ch: json.dumps(generate_game_elements_from_chapter("matthew", ch), indent=2), + inputs=[gen_chapter], + outputs=[output_json] + ) + + return story_inspiration diff --git a/condition_evaluator.py b/condition_evaluator.py new file mode 100644 index 0000000000000000000000000000000000000000..dc000824812600337ed4ad50d763df8dc71cc0ae --- /dev/null +++ b/condition_evaluator.py @@ -0,0 +1,458 @@ +""" +Condition Evaluator - Logic system for game config conditions and transitions. + +Provides: +- ConditionEvaluator: Evaluate condition expressions against GameState +- TransitionResolver: Resolve dynamic transitions (random, conditional) +- EffectApplicator: Apply declarative effects to GameState +""" + +from typing import Any, Dict, List, Optional, Union +import random + +from game_state import GameState + + +class ConditionEvaluator: + """ + Evaluates condition expressions against GameState. + + Supports: + - Atomic conditions: has_item, met_person, flag, visited, mission_*, money, counter, knowledge + - Compound conditions: and, or, not + - Numeric comparisons: gte, lte, gt, lt, eq + """ + + def __init__(self, game_state: GameState): + self.state = game_state + + def evaluate(self, condition: Any) -> bool: + """ + Evaluate a condition expression. + + Args: + condition: Can be: + - None/empty dict: Always True (no condition) + - str: Flag name to check + - dict: Condition expression + + Returns: + bool: Whether condition is satisfied + """ + # No condition = always true (backwards compatible) + if condition is None or condition == {}: + return True + + # Simple string = flag check + if isinstance(condition, str): + return self.state.has_flag(condition) + + if not isinstance(condition, dict): + return False + + # Compound operators + if "and" in condition: + return all(self.evaluate(c) for c in condition["and"]) + + if "or" in condition: + return any(self.evaluate(c) for c in condition["or"]) + + if "not" in condition: + return not self.evaluate(condition["not"]) + + # Atomic conditions + return self._evaluate_atomic(condition) + + def _evaluate_atomic(self, condition: Dict) -> bool: + """Evaluate a single atomic condition.""" + + # ==================== Item Checks ==================== + if "has_item" in condition: + return self.state.has_item(condition["has_item"]) + + if "not_has_item" in condition: + return not self.state.has_item(condition["not_has_item"]) + + # ==================== Person Checks ==================== + if "met_person" in condition: + return self.state.has_met(condition["met_person"]) + + if "not_met_person" in condition: + return not self.state.has_met(condition["not_met_person"]) + + # ==================== Flag Checks ==================== + if "flag" in condition: + return self.state.has_flag(condition["flag"]) + + if "not_flag" in condition: + return not self.state.has_flag(condition["not_flag"]) + + # ==================== Location Checks ==================== + if "visited" in condition: + return self.state.has_visited(condition["visited"]) + + if "not_visited" in condition: + return not self.state.has_visited(condition["not_visited"]) + + if "discovered" in condition: + return self.state.has_discovered(condition["discovered"]) + + # ==================== Mission Checks ==================== + if "mission_complete" in condition: + return self.state.is_mission_complete(condition["mission_complete"]) + + if "mission_active" in condition: + return self.state.is_mission_active(condition["mission_active"]) + + if "mission_failed" in condition: + return self.state.is_mission_failed(condition["mission_failed"]) + + # ==================== Money Comparison ==================== + if "money" in condition: + return self._compare_numeric(self.state.money, condition["money"]) + + # ==================== Counter Comparison ==================== + if "counter" in condition: + counter_cond = condition["counter"] + for counter_name, comparison in counter_cond.items(): + value = self.state.get_counter(counter_name) + if not self._compare_numeric(value, comparison): + return False + return True + + # ==================== Knowledge Checks ==================== + if "knowledge" in condition: + return self.state.has_knowledge(condition["knowledge"]) + + if "knowledge_value" in condition: + kv = condition["knowledge_value"] + key = kv.get("key") + actual = self.state.get_knowledge(key) + if "eq" in kv: + return actual == kv["eq"] + if "neq" in kv: + return actual != kv["neq"] + return False + + # ==================== Reputation Check ==================== + if "reputation" in condition: + rep_cond = condition["reputation"] + npc = rep_cond.get("npc") + actual = self.state.get_reputation(npc) + return self._compare_numeric(actual, rep_cond) + + # ==================== Visit Count Check ==================== + if "visit_count" in condition: + vc = condition["visit_count"] + state_key = vc.get("state") + actual = self.state.get_visit_count(state_key) + return self._compare_numeric(actual, vc) + + # Unknown condition type - return False (safe default) + return False + + def _compare_numeric(self, actual: int, comparison: Any) -> bool: + """ + Evaluate numeric comparisons. + + comparison can be: + - int: exact match + - {"gte": n}: >= + - {"lte": n}: <= + - {"gt": n}: > + - {"lt": n}: < + - {"eq": n}: == + - {"neq": n}: != + """ + if isinstance(comparison, (int, float)): + return actual == comparison + + if isinstance(comparison, dict): + if "gte" in comparison: + return actual >= comparison["gte"] + if "lte" in comparison: + return actual <= comparison["lte"] + if "gt" in comparison: + return actual > comparison["gt"] + if "lt" in comparison: + return actual < comparison["lt"] + if "eq" in comparison: + return actual == comparison["eq"] + if "neq" in comparison: + return actual != comparison["neq"] + + return False + + +class TransitionResolver: + """ + Resolves transition specifications to concrete target states. + Handles deterministic, random, and conditional transitions. + """ + + def __init__(self, game_state: GameState): + self.state = game_state + self.evaluator = ConditionEvaluator(game_state) + + def resolve(self, transition: Any) -> str: + """ + Resolve a transition specification to a target state. + + Args: + transition: Can be: + - str: Direct target (current behavior, deterministic) + - dict: Complex transition spec (random, conditional) + + Returns: + str: Target state name + + Raises: + ValueError: If transition format is invalid or no condition matches + """ + # Simple string = deterministic transition (backwards compatible) + if isinstance(transition, str): + return transition + + if not isinstance(transition, dict): + raise ValueError(f"Invalid transition type: {type(transition)}") + + # Weighted random: {"random": [["state_a", 0.7], ["state_b", 0.3]]} + if "random" in transition: + return self._resolve_weighted_random(transition["random"]) + + # Equal-weight pool: {"random_from": ["a", "b", "c"]} + if "random_from" in transition: + pool = transition["random_from"] + if not pool: + raise ValueError("random_from pool is empty") + return random.choice(pool) + + # Simple conditional: {"if": condition, "then": target, "else": fallback} + if "if" in transition: + condition = transition["if"] + if self.evaluator.evaluate(condition): + then_target = transition.get("then") + if then_target: + return self.resolve(then_target) + else: + else_target = transition.get("else") + if else_target: + return self.resolve(else_target) + # If no matching branch, this is an error + raise ValueError("Conditional transition has no matching branch") + + # Chained conditions: {"conditions": [{if, then}, {if, then}, {default}]} + if "conditions" in transition: + for cond_block in transition["conditions"]: + # Default case (no condition) + if "default" in cond_block: + return self.resolve(cond_block["default"]) + # Conditional case + if "if" in cond_block and self.evaluator.evaluate(cond_block["if"]): + return self.resolve(cond_block["then"]) + # No condition matched and no default + raise ValueError("No condition matched and no default provided") + + raise ValueError(f"Unknown transition format: {transition}") + + def _resolve_weighted_random(self, weights: List) -> str: + """ + Select from weighted random options. + + Args: + weights: List of [state, probability] pairs + + Returns: + Selected state name + """ + if not weights: + raise ValueError("Weighted random list is empty") + + states = [w[0] for w in weights] + probs = [w[1] for w in weights] + + # Normalize probabilities if they don't sum to 1 + total = sum(probs) + if total <= 0: + raise ValueError("Weights must sum to positive number") + if abs(total - 1.0) > 0.001: + probs = [p / total for p in probs] + + return random.choices(states, weights=probs, k=1)[0] + + +class EffectApplicator: + """ + Applies declarative effect specifications to GameState. + + Supports: + - Items: add_item, remove_item + - Money: add_money, remove_money + - People: add_person + - Locations: add_location + - Flags: set_flag, clear_flag + - Counters: set_counter, increment, decrement + - Knowledge: set_knowledge + - Missions: start_mission, complete_mission, fail_mission + - Reputation: adjust_reputation + """ + + def __init__(self, game_state: GameState): + self.state = game_state + + def apply(self, effects: Dict) -> None: + """ + Apply a set of effects to the game state. + + Args: + effects: Dict of effect specifications + """ + if not effects: + return + + # ==================== Item Effects ==================== + if "add_item" in effects: + item = effects["add_item"] + if isinstance(item, list): + self.state.add_items(item) + else: + self.state.add_item(item) + + if "remove_item" in effects: + item = effects["remove_item"] + if isinstance(item, list): + for i in item: + self.state.remove_item(i) + else: + self.state.remove_item(item) + + # ==================== Money Effects ==================== + if "add_money" in effects: + self.state.add_money(effects["add_money"]) + + if "remove_money" in effects: + self.state.remove_money(effects["remove_money"]) + + if "set_money" in effects: + self.state.money = effects["set_money"] + + # ==================== Person Effects ==================== + if "add_person" in effects: + person = effects["add_person"] + if isinstance(person, list): + for p in person: + self.state.meet_person(p) + else: + self.state.meet_person(person) + + # ==================== Location Effects ==================== + if "add_location" in effects: + location = effects["add_location"] + if isinstance(location, list): + for loc in location: + self.state.discover_location(loc) + else: + self.state.discover_location(location) + + if "visit_location" in effects: + location = effects["visit_location"] + if isinstance(location, list): + for loc in location: + self.state.visit_location(loc) + else: + self.state.visit_location(location) + + # ==================== Flag Effects ==================== + if "set_flag" in effects: + flag = effects["set_flag"] + if isinstance(flag, list): + for f in flag: + self.state.set_flag(f, True) + elif isinstance(flag, dict): + for f, v in flag.items(): + self.state.set_flag(f, v) + else: + self.state.set_flag(flag, True) + + if "clear_flag" in effects: + flag = effects["clear_flag"] + if isinstance(flag, list): + for f in flag: + self.state.clear_flag(f) + else: + self.state.clear_flag(flag) + + if "toggle_flag" in effects: + flag = effects["toggle_flag"] + if isinstance(flag, list): + for f in flag: + self.state.toggle_flag(f) + else: + self.state.toggle_flag(flag) + + # ==================== Counter Effects ==================== + if "set_counter" in effects: + for name, value in effects["set_counter"].items(): + self.state.set_counter(name, value) + + if "increment" in effects: + for name, amount in effects["increment"].items(): + self.state.increment_counter(name, amount) + + if "decrement" in effects: + for name, amount in effects["decrement"].items(): + self.state.decrement_counter(name, amount) + + # ==================== Knowledge Effects ==================== + if "set_knowledge" in effects: + for key, value in effects["set_knowledge"].items(): + self.state.update_knowledge(key, value) + + if "remove_knowledge" in effects: + key = effects["remove_knowledge"] + if isinstance(key, list): + for k in key: + self.state.remove_knowledge(k) + else: + self.state.remove_knowledge(key) + + # ==================== Mission Effects ==================== + if "start_mission" in effects: + mission = effects["start_mission"] + if isinstance(mission, str): + self.state.start_mission(mission) + elif isinstance(mission, dict): + for m_id, m_data in mission.items(): + self.state.start_mission(m_id, m_data if isinstance(m_data, dict) else None) + elif isinstance(mission, list): + for m in mission: + self.state.start_mission(m) + + if "complete_mission" in effects: + mission = effects["complete_mission"] + if isinstance(mission, list): + for m in mission: + self.state.complete_mission(m) + else: + self.state.complete_mission(mission) + + if "fail_mission" in effects: + mission = effects["fail_mission"] + if isinstance(mission, list): + for m in mission: + self.state.fail_mission(m) + else: + self.state.fail_mission(mission) + + if "update_mission" in effects: + for mission_id, updates in effects["update_mission"].items(): + self.state.update_mission(mission_id, updates) + + # ==================== Reputation Effects ==================== + if "adjust_reputation" in effects: + for npc, change in effects["adjust_reputation"].items(): + self.state.adjust_reputation(npc, change) + + if "set_reputation" in effects: + for npc, value in effects["set_reputation"].items(): + self.state.npc_reputation[npc] = value diff --git a/config_dev_assistance_reference.md b/config_dev_assistance_reference.md new file mode 100644 index 0000000000000000000000000000000000000000..d4b3d8d747418ad48e1fe30fb2c1e1918633567c --- /dev/null +++ b/config_dev_assistance_reference.md @@ -0,0 +1,273 @@ +# Config Development Assistance - Reference Guide + +> End Goal for this tab is to be able to make Story based games using mechanics based games (Chess, Poker, Sudoku, Rubiks Cube, etc.) as a conduit + +## Core Principles + +- Sometimes a fun game includes unfairness - Elden Ring (Controls and movement restrictions), Boss fights in every game +- Jonas Tyroller (https://youtu.be/o5K0uqhxgsE) - This problem changes your perspective on game dev - minimise the cost of exploration so you can explore more (17:00) | dont make the same game again but worse. How can we measure worse? (Negative Reviews (Eg. Steam / Play or Apple Store / Game Journalists) / Reddit posts with complaints / Youtube Video Analysis Conclusions) +- Every theme in the world can be turned into a battle story as that is how resource accumulation works --> Resources are normally hard to find and therefore scatter across a map +- Ending Conditions --> Main Villain Antagonist Themes --> Travel, Politics and Combat needed to reach ending --> Characters + +--- + +## 1. Some Ideas About Games Fundamentals + +- Is the universal rule as something done in less time is always harder and sought after +- Games are ruled by one rule that determines win / loss (personal objective / competition). The main rule comes from the environment of the game eg. soccer = field + players + ball + marked goals and only then main rule is when ball goes into goal then goal scored. After that extra rules try keep the game interesting and sustainable eg. offside, deadballs, cards +- **Tangible environments** (mainly real life) +- **Intangible environments** (void / in your head / fictional places) +- Fun = Avoiding Negatives and experiencing all content. Need a list of negative things that can happen +- A game is alive when the same decisions wont work over and over - this means the game is designed to have the user answer all options and then have random choices get random answers +- Good environments for randomness = anything with a queue like traffic or even the weather (rain / flood chance) - lead to player being overprepared for some situations and underprepared at the same time for others + +--- + +## 2. Simple with Consequences and 'Fun' Built + +- Ask for x stories reached through x decision points +- **Prompt 1** - Is there a way to tell 8 stories from the same opening using 2 decision points to reach them +- **Prompt 2** - For each verb item list things a normal person wouldn't want to happen + +--- + +## 3. Simple Structure + +> AI is good walking a tightrope but not setting up the tightrope. Structure in final format (JSON / Twee) --> current prompt: what would this config look like in Twee? + +### 3.1 Generator +- A long game is a bunch of short games +- Click the button to generate a random timeline and story based on UI elements and story events. Ask an LLM to use this to write a story around + +--- + +## 4. More Robust + +- The main idea is locations + Personal Relationships + bystanders = foundation to write game mechanics into eg. random bystander = candidate for a DLC / sequel story and can be rewritten as character acquaintances / relatives +- Conflict Ideas - in Overengineered Tab + +--- + +## 5. Overengineered + +> Current [Timeline] Workflow = [Precreation] Mermaid Diagram to (1) Story to (2) [Creation] Initial JSON (through LLM and fix JSON by hand) to (3) [Postcreation] JSON Corrections (through LLM and fix JSON by hand) to (4) Media prompts to (5) Asset Generation to (6) JSON Media field population + +### 5.1 Pre - Qualitative Considerations + +- Ending Conditions --> Main Villain Antagonist Themes --> Travel, Politics and Combat needed to reach ending --> Characters +- Modern game commonalities = travel, 'politics' (cheatcode is related family members in high positions) (personal relationship effects on the world) and combat (physical or verbal eg. negotiation) +- Politics within a group origins = Easiest to understand interpersonal issues = betrayal and lack of sympathy, unfair expectations (Oblivious Decision making) or misinterpretation of actions under these categories +- A game is about conveying an experience to the user - this is why media is heavily used +- Need to learn how rules (our preferences) affect the config creation process. eg. core game loop - Write a story where the plot only moves based on - [insert core game loop] + +#### 5.1.1 Ending as the Start + +- The issue with a comprehensive game is that decisions with consequences can be made therefore as many endings as possible so one ending as the start is an incorrect approach + +**Prompts:** +- "Analyse the following statement and provide 50 ideas for good starting points to develop from - A good ending is predictable in shape from the beginning of the story but the finer details hidden till the very end" (Tested with o1) +- "List 30 stereotypical story endings" (Tested with groq llama-3.3-70B-specdec) + +**How to turn an ending into an intro:** +- What is the furthest away point from this that the main character can be? + +#### 5.1.2 In Between the Ending and the Intro + +- Travel, Politics and Combat needed to reach ending and Character development + +#### 5.1.3 Review / Parody as the Start + +- For a story Movies are a better source of inspiration for structures you can use (There's way more popular movies than popular games) - Screen Junkies (For Cliches Exposure) / Cinema Sins (For plotholes/inconsistencies to avoid) + +#### 5.1.4 Game/Story Wiki Contents Pages + +- Game/Story Wiki Contents pages are normally long lists so they help you to see how many lists you need and the length of the lists to give an idea / reference for whether you need more or less complexity + +#### 5.1.5 Media as Inspiration + +- Models like QVQ-72B-Preview can turn any picture into a list of potential scenes the image can represent. Need to find a way for LLMs to judge melodies and songs +- Some Songs I would like to be able base a game around - Tank and Various = If you Dream, Michael Buble = Home + +#### 5.1.6 Main Parts of Stories to Put into Structures + +**Purpose (The types of decisions you want the user to make) (Main Theme):** +- Unpredictable Nuances (Any movie) / Simulation (eg. Work - mixing rare industry crossover) (eg. A phrasebook into interactive game) / Time capsule / Tangible Metaphor / Song Concept as game / Advert as a game +- References: https://en.wikipedia.org/wiki/Philosophy, https://en.wikipedia.org/wiki/Moral_injury + +**Conflicts:** +- Most stories are based around conflicts + +**Setting:** +- Reference: https://en.wikipedia.org/wiki/History#Periods + +**Locations:** +- Jungle, Sea, Desert, Snow, City, Village, Space + +**Character Relations:** +- Friend or foe + +**Character Archetypes:** +- (To be expanded) + +**Additional Themes:** +- (To be expanded) + +#### 5.1.7 Generalized Inspiration from Existing Games + +- Random Scenario / Song to 'full game' manual or auto is end goal +- Components (outside Code Support for Config): Decisions (and context explanation), Nested Sections, Media (Especially to affect decisions), Replayability (GTA and Tekken type mechanics in text form), Theme integration (Modified Variables that affect UI or config order) +- Existing Games eg. GTA Heists - Same Map with overlapping branching narratives, Battlefront - Elites amongst Commoners, Tekken Casino (one mistake = 1/2 or 1/3 of your Resources) and Turn based: 'Tactics' type nintendo games, Chess (and any other tile based game) +- Existing Game Rules for text - Cyberpunk RED +- Community playthrough = Tally of players choices, Random item placed in a random location - first person to get it wins, Survival by location or characters met +- Some Kinds of game skeletons ideas - Timelines, Graph as State machine paths, Economy ecosystem + +**Structural Inspirations:** +- GTA Heists - Replayability and stakes +- Tekken - 2/3 mistakes = lost round +- Elden Ring - Story telling by traversal of map +- Sports Scores + +**Test Prompt for Models:** +> Please make 10 python lists for the types of media files and their purposes in a game and then use those lists to random generate a timeline of 20 items when the function is called. Great next suggest ways to improve this function to create better timelines + +#### 5.1.8 Worldbuilding Tools + +- Good worldbuilding makes new scenarios automatic +- In a game awareness = directly affect one of the characters - Any time fights are involved, paralympics categorisations can be inspiration (watched paralympics and wanted to learn the categories eg. s10) + +**Using Law as inspiration:** +- SA Constitution +- SA Companies Act + +#### 5.1.9 Concept Combination Brainstorm + +- The story and the gameplay don't have to occur at the same time +- A timeline is the most important part of the story - once that is set you can do anything? +- Media Critiques (eg. Youtube Rants) as Prompts to whole games as interactive explanation + +--- + +### 5.2 Mermaid Diagram / Structural Considerations to Story + +#### 5.2.1 HF Datasets as Scenario Inspiration + +- https://huggingface.co/datasets/thesven/gsm8k-reasoning +- https://huggingface.co/datasets/HuggingFaceFW/fineweb +- HuggingFaceFW/fineweb-edu +- https://huggingface.co/datasets/HuggingFaceFV/finevideo +- https://huggingface.co/spaces/HuggingFaceFV/FineVideo-Explorer +- Translations: https://huggingface.co/datasets/opencsg/chinese-fineweb-edu + +#### 5.2.2 Structure - (Non/)Linear Storylines + +> Trying to abstract the process into one workflow is beyond me so multiple paths to goal (config) is the aim now + +**Branching - Decisions / Timeline Creation:** +- Structures for interesting timeline progression +- Claude Artifacts to illustrate nested structure brainstorms: + - https://claude.site/artifacts/4a910d81-1541-49f4-8531-4f27fe56cd1e + - https://claude.site/artifacts/265e9242-2093-46e1-9011-ed6ad938be90?fullscreen=false +- Consideration: Story from the perspective of Main character or NPC in the LLM generated story + +**Linear - Existing Media eg. Songs and Screenshots:** +- Media position in the story part: beginning, during or end +- Create media first and then ask LLM to make join points in the config. For images need to add the location to every prompt or images will be in random locations + +**Linear - Machine Learning Architectures as Game Maps:** +- Transformers, SSMs, Image and Video Generation Architectures, GANs, RNNs, etc. + +**Linear - Prompt Engineering as Basis for Ideation:** +- Current Assisted workflow idea: Story timeline events suggestions (LLM / Premade List) | Merging events with premade mermaid structures (LLM + Story Text + Mermaid Text) | Edit mermaid till satisfied (LLM + Story Text) | Ask LLM to convert to config (LLM + JSON Text) | Edit config (LLM / User with format assistance or not) | Playtest and go back to mermaid or config if there are problems +- Interactive movie (UI interaction or no progress) vs Branching Paths (Maze) +- Things that can change the workflow: Asset First (Make Asset and make the transitions using LLM), Export First (Custom JS config, Playcanvas, Unreal Engine reverse engineered to this spaces config?) Game Mechanics First (eg. Player Stats, Inventory and NPCS not implemented yet, so traversal type games best aka graph like structures) +- Config writing = Remix old one, Ask LLM to make one, Endless combination testing using the prompt engineering above or writing by hand (prompt engineering on yourself) +- Can use song lyrics as thematic source + +**Branching - Network Analysis to Game Config:** +- Placeholder for analysing multiple stories for their network structures and creating general rules for a structure generator based on named entity recognition and bias to locations or people - The extreme long way + +**Linear - Chess PNG to Game Config:** +- Any Chess match can serve as end of game final battle + +**Note:** Below 70B (April 2024) seem to struggle here + +--- + +### 5.3 Config Creation + +> Can use Mermaid scaffold or not + +**AI Generates Scaffolds - Notes:** +- 8b size models make Format mistakes. For extension purposes this step specifically might have to be function call only if you explain the tool can take as many blocks as necessary + +**Extension Ideas:** +- The basic idea is just make a new story and fight the JSON issues to make them fit and then rinse and repeat +- Extension = any description in original JSON as the theme using the prompt template +- Ask for incorporation of mermaid structure into JSON or ask for how the mermaid structure would change the JSON / story + +--- + +### 5.4 Post-creation + +> Assumed story is complete at this stage / will be continued in separate config + +#### 5.4.1 Initial Media Generation +- Placeholder for verb extraction from description and choices - to help guide initial media population +- To do a full test need media - Fast and quality: https://huggingface.co/spaces/black-forest-labs/FLUX.1-schnell + +#### 5.4.2 JSON Corrections + +**Prompt:** "Let's critique this JSON to find areas to fix" + +#### 5.4.3 JSON to Media Prompts + +- This Step specifically has to be function call only +- Gemma-9b and Mistral 8x7b is better at this prompt than llama 3.1 8b and 70b. Can add (each media field must get an entry) and (in python list of list format for plug and play) but they affect final output + +**Prompt:** "Let's make a list for the prompts we will use to make media objects in this JSON. Make one for a person to interpret and one for direct media generators that focus on keywords:" + +#### 5.4.4 Media Prompts to Asset Generation to JSON Population + +- This Step specifically has to be function call only +- This Step can be merged with the next if we can make an editor like in the semi-Auto space in test and edit tailored to just accepting the JSON and exposing only media part for editing + +**Prompts:** +- "For each Media item described classify it by media type and comment if in a story setting it would need timing" +- "Here is a list of file names - assume they are in the order of the empty media sections of the JSON and rewrite the JSON" + +#### 5.4.5 Quick Ways to Evaluate Current Config + +**Prompt to use with SOTA LLMs:** +> This config is for a basic text based game engine. I don't have any structural metrics to assess the quality of the config. What JSON things can we look at to see if it may be too bland for a person testing the game? + +**Original Claude 3.5 Sonnet Response snippets:** + +1. **Limited state variety:** With only 13 states across 5 locations, the game might feel short and lacking in diversity. Consider adding more locations or states within existing locations. +2. **Low average choices:** An average of 1.92 choices per state might make the game feel linear. Increasing the number of choices in more states could improve player engagement. +3. **Limited consequences:** Only 3 states have consequences, which might make player choices feel less impactful. Adding more consequences could increase the sense of agency. +4. **Short descriptions:** The average description length of 13.15 words might not provide enough detail to immerse players. Consider expanding descriptions to create a richer narrative. +5. **Lack of media:** No states currently use media elements, which could make the game feel less engaging. Adding images, sound effects, or other media could enhance the player experience. +6. **Limited narrative branching:** While there are some loops and choices, the overall structure is relatively linear. Adding more branching paths could increase replayability and player interest. + +**To make the game less bland, consider:** +- Adding more states and locations +- Increasing the number of choices in each state +- Implementing more consequences for player actions +- Expanding descriptions to create a richer narrative +- Incorporating media elements +- Creating more diverse paths through the game + +#### 5.4.6 Improvement of the Default Config + +- Example of how to advance a game config with LLM - end goal is to have automatic workflow that takes these considerations into account +- Things missing from the game engine - Economics and Basic Politics (NPC affiliation) + +--- + +## 6. Translated/Transposed Mechanics + +- Interactive Fun (one form) = Avoiding something you don't want to happen in as dramatic fashion as possible = in '1D' format that can mean avoiding deadends and if you stack stories on stories the deadends can pile up +- 1D games fun = ??? (Haven't figured it out yet) same as a navigation game? Reaching the end is the current end aka the game is essentially a number password and wrong input means a deadend where the full password = full story - chained stories with deadends = same missing back story in a long story = a punishment to some and a reward to others +- Ideas for options based on existing 2D / 3D games +- Most game mechanics = button press + animation == in 1D animations = text description / picture +- Tekken - Kick and Punch in extreme detail + complex input buttons diff --git a/config_dev_assistance_variables.md b/config_dev_assistance_variables.md new file mode 100644 index 0000000000000000000000000000000000000000..d5f1eef26d5b03f892a454041a2888da100868df --- /dev/null +++ b/config_dev_assistance_variables.md @@ -0,0 +1,3062 @@ +# Additional Reference Content (Variables) + +This file contains the content from gr.Textbox, gr.Code variables used in Config Development Assistance. + +--- + +## Simple Structure - Example Process + +### SimpleStructureTimeline1stPrompt +``` +Here is a config skeleton to turn into a story that ends with: The villain gets their comeuppance and is punished for their crimes. + +{ + "masterlocation1": { + "location1": { + "description": "UI - Character Model Viewer", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": [], + "developernotes": [] + }, + "location2": { + "description": "UI - Crafting Progress Bar", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location3" + }, + "media": [], + "developernotes": [] + }, + "location3": { + "description": "UI - Scoreboard", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location4" + }, + "media": [], + "developernotes": [] + }, + "location4": { + "description": "Story: overcomeLoss", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location5" + }, + "media": [], + "developernotes": [] + }, + "location5": { + "description": "Story: formAlliance", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location6" + }, + "media": [], + "developernotes": [] + }, + "location6": { + "description": "Story: makeDecision", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location7" + }, + "media": [], + "developernotes": [] + }, + "location7": { + "description": "Story: exploreLocation", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location8" + }, + "media": [], + "developernotes": [] + }, + "location8": { + "description": "UI - World Map", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location9" + }, + "media": [], + "developernotes": [] + }, + "location9": { + "description": "UI - Crafting Recipe List", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location10" + }, + "media": [], + "developernotes": [] + }, + "location10": { + "description": "Story: faceChallenge", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location11" + }, + "media": [], + "developernotes": [] + }, + "location11": { + "description": "Story: experienceSetback", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location12" + }, + "media": [], + "developernotes": [] + }, + "location12": { + "description": "UI - Skill Path Highlight", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location13" + }, + "media": [], + "developernotes": [] + }, + "location13": { + "description": "Story: makeDiscovery", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location14" + }, + "media": [], + "developernotes": [] + }, + "location14": { + "description": "UI - System Messages", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location15" + }, + "media": [], + "developernotes": [] + }, + "location15": { + "description": "Story: meetCharacter", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location16" + }, + "media": [], + "developernotes": [] + }, + "location16": { + "description": "Story: engageBattle", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location17" + }, + "media": [], + "developernotes": [] + }, + "location17": { + "description": "Story: unlockAbility", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location18" + }, + "media": [], + "developernotes": [] + }, + "location18": { + "description": "UI - Quest Status Indicators", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location19" + }, + "media": [], + "developernotes": [] + }, + "location19": { + "description": "UI - Mini-Map", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location20" + }, + "media": [], + "developernotes": [] + }, + "location20": { + "description": "UI - Quick Equip Slots", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "end" + }, + "media": [], + "developernotes": [] + }, + "end": { + "description": "The adventure ends here.", + "choices": [ + "restart" + ], + "transitions": { + "restart": "location1" + } + } + } +} + +``` + +### Outputof1stSimpleStructurePrompt +``` +{ + "masterlocation1": { + "location1": { + "description": "UI - Character Model Viewer: The hero prepares for the journey.", + "events": ["intro"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": ["hero_model"], + "developernotes": ["Introduce the hero."] + }, + "location2": { + "description": "UI - Crafting Progress Bar: The hero crafts essential tools.", + "events": ["craftItem"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location3" + }, + "media": ["crafting_ui"], + "developernotes": ["Player learns crafting."] + }, + "location3": { + "description": "UI - Scoreboard: The hero reflects on previous challenges.", + "events": ["reflect"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location4" + }, + "media": ["scoreboard"], + "developernotes": ["Introduce past successes and failures."] + }, + "location4": { + "description": "Story: Overcome the loss of an ally during a surprise attack.", + "events": ["overcomeLoss"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location5" + }, + "media": ["battle_scene"], + "developernotes": ["Set up stakes with personal loss."] + }, + "location5": { + "description": "Story: Form an alliance with unlikely allies.", + "events": ["formAlliance"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location6" + }, + "media": ["alliance_image"], + "developernotes": ["Highlight camaraderie."] + }, + "location6": { + "description": "Story: Make a critical decision about pursuing the villain.", + "events": ["makeDecision"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location7" + }, + "media": ["decision_ui"], + "developernotes": ["Player chooses pursuit strategy."] + }, + "location7": { + "description": "Story: Explore the villains hideout for clues.", + "events": ["exploreLocation"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location8" + }, + "media": ["hideout_map"], + "developernotes": ["Introduce villains presence."] + }, + "location8": { + "description": "UI - World Map: The path to confront the villain is charted.", + "events": ["chartPath"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location9" + }, + "media": ["world_map"], + "developernotes": ["Provide player overview."] + }, + "location9": { + "description": "UI - Crafting Recipe List: Finalize preparations for battle.", + "events": ["prepareGear"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location10" + }, + "media": ["recipes_ui"], + "developernotes": ["Final crafting before the climax."] + }, + "location10": { + "description": "Story: Face the villains henchmen in a challenging fight.", + "events": ["faceChallenge"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location11" + }, + "media": ["battle_ui"], + "developernotes": ["Player engages in combat."] + }, + "location11": { + "description": "Story: Experience a setback as the villain escapes.", + "events": ["experienceSetback"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location12" + }, + "media": ["setback_ui"], + "developernotes": ["Raise tension and stakes."] + }, + "location12": { + "description": "UI - Skill Path Highlight: Unlock a hidden ability to counter the villain.", + "events": ["unlockAbility"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location13" + }, + "media": ["skill_ui"], + "developernotes": ["Introduce player growth."] + }, + "location13": { + "description": "Story: Discover a critical weakness in the villains plan.", + "events": ["makeDiscovery"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location14" + }, + "media": ["discovery_scene"], + "developernotes": ["Hint at victory."] + }, + "location14": { + "description": "UI - System Messages: Allies send reinforcements.", + "events": ["callReinforcements"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location15" + }, + "media": ["system_ui"], + "developernotes": ["Build momentum for final battle."] + }, + "location15": { + "description": "Story: Confront the villain in their lair.", + "events": ["meetCharacter", "engageBattle"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location16" + }, + "media": ["final_battle"], + "developernotes": ["Player confronts villain."] + }, + "location16": { + "description": "Story: Defeat the villain and see their crimes revealed.", + "events": ["revealTruth", "punishVillain"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_end" + }, + "media": ["villain_defeat"], + "developernotes": ["Resolution of story."] + }, + "end": { + "description": "The villain gets their comeuppance and is punished for their crimes. Justice is served.", + "choices": ["restart"], + "transitions": { + "restart": "masterlocation1_location1" + }, + "media": ["justice_scene"], + "developernotes": ["Satisfying conclusion."] + } + } +} + + +``` + +--- + +## More Robust - Example Workflow + +### MoreRobustTimeline1stPrompt +``` +Please integrate this story into the format below - + +Story info - + + --- PASTE More Robust JSON here --- + +Format Info - + +JSON format = Please use the below config as a template to write a story in config format (dont use the template values like masterlocation1): + +{ + "masterlocation1": { + "location1": { + "description": "UI - Experience Bar", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": [], + "developernotes": [] + }, + + +Loading Code (Dont make media for now) (Avoid Syntax errors when writing the config so the parser loads it) = +Main error to avoid = Error loading custom configuration: too many values to unpack (expected 2) + +class GameSession: + def __init__(self, starting_location='village', starting_state='start'): + self.player = Player() + self.current_location = starting_location + self.current_state = starting_state + self.game_log = [] + + def make_choice(self, choice_index): + state = all_states[self.current_location][self.current_state] + if 0 <= choice_index < len(state['choices']): + choice = state['choices'][choice_index] + next_state = state['transitions'][choice] + + self.game_log.append(f"You chose: {choice}") + self.game_log.append(state['description']) + + if 'consequences' in state and choice in state['consequences']: + if state['consequences'][choice]: + state['consequences'][choice](self.player) + else: + # Handle empty consequence, e.g., log a message or provide a default action + print(f"No consequence for choice: {choice}") + # You can add any default action here if needed + + if '_' in next_state: + self.current_location, self.current_state = next_state.split('_') + else: + self.current_state = next_state + + return self.get_current_state_info() + else: + return "Invalid choice. Please try again." + + def get_current_state_info(self): + state = all_states[self.current_location][self.current_state] + choices = [f"{idx + 1}. {choice}" for idx, choice in enumerate(state['choices'])] + return state['description'], choices, " +".join(self.game_log) + + def get_current_state_media(self): + media = all_states[self.current_location][self.current_state]['media'] + return media + +``` + +### Outputof1stMoreRobustJSONcreator +``` +{ + "steps": [ + { + "step_number": 1, + "location": "The Fiery Caverns Beneath a Sleeping Volcano", + "characters": [ + "Orin", + "Voss" + ], + "interpersonal_event": "They part on bitter terms, trust erodes further", + "relationship_change": -2, + "modification_to_move_forward": "Resolve a longstanding family feud", + "third_party_role": "A beggar listening quietly in the shadows", + "narrative_text": "Step 1: In The Fiery Caverns Beneath a Sleeping Volcano, Orin and Voss experience the following: They part on bitter terms, trust erodes further. Observing from the sidelines: A beggar listening quietly in the shadows. They then proceed to: Resolve a longstanding family feud." + }, + { + "step_number": 2, + "location": "A Haunted Marsh Shrouded in Fog", + "characters": [ + "Orin", + "Voss" + ], + "interpersonal_event": "A surprising confession that changes everything", + "relationship_change": 1, + "modification_to_move_forward": "Recover from a crippling setback", + "third_party_role": "A beggar listening quietly in the shadows", + "narrative_text": "Step 2: In A Haunted Marsh Shrouded in Fog, Orin and Voss experience the following: A surprising confession that changes everything. Observing from the sidelines: A beggar listening quietly in the shadows. They then proceed to: Recover from a crippling setback." + }, + { + "step_number": 3, + "location": "A Remote Mountain Village", + "characters": [ + "Tali", + "Voss" + ], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1, + "modification_to_move_forward": "Escape a dangerous situation unscathed", + "third_party_role": "A curious child fascinated by adventurers", + "narrative_text": "Step 3: In A Remote Mountain Village, Tali and Voss experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: A curious child fascinated by adventurers. They then proceed to: Escape a dangerous situation unscathed." + }, + { + "step_number": 4, + "location": "The Secret Hideout Beneath a Ruined Fort", + "characters": [ + "Nyx", + "Orin" + ], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1, + "modification_to_move_forward": "Acquire a powerful ancient artifact", + "third_party_role": "An inquisitive guard, always seeking the truth", + "narrative_text": "Step 4: In The Secret Hideout Beneath a Ruined Fort, Nyx and Orin experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: An inquisitive guard, always seeking the truth. They then proceed to: Acquire a powerful ancient artifact." + }, + { + "step_number": 5, + "location": "The Fiery Caverns Beneath a Sleeping Volcano", + "characters": [ + "Orin", + "Tali" + ], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1, + "modification_to_move_forward": "Resolve a longstanding family feud", + "third_party_role": "A local gossip eager to spread rumors", + "narrative_text": "Step 5: In The Fiery Caverns Beneath a Sleeping Volcano, Orin and Tali experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: A local gossip eager to spread rumors. They then proceed to: Resolve a longstanding family feud." + } + ], + "relationships": { + "Orin|Voss": -1, + "Tali|Voss": 1, + "Nyx|Orin": 1, + "Orin|Tali": 1 + }, + "chosen_ending": "A New Era Dawns with Cautious Hope" +} + + +``` + +### Outputof1stMoreRobustPrompt +``` +{ + "FieryCaverns": { + "step1": { + "description": "Step 1: In The Fiery Caverns Beneath a Sleeping Volcano, Orin and Voss experience the following: They part on bitter terms, trust erodes further. Observing from the sidelines: A beggar listening quietly in the shadows. They then proceed to: Resolve a longstanding family feud.", + "events": [ + { + "characters": ["Orin", "Voss"], + "interpersonal_event": "They part on bitter terms, trust erodes further", + "relationship_change": -2 + }, + { + "third_party_role": "A beggar listening quietly in the shadows" + } + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "HauntedMarsh_step2" + }, + "media": [], + "developernotes": [ + "modification_to_move_forward: Resolve a longstanding family feud" + ] + }, + "step5": { + "description": "Step 5: In The Fiery Caverns Beneath a Sleeping Volcano, Orin and Tali experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: A local gossip eager to spread rumors. They then proceed to: Resolve a longstanding family feud.", + "events": [ + { + "characters": ["Orin", "Tali"], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1 + }, + { + "third_party_role": "A local gossip eager to spread rumors" + } + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "Ending_final" + }, + "media": [], + "developernotes": [ + "modification_to_move_forward: Resolve a longstanding family feud" + ] + } + }, + "HauntedMarsh": { + "step2": { + "description": "Step 2: In A Haunted Marsh Shrouded in Fog, Orin and Voss experience the following: A surprising confession that changes everything. Observing from the sidelines: A beggar listening quietly in the shadows. They then proceed to: Recover from a crippling setback.", + "events": [ + { + "characters": ["Orin", "Voss"], + "interpersonal_event": "A surprising confession that changes everything", + "relationship_change": 1 + }, + { + "third_party_role": "A beggar listening quietly in the shadows" + } + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "RemoteMountainVillage_step3" + }, + "media": [], + "developernotes": [ + "modification_to_move_forward: Recover from a crippling setback" + ] + } + }, + "RemoteMountainVillage": { + "step3": { + "description": "Step 3: In A Remote Mountain Village, Tali and Voss experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: A curious child fascinated by adventurers. They then proceed to: Escape a dangerous situation unscathed.", + "events": [ + { + "characters": ["Tali", "Voss"], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1 + }, + { + "third_party_role": "A curious child fascinated by adventurers" + } + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "SecretHideout_step4" + }, + "media": [], + "developernotes": [ + "modification_to_move_forward: Escape a dangerous situation unscathed" + ] + } + }, + "SecretHideout": { + "step4": { + "description": "Step 4: In The Secret Hideout Beneath a Ruined Fort, Nyx and Orin experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: An inquisitive guard, always seeking the truth. They then proceed to: Acquire a powerful ancient artifact.", + "events": [ + { + "characters": ["Nyx", "Orin"], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1 + }, + { + "third_party_role": "An inquisitive guard, always seeking the truth" + } + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "FieryCaverns_step5" + }, + "media": [], + "developernotes": [ + "modification_to_move_forward: Acquire a powerful ancient artifact" + ] + } + }, + "Ending": { + "final": { + "description": "A New Era Dawns with Cautious Hope", + "events": [], + "choices": [], + "transitions": {}, + "media": [], + "developernotes": [ + "Final relationships:", + "Orin|Voss = -1", + "Tali|Voss = 1", + "Nyx|Orin = 1", + "Orin|Tali = 1" + ] + } + } +} + + +``` + +### OutputMoreRobustTimeline2ndPrompt +``` +Here are some ideas and best practices for making your story more believable and immersiveand then weaving in meaningful, player-driven activities at each location: + +--- + +## 1. Deepen Character Motivations + +1. **Flesh Out Personal Motivations** + - Each major character (Orin, Voss, Tali, Nyx) should have something they stand to gain or lose at each location. For instance, maybe Voss is haunted by a personal failure at the Haunted Marsh, so a surprising confession ties directly into that trauma. + - Even side characters (like the beggar, gossip, inquisitive guard, etc.) can have small yet relevant reasons for being present that feed into the worlds coherence. + +2. **Connect Past Conflicts and Future Goals** + - Orin and Voss already have deteriorating trust after the fiery caverns. Show how that tension carries over into the Marsh. Maybe they speak in half-truths or withhold critical information to avoid vulnerability. + - Tali may be traveling for a personal quest, such as searching for missing siblings. If that family feud is relevant, it can appear in subtle ways (e.g., she overhears gossip or sees a crest in the Hideout that ties to her clan). + +--- + +## 2. Align Environment and Events + +1. **Location-Specific Challenges** + - **Fiery Caverns Beneath a Sleeping Volcano**: Extreme heat, magma flows, or trembling ground force characters to make quick decisions or reveal their personalities in high-stress situations. This environment itself can be the cause of tension or unity. + - **Haunted Marsh Shrouded in Fog**: Low visibility or ghostly apparitions can cause paranoia, spurring confessions or alliances. Perhaps the Marsh is known for revealing illusions of a characters past regrets, fueling pivotal story revelations. + +2. **Tie Plot to the Locations Lore** + - When you say they must recover from a crippling setback in a Marsh, maybe the Marshs local legend is about lost travelers or a hidden cure. This gives the story event a narrative reason for happening *there*. + - If the Ruined Fort has rumors about a powerful artifact, it justifies why Nyx and Orin would forge an alliance to seek it in that very place. + +--- + +## 3. Interweave Third-Party Roles and Consequences + +1. **Third-Party Observers With Agency** + - A beggar who is quietly listening might later use that overheard information for blackmail or might warn someone else of the characters plans. Dont let them vanish without consequence. + - The curious child in the village might become an unexpected messenger, revealing the partys presence to foes or letting them in on a local secret. + +2. **NPC Network** + - Give each onlooker or side character a small web of connections to the main cast or the environment. This maintains continuity and a sense that the world reacts to the players/characters actions. + +--- + +## 4. Introduce Player Agency in Each Location + +Right now, the story steps are fairly linear. You can expand player options by designing activities that support or contrast the main narrative: + +1. **Side Quests / Sub-Activities** + - **Fiery Caverns**: + - **Gather Rare Materials**: The player can explore small side tunnels to collect rare volcanic crystals for trading or forging. This quest can reveal more about Orins or Talis resourcefulness and the regions culture. + - **Assist the Beggar**: The beggar might offer a cryptic tip in exchange for help finding safer passage. + + - **Haunted Marsh**: + - **Marsh Trials**: Fog-based illusions make the player confront personal regrets, which might tie into Orins or Vosss confessions. + - **Local Fishing or Foraging**: A calm moment to gather supplies or unique Marsh reagentsthis can enhance survival elements and make the world feel alive. + +2. **Dialogue Choices** + - During each interpersonal event, let the player influence how a conversation unfolds. This can alter relationship changes or immediate next steps (e.g., does Orin soften a bit after hearing Vosss confession, or does he hold onto anger?). + +3. **Location-Specific Challenges** + - **Environmental Hazards**: Heat exhaustion, sinkholes, hidden traps in ruinsthese can become small gameplay loops that reinforce the locations identity. + - **Stealth vs. Direct Confrontation**: If you have an inquisitive guard, does the player try to sneak past, or talk their way through? Each approach might yield different consequences. + +--- + +## 5. Reinforce Relationship Dynamics with Gameplay + +1. **Team-Based Obstacles** + - Create puzzles or battles that require the skills or trust of both characters involved. For example, Orins knowledge of ancient scripts plus Vosss brute strength might be needed to open a hidden door. If trust is low, they might fail or argue, changing the outcome. + +2. **Branching Relationship Arcs** + - If the relationship meter (e.g., Orin|Voss = -1) goes too low, maybe they fight at inopportune times or sabotage each other. If it climbs high, they might protect each other from certain traps. + - This ensures that relationship changes do more than just appear on a stat screenthey alter the games path or difficulty in certain areas. + +--- + +## 6. Maintain Cohesive Narrative Flow + +1. **Chekhovs Gun Principle** + - If something is mentioned (like a longstanding family feud or a powerful ancient artifact), make sure it shows up again in a meaningful way, paying it off narratively. + - For example, mention early on that the feud might lead to someone betraying the group, then pay it off by having that tension climax at the final location. + +2. **Ephemeral Continuity** + - Each step influences the next. If Orin left Voss behind in the Marsh, maybe Voss arrives late in the next location with a new grudge or a new ally. That sense of cause and effect keeps the world believable. + +--- + +## 7. Example Outline: Adding Depth and Activities + +Heres a more fleshed-out partial example focusing on the **Haunted Marsh**: + +- **HauntedMarsh: step2** + - **Description**: Orin and Voss must travel through the fog-enshrouded bog to reach a crucial relic rumored to cleanse curses. + - **Events**: + - A surprising confession that changes everything could be spurred by illusions dredging up guilt. + - **Side Quests**: + 1. **Warding Lantern**: A local hermit begs for help collecting phosphorescent mushrooms to craft a lantern that repels marsh spirits. This ties to the environment and helps players survive. + 2. **Marsh Oracle**: The beggar might actually be a visionary who offers cryptic advice if the players bring them a rare Marsh Lily. + - **Relationship Mechanics**: + - If Orin is suspicious of Voss, let that seep into how they approach illusions (e.g., Orin might see illusions of Voss betraying him). A dialogue choice can let players choose to trust or deny those illusions. + - **Transitions**: Possibly branch out (stay to do side quests or push on immediately) but eventually funnel back to the next big location. + +--- + +## Final Thoughts + +- **Build each location so it reflects (1) the storys history and (2) the characters personal arcs.** +- **Embed meaningful player actions** that reveal backstory, test alliances, and provide tangible rewards or punishments (equipment, local standing, new information). +- Keep track of relationship metrics **and** how each story beat impacts them, ensuring the effects show up in dialogue, choices, and gameplay consequences. + +By following these steps, youll create a more cohesive, believable world where the **story, characters, and gameplay** are closely interwovenenhancing the players sense of agency and immersion. + +``` + +--- + +## Ending Ideas and Conflict Concepts + +### sterotypicalstoryendings +``` +Here are 30 stereotypical story endings: + +1. The hero rides off into the sunset with their loved one. +2. The villain gets their comeuppance and is punished for their crimes. +3. The protagonist finds a long-lost treasure or inheritance. +4. The underdog team wins the championship game. +5. The main character learns a valuable lesson about the importance of friendship and family. +6. The hero sacrifices themselves to save the world. +7. The couple gets married and lives happily ever after. +8. The villain is revealed to be a long-lost relative or friend. +9. The protagonist discovers a hidden talent or skill. +10. The world is saved from an alien invasion. +11. The hero and villain engage in an epic battle, with the hero emerging victorious. +12. The main character returns home after a long journey, wiser and more experienced. +13. The underdog achieves their dream despite the odds being against them. +14. The story ends with a cliffhanger, setting up a sequel. +15. The hero's secret identity is revealed to the public. +16. The couple reunites after a long separation. +17. The protagonist discovers a conspiracy that threatens the world. +18. The villain's backstory is revealed, explaining their motivations. +19. The hero must make a difficult choice between two desirable outcomes. +20. The story concludes with a sense of closure and new beginnings. +21. The protagonist's journey is revealed to be a dream or simulation. +22. The hero and villain team up to take down a greater threat. +23. The main character finds redemption for past mistakes. +24. The story ends with a twist, revealing a surprise traitor or unexpected outcome. +25. The hero's loved one is rescued from captivity. +26. The protagonist achieves a long-sought goal, but at great personal cost. +27. The world is rebuilt after a catastrophic event. +28. The hero must confront their own dark past or inner demons. +29. The story concludes with a sense of hope and renewal. +30. The protagonist discovers a hidden prophecy or destiny that they must fulfill. + +These are just a few examples of stereotypical story endings. Many stories subvert or play with these tropes, and there are countless other ways to conclude a narrative. + +``` + +### InterpersonalConflictIdeasBetraylacksympathyunfairexpectations +``` +Confide a deeply personal secret in a friend who immediately gossips about it. +Promise a neighbor to water their plants, then forget completely and deny responsibility. +Ignore a roommate's obvious distress when they lose their job. +Cancel a friend's invitation at the last minute with no explanation, expecting them to be fine with it. +Blame a colleague for missing a deadline without giving them the information they requested. +Borrow an expensive item from a friend, then return it damaged and refuse to pay for repairs. +Dismiss a sibling's request for emotional support, insisting "it's not a big deal." +Interrupt every time a friend tries to share good news, redirecting the conversation to your own achievements. +Overlook a loved one's birthday after they meticulously planned yours. +Ridicule a coworker's ideas in a meeting, yet count on their help afterward without apology. +Demand a partner sacrifice their free time for your projects but never offer to help with theirs. +Confront a friend about their small oversight but refuse to hear them out when they raise your mistakes. +Lose a close friend's trust by revealing a secret you promised to keep, then acting as though nothing happened. +Dismiss a teammate's contributions as trivial, only to claim credit if they succeed. +Invite a friend to a group event, then ignore them all evening. +Ask a coworker to cover for you repeatedly while you take extended breaks, offering no reciprocation. +Invalidate a family member's feelings by calling them "too sensitive" whenever they express sadness. +Exaggerate your role in a group project to get all the praise, overlooking everyone else's input. +Refuse to accommodate an employee's request for flexible hours but expect them to work late for your convenience. +Criticize a friend for not showing up to your party, even though they were caring for a sick relative. +Pressure a partner to spend money on your hobbies, then complain if they question the budget. +Downplay a child's accomplishment, making them feel insignificant, and then wonder why they're unenthusiastic later. +Repeat a rumor about a friend's personal life, shrugging it off as "harmless talk." +Persuade a colleague to take a risky approach, then blame them if it goes wrong. +Shut down someone's request for help with a project, saying "figure it out," while you rely on them for your tasks. +Lecture a friend about their choices without listening to their perspective, then wonder why they stop confiding in you. +Overstate the ease of a challenging task to a junior teammate, then criticize their performance when they struggle. +Downplay the effort a partner puts into daily chores, assuming they do "nothing all day." +Dodge responsibility for a shared error by pinning blame on a less influential teammate. +Arrange a high-stakes meeting and expect your subordinates to do all the prep without notice or support. +Reject a neighbor's plea for help fixing something, yet ask them to collect your mail every time you travel. +Neglect a friend's boundaries by repeatedly forcing them into uncomfortable social situations. +Publicize a colleague's personal struggles on social media for likes and sympathy points. +Interrupt a spouse's heartfelt apology to complain about your day, ignoring their sincere regret. +Ostracize a former friend for one minor mistake, never allowing them a chance to explain or make amends. +Devalue a friend's emotions by saying, "Just get over it," whenever they talk about something painful. +Push a team member to work overtime with no compensation, acting like it's a normal expectation. +Gossip about a friend who confided in you, then tell them you had no choice. +Force a coworker to share personal details at an office event, even though they repeatedly decline. +Accuse a loved one of being selfish simply because they took time for self-care. +Refuse to celebrate a friend's success because it "overshadows" your own achievements. +Demand that an introverted family member attend every social gathering, ignoring their comfort level. +Subtly bully a peer into helping with your tasks by saying you'll "remember this favor," implying future consequences. +Coerce a friend to defend you in an argument even when you know you're in the wrong. +Deny your child's request for basic emotional support, telling them they're "too dramatic." +Guilt-trip a sibling into giving up their personal time because "family obligations come first." +Promise a big reward to a classmate for doing extra group project work, then conveniently forget your end of the bargain. +Exploit a friend's kindness by constantly borrowing money without paying it back. +Discount a partner's illness as "not that serious," insisting they still handle household chores. +Walk away from someone in tears, believing they just "need to toughen up." + +``` + +--- + +## Concept Brainstorm Ideas + +### TimeRelatedConceptsForIdeaGeneration +``` +Sonnet 3.5 + +Let's explore some other ways we can conceptualize time tangibly for storytelling purposes: + +Time as a Currency: +Imagine a world where time itself is a tradable resource. People can buy, sell, or exchange units of time. +Time as a Landscape: +Time could be visualized as a physical terrain that characters can traverse, with different eras represented by distinct geographical features. +Time as a Library: +Similar to the warehouse concept, but each moment in time is a book that can be read, written, or edited. +Time as a Network: +Envision time as an interconnected web of nodes, where each node represents an event or moment that can be accessed non-linearly. +Time as a Crafting Material: +In this concept, time could be molded, shaped, or combined like clay to create new realities or alter existing ones. +Time as a Ecosystem: +Here, different time periods interact like species in an ecosystem, with cause-and-effect relationships spanning across eras. +Time as a Computer Program: +Time could be seen as a vast codebase, where altering a line of code in the past changes the program's output in the future. +Time as a Painting: +Imagine time as a giant canvas where past, present, and future are all visible simultaneously, and characters can step into different parts of the painting. +Time as a Puzzle: +Each moment in time is a puzzle piece that can be rearranged to create different realities or outcomes. +Time as a Musical Composition: +Different time periods are like instruments in an orchestra, and changing the past is like altering the score, creating ripple effects through the entire composition. + +Investing (as you mentioned): + +Go back in time to invest in successful companies early +Preview future market trends + + +Law (as you mentioned): + +Go back in time to gather evidence or witness events firsthand +Preview potential outcomes of different legal strategies + + +Science (as you mentioned): + +Go to the future to see long-term experimental results +Observe long-term environmental changes + + +Medicine: + +Travel to the future to see long-term effects of treatments +Go back in time to prevent diseases before they start + + +Archaeology: + +Travel to the past to directly observe historical events +Verify theories about ancient civilizations + + +Journalism: + +Witness and report on historical events firsthand +Preview future events to prepare in-depth coverage + + +Environmental Conservation: + +Go to the future to see the impacts of current policies +Travel to the past to preserve extinct species + + +Engineering: + +Test structures over centuries in a matter of days +Go back in time to prevent disasters by fixing design flaws + + +Agriculture: + +Observe full growth cycles of crops in an instant +Travel to the future to develop crops resistant to future climate conditions + + +Criminal Investigation: + +Witness crimes as they happen in the past +Prevent crimes by foreseeing them in the future + + +Art Restoration: + +Go back in time to see artwork in its original state +Travel to the future to see how current restoration techniques hold up + + +Education: + +Take students on field trips to historical events +Preview future job markets to tailor current curricula + + +Disaster Management: + +Go back in time to implement better preventive measures +Travel to the future to assess long-term impacts of disasters + + +Politics: + +See the long-term effects of policies instantly +Go back in time to prevent international conflicts + + +Sports Coaching: + +Observe an athlete's entire career to optimize training from the start +Preview future competitions to develop winning strategies + + + +From this exercise, we can see some common themes emerging: + +Verification of past events or theories +Prevention of negative outcomes +Optimization of current processes +Long-term impact assessment +Preparation for future events + + + +This table outlines potential risk elements and game ideas for each of the themes we identified. Let's discuss these in more detail: + +Verification of past events: + +Risk: The butterfly effect. Even small interactions in the past could dramatically alter the present. +Game Idea: Players could be historians or detectives, tasked with observing past events to solve present-day mysteries. The challenge is to gather information without interfering with the past. + + +Prevention of negative outcomes: + +Risk: Unintended consequences. Preventing one disaster might lead to an even worse one. +Game Idea: Players manage multiple timelines, trying to create the best overall outcome. They must balance preventing catastrophes in one timeline against potential negative impacts in others. + + +Optimization of current processes: + +Risk: Over-optimization leading to system collapse. +Game Idea: Players could manage a company or even a country, tweaking past decisions to improve the present. However, pushing too hard for efficiency or profit could lead to unforeseen problems or total collapse. + + +Long-term impact assessment: + +Risk: Increasing uncertainty and danger the further into the future you go. +Game Idea: Players explore future timelines to gather data, but risk getting trapped in increasingly divergent and potentially hostile futures. The challenge is to gather enough information without going so far that you can't return. + + +Preparation for future events: + +Risk: Self-fulfilling prophecies. Preparing for a predicted future might be what causes it to happen. +Game Idea: Players navigate through potential futures, trying to prepare for or prevent them without inadvertently causing them. The game could involve complex cause-and-effect chains where players must carefully consider the implications of their actions. + + + +To further develop these into game concepts, we could consider: + +Resource management: Time itself could be a limited resource that players must manage carefully. +Paradox potential: Create tension by introducing the risk of paradoxes that could erase the player from existence. +Competing factions: Introduce other time travelers with different agendas, creating conflict and competition. +Technological limitations: Time travel devices could have cooldown periods, limited charges, or other restrictions that force strategic use. +Ethical dilemmas: Present players with morally ambiguous choices that have far-reaching consequences across time. + +``` + +### Nonlinearprogressionideas +``` +Llama 405B + +Non-linear storytelling involves presenting events out of chronological order. This technique can be used in literature, film, and other forms of narrative to create suspense, surprise, or to highlight certain themes or character developments. Here are some common examples of non-linear storytelling events: + +1. Flashbacks: These are scenes that take the story back in time, often used to explain a character's past or to provide context for the current situation. + +2. Flashforwards: Similar to flashbacks, but these scenes take the story forward in time, often used to create suspense or to show the consequences of current actions. + +3. Parallel storylines: This involves telling multiple stories that happen at the same time but are presented in a non-linear fashion, often intersecting or influencing each other at certain points. + +4. Reverse chronology: The story is presented in reverse order, starting with the conclusion and moving backward in time. + +5. Non-chronological chapters or scenes: The story is presented in a non-linear order, with each chapter or scene taking place at a different point in time. + +6. Time loops: A character relives the same period of time repeatedly, often with slight variations. + +7. Unreliable narrators: The narrator withholds or distorts information, leading to a non-linear presentation of events. + +8. Dream sequences: The story jumps into a character's dreams or imagination, which can be presented in a non-linear fashion. + +These are just a few examples of non-linear storytelling events. The possibilities are endless, and the technique can be adapted to fit the needs of the story and the creativity of the storyteller. + +``` + +### Adjectivebasedcombinationideatexts +``` +For control over the flow of the game first need to solve chicken egg of story vs json then for each concept to mixed need to do the same. + +Gemini-test (23/07/2023) + +The kind of analysis you're describing, where you're looking for shared qualities and connections between seemingly disparate concepts like storytelling and JSON, can be referred to by several terms, depending on the specific focus and context: + +General Terms: + +Comparative Analysis: This is a broad term that encompasses any analysis where you're comparing and contrasting two or more things. +Analogical Reasoning: This involves identifying similarities between two things and using those similarities to understand or explain one in terms of the other. +Conceptual Blending: This refers to the cognitive process of combining different concepts to create new meanings or understandings. +Interdisciplinary Thinking: This involves drawing connections and insights from different fields of knowledge. +More Specific Terms (depending on the goal): + +Metaphorical Analysis: If you're focusing on the metaphorical connections between the concepts, this term would be appropriate. +Cross-Domain Mapping: This term is often used in cognitive science and artificial intelligence to describe the process of finding correspondences between different domains of knowledge. +Design Thinking: If your goal is to use these insights to create something new or solve a problem, this term might be relevant. +In the context of your specific example (storytelling and JSON): + +You could describe this analysis as: + +Finding Shared Principles of Effective Communication: Both storytelling and JSON are ultimately about conveying information effectively. +Exploring the Art of Structure and Organization: Both storytelling and JSON rely on clear structure and organization to achieve their respective goals. +Bridging the Gap Between Narrative and Data: This analysis highlights the unexpected connections between the seemingly disparate worlds of storytelling and data representation. +To further refine your search for resources and methods, consider these questions: + +What is your ultimate goal in merging these concepts? Are you trying to understand one better through the lens of the other? Are you trying to create something new? +What specific aspects of each concept are you most interested in comparing? Are you focusing on structure, clarity, engagement, or something else? +By clarifying your goals and focusing your analysis, you can more effectively leverage these different approaches and find the most relevant resources to guide your exploration. +``` + +### Adjectivebasedcombinationideatextsv2 +``` +Nemotron 4 340B Perplexity Labs + +Original Prompt +Please help me. If I want to merge the idea of a bakery with the plot of star wars I need to find points where they match and dont match. I decided to use adjectives as a proxy so the combinatorics is initiated by close adjectives to generate seeds for combination ideas that we can send to an llm for each seed. How would this look in mermaid + + + +Mermaid Struxture + + +The workflow or process you've described can be generally categorized as a "concept blending" or "idea fusion" approach, which involves combining elements from two or more distinct concepts to generate new ideas. In your specific case, you're using adjectives as a proxy to facilitate the blending of a bakery concept with the plot of Star Wars. This process can also be considered a form of "creative problem-solving" or "design thinking," where you're systematically exploring and combining elements to generate innovative solutions or ideas. +More specifically, the steps in your workflow can be broken down into the following stages: +Concept Identification: Identifying the two distinct concepts (bakery and Star Wars) to be blended. +Attribute Extraction: Extracting relevant attributes (adjectives) from each concept. +Combinatorics: Systematically combining the attributes from both concepts to generate new combinations. +Seed Idea Generation: Translating the combined attributes into seed ideas that merge the two concepts. +LLM Processing: Utilizing a Large Language Model (LLM) to further develop and refine the seed ideas into more detailed and cohesive concepts. +This workflow can be applied to various creative problem-solving scenarios, not just limited to the bakery and Star Wars example + + + +Indeed, design can be viewed as a search algorithm, where you explore a vast space of possibilities to find optimal or innovative solutions. Here's how you can apply this idea to five more scenarios: +Merging a Library with a Music Festival +Concept Identification: Library and Music Festival +Attribute Extraction: Quiet, Knowledgeable, Organized (Library) vs. Loud, Energetic, Creative (Music Festival) +Combinatorics: Quiet Creative Spaces, Knowledgeable Music Workshops, Organized Jam Sessions +Seed Idea Generation: Silent Disco Library, Music Theory Workshops, Collaborative Songwriting Sessions +LLM Processing: Develop detailed event plans, marketing materials, and logistical considerations +Blending a Fitness Center with a Video Game Arcade +Concept Identification: Fitness Center and Video Game Arcade +Attribute Extraction: Active, Healthy, Competitive (Fitness Center) vs. Interactive, Fun, Social (Video Game Arcade) +Combinatorics: Active Interactive Games, Healthy Fun Challenges, Competitive Social Workouts +Seed Idea Generation: Exergaming Stations, Virtual Reality Fitness Challenges, Multiplayer Workout Games +LLM Processing: Create game concepts, rules, and scoring systems, as well as marketing and branding strategies +Fusing a Vegan Restaurant with a Science Museum +Concept Identification: Vegan Restaurant and Science Museum +Attribute Extraction: Plant-Based, Sustainable, Healthy (Vegan Restaurant) vs. Educational, Interactive, Innovative (Science Museum) +Combinatorics: Plant-Based Interactive Exhibits, Sustainable Educational Dining, Healthy Innovative Cuisine +Seed Idea Generation: Edible Plant Exhibits, Cooking Classes with a Scientific Twist, Molecular Gastronomy Menu +LLM Processing: Develop exhibit designs, class curricula, and menu items, as well as marketing and branding strategies +Combining a Co-working Space with a Pet Store +Concept Identification: Co-working Space and Pet Store +Attribute Extraction: Collaborative, Productive, Professional (Co-working Space) vs. Friendly, Playful, Nurturing (Pet Store) +Combinatorics: Collaborative Pet-Friendly Spaces, Productive Play Areas, Professional Pet Care Services +Seed Idea Generation: Pet-Friendly Co-working Desks, On-site Pet Daycare, Pet-themed Networking Events +LLM Processing: Create space layouts, pet care service offerings, and event plans, as well as marketing and branding strategies +Integrating a Fashion Boutique with an Art Gallery +Concept Identification: Fashion Boutique and Art Gallery +Attribute Extraction: Stylish, Trendy, Creative (Fashion Boutique) vs. Artistic, Thought-provoking, Inspirational (Art Gallery) +Combinatorics: Stylish Artistic Installations, Trendy Thought-provoking Fashion, Creative Inspirational Clothing +Seed Idea Generation: Art-inspired Fashion Collections, Interactive Fashion Exhibits, Artist-designed Clothing Lines +LLM Processing: Develop fashion collection concepts, exhibit designs, and artist collaborations, as well as marketing and branding strategies + + +``` + +--- + +## Workflow Stage Prompts + +### WFStage1prompt +``` +Use this to write a story and explain how it represents the structure - + +graph TD +subgraph "Actual Timeline" +A1[Location 1 - First Visit] +A2[Location 2 - First Visit] +A3[Location 3 - First Visit] +A4[Location 4 - First Visit] +A1 --> A2 --> A3 --> A4 +end +subgraph "Narrative Timeline" +N1[Location 2 - Story Event 1] +N2[Location 4 - Story Event 2] +N3[Location 1 - Story Event 3] +N4[Location 3 - Story Event 4] +N1 --> N2 --> N3 --> N4 +end +subgraph "Hidden Timeline" +H1[Hidden Event 1] +H2[Hidden Event 2] +H3[Hidden Event 3] +H4[Hidden Event 4] +end +A1 -.-> N3 +A2 -.-> N1 +A3 -.-> N4 +A4 -.-> N2 +H1 -.- A1 +H1 -.- A2 +H1 -.- A3 +H1 -.- A4 +H2 -.- A1 +H2 -.- A2 +H2 -.- A3 +H2 -.- A4 +H3 -.- A1 +H3 -.- A2 +H3 -.- A3 +H3 -.- A4 +H4 -.- A1 +H4 -.- A2 +H4 -.- A3 +H4 -.- A4 +``` + +### TimeRelatedMermaidStoryAttempttoRefinefrom (5 examples) +#### Example 1 +``` +gpt-4o + +Once upon a time in a land where time flowed like a river with hidden currents, there was a story that unfolded across three intertwined timelines: the Actual Timeline, the Narrative Timeline, and the Hidden Timeline. + +Actual Timeline +The protagonist, Elena, embarked on a journey through four distinct locations, each holding a piece of a grand puzzle she needed to solve. + +Location 1 - First Visit: Elena started her journey here, uncovering an ancient map that hinted at a deeper mystery. +Location 2 - First Visit: She traveled to a bustling marketplace, where she found a cryptic message carved into a stone. +Location 3 - First Visit: In an abandoned library, Elena discovered a journal detailing strange occurrences and hidden events. +Location 4 - First Visit: Her journey led her to a serene forest where she encountered an enigmatic figure who offered her a crucial clue. +Narrative Timeline +As Elena pieced together the clues, the story she narrated to her friends followed a different sequence, highlighting the events that made the most sense from her perspective at the time. + +Location 2 - Story Event 1: Elena recalled the marketplace first, sharing the mystery of the cryptic message. +Location 4 - Story Event 2: She then described her encounter in the serene forest, emphasizing the importance of the clue she received. +Location 1 - Story Event 3: Elena recounted the discovery of the ancient map, giving context to the overall mystery. +Location 3 - Story Event 4: Lastly, she talked about the abandoned library and the journal, which tied all the events together. +Hidden Timeline +Unbeknownst to Elena, there was a hidden layer of events influencing her journey. + +Hidden Event 1: In the shadows, an ancient order monitored her progress from the moment she found the map, ensuring she remained on the right path. +Hidden Event 2: They subtly guided her towards the marketplace, planting the cryptic message for her to find. +Hidden Event 3: The journal in the library was placed there by the order, containing information they wanted her to uncover. +Hidden Event 4: The enigmatic figure in the forest was a member of the order, providing her with the final piece of the puzzle. +Interconnections +Actual Timeline to Narrative Timeline: +Location 1 (First Visit) led to Story Event 3 (Elena's recollection of the map). +Location 2 (First Visit) led to Story Event 1 (marketplace mystery). +Location 3 (First Visit) led to Story Event 4 (the journal). +Location 4 (First Visit) led to Story Event 2 (forest encounter). +Actual Timeline to Hidden Timeline: +Each visit in the Actual Timeline was influenced by the Hidden Events, which shaped Elena's experiences without her knowledge. +Story Representation +This structure represents the complexity of storytelling, where the sequence of events (Actual Timeline) is often different from how the story is told (Narrative Timeline), and there are unseen forces (Hidden Timeline) that influence the course of events. Elenas journey through the four locations, her recollection of events, and the hidden manipulations all intertwine to create a rich, multi-layered narrative. + +In this story, the Actual Timeline shows Elenas chronological journey. The Narrative Timeline reveals how she chose to share her experiences, emphasizing different aspects of her journey. The Hidden Timeline uncovers the secret influences that guided her path, adding depth and intrigue to the overall tale. + + + +Great attempt. Lets see how the story changes if we assume the main character goes to every location twice + + + +Certainly! Let's expand the story to include two visits to each location, which will add more depth and complexity to the narrative. + +Actual Timeline +Elena revisits each location, uncovering new layers of the mystery with each visit. + +Location 1 - First Visit: Elena starts her journey here, uncovering an ancient map that hints at a deeper mystery. +Location 2 - First Visit: She travels to a bustling marketplace, where she finds a cryptic message carved into a stone. +Location 3 - First Visit: In an abandoned library, Elena discovers a journal detailing strange occurrences and hidden events. +Location 4 - First Visit: Her journey leads her to a serene forest where she encounters an enigmatic figure who offers her a crucial clue. +Location 1 - Second Visit: Elena returns to the first location, where she deciphers the ancient map, revealing a hidden passage. +Location 2 - Second Visit: Back at the marketplace, she meets a mysterious merchant who provides additional information about the cryptic message. +Location 3 - Second Visit: In the library, she finds a hidden compartment in the journal containing a key. +Location 4 - Second Visit: Finally, in the forest, the enigmatic figure reappears, guiding her to the entrance of an ancient temple. +Narrative Timeline +Elena pieces together her adventure in a sequence that makes the most sense to her story. + +Location 2 - Story Event 1: Elena recalls the marketplace first, sharing the mystery of the cryptic message and the merchants information. +Location 4 - Story Event 2: She then describes her encounters in the serene forest, focusing on both the clue and the guidance to the temple. +Location 1 - Story Event 3: Elena recounts the discovery of the ancient map and the hidden passage. +Location 3 - Story Event 4: Lastly, she talks about the abandoned library, the journal, and the key. +Hidden Timeline +The ancient order continues to guide Elenas journey, intervening subtly. + +Hidden Event 1: The order monitors Elena from the moment she finds the map, ensuring she is on the right path. +Hidden Event 2: They guide her towards the marketplace, planting the cryptic message and arranging the merchants appearance. +Hidden Event 3: The journal in the library is placed by the order, with the hidden compartment and key added during her first visit. +Hidden Event 4: The enigmatic figure in the forest is a member of the order, orchestrating both encounters to provide the necessary guidance. +Interconnections +Actual Timeline to Narrative Timeline: +Location 1 First Visit (map discovery) leads to Story Event 3 (hidden passage). +Location 2 First Visit (cryptic message) and Second Visit (merchant) combine into Story Event 1 (marketplace mystery). +Location 3 First Visit (journal) and Second Visit (key) combine into Story Event 4 (library findings). +Location 4 First Visit (clue) and Second Visit (temple guidance) combine into Story Event 2 (forest encounters). +Actual Timeline to Hidden Timeline: +Each visit in the Actual Timeline is influenced by the Hidden Events, shaping Elenas experiences without her knowledge. +Expanded Story Representation +In this expanded story, the Actual Timeline now includes two visits to each location, reflecting Elena's deepening understanding of the mystery. The Narrative Timeline still presents her recollection of events, but now with added complexity as she integrates discoveries from both visits. The Hidden Timeline continues to reveal the secret influences that guide her path, adding further depth and intrigue. + +Elenas repeated visits to each location allow her to uncover additional layers of the mystery, with each visit building on the previous one. This structure illustrates how stories can evolve with new information and perspectives, and how unseen forces can shape the journey in subtle but significant ways. + +``` + +#### Example 2 +``` +meta-llama-3.1-405b-instruct + +Here's a story based on the graph structure: + +Emily had always been fascinated by the mysterious disappearances that had taken place in the four locations she was about to visit. She packed her bags, said goodbye to her family, and set off on her journey. + +Her first stop was Location 1, an abandoned mansion on the outskirts of town. As she walked through the dusty halls, she stumbled upon a cryptic message scrawled on the wall: "The eyes that see are not the ones that look." Emily was intrigued but couldn't decipher the meaning. + +Next, she visited Location 2, an old bookstore in the city. While browsing through the shelves, she met the owner, an eccentric old man who told her about a mysterious book that was said to hold the key to the disappearances. Emily was skeptical, but the old man's words lingered in her mind. + +From there, she traveled to Location 3, a secluded monastery in the mountains. The monks welcomed her warmly and shared stories of a long-forgotten event that had taken place in the monastery's past. Emily sensed that there was more to the story, but the monks were tight-lipped about the details. + +Finally, she arrived at Location 4, a small island with a reputation for being cursed. As she explored the island, she stumbled upon a journal belonging to one of the disappeared individuals. The journal hinted at a dark secret, but the writer's words were cryptic and unclear. + +As Emily returned home, she began to piece together the events of her journey. She realized that the disappearances were not random events, but rather, they were all connected. The message on the wall in Location 1 was a warning, the book in Location 2 held a vital clue, the monastery's past was crucial to understanding the present, and the journal on the island was the key to unlocking the mystery. + +However, what Emily didn't know was that her journey was not just about uncovering the truth about the disappearances. Unbeknownst to her, a series of hidden events had been set in motion, influencing her actions and guiding her towards a specific outcome. These events, though invisible to Emily, were the true driving force behind the story. + +The story structure represented by the graph is non-linear. The actual timeline shows Emily visiting the four locations in a linear sequence (A1 -> A2 -> A3 -> A4). However, the narrative timeline is different, with the story events being presented out of chronological order (N1 -> N2 -> N3 -> N4). This non-linearity allows the story to build suspense and create a sense of mystery. + +The hidden timeline represents the unseen forces that shape the story. These events are not directly revealed to the reader, but they have a profound impact on the narrative. The connections between the hidden events and the actual timeline suggest that the story is more complex and layered than initially apparent. + +The graph structure highlights the relationships between the different timelines and events, demonstrating how a non-linear narrative can be constructed to create a engaging and mysterious story. + + + +Very Good Attempt. Lets see how your story will change under the assumption that each location is visited twice + + + +That's an interesting twist. Here's a revised story based on the assumption that each location is visited twice: + +Emily had always been fascinated by the mysterious disappearances that had taken place in the four locations she was about to visit. She packed her bags, said goodbye to her family, and set off on her journey. + +Her first stop was Location 1, an abandoned mansion on the outskirts of town. As she walked through the dusty halls, she stumbled upon a cryptic message scrawled on the wall: "The eyes that see are not the ones that look." Emily was intrigued but couldn't decipher the meaning. + +From there, she visited Location 2, an old bookstore in the city. While browsing through the shelves, she met the owner, an eccentric old man who told her about a mysterious book that was said to hold the key to the disappearances. Emily was skeptical, but the old man's words lingered in her mind. + +Next, she traveled to Location 3, a secluded monastery in the mountains. The monks welcomed her warmly and shared stories of a long-forgotten event that had taken place in the monastery's past. Emily sensed that there was more to the story, but the monks were tight-lipped about the details. + +Finally, she arrived at Location 4, a small island with a reputation for being cursed. As she explored the island, she stumbled upon a journal belonging to one of the disappeared individuals. The journal hinted at a dark secret, but the writer's words were cryptic and unclear. + +However, Emily's journey didn't end there. She felt that she had only scratched the surface of the mystery, and so she decided to revisit each location, hoping to uncover more clues. + +Upon her second visit to Location 1, Emily discovered a hidden room deep within the mansion's basement. Inside, she found a series of letters exchanged between the mansion's former occupants, revealing a dark family secret that seemed to be connected to the disappearances. + +Her second visit to Location 2 led her to a hidden compartment in the bookstore's shelves, where she found the mysterious book the old man had told her about. As she flipped through its pages, she realized that the book was more than just a simple novel it was a guide to unlocking the secrets of the disappearances. + +During her second visit to Location 3, Emily managed to gain the trust of one of the monks, who shared with her the true story of the monastery's past. The monk's words revealed a shocking connection between the monastery and the disappearances, and Emily began to piece together the puzzle. + +Finally, her second visit to Location 4 took her to a hidden cave on the island, where she found a series of cryptic messages etched into the walls. The messages seemed to be a code, and Emily realized that they were the key to unlocking the secrets of the journal she had found during her first visit. + +As Emily returned home, she began to piece together the events of her journey. She realized that the disappearances were not random events, but rather, they were all connected. The message on the wall in Location 1 was a warning, the book in Location 2 held a vital clue, the monastery's past was crucial to understanding the present, and the journal on the island was the key to unlocking the mystery. + +However, what Emily didn't know was that her journey was not just about uncovering the truth about the disappearances. Unbeknownst to her, a series of hidden events had been set in motion, influencing her actions and guiding her towards a specific outcome. These events, though invisible to Emily, were the true driving force behind the story. + +The story structure remains non-linear, with the narrative timeline presenting the story events out of chronological order. The actual timeline shows Emily visiting each location twice, with the second visit revealing more clues and pieces of the puzzle. The hidden timeline continues to represent the unseen forces that shape the story, influencing Emily's actions and guiding her towards a specific outcome. + +Here's an updated graph to reflect the changes: + +graph TD +subgraph "Actual Timeline" +A1[Location 1 - First Visit] +A2[Location 2 - First Visit] +A3[Location 3 - First Visit] +A4[Location 4 - First Visit] +A5[Location 1 - Second Visit] +A6[Location 2 - Second Visit] +A7[Location 3 - Second Visit] +A8[Location 4 - Second Visit] +A1 --> A2 --> A3 --> A4 --> A5 --> A6 --> A7 --> A8 +end +subgraph "Narrative Timeline" +N1[Location 2 - Story Event 1] +N2[Location 4 - Story Event 2] +N3[Location 1 - Story Event 3] +N4[Location 3 - Story Event 4] +N5[Location 1 - Story Event 5] +N + +``` + +#### Example 3 +``` +mistral-large-2407 + +Story: The Unraveling of Time +Actual Timeline +In the quaint town of Harborside, four locations form the backbone of daily life: the Library (Location 1), the Market (Location 2), the Park (Location 3), and the Lighthouse (Location 4). The story unfolds as the protagonist, Emily, visits these locations in sequence. + +Location 1 - First Visit (Library): Emily starts her day at the library, researching old town records. +Location 2 - First Visit (Market): She then heads to the market, picking up fresh produce and chatting with the locals. +Location 3 - First Visit (Park): After lunch, Emily takes a leisurely stroll in the park, enjoying the serene environment. +Location 4 - First Visit (Lighthouse): Finally, she visits the lighthouse, where she meets the old lighthouse keeper who tells her stories about the town's past. +Narrative Timeline +The story, however, is not told in the sequence of Emily's visits. Instead, it jumps between events in a non-linear fashion, creating a sense of mystery and intrigue. + +Location 2 - Story Event 1 (Market): The story begins with a mysterious theft at the market, where Emily witnesses a valuable artifact being stolen. +Location 4 - Story Event 2 (Lighthouse): The narrative then shifts to the lighthouse, where Emily discovers hidden letters that hint at a decades-old secret. +Location 1 - Story Event 3 (Library): The story then flashes back to Emily's visit to the library, where she uncovers ancient documents that reveal a dark history of the town. +Location 3 - Story Event 4 (Park): The climax of the story takes place in the park, where Emily confronts the thief and unveils the truth behind the town's secrets. +Hidden Timeline +Underlying the visible events is a hidden timeline that influences the narrative without being directly visible. These hidden events represent the dark history of the town, hinted at but not fully revealed until the end. + +Hidden Event 1: A series of unexplained disappearances that occurred decades ago. +Hidden Event 2: A forgotten curse placed on the town by a disgruntled resident. +Hidden Event 3: A secret society formed to protect the town's dark secrets. +Hidden Event 4: A powerful artifact that holds the key to understanding the town's past. +Explanation of the Structure +The structure of the story is represented by the graph, which consists of three timelines: + +Actual Timeline: This represents the chronological order of Emily's visits to the locations. It shows the sequence of events as they actually happened. +Narrative Timeline: This represents the order in which the story is told. The events are not presented in chronological order but are arranged to build suspense and reveal information gradually. +Hidden Timeline: This represents the underlying events that influence the story but are not directly visible. These hidden events provide depth and complexity to the narrative. +The connections between the timelines (indicated by dashed lines) show how the actual events are linked to the narrative events and how the hidden events influence the overall story. This non-linear structure allows for a more engaging and mysterious narrative, keeping the reader intrigued until the final revelation. + + + +Very Good Attempt. Lets see how your story will change under the assumption that each location is visited twice + + + +Story: The Unraveling of Time (Revisited) +Actual Timeline +In the quaint town of Harborside, four locations form the backbone of daily life: the Library (Location 1), the Market (Location 2), the Park (Location 3), and the Lighthouse (Location 4). The story unfolds as the protagonist, Emily, visits these locations in sequence, but this time, she visits each location twice. + +Location 1 - First Visit (Library): Emily starts her day at the library, researching old town records. +Location 2 - First Visit (Market): She then heads to the market, picking up fresh produce and chatting with the locals. +Location 3 - First Visit (Park): After lunch, Emily takes a leisurely stroll in the park, enjoying the serene environment. +Location 4 - First Visit (Lighthouse): Finally, she visits the lighthouse, where she meets the old lighthouse keeper who tells her stories about the town's past. +Location 1 - Second Visit (Library): Emily returns to the library, this time finding a hidden compartment in an old book. +Location 2 - Second Visit (Market): She revisits the market and notices a strange symbol carved into one of the stalls. +Location 3 - Second Visit (Park): Emily returns to the park and discovers a hidden underground tunnel. +Location 4 - Second Visit (Lighthouse): She goes back to the lighthouse and finds a secret room containing old photographs and letters. +Narrative Timeline +The story is not told in the sequence of Emily's visits. Instead, it jumps between events in a non-linear fashion, creating a sense of mystery and intrigue. + +Location 2 - Story Event 1 (Market): The story begins with a mysterious theft at the market, where Emily witnesses a valuable artifact being stolen. +Location 4 - Story Event 2 (Lighthouse): The narrative then shifts to the lighthouse, where Emily discovers hidden letters that hint at a decades-old secret. +Location 1 - Story Event 3 (Library): The story then flashes back to Emily's visit to the library, where she uncovers ancient documents that reveal a dark history of the town. +Location 3 - Story Event 4 (Park): The climax of the story takes place in the park, where Emily confronts the thief and unveils the truth behind the town's secrets. +Location 1 - Story Event 5 (Library): Emily finds a hidden compartment in an old book, leading her to a cryptic map. +Location 2 - Story Event 6 (Market): She notices a strange symbol carved into one of the stalls, matching the symbol on the map. +Location 3 - Story Event 7 (Park): Emily discovers a hidden underground tunnel, which she believes is connected to the town's dark past. +Location 4 - Story Event 8 (Lighthouse): She finds a secret room containing old photographs and letters that provide the final pieces of the puzzle. +Hidden Timeline +Underlying the visible events is a hidden timeline that influences the narrative without being directly visible. These hidden events represent the dark history of the town, hinted at but not fully revealed until the end. + +Hidden Event 1: A series of unexplained disappearances that occurred decades ago. +Hidden Event 2: A forgotten curse placed on the town by a disgruntled resident. +Hidden Event 3: A secret society formed to protect the town's dark secrets. +Hidden Event 4: A powerful artifact that holds the key to understanding the town's past. +Hidden Event 5: The existence of a hidden map that leads to a secret underground chamber. +Hidden Event 6: The symbol that marks the entrance to the hidden chamber. +Hidden Event 7: The underground tunnel that connects all the locations. +Hidden Event 8: The secret room in the lighthouse that contains the final clues. +Explanation of the Structure +The structure of the story is represented by the graph, which consists of three timelines: + +Actual Timeline: This represents the chronological order + +``` + +#### Example 4 +``` +gpt-4o-mini-2024-07-18 + +Title: The Hidden Threads of Time + +Story: +In the quiet town of Eldridge, four locations held the secrets of a past intertwined with the present: The Old Oak Library, The Riverside Caf, The Abandoned Mill, and The Clock Tower. Over a single summer, Sarah, a curious young woman, embarked on a journey that would reveal the layers of time hidden beneath the surface of her seemingly mundane life. + +Actual Timeline: A Journey Begins +Location 1: The Old Oak Library (A1) +Sarahs adventure began at the Old Oak Library. As she browsed through the dusty shelves, she stumbled upon an ancient book that spoke of the towns lore and the hidden events that shaped its history. Intrigued, she decided to explore more. + +Location 2: The Riverside Caf (A2) +Her next stop was the Riverside Caf, where she overheard a conversation between two elderly patrons discussing a tragic event tied to the town's history. They spoke in hushed tones about a long-lost love and a betrayal that had forever altered the lives of those involved. + +Location 3: The Abandoned Mill (A3) +The following day, Sarah ventured to the Abandoned Mill. There, she discovered remnants of a past life: photographs, letters, and a diary that hinted at a hidden romance. She felt a strange connection to the stories within these walls. + +Location 4: The Clock Tower (A4) +Finally, she arrived at the Clock Tower, where she learned about a secret society that once met there to protect the towns secrets. This location felt alive with energy, as if the whispers of the past were echoing through time. + +Narrative Timeline: Unraveling the Mystery +Story Event 1: The Riverside Caf (N1) +At the caf, Sarah confronted the two patrons about their conversation. She learned that the tragic love story involved her great-grandparents, whose destinies were forever altered by a choice made in that very caf. + +Story Event 2: The Clock Tower (N2) +Back at the Clock Tower, she discovered a hidden room filled with documents that detailed the secret society's attempts to mend the heartbreak caused by her ancestors. Here, she found a letter addressed to her great-grandmother, containing a heartfelt apology from her great-grandfather. + +Story Event 3: The Old Oak Library (N3) +Returning to the library, Sarah pieced together her familys history through the ancient book. It revealed that the love story had been a catalyst for change in Eldridge, leading to the establishment of the society that aimed to protect the town from repeating past mistakes. + +Story Event 4: The Abandoned Mill (N4) +In the mill, Sarah re-read the diary and uncovered the truth: the betrayal was a misunderstanding fueled by jealousy and miscommunication. Understanding this allowed Sarah to forgive her ancestors and break the cycle of pain that had lingered in her family for generations. + +Hidden Timeline: The Secrets Beneath +Throughout her journey, Sarah was unaware of the Hidden Events that had shaped her familys history and the town of Eldridge: + +Hidden Event 1 (H1): A Betrayal +Long ago, a betrayal occurred that led to the rift between Sarahs great-grandparents. This event was the catalyst for the tragic love story that would echo through time. + +Hidden Event 2 (H2): The Secret Society's Formation +In the wake of the betrayal, a secret society formed to protect the town from the repercussions of the past, striving to mend what was broken. + +Hidden Event 3 (H3): The Lost Letter +A letter written by Sarahs great-grandfather remained hidden for decades, containing apologies and hopes for reconciliation that were never delivered. + +Hidden Event 4 (H4): The Towns Lore +The stories of Eldridge were carefully curated, with certain truths obscured to protect the living from the pain of the past. + +Structure Explanation: +The storys structure represents three distinct timelinesActual Timeline, Narrative Timeline, and Hidden Timelinewhich interact in complex ways: + +Actual Timeline: This is the chronological order of Sarahs visits to the four locations, marking her physical journey through the town. It serves as the backbone of the story, grounding it in reality and providing a structure for her exploration. +Narrative Timeline: This timeline unfolds the key story events that arise from Sarahs discoveries at each location. Unlike the Actual Timeline, the Narrative Timeline does not follow a linear path but instead reveals the emotional and thematic significance of her journey, highlighting the connections between past and present. +Hidden Timeline: This timeline consists of events and truths that remain obscured until Sarah unearths them through her exploration. The hidden events symbolize the burdens of history that affect the present, illustrating how the past can shape ones + +``` + +#### Example 5 +``` +athene-70b + +Story: The Cartographer's Enigma + +Protagonist Emilia, a skilled cartographer, receives a cryptic atlas from her reclusive mentor, featuring four locations: Ravenshire, Luminaria, Celestia's Peak, and Argentum Falls. The atlas hints at a centuries-old conspiracy, weaving together seemingly unrelated events across these sites. + +Narrative Timeline: + +Luminaria - The Celestial Alignment (N1): Emilia discovers an ancient celestial map etched into Luminaria's observatory walls, forecasting a rare alignment that unlocks a hidden chamber beneath Argentum Falls (A4). +Argentum Falls - The Forgotten Treaty (N2): Within the chamber, Emilia uncovers a long-lost treaty between ancient nations, which was negotiated at Ravenshire (A1) and ratified at Celestia's Peak (A3). +Ravenshire - The Cartographer's Legacy (N3): Emilia finds her mentor's hidden journals in Ravenshire, detailing the atlas's creation and the significance of Celestia's Peak, where an ancient order once protected the treaty. +Celestia's Peak - The Order's Reckoning (N4): At the peak, Emilia confronts a descendant of the ancient order, who reveals that they had been safeguarding the treaty against rogue factions. The descendant hands over a crucial, annotated atlas page, exposing the hidden events. +Actual Timeline (Emilia's Journey): + +Emilia visits the locations in the following order: Ravenshire (A1), Luminaria (A2), Celestia's Peak (A3), and Argentum Falls (A4). Each location builds upon her understanding, though the narrative jumps between them to create suspense and reveal connections. + +Hidden Timeline (Influencing Events): + +Hidden Event 1: A centuries-old astronomer in Luminaria (H1) calculates the celestial alignment, inspiring the treaty. +Hidden Event 2: A rogue faction infiltrates the ancient order at Celestia's Peak (H2), seeding corruption. +Hidden Event 3: Emilia's mentor, aware of the conspiracy, crafts the atlas (H3) to guide a worthy successor. +Hidden Event 4: The ancient order's remnants secretly aid Emilia's journey (H4), ensuring her discovery of the truth. +Structure Representation: + +Non-linear narrative: The narrative timeline (N1-N4) rearranges Emilia's actual visits (A1-A4) to build tension, reveal connections, and create an engaging mystery. +Hidden influences: The hidden events (H1-H4) permeate the actual timeline, motivating character actions, and informing the narrative's twists. Dotted lines illustrate how these events subtly shape Emilia's journey, enriching the story's depth and complexity. +Interconnectedness: The graph visually demonstrates how each location, narrative beat, and hidden event intertwines, producing a rich, layered narrative that rewards close attention and reflection. As Emilia unravels the enigma, the reader experiences a satisfying convergence of threads, highlighting the power of deliberate, non-linear storytelling. + +``` + +### WFStage2prompt +``` +Please use the below config as a template to write a story in config format (dont use the template values like masterlocation1): + +{ + "masterlocation1": { + "location1": { + "description": "UI - Experience Bar", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": [], + "developernotes": [] + }, + + eg. {"tavern": {"start": {"description": "You wake up in a dimly lit tavern, the smell of ale and salt heavy in the air. A grizzled sailor at the bar boasts about a hidden island overflowing with "more gold than ye can dream of!" ", "choices": ["approach sailor", "ignore sailor", "order grog"], "transitions": {"approach sailor": "tavern_rumor", "ignore sailor": "tavern_start", "order grog": "tavern_grog"}, "consequences": {}, "media": []}, "rumor": {"description": "The sailor, three sheets to the wind, spins a tale of treacherous waters, ancient curses, and a map hidden on a ghost ship. He claims to know the way.", "choices": ["offer to buy him another drink", "dismiss him as a drunkard"], "transitions": {"offer to buy him another drink": "tavern_map", "dismiss him as a drunkard": "tavern_start"}, "consequences": {}, "media": []}, "grog": {"description": "The tavern keeper slides you a tankard of potent grog. It burns pleasantly as it goes down, and you feel your senses dull.", "choices": ["order another", "eavesdrop on nearby pirates"], "transitions": {"order another": "tavern_grog", "eavesdrop on nearby pirates": "tavern_start"}, "media": []}, "map": {"description": "With a wink and a hiccup, the sailor produces a crumpled piece of parchment. It's stained with salt and what looks suspiciously like blood, but the markings... they resemble a map! ", "choices": ["propose a partnership", "try to buy the map", "steal the map"], "transitions": {"propose a partnership": "docks_prepare", "try to buy the map": "tavern_haggle", "steal the map": "tavern_fight"}, "consequences": {}, "media": []}, "haggle": {"description": "The sailor eyes you shrewdly. "This here map's worth a king's ransom," he slurs.", "choices": ["offer a pouch of gold", "walk away"], "transitions": {"offer a pouch of gold": "docks_prepare", "walk away": "tavern_start"}, "consequences": {}, "media": []}, "fight": {"description": "You lunge for the map, but the sailor's quicker than he looks! A brawl erupts, tables are overturned, and mugs fly through the air.", "choices": ["fight dirty", "try to reason with him"], "transitions": {"fight dirty": "tavern_victory", "try to reason with him": "tavern_defeat"}, "consequences": {}, "media": []}, "victory": {"description": "With a final blow, you knock the sailor unconscious. You grab the map and make a hasty exit.", "choices": ["head to the docks"], "transitions": {"head to the docks": "docks_prepare"}, "consequences": {}, "media": []}, "defeat": {"description": "The sailor and a couple of his cronies land some solid punches. You're tossed out of the tavern, bruised and map-less.", "choices": ["nurse your wounds"], "transitions": {"nurse your wounds": "tavern_start"}, "consequences": {}, "media": []}}, "docks": {"prepare": {"description": "The salty air of the docks fills your lungs. You can practically taste the adventure on the horizon. But first, you need a ship and a crew...", "choices": ["find a ship", "assemble a crew"], "transitions": {"find a ship": "docks_ship", "assemble a crew": "docks_crew"}, "media": []}, "ship": {"description": "You spot a sturdy-looking galleon with a "For Sale" sign hanging precariously from its mast.", "choices": ["inspect the ship", "look for another vessel"], "transitions": {"inspect the ship": "docks_captain", "look for another vessel": "docks_prepare"}, "media": []}, "crew": {"description": "The docks are teeming with salty dogs of all shapes and sizes.", "choices": ["recruit seasoned sailors", "round up some desperate souls"], "transitions": {"recruit seasoned sailors": "docks_captain", "round up some desperate souls": "docks_captain"}, "consequences": {}, "media": []}, "captain": {"description": "With a ship and a crew (of varying competence), you're ready to set sail! ", "choices": ["set sail for the open sea!"], "transitions": {"set sail for the open sea!": "openSea_start"}, "media": []}}, "openSea": {"start": {"description": "The open sea stretches before you, vast and unforgiving. The wind whips at your sails as you set course for the uncharted waters where the treasure awaits.", "choices": ["consult the map", "enjoy the voyage"], "transitions": {"consult the map": "openSea_storm", "enjoy the voyage": "openSea_mutiny"}, "media": []}, "storm": {"description": "A storm gathers on the horizon, dark clouds swirling ominously. Waves crash against the hull, threatening to swallow you whole.", "choices": ["weather the storm", "change course"], "transitions": {"weather the storm": "openSea_ghostShip", "change course": "openSea_start"}, "media": []}, "mutiny": {"description": "Your crew, a motley bunch at best, begin to grumble. They're growing impatient and greedy, their eyes glinting with mutiny.", "choices": ["quell the mutiny", "reason with them"], "transitions": {"quell the mutiny": "openSea_ghostShip", "reason with them": "openSea_start"}, "consequences": {}, "media": []}, "ghostShip": {"description": "Through the mist and fog, a ghostly silhouette emerges. A ship, its sails in tatters, manned by skeletal figures. This... is the ghost ship.", "choices": ["board the ghost ship", "flee"], "transitions": {"board the ghost ship": "ghostShip_deck", "flee": "openSea_start"}, "media": []}}, "ghostShip": {"deck": {"description": "You step aboard the ghost ship, the air heavy with the stench of decay. Eerie silence hangs over the vessel.", "choices": ["explore the ship", "search for the captain's quarters"], "transitions": {"explore the ship": "ghostShip_brig", "search for the captain's quarters": "ghostShip_quarters"}, "media": []}, "brig": {"description": "You stumble upon the ship's brig, its cells still occupied by skeletal remains shackled to the walls.", "choices": ["examine the skeletons", "leave the brig"], "transitions": {"examine the skeletons": "ghostShip_clue", "leave the brig": "ghostShip_deck"}, "media": []}, "quarters": {"description": "The captain's quarters are in disarray, as if a struggle took place. A tattered journal lies open on the floor.", "choices": ["read the journal", "search for the map"], "transitions": {"read the journal": "ghostShip_clue", "search for the map": "ghostShip_treasure"}, "media": []}, "clue": {"description": "You piece together clues from the journal and the skeletons. The ghost ship's captain hid the real treasure map to protect it from mutineers.", "choices": ["continue searching"], "transitions": {"continue searching": "ghostShip_treasure"}, "media": []}, "treasure": {"description": "After careful searching, you find a hidden compartment behind a portrait. Inside, a gleaming treasure chest awaits!", "choices": ["open the chest", "take the chest and flee"], "transitions": {"open the chest": "ghostShip_victory", "take the chest and flee": "openSea_return"}, "consequences": {}, "media": []}, "victory": {"description": "The treasure chest overflows with gold, jewels, and artifacts beyond your wildest dreams! You've found the legendary pirate treasure!", "choices": ["claim your prize and set sail"], "transitions": {"claim your prize and set sail": "openSea_return"}, "consequences": {}, "media": []}}, "openSea_return": {"return": {"description": "With the treasure safely stowed on your own ship, you set sail for home, leaving the ghost ship to its eternal slumber.", "choices": ["sail into the sunset"], "transitions": {"sail into the sunset": "end_end"}, "media": []}}, "end": {"end": {"description": "The sun dips below the horizon, painting the sky in hues of gold and crimson. You've cheated death, outsmarted ghosts, and claimed a fortune in pirate treasure. Your legend will be sung in taverns for centuries to come.", "choices": [], "transitions": {}, "media": []}}} + +The user will provide a theme + +``` + +### WFStage2prompt2 +``` +Please use the below config as a template to write a story in config format (dont use the template values like masterlocation1): + +{ + "masterlocation1": { + "location1": { + "description": "UI - Experience Bar", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": [], + "developernotes": [] + }, + + There should be 3 master and 10 total sublocations. + The theme is provided by the user + +``` + +### MoreRobustWFStage2prompt +``` +We need to brainstorm how to fit this shape into into JSON format I will provide below. + +Shape = Use this to write a story and explain how it represents the structure - + +graph TD +subgraph "Actual Timeline" +A1[Location 1 - First Visit] +A2[Location 2 - First Visit] +A3[Location 3 - First Visit] +A4[Location 4 - First Visit] +A1 --> A2 --> A3 --> A4 +end +subgraph "Narrative Timeline" +N1[Location 2 - Story Event 1] +N2[Location 4 - Story Event 2] +N3[Location 1 - Story Event 3] +N4[Location 3 - Story Event 4] +N1 --> N2 --> N3 --> N4 +end +subgraph "Hidden Timeline" +H1[Hidden Event 1] +H2[Hidden Event 2] +H3[Hidden Event 3] +H4[Hidden Event 4] +end +A1 -.-> N3 +A2 -.-> N1 +A3 -.-> N4 +A4 -.-> N2 +H1 -.- A1 +H1 -.- A2 +H1 -.- A3 +H1 -.- A4 +H2 -.- A1 +H2 -.- A2 +H2 -.- A3 +H2 -.- A4 +H3 -.- A1 +H3 -.- A2 +H3 -.- A3 +H3 -.- A4 +H4 -.- A1 +H4 -.- A2 +H4 -.- A3 +H4 -.- A4 + + +JSON format = Please use the below config as a template to write a story in config format (dont use the template values like masterlocation1): + +{ + "masterlocation1": { + "location1": { + "description": "UI - Experience Bar", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": [], + "developernotes": [] + }, + + +Loading Code (Dont make media for now) (Avoid Syntax errors when writing the config so the parser loads it) = +class GameSession: + def __init__(self, starting_location='village', starting_state='start'): + self.player = Player() + self.current_location = starting_location + self.current_state = starting_state + self.game_log = [] + + def make_choice(self, choice_index): + state = all_states[self.current_location][self.current_state] + if 0 <= choice_index < len(state['choices']): + choice = state['choices'][choice_index] + next_state = state['transitions'][choice] + + self.game_log.append(f"You chose: {choice}") + self.game_log.append(state['description']) + + if 'consequences' in state and choice in state['consequences']: + if state['consequences'][choice]: + state['consequences'][choice](self.player) + else: + # Handle empty consequence, e.g., log a message or provide a default action + print(f"No consequence for choice: {choice}") + # You can add any default action here if needed + + if '_' in next_state: + self.current_location, self.current_state = next_state.split('_') + else: + self.current_state = next_state + + return self.get_current_state_info() + else: + return "Invalid choice. Please try again." + + def get_current_state_info(self): + state = all_states[self.current_location][self.current_state] + choices = [f"{idx + 1}. {choice}" for idx, choice in enumerate(state['choices'])] + return state['description'], choices, " +".join(self.game_log) + + def get_current_state_media(self): + media = all_states[self.current_location][self.current_state]['media'] + return media + +``` + +### MoreRobustWFStage2promptwithExample +``` + +Below is a config based on the mermaid below as a template - You must make it more of a game experience for the user + +{ + "actualtimeline": { + "A1": { + "description": "You arrive at the Windfall Orchard. Soft breezes carry whispers of hidden stories.", + "events": [ + "arrived" + ], + "choices": [ + "continue actual", + "explore orchard story (N3)", + "investigate orchard hidden (H1)" + ], + "transitions": { + "continue actual": "actualtimeline_A2", + "explore orchard story (N3)": "narrativetimeline_N3", + "investigate orchard hidden (H1)": "hiddentimeline_H1" + }, + "media": [], + "developernotes": [ + "Location 1 in Actual Timeline", + "Connects to Narrative N3 and Hidden H1" + ] + }, + "A2": { + "description": "You enter the Misty Bazaar, abuzz with trade and secrets.", + "events": [ + "traveled", + "observed" + ], + "choices": [ + "continue actual", + "uncover bazaar story (N1)", + "probe bazaar hidden (H2)" + ], + "transitions": { + "continue actual": "actualtimeline_A3", + "uncover bazaar story (N1)": "narrativetimeline_N1", + "probe bazaar hidden (H2)": "hiddentimeline_H2" + }, + "media": [], + "developernotes": [ + "Location 2 in Actual Timeline", + "Connects to Narrative N1 and Hidden H2" + ] + }, + "A3": { + "description": "You stand upon the Northern Cliffs, where distant waves roar with mystery.", + "events": [ + "arrived", + "reflected" + ], + "choices": [ + "continue actual", + "witness cliffside story (N4)", + "examine cliffside hidden (H3)" + ], + "transitions": { + "continue actual": "actualtimeline_A4", + "witness cliffside story (N4)": "narrativetimeline_N4", + "examine cliffside hidden (H3)": "hiddentimeline_H3" + }, + "media": [], + "developernotes": [ + "Location 3 in Actual Timeline", + "Connects to Narrative N4 and Hidden H3" + ] + }, + "A4": { + "description": "At last, you reach the Heartwood Temple. An otherworldly calm envelops you.", + "events": [ + "approached", + "pondered" + ], + "choices": [ + "loop actual", + "unearth temple story (N2)", + "search temple hidden (H4)" + ], + "transitions": { + "loop actual": "actualtimeline_A4", + "unearth temple story (N2)": "narrativetimeline_N2", + "search temple hidden (H4)": "hiddentimeline_H4" + }, + "media": [], + "developernotes": [ + "Location 4 in Actual Timeline", + "Connects to Narrative N2 and Hidden H4", + "Loops to itself for demonstration but typically you'd end or branch elsewhere" + ] + } + }, + "narrativetimeline": { + "N1": { + "description": "Narrative Event 1: At the bazaar, you hear a whispered rumor that sparks your quest.", + "events": [ + "heard", + "questioned" + ], + "choices": [ + "continue narrative", + "return to actual A2", + "peek hidden (H2)" + ], + "transitions": { + "continue narrative": "narrativetimeline_N2", + "return to actual A2": "actualtimeline_A2", + "peek hidden (H2)": "hiddentimeline_H2" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A2" + ] + }, + "N2": { + "description": "Narrative Event 2: At the temple, an important secret is revealed.", + "events": [ + "uncovered" + ], + "choices": [ + "continue narrative", + "return to actual A4", + "peek hidden (H4)" + ], + "transitions": { + "continue narrative": "narrativetimeline_N3", + "return to actual A4": "actualtimeline_A4", + "peek hidden (H4)": "hiddentimeline_H4" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A4" + ] + }, + "N3": { + "description": "Narrative Event 3: At the orchard, a forgotten memory emerges from the past.", + "events": [ + "remembered" + ], + "choices": [ + "continue narrative", + "return to actual A1", + "peek hidden (H1)" + ], + "transitions": { + "continue narrative": "narrativetimeline_N4", + "return to actual A1": "actualtimeline_A1", + "peek hidden (H1)": "hiddentimeline_H1" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A1" + ] + }, + "N4": { + "description": "Narrative Event 4: At the cliffside, you discover the final clue of this storyline.", + "events": [ + "realized" + ], + "choices": [ + "finish narrative", + "return to actual A3", + "peek hidden (H3)" + ], + "transitions": { + "finish narrative": "narrativetimeline_N4", + "return to actual A3": "actualtimeline_A3", + "peek hidden (H3)": "hiddentimeline_H3" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A3", + "Finishes or loops for demonstration" + ] + } + }, + "hiddentimeline": { + "H1": { + "description": "Hidden Event 1: A mysterious whisper in the orchard hints at something greater.", + "events": [ + "listened" + ], + "choices": [ + "continue hidden", + "return to A1", + "narrative check (N3)" + ], + "transitions": { + "continue hidden": "hiddentimeline_H2", + "return to A1": "actualtimeline_A1", + "narrative check (N3)": "narrativetimeline_N3" + }, + "media": [], + "developernotes": [ + "Hidden event connected to orchard (A1) and story event (N3)" + ] + }, + "H2": { + "description": "Hidden Event 2: Faint footprints in the bazaar's back alley confirm others are guiding your path.", + "events": [ + "noticed" + ], + "choices": [ + "continue hidden", + "return to A2", + "narrative check (N1)" + ], + "transitions": { + "continue hidden": "hiddentimeline_H3", + "return to A2": "actualtimeline_A2", + "narrative check (N1)": "narrativetimeline_N1" + }, + "media": [], + "developernotes": [ + "Hidden event connected to bazaar (A2) and story event (N1)" + ] + }, + "H3": { + "description": "Hidden Event 3: Carved runes under the cliffs suggest all locations are interwoven.", + "events": [ + "deciphered" + ], + "choices": [ + "continue hidden", + "return to A3", + "narrative check (N4)" + ], + "transitions": { + "continue hidden": "hiddentimeline_H4", + "return to A3": "actualtimeline_A3", + "narrative check (N4)": "narrativetimeline_N4" + }, + "media": [], + "developernotes": [ + "Hidden event connected to cliffs (A3) and story event (N4)" + ] + }, + "H4": { + "description": "Hidden Event 4: A final revelation in the temple ties orchard, bazaar, and cliffs to this hallowed ground.", + "events": [ + "comprehended" + ], + "choices": [ + "close hidden", + "return to A4", + "narrative check (N2)" + ], + "transitions": { + "close hidden": "hiddentimeline_H4", + "return to A4": "actualtimeline_A4", + "narrative check (N2)": "narrativetimeline_N2" + }, + "media": [], + "developernotes": [ + "Hidden event connected to temple (A4) and story event (N2)", + "Loops to itself for demonstration of an end state" + ] + } + } +} + +Shape = Use this to write a story and explain how it represents the structure - + +graph TD +subgraph "Actual Timeline" +A1[Location 1 - First Visit] +A2[Location 2 - First Visit] +A3[Location 3 - First Visit] +A4[Location 4 - First Visit] +A1 --> A2 --> A3 --> A4 +end +subgraph "Narrative Timeline" +N1[Location 2 - Story Event 1] +N2[Location 4 - Story Event 2] +N3[Location 1 - Story Event 3] +N4[Location 3 - Story Event 4] +N1 --> N2 --> N3 --> N4 +end +subgraph "Hidden Timeline" +H1[Hidden Event 1] +H2[Hidden Event 2] +H3[Hidden Event 3] +H4[Hidden Event 4] +end +A1 -.-> N3 +A2 -.-> N1 +A3 -.-> N4 +A4 -.-> N2 +H1 -.- A1 +H1 -.- A2 +H1 -.- A3 +H1 -.- A4 +H2 -.- A1 +H2 -.- A2 +H2 -.- A3 +H2 -.- A4 +H3 -.- A1 +H3 -.- A2 +H3 -.- A3 +H3 -.- A4 +H4 -.- A1 +H4 -.- A2 +H4 -.- A3 +H4 -.- A4 + +------ + +Remember to write in same JSON format after analysing the above so I can test the turn by turn transitions + + +``` + +--- + +## Mermaid Diagram Structures + +### examplemermaidconceptblendingstrutures +#### Star Wars Bakery Example +```mermaid +flowchart TD + + A[Bakery] + B[Star Wars] + C[Adjectives] + D[Combinatorics] + E[Seed Ideas] + F[LLM Processing] + + A -- has attributes --> C + B -- has attributes --> C + C -- generates --> D + D -- creates --> E + E -- inputs to --> F + + subgraph Bakery Attributes + direction TB + A1[Sweet] + A2[Warm] + A3[Comforting] + A4[Artisanal] + A -- includes --> A1 + A -- includes --> A2 + A -- includes --> A3 + A -- includes --> A4 + end + + subgraph Star Wars Attributes + direction TB + B1[Galactic] + B2[Epic] + B3[Heroic] + B4[Dramatic] + B -- includes --> B1 + B -- includes --> B2 + B -- includes --> B3 + B -- includes --> B4 + end + + subgraph Combinatorics + direction TB + C1[Sweet Galactic] + C2[Warm Epic] + C3[Comforting Heroic] + C4[Artisanal Dramatic] + D -- generates --> C1 + D -- generates --> C2 + D -- generates --> C3 + D -- generates --> C4 + end + + subgraph Seed Ideas + direction TB + E1[Galactic Pastries] + E2[Epic Bread Battles] + E3[Heroic Baker's Quest] + E4[Dramatic Cake Decor] + E -- includes --> E1 + E -- includes --> E2 + E -- includes --> E3 + E -- includes --> E4 + end +``` + +### mermaidstorystructures +#### Hidden Progression 1 +```mermaid +graph TD + subgraph "Actual Timeline" + A1[Location 1 - First Visit] + A2[Location 2 - First Visit] + A3[Location 3 - First Visit] + A4[Location 4 - First Visit] + A1 --> A2 --> A3 --> A4 + end + subgraph "Narrative Timeline" + N1[Location 2 - Story Event 1] + N2[Location 4 - Story Event 2] + N3[Location 1 - Story Event 3] + N4[Location 3 - Story Event 4] + N1 --> N2 --> N3 --> N4 + end + subgraph "Hidden Timeline" + H1[Hidden Event 1] + H2[Hidden Event 2] + H3[Hidden Event 3] + H4[Hidden Event 4] + end + A1 -.-> N3 + A2 -.-> N1 + A3 -.-> N4 + A4 -.-> N2 + H1 -.- A1 + H1 -.- A2 + H1 -.- A3 + H1 -.- A4 + H2 -.- A1 + H2 -.- A2 + H2 -.- A3 + H2 -.- A4 + H3 -.- A1 + H3 -.- A2 + H3 -.- A3 + H3 -.- A4 + H4 -.- A1 + H4 -.- A2 + H4 -.- A3 + H4 -.- A4 +``` + +#### Hidden Progression 2 +```mermaid +graph LR + subgraph "Actual Timeline" + A1[Location 1 - First Visit] + A2[Location 2 - First Visit] + A3[Location 3 - First Visit] + A4[Location 4 - First Visit] + A1 --> A2 --> A3 --> A4 + end + subgraph "Narrative Timeline" + N1[Location 2 - Story Event 1] + N2[Location 4 - Story Event 2] + N3[Location 1 - Story Event 3] + N4[Location 3 - Story Event 4] + N1 --> N2 --> N3 --> N4 + end + subgraph "Hidden Timeline" + H1[Hidden Event 1] + H2[Hidden Event 2] + H3[Hidden Event 3] + H4[Hidden Event 4] + H1 --> H2 --> H3 --> H4 + end + A1 -.-> N3 + A2 -.-> N1 + A3 -.-> N4 + A4 -.-> N2 + H1 -.- N1 + H2 -.- N2 + H3 -.- N3 + H4 -.- N4 +``` + +#### Hidden Progression 3 +```mermaid +graph LR + subgraph "Actual Timeline" + A1[Location 1 - First Visit] + A2[Location 2 - First Visit] + A3[Location 3 - First Visit] + A4[Location 4 - First Visit] + A1 --> A2 --> A3 --> A4 + end + subgraph "Narrative Timeline" + N1[Location 2 - Story Event 1] + N2[Location 4 - Story Event 2] + N3[Location 1 - Story Event 3] + N4[Location 3 - Story Event 4] + N1 --> N2 --> N3 --> N4 + end + A1 -.-> N3 + A2 -.-> N1 + A3 -.-> N4 + A4 -.-> N2 +``` + +#### Parallel Storylines Progression +```mermaid +graph TD + A[Start] --> B[Character 1 Introduction] + A --> C[Character 2 Introduction] + B --> D[Character 1 Event 1] + C --> E[Character 2 Event 1] + D --> F[Character 1 Event 2] + E --> G[Character 2 Event 2] + F --> H[Characters Meet] + G --> H + H --> I[Climax] + I --> J[Resolution] + +``` + + +--- + +## Config Improvement Examples + +### originalconfigatbeinningofthisspace +```json +{ + "village": { + "start": { + "description": "You wake up in a small village. You hear a rumor about a lost treasure.", + "choices": ['explore village', 'gather supplies', 'rest'], + "transitions": {'explore village': 'village_rumor', 'gather supplies': 'village_supplies', 'rest': 'village_start'}, + "consequences": { + 'gather supplies': lambda player: player.add_item('basic supplies') + }, + "media": [] + }, + "rumor": { + "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", + "choices": ['decide to go', 'ignore'], + "transitions": {'decide to go': 'village_supplies', 'ignore': 'village_start'}, + "consequences": { + 'decide to go': lambda player: player.update_knowledge('treasure location') + }, + "media": [] + }, + "supplies": { + "description": "You gather supplies for your journey.", + "choices": ['head to forest', 'stay in village'], + "transitions": {'head to forest': 'forest_forest', 'stay in village': 'village_start'}, + "media": [] + }, + }, + "forest": { + "forest": { + "description": "You enter the dense forest, heading towards the ruins.", + "choices": ['travel further', 'return to village'], + "transitions": {'travel further': 'ruins_ruins', 'return to village': 'village_start'}, + "media": [] + }, + }, + "ruins": { + "ruins": { + "description": "You reach the ancient ruins. The entrance is dark and eerie.", + "choices": ['enter ruins', 'return to forest'], + "transitions": {'enter ruins': 'ruins_explore', 'return to forest': 'forest_forest'}, + "media": [] + }, + "explore": { + "description": "You explore the ruins, encountering traps and puzzles.", + "choices": ['solve puzzle', 'avoid traps'], + "transitions": {'solve puzzle': 'ruins_hiddenPassage', 'avoid traps': 'ruins_ruins'}, + "media": [] + }, + "hiddenPassage": { + "description": "You solve a challenging puzzle and unlock a hidden passage.", + "choices": ['enter passage', 'go back'], + "transitions": {'enter passage': 'ruins_treasureRoom', 'go back': 'ruins_explore'}, + "media": [] + }, + "treasureRoom": { + "description": "You enter the treasure room and find the treasure chest.", + "choices": ['take treasure', 'leave'], + "transitions": {'take treasure': 'ruins_celebrate', 'leave': 'ruins_ruins'}, + "consequences": { + 'take treasure': lambda player: player.add_item('treasure') + }, + "media": [] + }, + "celebrate": { + "description": "You celebrate your discovery and decide to bring the treasure back to the village.", + "choices": ['return to village'], + "transitions": {'return to village': 'village_return_return'}, + "media": [] + }, + }, + "village_return": { + "return": { + "description": "You return to the village with the treasure and share it with the villagers.", + "choices": ['end adventure'], + "transitions": {'end adventure': 'end_end'}, + "media": [] + }, + }, + "end": { + "end": { + "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", + "choices": [], + "transitions": {}, + "media": [] + }, + } +} +``` + +### claude3_5_06072024configtips +``` +Example Config Development Tab - Claude 3.5 Sonnet as the LLM Assitant + +This config is for a basic text based game engine I made with help many llms including you. +Thanks to all the help it loads which presented another problem, I dont have any structural metrics to assess the quality of the config. +What JSON things can we look at to see if it may be too bland for a person testing the game + + +Text-Based Game Configuration Analysis +1. State Variety + +Total number of states: 13 +Number of unique locations: 5 (village, forest, ruins, village_return, end) + +2. Choice Diversity + +Average choices per state: 1.92 +States with no choices: 1 (end state) +States with most choices: 3 (village_start with 3 choices) +Unique choices across all states: 17 + +3. Transition Complexity + +Total number of transitions: 25 +Average transitions per state: 1.92 +States with most transitions: 3 (village_start with 3 transitions) + +4. Consequence Implementation + +States with consequences: 3 +Total number of consequences: 3 +Types of consequences: Adding items, Updating knowledge + +5. Description Length + +Average description length: 13.15 words +Longest description: 24 words (village_return) +Shortest description: 4 words (village_supplies) + +6. Cyclic Paths + +Presence of loops: Yes (e.g., village_start -> village_supplies -> village_start) + +7. Dead Ends + +Number of dead ends: 1 (end state) + +8. Media Integration + +States with media: 0 +Total media elements: 0 + +9. Narrative Progression + +Clear beginning (village_start) and end (end) states +Multiple paths to reach the end + +10. Player Agency + +Choices affect game progression +Some choices have consequences (3 instances) + +11. Game Length + +Minimum steps to complete: 6 (village_start -> village_supplies -> forest_forest -> ruins_ruins -> ruins_explore -> ruins_hiddenPassage -> ruins_treasureRoom -> ruins_celebrate -> village_return -> end) +Maximum steps (including loops): Potentially infinite due to cycles + +12. Replayability + +Multiple paths through the game +Some variability in outcomes based on choices + +#============================================= + +To assess the quality and potential blandness of your game configuration, we can analyze several structural metrics within the JSON. Here are some aspects we can examine: +Text-Based Game Configuration AnalysisClick to open document +Based on this analysis, here are some observations about the potential blandness of the game: + +Limited state variety: With only 13 states across 5 locations, the game might feel short and lacking in diversity. Consider adding more locations or states within existing locations. +Low average choices: An average of 1.92 choices per state might make the game feel linear. Increasing the number of choices in more states could improve player engagement. +Limited consequences: Only 3 states have consequences, which might make player choices feel less impactful. Adding more consequences could increase the sense of agency. +Short descriptions: The average description length of 13.15 words might not provide enough detail to immerse players. Consider expanding descriptions to create a richer narrative. +Lack of media: No states currently use media elements, which could make the game feel less engaging. Adding images, sound effects, or other media could enhance the player experience. +Limited narrative branching: While there are some loops and choices, the overall structure is relatively linear. Adding more branching paths could increase replayability and player interest. + +To make the game less bland, consider: + +Adding more states and locations +Increasing the number of choices in each state +Implementing more consequences for player actions +Expanding descriptions to create a richer narrative +Incorporating media elements +Creating more diverse paths through the game + +#=============================================== + + +# Text-Based Game Improvement Suggestions + +## 1. Expand State Variety +- Add a new location: "Mountain Pass" + - Include states like "steep_climb", "avalanche_risk", "hidden_cave" +- Expand the "forest" location: + - Add states like "dense_thicket", "mysterious_clearing", "ancient_tree" + +## 2. Increase Choice Diversity +- Add more choices to existing states. For example, in "village_start": + - "talk to elder" + - "visit market" + - "investigate strange noises" +- Ensure most states have at least 3 choices + +## 3. Enhance Transition Complexity +- Create more interconnected states. For example: + - Allow travel between "forest" and "mountain_pass" + - Add shortcuts or secret passages between non-adjacent locations + +## 4. Implement More Consequences +- Add consequences to more choices. For example: + - In "forest_forest": + - "forage for food" -> `lambda player: player.add_item('wild berries')` + - "take shortcut" -> `lambda player: player.update_status('lost')` + +## 5. Enrich Descriptions +- Expand existing descriptions with more vivid details +- Example for "village_start": + ```python + "description": "You wake up in a small village nestled between rolling hills. The air is filled with the scent of freshly baked bread and the sound of a distant waterfall. Villagers bustle about, whispering excitedly about a rumor of lost treasure." + ``` + +## 6. Introduce Cyclic Content with Variations +- Add time-based changes to revisited locations +- Example: + ```python + 'village_start': { + "description": lambda player: f"You return to the village square. It's now {player.time_of_day()}. " + + ("The market is bustling with activity." if player.time_of_day() == "day" else + "The streets are quiet, lit by flickering torches.") + } + ``` + +## 7. Create Meaningful Dead Ends +- Add optional quests or side adventures +- Example: "help_lost_child" state in the village that doesn't progress main story but provides rewards + +## 8. Integrate Media Elements +- Add placeholder media to enhance immersion: + ```python + "media": [ + {"type": "image", "url": "/api/placeholder/400/300", "alt": "A picturesque village at dawn"}, + {"type": "sound", "url": "village_ambience.mp3"} + ] + ``` + +## 9. Enhance Narrative Progression +- Add a main quest storyline with multiple stages +- Introduce side quests for additional depth + +## 10. Increase Player Agency +- Add more impactful choices that significantly alter the game's direction +- Implement a simple inventory or skill system that affects available choices + +## 11. Extend Game Length +- Add more locations and states to create a longer, more involved adventure +- Implement quest chains that span multiple locations + +## 12. Improve Replayability +- Add random events that can occur in each location +- Implement multiple endings based on player choices and achievements + + +#=============================================== + +# Updated Text-Based Game Configuration with Side Quest + +## New State: help_lost_child + +Add this new state to the 'village' section of your `all_states` dictionary: + +```python +'village': { + # ... existing states ... + + 'help_lost_child': { + "description": "You encounter a crying child in the village square. They seem to have lost their way home.", + "choices": ['help child', 'ignore and continue'], + "transitions": { + 'help child': 'village_child_quest', + 'ignore and continue': 'village_start' + }, + "consequences": { + 'help child': lambda player: player.update_status('helping_child') + }, + "media": [] + }, + 'child_quest': { + "description": "You decide to help the lost child. After asking around and searching the village, you finally locate their home.", + "choices": ['return child', 'ask for reward'], + "transitions": { + 'return child': 'village_child_reunion', + 'ask for reward': 'village_child_reward' + }, + "media": [] + }, + 'child_reunion': { + "description": "You return the child to their grateful parents. They thank you profusely for your kindness.", + "choices": ['accept thanks', 'request reward'], + "transitions": { + 'accept thanks': 'village_start', + 'request reward': 'village_child_reward' + }, + "consequences": { + 'accept thanks': lambda player: player.update_status('local_hero') + }, + "media": [] + }, + 'child_reward': { + "description": "The child's parents offer you a small pouch of coins as a reward for your help.", + "choices": ['accept reward', 'decline reward'], + "transitions": { + 'accept reward': 'village_start', + 'decline reward': 'village_start' + }, + "consequences": { + 'accept reward': lambda player: player.add_item('coin pouch'), + 'decline reward': lambda player: player.update_status('selfless_hero') + }, + "media": [] + }, +} +``` + +## Updated Transitions + +To incorporate this side quest into the existing game flow, we need to update some transitions in the 'village_start' state: + +```python +'village': { + 'start': { + "description": "You're in the village square. The usual bustle of village life surrounds you.", + "choices": ['explore village', 'gather supplies', 'rest', 'look around'], # Added 'look around' + "transitions": { + 'explore village': 'village_rumor', + 'gather supplies': 'village_supplies', + 'rest': 'village_start', + 'look around': 'village_help_lost_child' # New transition + }, + "consequences": { + 'gather supplies': lambda player: player.add_item('basic supplies') + }, + "media": [] + }, + # ... other states ... +} +``` + +## Explanation of Changes + +1. New States: + - `help_lost_child`: Introduces the side quest + - `child_quest`: Represents the process of helping the child + - `child_reunion`: Concludes the quest if you don't ask for a reward + - `child_reward`: Offers a choice to accept or decline a reward + +2. Updated `village_start`: + - Added a new choice: 'look around' + - Added a new transition to `village_help_lost_child` + +3. Consequences: + - Added status updates: 'helping_child', 'local_hero', 'selfless_hero' + - Added item reward: 'coin pouch' + +These changes create a meaningful dead end by: +1. Providing an optional quest that doesn't directly progress the main treasure hunt story +2. Offering multiple choices within the side quest, allowing for different outcomes +3. Implementing consequences that affect the player's status or inventory, providing rewards for engagement +4. Allowing the player to return to the main story flow after completing (or ignoring) the side quest + +This side quest adds depth to the village location, gives the player more to do, and provides character-building opportunities without derailing the main narrative. It also demonstrates how you can implement similar side quests or meaningful dead ends in other locations of your game. + +#================================================================================ + +Certainly! Let's implement the "help_lost_child" side quest as a meaningful dead end in the village. This will add depth to the game without necessarily progressing the main story. Here's how we can incorporate this into your current configuration: +Updated Text-Based Game Configuration with Side QuestClick to open document +To further improve this side quest and make it even more engaging, you could consider: + +Time sensitivity: Make the quest available only at certain times or for a limited number of turns. +Skill checks: If your game has a skill system, you could add checks for persuasion or perception that affect the outcome. +Long-term consequences: The status changes (like 'local_hero') could affect how NPCs interact with the player later in the game. +Branching outcomes: You could add more complex branches, such as the child being in actual danger, leading to a mini-adventure. + +Would you like me to elaborate on any of these ideas or show how to implement them in your configuration? + + +``` + +### tipsupdatedconfigatbeinningofthisspace +```json +{ + "village": { + "start": { + "description": "You wake up in a small village. You hear a rumor about a lost treasure.", + "choices": ['explore village', 'gather supplies', 'rest', 'look around'], + "transitions": {'explore village': 'village_rumor', 'gather supplies': 'village_supplies', 'rest': 'village_start', 'look around': 'village_help_lost_child'}, + "consequences": { + 'gather supplies': lambda player: player.add_item('basic supplies') + }, + "media": [] + }, + "rumor": { + "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", + "choices": ['decide to go', 'ignore'], + "transitions": {'decide to go': 'village_supplies', 'ignore': 'village_start'}, + "consequences": { + 'decide to go': lambda player: player.update_knowledge('treasure location') + }, + "media": [] + }, + "supplies": { + "description": "You gather supplies for your journey.", + "choices": ['head to forest', 'stay in village'], + "transitions": {'head to forest': 'forest_forest', 'stay in village': 'village_start'}, + "media": [] + }, + "help_lost_child": { + "description": "You encounter a crying child in the village square. They seem to have lost their way home.", + "choices": ['help child', 'ignore and continue'], + "transitions": { + 'help child': 'village_child_quest', + 'ignore and continue': 'village_start' + }, + "consequences": { + 'help child': lambda player: player.update_status('helping_child') + }, + "media": [] + }, + "child_quest": { + "description": "You decide to help the lost child. After asking around and searching the village, you finally locate their home.", + "choices": ['return child', 'ask for reward'], + "transitions": { + 'return child': 'village_child_reunion', + 'ask for reward': 'village_child_reward' + }, + "media": [] + }, + "child_reunion": { + "description": "You return the child to their grateful parents. They thank you profusely for your kindness.", + "choices": ['accept thanks', 'request reward'], + "transitions": { + 'accept thanks': 'village_start', + 'request reward': 'village_child_reward' + }, + "consequences": { + 'accept thanks': lambda player: player.update_status('local_hero') + }, + "media": [] + }, + "child_reward": { + "description": "The child's parents offer you a small pouch of coins as a reward for your help.", + "choices": ['accept reward', 'decline reward'], + "transitions": { + 'accept reward': 'village_start', + 'decline reward': 'village_start' + }, + "consequences": { + 'accept reward': lambda player: player.add_item('coin pouch'), + 'decline reward': lambda player: player.update_status('selfless_hero') + }, + "media": [] + }, + }, + "forest": { + "forest": { + "description": "You enter the dense forest, heading towards the ruins.", + "choices": ['travel further', 'return to village'], + "transitions": {'travel further': 'ruins_ruins', 'return to village': 'village_start'}, + "media": [] + }, + }, + "ruins": { + "ruins": { + "description": "You reach the ancient ruins. The entrance is dark and eerie.", + "choices": ['enter ruins', 'return to forest'], + "transitions": {'enter ruins': 'ruins_explore', 'return to forest': 'forest_forest'}, + "media": [] + }, + "explore": { + "description": "You explore the ruins, encountering traps and puzzles.", + "choices": ['solve puzzle', 'avoid traps'], + "transitions": {'solve puzzle': 'ruins_hiddenPassage', 'avoid traps': 'ruins_ruins'}, + "media": [] + }, + "hiddenPassage": { + "description": "You solve a challenging puzzle and unlock a hidden passage.", + "choices": ['enter passage', 'go back'], + "transitions": {'enter passage': 'ruins_treasureRoom', 'go back': 'ruins_explore'}, + "media": [] + }, + "treasureRoom": { + "description": "You enter the treasure room and find the treasure chest.", + "choices": ['take treasure', 'leave'], + "transitions": {'take treasure': 'ruins_celebrate', 'leave': 'ruins_ruins'}, + "consequences": { + 'take treasure': lambda player: player.add_item('treasure') + }, + "media": [] + }, + 'celebrate': { + "description": "You celebrate your discovery and decide to bring the treasure back to the village.", + "choices": ['return to village'], + "transitions": {'return to village': 'village_return_return'}, + "media": [] + }, + }, + "village_return": { + "return": { + "description": "You return to the village with the treasure and share it with the villagers.", + "choices": ['end adventure'], + "transitions": {'end adventure': 'end_end'}, + "media": [] + }, + }, + "end": { + "end": { + "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", + "choices": [], + "transitions": {}, + "media": [] + }, + } +} +``` + +--- + +## Platform Export Templates + +### ExampleTwineFileStructureasInitialPrompt +``` +:: StoryData +{ + "ifid": "12345678-9abc-def0-1234-56789abcdef0", + "format": "SugarCube", + "format-version": "2.35.0", + "start": "Startup" +} + +:: Startup [startup] +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> + +Welcome to the story! +This time, your characters **stats** will matter. Your decisions will affect these metrics, which will shape the final outcome. + +[[Choose Your Role|Choose Role]] + +:: Choose Role +You can experience the upcoming events as one of three roles: + +* [[Be a Guard|SetGuard]] +* [[Be a Merchant|SetMerchant]] +* [[Be a Villager|SetVillager]] + +:: SetGuard +<> +<> +<> +You don the Guards mantle. Your starting stats: +**Reputation:** 5 +**Authority:** 5 +<> + +:: SetMerchant +<> +<> +<> +You take the role of a Merchant. Your starting stats: +**Wealth:** 10 +**Security:** 3 +<> + +:: SetVillager +<> +<> +<> +You become a Villager. Your starting stats: +**Knowledge:** 2 +**Trust:** 5 +<> + +:: Market Riot +The marketplace is in chaos. Shouts and screams echo between the stalls. + +<> +You see your fellow guards struggling to hold the line. This is your moment to either prove your dedication or risk your standing. +**Current Stats:** Reputation: $guardReputation, Authority: $guardAuthority +<> + +<> +Overturned carts and scattered goods mean losses. How you respond could preserve or squander your wealth and security. +**Current Stats:** Wealth: $merchantWealth, Security: $merchantSecurity +<> + +<> +Your neighbors panic. How you handle this crisis could increase your knowledge of the towns issues or build trust among the people. +**Current Stats:** Knowledge: $villagerKnowledge, Trust: $villagerTrust +<> + +[[Observe more details|Market Details]] +[[View Stats|Stats]] + +:: Market Details +The riot intensifies. Guards form a barrier, vendors cry for help, and villagers seek shelter. + +<> +A suspicious figure darts behind a stall. You can: +* [[Pursue the figure|GuardAction_Pursue]] +* [[Hold the line|GuardAction_Hold]] +<> + +<> +Your ledger is underfoot. +* [[Retrieve the ledger (risking harm)|MerchantAction_Retrieve]] +* [[Seek safety and abandon the ledger|MerchantAction_Safety]] +<> + +<> +Rumors swirl about corrupt officials. +* [[Listen and gather info|VillagerAction_Listen]] +* [[Calm the crowd and dispel rumors|VillagerAction_Calm]] +<> + +[[View Stats|Stats]] + +:: GuardAction_Pursue +<> +<> +<> +You chase the suspicious figure, boosting your reputation but weakening your authority for leaving your post. +[[Next|Market Aftermath]] + +:: GuardAction_Hold +<> +<> +<> +You hold the line, reinforcing your authority but losing a bit of daring reputation. +[[Next|Market Aftermath]] + +:: MerchantAction_Retrieve +<> +<> +<> +You dive into danger, salvaging your ledger (+Wealth) but feeling less secure. +[[Next|Market Aftermath]] + +:: MerchantAction_Safety +<> +<> +<> +You choose personal safety over profit, losing some wealth but feeling more secure. +[[Next|Market Aftermath]] + +:: VillagerAction_Listen +<> +<> +<> +You gather information quietly, increasing knowledge but losing a bit of trust. +[[Next|Market Aftermath]] + +:: VillagerAction_Calm +<> +<> +<> +You try to calm everyone, increasing trust at the expense of missing out on details. +[[Next|Market Aftermath]] + +:: Market Aftermath +As the riot disperses, the final scene reflects both your role and your stats: + +<> +**Final Stats:** Reputation: $guardReputation, Authority: $guardAuthority + +<> +You found clues about instigators. Your higher reputation means the captain respects your initiative, even if your authority slipped. +<> +You maintained order. Solid authority assures your superiors youre reliable, though less bold than some. +<> +<> + +<> +**Final Stats:** Wealth: $merchantWealth, Security: $merchantSecurity + +<> +Your ledger ensures long-term financial potential despite feeling more vulnerable now. +<> +Youre poorer but safer, alive to rebuild another day. +<> +<> + +<> +**Final Stats:** Knowledge: $villagerKnowledge, Trust: $villagerTrust + +<> +With greater knowledge, youre poised to influence future events, though some neighbors mistrust your quiet gathering of intel. +<> +Higher trust ensures your opinions hold weight in the community, even if you know fewer specifics. +<> +<> + +**Try playing again to see how different choices shape your stats and outcomes.** + +[[Restart|Startup]] +[[View Stats|Stats]] + +:: Stats +**Your Stats:** + +<>> +- Reputation: $guardReputation +- Authority: $guardAuthority +<> + +<>> +- Wealth: $merchantWealth +- Security: $merchantSecurity +<> + +<>> +- Knowledge: $villagerKnowledge +- Trust: $villagerTrust +<> + +[[Return|Market Aftermath]] + +``` diff --git a/config_linting.py b/config_linting.py new file mode 100644 index 0000000000000000000000000000000000000000..917fdbf81c3e7edf14ca5f8cda08702ceb3572d1 --- /dev/null +++ b/config_linting.py @@ -0,0 +1,1256 @@ +""" +Config Linting / Story Health Validation + +Automated checks for game config quality: +- Branching validation (graph integrity) +- Story flags (potential issues) +- Mermaid diagram generation +""" + +import json +from collections import deque + + +def parse_config(config_input): + """Parse config from string or dict, handle wrapper keys.""" + if isinstance(config_input, str): + try: + data = json.loads(config_input) + except json.JSONDecodeError as e: + return None, f"JSON Parse Error: {e}" + else: + data = config_input + + # Handle wrapper keys (masterlocation1, etc.) + if len(data) == 1: + key = list(data.keys())[0] + if isinstance(data[key], dict) and 'end' in data[key]: + return data[key], None + + return data, None + + +def flatten_nested_config(data): + """ + Flatten a nested location->state config into flat state dict. + + Detects if config is nested (location contains states with 'description') + vs flat (states directly have 'description'). + + Returns: (flattened_dict, is_nested) + """ + if not data or not isinstance(data, dict): + return data, False + + # Check if this is a nested structure + # A nested structure has: location -> state -> {description, choices, transitions} + # A flat structure has: state -> {description, choices, transitions} + + first_key = list(data.keys())[0] + first_value = data[first_key] + + if not isinstance(first_value, dict): + return data, False + + # If the first value has 'description', it's flat + if 'description' in first_value: + return data, False + + # Check if first value contains dicts with 'description' (nested) + for inner_key, inner_value in first_value.items(): + if isinstance(inner_value, dict) and 'description' in inner_value: + # This is nested - flatten it + flattened = {} + for location, states in data.items(): + if isinstance(states, dict): + for state_name, state_data in states.items(): + if isinstance(state_data, dict) and 'description' in state_data: + flat_key = f"{location}_{state_name}" + flattened[flat_key] = state_data + return flattened, True + + # Not clearly nested, return as-is + return data, False + + +def validate_branching(config_input): + """ + Validate config branching/graph integrity. + + Returns dict with: + - errors: Critical issues (broken transitions) + - warnings: Potential issues (orphaned states, dead ends) + - stats: Metrics about the config + """ + data, error = parse_config(config_input) + if error: + return {"errors": [error], "warnings": [], "stats": {}} + + # Flatten nested configs (location -> state -> data) + data, is_nested = flatten_nested_config(data) + + errors = [] + warnings = [] + + all_states = set(data.keys()) + + # Track which states are targeted by transitions + targeted_states = set() + + for state_name, state_data in data.items(): + if not isinstance(state_data, dict): + continue + + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + # Check: All transition targets exist + for choice, target in transitions.items(): + # Handle nested location_state format + if '_' in target and target not in all_states: + # Try parsing as location_state + parts = target.rsplit('_', 1) + if len(parts) == 2: + location, inner_state = parts + if location in all_states: + loc_data = data.get(location, {}) + if isinstance(loc_data, dict) and inner_state in loc_data: + targeted_states.add(location) + continue + + errors.append(f"'{state_name}': transition '{choice}' -> '{target}' (target doesn't exist)") + else: + if target in all_states: + targeted_states.add(target) + else: + errors.append(f"'{state_name}': transition '{choice}' -> '{target}' (target doesn't exist)") + + # Check: Choices match transition keys + choice_set = set(choices) if choices else set() + transition_keys = set(transitions.keys()) + + missing_transitions = choice_set - transition_keys + extra_transitions = transition_keys - choice_set + + if missing_transitions: + errors.append(f"'{state_name}': choices without transitions: {missing_transitions}") + if extra_transitions: + warnings.append(f"'{state_name}': transitions without choices: {extra_transitions}") + + # Find orphaned states (not targeted by any transition, except start states) + # Assume first state or common names are start states + start_candidates = {'start', 'intro', 'beginning', 'arrival', 'location1'} + first_state = list(data.keys())[0] if data else None + + orphaned = all_states - targeted_states - {'end'} - start_candidates + if first_state: + orphaned.discard(first_state) + + if orphaned: + warnings.append(f"Potentially orphaned states (unreachable): {orphaned}") + + # Find dead ends (no transitions out, excluding valid ending states) + dead_ends = [] + valid_endings = [] + for state_name, state_data in data.items(): + if not isinstance(state_data, dict): + continue + transitions = state_data.get('transitions', {}) + if not transitions: + # Check if this looks like an intentional ending + state_lower = state_name.lower() + is_ending = ( + state_lower == 'end' or + state_lower.startswith('end_') or + state_lower.startswith('ending_') or + '_end' in state_lower or + '_ending' in state_lower or + 'finale' in state_lower or + 'conclusion' in state_lower + ) + if is_ending: + valid_endings.append(state_name) + else: + dead_ends.append(state_name) + + if dead_ends: + warnings.append(f"Dead-end states (no exits): {dead_ends}") + + # Calculate stats + stats = { + "total_states": len(all_states), + "valid_endings": len(valid_endings), + "reachable_states": len(targeted_states) + 1, # +1 for start + "orphaned_count": len(orphaned), + "dead_end_count": len(dead_ends), + "total_transitions": sum( + len(d.get('transitions', {})) + for d in data.values() + if isinstance(d, dict) + ), + "total_choices": sum( + len(d.get('choices', [])) + for d in data.values() + if isinstance(d, dict) + ), + } + + if stats["total_states"] > 0: + stats["reachability_pct"] = round( + (stats["reachable_states"] / stats["total_states"]) * 100, 1 + ) + else: + stats["reachability_pct"] = 0 + + return { + "errors": errors, + "warnings": warnings, + "stats": stats + } + + +def generate_story_flags(config_input): + """ + Generate story-level flags/warnings (qualitative hints). + + These are softer warnings about potential narrative issues. + """ + data, error = parse_config(config_input) + if error: + return [f"Cannot analyze: {error}"] + + # Flatten nested configs + data, is_nested = flatten_nested_config(data) + + flags = [] + + # Collect path lengths from each state + state_depths = {} + descriptions = {} + + for state_name, state_data in data.items(): + if not isinstance(state_data, dict): + continue + + desc = state_data.get('description', '') + descriptions[state_name] = desc + choices = state_data.get('choices', []) + + # Flag: Empty or very short description + if len(desc) < 20: + flags.append({ + "type": "sparse_content", + "state": state_name, + "message": f"Very short description ({len(desc)} chars)", + "severity": "info" + }) + + # Flag: Many choices (complexity) + if len(choices) > 5: + flags.append({ + "type": "high_complexity", + "state": state_name, + "message": f"Many choices ({len(choices)}) - may overwhelm player", + "severity": "info" + }) + + # Flag: Description doesn't mention any choices + # Note: This is just informational - good narrative often sets scene + # without literally mentioning the choice text + if choices and desc: + desc_lower = desc.lower() + mentioned = any(c.lower() in desc_lower for c in choices) + if not mentioned: + flags.append({ + "type": "disconnected_choices", + "state": state_name, + "message": "Description doesn't reference any of the available choices", + "severity": "info" # Changed from warning - this is just a hint + }) + + # Flag: No media in state (if others have media) + media = state_data.get('media', []) + if not media: + # This is just tracked, we'll flag if inconsistent + pass + + # Flag: Inconsistent media usage + states_with_media = sum( + 1 for s, d in data.items() + if isinstance(d, dict) and d.get('media') + ) + states_without_media = len([ + s for s, d in data.items() + if isinstance(d, dict) and not d.get('media') and s != 'end' + ]) + + if states_with_media > 0 and states_without_media > 0: + ratio = states_with_media / (states_with_media + states_without_media) + if 0.2 < ratio < 0.8: + flags.append({ + "type": "inconsistent_media", + "state": "global", + "message": f"Inconsistent media: {states_with_media} states have media, {states_without_media} don't", + "severity": "info" + }) + + # Flag: Very different description lengths (tone/pacing inconsistency) + desc_lengths = [len(d) for d in descriptions.values() if d] + if desc_lengths: + avg_len = sum(desc_lengths) / len(desc_lengths) + for state_name, desc in descriptions.items(): + if desc and len(desc) > avg_len * 3: + flags.append({ + "type": "length_outlier", + "state": state_name, + "message": f"Description much longer than average ({len(desc)} vs avg {int(avg_len)})", + "severity": "info" + }) + elif desc and len(desc) < avg_len * 0.3 and len(desc) > 0: + flags.append({ + "type": "length_outlier", + "state": state_name, + "message": f"Description much shorter than average ({len(desc)} vs avg {int(avg_len)})", + "severity": "info" + }) + + return flags + + +def generate_mermaid_diagram(config_input): + """ + Generate a Mermaid flowchart diagram from config. + + Returns mermaid code string. + """ + data, error = parse_config(config_input) + if error: + return f"%%Error: {error}" + + # Flatten nested configs + data, is_nested = flatten_nested_config(data) + + lines = ["flowchart TD"] + + # Mermaid reserved keywords that need escaping + RESERVED_KEYWORDS = {'end', 'graph', 'subgraph', 'direction', 'click', 'style', 'class', 'linkStyle', 'classDef'} + + def make_safe_name(name): + """Convert state name to mermaid-safe identifier.""" + safe = name.replace(' ', '_').replace('-', '_') + # Escape reserved keywords by prefixing with 'state_' + if safe.lower() in RESERVED_KEYWORDS: + safe = f"state_{safe}" + return safe + + # Track states for styling + all_states = set(data.keys()) + targeted_states = set() + dead_ends = set() + + # First pass: identify targeted states + for state_name, state_data in data.items(): + if not isinstance(state_data, dict): + continue + for target in state_data.get('transitions', {}).values(): + if target in all_states: + targeted_states.add(target) + elif '_' in target: + parts = target.rsplit('_', 1) + if parts[0] in all_states: + targeted_states.add(parts[0]) + + # Generate nodes and edges + for state_name, state_data in data.items(): + if not isinstance(state_data, dict): + continue + + # Sanitize state name for mermaid + safe_name = make_safe_name(state_name) + + # Get short description for node label + desc = state_data.get('description', state_name) + short_desc = desc[:30] + '...' if len(desc) > 30 else desc + short_desc = short_desc.replace('"', "'").replace('\n', ' ') + + # Node shape based on type + if state_name == 'end': + lines.append(f' {safe_name}(["{short_desc}"])') + elif state_name in ['start', 'intro', 'beginning', 'arrival'] or state_name == list(data.keys())[0]: + lines.append(f' {safe_name}[/"{short_desc}"\\]') + else: + lines.append(f' {safe_name}["{short_desc}"]') + + # Edges for transitions + transitions = state_data.get('transitions', {}) + if not transitions: + dead_ends.add(state_name) + + for choice, target in transitions.items(): + safe_target = make_safe_name(target) + safe_choice = choice[:20].replace('"', "'") + lines.append(f' {safe_name} -->|"{safe_choice}"| {safe_target}') + + # Add styling + lines.append("") + lines.append(" %% Styling") + + # Start state styling + first_state = list(data.keys())[0] if data else None + if first_state: + safe_first = make_safe_name(first_state) + lines.append(f" style {safe_first} fill:#90EE90") + + # End state styling + if 'end' in all_states: + lines.append(f" style {make_safe_name('end')} fill:#FFB6C1") + + # Dead end styling + for de in dead_ends: + if de != 'end': + safe_de = make_safe_name(de) + lines.append(f" style {safe_de} fill:#FFD700") + + # Orphaned state styling + orphaned = all_states - targeted_states - {'end'} + if first_state: + orphaned.discard(first_state) + for orph in orphaned: + safe_orph = make_safe_name(orph) + lines.append(f" style {safe_orph} fill:#FF6347") + + return "\n".join(lines) + + +def get_config_health_summary(config_input): + """ + Get a comprehensive health summary of the config. + + Returns formatted string for display. + """ + validation = validate_branching(config_input) + flags = generate_story_flags(config_input) + + stats = validation['stats'] + errors = validation['errors'] + warnings = validation['warnings'] + + lines = [] + + # Overall health score (simple heuristic) + error_penalty = len(errors) * 20 + warning_penalty = len(warnings) * 5 + flag_penalty = len([f for f in flags if f.get('severity') == 'warning']) * 3 + + health_score = max(0, 100 - error_penalty - warning_penalty - flag_penalty) + + lines.append(f"## Config Health Score: {health_score}/100") + lines.append("") + + # Stats + lines.append("### Statistics") + lines.append(f"- Total States: {stats.get('total_states', 0)}") + lines.append(f"- Reachability: {stats.get('reachability_pct', 0)}%") + lines.append(f"- Total Transitions: {stats.get('total_transitions', 0)}") + lines.append(f"- Valid Endings: {stats.get('valid_endings', 0)}") + lines.append(f"- Dead Ends (unintentional): {stats.get('dead_end_count', 0)}") + lines.append(f"- Orphaned States: {stats.get('orphaned_count', 0)}") + lines.append("") + + # Errors + if errors: + lines.append("### Errors (Must Fix)") + for err in errors: + lines.append(f"- {err}") + lines.append("") + + # Warnings + if warnings: + lines.append("### Warnings") + for warn in warnings: + lines.append(f"- {warn}") + lines.append("") + + # Story Flags + if flags: + lines.append("### Story Flags") + for flag in flags: + severity_icon = "" if flag.get('severity') == 'warning' else "" + lines.append(f"- {severity_icon} [{flag.get('state')}] {flag.get('message')}") + lines.append("") + + if not errors and not warnings and not flags: + lines.append("*No issues detected*") + + return "\n".join(lines) + + +def get_llm_cohesion_prompts(): + """ + Return preset prompts for LLM-assisted story cohesion checks. + """ + return { + "summarize_paths": """Analyze this game config and summarize each possible path through the story. +For each major branch, describe: +1. The path taken (which states) +2. Key events/decisions +3. The ending reached + +Config: +{config}""", + + "find_contradictions": """Review this game config for narrative contradictions or plot holes. +Look for: +1. Events that contradict each other across branches +2. Character actions that don't make sense +3. World/setting inconsistencies +4. Logical impossibilities + +Config: +{config}""", + + "tone_check": """Analyze the tone and mood consistency across this game config. +Check: +1. Does the writing style stay consistent? +2. Are there jarring tone shifts? +3. Does the atmosphere match the story type? + +Config: +{config}""", + + "improve_descriptions": """Review the descriptions in this game config and suggest improvements. +For each state, note if the description: +1. Sets the scene adequately +2. Connects to the available choices +3. Maintains narrative flow + +Config: +{config}""", + + "suggest_branches": """Analyze this game config and suggest additional branching opportunities. +Look for: +1. States where more choices would make sense +2. Missing consequences for actions +3. Opportunities for alternate paths + +Config: +{config}""" + } + + +# ============================================================ +# LOGIC GATES VALIDATION +# ============================================================ + +# Valid keys for condition expressions +VALID_CONDITION_KEYS = { + 'and', 'or', 'not', + 'has_item', 'not_has_item', + 'met_person', 'not_met_person', + 'flag', 'not_flag', + 'visited', 'not_visited', 'discovered', + 'mission_complete', 'mission_active', 'mission_failed', + 'money', 'counter', + 'knowledge', 'knowledge_value', + 'reputation', 'visit_count' +} + +# Valid keys for effect specifications +VALID_EFFECT_KEYS = { + 'add_item', 'remove_item', + 'add_money', 'remove_money', 'set_money', + 'add_person', + 'add_location', 'visit_location', + 'set_flag', 'clear_flag', 'toggle_flag', + 'set_counter', 'increment', 'decrement', + 'set_knowledge', 'remove_knowledge', + 'start_mission', 'complete_mission', 'fail_mission', 'update_mission', + 'adjust_reputation', 'set_reputation' +} + +# Valid keys for dynamic transitions +VALID_TRANSITION_KEYS = { + 'random', 'random_from', + 'if', 'then', 'else', + 'conditions', 'default' +} + + +def validate_condition(condition, path, issues): + """ + Recursively validate a condition expression. + + Args: + condition: The condition to validate + path: String path for error reporting + issues: List to append issues to + """ + if condition is None or condition == {}: + return + + if isinstance(condition, str): + return # Simple flag name - valid + + if not isinstance(condition, dict): + issues.append({ + "type": "invalid_condition", + "path": path, + "message": f"Condition must be dict or string, got {type(condition).__name__}" + }) + return + + for key in condition: + if key not in VALID_CONDITION_KEYS: + issues.append({ + "type": "unknown_condition_key", + "path": path, + "message": f"Unknown condition key: '{key}'" + }) + + # Recurse into compound conditions + if key in ('and', 'or'): + if not isinstance(condition[key], list): + issues.append({ + "type": "invalid_condition", + "path": path, + "message": f"'{key}' must be a list" + }) + else: + for i, sub in enumerate(condition[key]): + validate_condition(sub, f"{path}.{key}[{i}]", issues) + + if key == 'not': + validate_condition(condition[key], f"{path}.not", issues) + + # Validate numeric comparisons + if key in ('money', 'counter', 'reputation', 'visit_count'): + val = condition[key] + if isinstance(val, dict): + valid_comparisons = {'gte', 'lte', 'gt', 'lt', 'eq', 'neq', 'npc', 'state'} + for cmp_key in val: + if cmp_key not in valid_comparisons and not isinstance(val[cmp_key], dict): + issues.append({ + "type": "invalid_comparison", + "path": f"{path}.{key}", + "message": f"Unknown comparison operator: '{cmp_key}'" + }) + + +def validate_effects(effects, path, issues): + """ + Validate effect specifications. + + Args: + effects: The effects dict to validate + path: String path for error reporting + issues: List to append issues to + """ + if not effects or not isinstance(effects, dict): + return + + for key in effects: + if key not in VALID_EFFECT_KEYS: + issues.append({ + "type": "unknown_effect_key", + "path": path, + "message": f"Unknown effect key: '{key}'" + }) + + +def validate_transition(transition, path, issues): + """ + Validate transition specification. + + Args: + transition: The transition spec to validate + path: String path for error reporting + issues: List to append issues to + """ + if isinstance(transition, str): + return # Simple string target - valid (basic check only) + + if not isinstance(transition, dict): + issues.append({ + "type": "invalid_transition", + "path": path, + "message": f"Transition must be string or dict, got {type(transition).__name__}" + }) + return + + # Check for unknown keys + for key in transition: + if key not in VALID_TRANSITION_KEYS: + issues.append({ + "type": "unknown_transition_key", + "path": path, + "message": f"Unknown transition key: '{key}'" + }) + + # Validate random weights + if 'random' in transition: + weights = transition['random'] + if not isinstance(weights, list): + issues.append({ + "type": "invalid_transition", + "path": path, + "message": "'random' must be a list of [state, weight] pairs" + }) + else: + total = 0 + for item in weights: + if not isinstance(item, list) or len(item) != 2: + issues.append({ + "type": "invalid_transition", + "path": path, + "message": f"Random item must be [state, weight]: {item}" + }) + else: + total += item[1] if isinstance(item[1], (int, float)) else 0 + + if abs(total - 1.0) > 0.01 and total > 0: + issues.append({ + "type": "weight_warning", + "path": path, + "message": f"Random weights sum to {total}, not 1.0 (will be normalized)" + }) + + # Validate random_from pool + if 'random_from' in transition: + pool = transition['random_from'] + if not isinstance(pool, list) or len(pool) == 0: + issues.append({ + "type": "invalid_transition", + "path": path, + "message": "'random_from' must be a non-empty list" + }) + + # Validate conditional transition + if 'if' in transition: + validate_condition(transition['if'], f"{path}.if", issues) + if 'then' not in transition and 'else' not in transition: + issues.append({ + "type": "invalid_transition", + "path": path, + "message": "Conditional transition needs 'then' or 'else'" + }) + if 'then' in transition: + validate_transition(transition['then'], f"{path}.then", issues) + if 'else' in transition: + validate_transition(transition['else'], f"{path}.else", issues) + + # Validate chained conditions + if 'conditions' in transition: + has_default = False + for i, cond_block in enumerate(transition['conditions']): + if 'default' in cond_block: + has_default = True + validate_transition(cond_block['default'], f"{path}.conditions[{i}].default", issues) + elif 'if' in cond_block: + validate_condition(cond_block['if'], f"{path}.conditions[{i}].if", issues) + if 'then' in cond_block: + validate_transition(cond_block['then'], f"{path}.conditions[{i}].then", issues) + else: + issues.append({ + "type": "invalid_transition", + "path": f"{path}.conditions[{i}]", + "message": "Condition block needs 'if' or 'default'" + }) + + if not has_default: + issues.append({ + "type": "missing_default", + "path": path, + "message": "Chained conditions should have a 'default' fallback" + }) + + +def validate_logic_gates(config_input): + """ + Validate all logic gates (conditions, effects, transitions) in a config. + + Returns list of validation issues. + """ + data, error = parse_config(config_input) + if error: + return [{"type": "parse_error", "path": "", "message": error}] + + data, _ = flatten_nested_config(data) + issues = [] + + for state_name, state_data in data.items(): + if not isinstance(state_data, dict): + continue + + base_path = state_name + + # Validate choice_config conditions + choice_config = state_data.get('choice_config', {}) + for choice, config in choice_config.items(): + if isinstance(config, dict) and 'condition' in config: + validate_condition( + config['condition'], + f"{base_path}.choice_config.{choice}.condition", + issues + ) + + # Validate effects + effects = state_data.get('effects', {}) + for choice, effect_spec in effects.items(): + if isinstance(effect_spec, dict): + validate_effects(effect_spec, f"{base_path}.effects.{choice}", issues) + + # Validate on_enter effects + on_enter = state_data.get('on_enter') + if on_enter and isinstance(on_enter, dict): + validate_effects(on_enter, f"{base_path}.on_enter", issues) + + # Validate transitions (including dynamic ones) + transitions = state_data.get('transitions', {}) + for choice, trans in transitions.items(): + validate_transition(trans, f"{base_path}.transitions.{choice}", issues) + + # Validate encounter_chance + encounter = state_data.get('encounter_chance') + if encounter and isinstance(encounter, dict): + if 'bypass_conditions' in encounter: + validate_condition( + encounter['bypass_conditions'], + f"{base_path}.encounter_chance.bypass_conditions", + issues + ) + if 'probability' in encounter: + prob = encounter['probability'] + if not isinstance(prob, (int, float)) or prob < 0 or prob > 1: + issues.append({ + "type": "invalid_probability", + "path": f"{base_path}.encounter_chance.probability", + "message": f"Probability must be between 0 and 1, got {prob}" + }) + if 'pool' in encounter: + pool = encounter['pool'] + if not isinstance(pool, list) or len(pool) == 0: + issues.append({ + "type": "invalid_encounter_pool", + "path": f"{base_path}.encounter_chance.pool", + "message": "Encounter pool must be a non-empty list" + }) + + return issues + + +def get_logic_gates_summary(config_input): + """ + Get a summary of logic gates validation results. + + Returns formatted string for display. + """ + issues = validate_logic_gates(config_input) + + if not issues: + return "**Logic Gates Validation:** No issues found" + + lines = ["**Logic Gates Validation Issues:**", ""] + + errors = [i for i in issues if i['type'] not in ('weight_warning', 'missing_default')] + warnings = [i for i in issues if i['type'] in ('weight_warning', 'missing_default')] + + if errors: + lines.append("### Errors") + for issue in errors: + lines.append(f"- [{issue['path']}] {issue['message']}") + lines.append("") + + if warnings: + lines.append("### Warnings") + for issue in warnings: + lines.append(f"- [{issue['path']}] {issue['message']}") + lines.append("") + + return "\n".join(lines) + + +# ==================== ACTIONABLE VALIDATION FUNCTIONS ==================== + +def get_validation_with_fixes(config_input): + """ + Enhanced validation that returns errors with suggested fixes. + + Returns dict with: + - issues: List of issues with fix suggestions + - quick_fixes: Dict of auto-fixable issues + - stats: Config statistics + """ + data, error = parse_config(config_input) + if error: + return { + "issues": [{"type": "parse_error", "message": error, "fixable": False}], + "quick_fixes": {}, + "stats": {} + } + + data, is_nested = flatten_nested_config(data) + + issues = [] + quick_fixes = {} + all_states = set(data.keys()) + + for state_name, state_data in data.items(): + if not isinstance(state_data, dict): + continue + + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + # Issue: Broken transitions + for choice, target in transitions.items(): + target_exists = False + + if target in all_states: + target_exists = True + elif '_' in target: + # Check nested format + for split_pos in range(len(target)): + if target[split_pos] == '_': + loc = target[:split_pos] + st = target[split_pos+1:] + if loc in all_states: + target_exists = True + break + + if not target_exists: + # Suggest similar state names + suggestions = find_similar_states(target, all_states) + issues.append({ + "type": "broken_transition", + "state": state_name, + "choice": choice, + "target": target, + "message": f"Transition '{choice}' points to non-existent state '{target}'", + "suggestions": suggestions, + "fixable": len(suggestions) > 0, + "fix_type": "replace_target" + }) + + if suggestions: + quick_fixes[f"{state_name}|{choice}"] = { + "action": "replace_transition_target", + "old_target": target, + "suggested_target": suggestions[0], + "all_suggestions": suggestions + } + + # Issue: Choice without transition + choice_set = set(choices) if choices else set() + transition_keys = set(transitions.keys()) + missing_transitions = choice_set - transition_keys + + for missing in missing_transitions: + # Suggest next logical state + suggestions = suggest_next_states(state_name, all_states) + issues.append({ + "type": "missing_transition", + "state": state_name, + "choice": missing, + "message": f"Choice '{missing}' has no transition defined", + "suggestions": suggestions, + "fixable": True, + "fix_type": "add_transition" + }) + + quick_fixes[f"{state_name}|{missing}|add"] = { + "action": "add_transition", + "choice": missing, + "suggested_target": suggestions[0] if suggestions else "end" + } + + # Issue: Dead end (not an ending state) + if not transitions and not is_ending_state(state_name): + issues.append({ + "type": "dead_end", + "state": state_name, + "message": f"State '{state_name}' has no exits and doesn't appear to be an ending", + "suggestions": ["Add transitions to continue the story", "Rename to indicate it's an ending (e.g., ending_X)"], + "fixable": True, + "fix_type": "add_ending_transition" + }) + + quick_fixes[f"{state_name}|dead_end"] = { + "action": "convert_to_ending", + "add_choices": ["restart"], + "add_transitions": {"restart": list(all_states)[0]} + } + + # Issue: Orphaned state + targeted_states = set() + for s, sd in data.items(): + if isinstance(sd, dict): + for t in sd.get('transitions', {}).values(): + targeted_states.add(t) + if '_' in t: + parts = t.split('_', 1) + targeted_states.add(parts[0]) + + first_state = list(data.keys())[0] if data else None + if state_name not in targeted_states and state_name != first_state and not is_ending_state(state_name): + # Find states that could link here + potential_sources = find_potential_source_states(state_name, data) + issues.append({ + "type": "orphaned_state", + "state": state_name, + "message": f"State '{state_name}' is not reachable from any other state", + "suggestions": [f"Add a transition from '{s}' to '{state_name}'" for s in potential_sources[:3]], + "fixable": len(potential_sources) > 0, + "fix_type": "add_incoming_transition" + }) + + # Calculate stats + stats = { + "total_states": len(all_states), + "total_issues": len(issues), + "fixable_issues": len([i for i in issues if i.get("fixable")]), + "issue_types": {} + } + + for issue in issues: + issue_type = issue["type"] + stats["issue_types"][issue_type] = stats["issue_types"].get(issue_type, 0) + 1 + + return { + "issues": issues, + "quick_fixes": quick_fixes, + "stats": stats + } + + +def find_similar_states(target, all_states, threshold=0.6): + """Find states with similar names to the target.""" + suggestions = [] + + for state in all_states: + # Simple similarity: common characters ratio + common = set(target.lower()) & set(state.lower()) + similarity = len(common) / max(len(target), len(state)) + + if similarity >= threshold: + suggestions.append(state) + + # Also check if target is a substring or vice versa + if target.lower() in state.lower() or state.lower() in target.lower(): + if state not in suggestions: + suggestions.append(state) + + return suggestions[:5] + + +def suggest_next_states(current_state, all_states): + """Suggest logical next states based on naming patterns.""" + suggestions = [] + + # If current state has a number, suggest the next number + import re + match = re.search(r'(\d+)$', current_state) + if match: + num = int(match.group(1)) + next_name = current_state[:match.start()] + str(num + 1) + if next_name in all_states: + suggestions.append(next_name) + + # Suggest 'end' if it exists + if 'end' in all_states: + suggestions.append('end') + + # Suggest states that start with similar prefix + prefix = current_state.split('_')[0] if '_' in current_state else current_state[:3] + for state in all_states: + if state.startswith(prefix) and state != current_state and state not in suggestions: + suggestions.append(state) + + return suggestions[:5] + + +def find_potential_source_states(target_state, data): + """Find states that could logically link to the target.""" + potential = [] + + # States with similar names/prefixes + target_prefix = target_state.split('_')[0] if '_' in target_state else target_state[:3] + + for state_name, state_data in data.items(): + if state_name == target_state: + continue + if not isinstance(state_data, dict): + continue + + # Check if this state has room for more choices + choices = state_data.get('choices', []) + if len(choices) < 4: # Can add more choices + # Prefer states with similar prefix + if state_name.startswith(target_prefix) or target_state.startswith(state_name.split('_')[0]): + potential.insert(0, state_name) + else: + potential.append(state_name) + + return potential + + +def is_ending_state(state_name): + """Check if a state name indicates it's an ending.""" + name_lower = state_name.lower() + return ( + name_lower == 'end' or + name_lower.startswith('end_') or + name_lower.startswith('ending_') or + '_end' in name_lower or + '_ending' in name_lower or + 'finale' in name_lower or + 'conclusion' in name_lower or + 'victory' in name_lower or + 'defeat' in name_lower or + 'death' in name_lower + ) + + +def apply_quick_fix(config_input, fix_key, fix_data): + """ + Apply a quick fix to the config. + + Args: + config_input: JSON config string + fix_key: Key identifying the fix (e.g., "state|choice") + fix_data: Fix data from quick_fixes dict + + Returns: + Updated JSON config string + """ + data, error = parse_config(config_input) + if error: + return config_input + + data, is_nested = flatten_nested_config(data) + + action = fix_data.get("action") + + if action == "replace_transition_target": + parts = fix_key.split("|") + if len(parts) >= 2: + state_name, choice = parts[0], parts[1] + if state_name in data and "transitions" in data[state_name]: + data[state_name]["transitions"][choice] = fix_data["suggested_target"] + + elif action == "add_transition": + parts = fix_key.split("|") + if len(parts) >= 2: + state_name, choice = parts[0], parts[1] + if state_name in data: + if "transitions" not in data[state_name]: + data[state_name]["transitions"] = {} + data[state_name]["transitions"][choice] = fix_data["suggested_target"] + + elif action == "convert_to_ending": + parts = fix_key.split("|") + state_name = parts[0] + if state_name in data: + data[state_name]["choices"] = fix_data.get("add_choices", []) + data[state_name]["transitions"] = fix_data.get("add_transitions", {}) + + return json.dumps(data, indent=2) + + +def apply_all_quick_fixes(config_input): + """ + Apply all auto-fixable issues to the config. + + Returns: + Tuple of (updated_config, fixes_applied_count, fixes_log) + """ + validation = get_validation_with_fixes(config_input) + quick_fixes = validation["quick_fixes"] + + if not quick_fixes: + return config_input, 0, ["No auto-fixable issues found"] + + current_config = config_input + fixes_log = [] + + for fix_key, fix_data in quick_fixes.items(): + try: + current_config = apply_quick_fix(current_config, fix_key, fix_data) + fixes_log.append(f"Applied: {fix_data['action']} for {fix_key}") + except Exception as e: + fixes_log.append(f"Failed: {fix_data['action']} for {fix_key} - {str(e)}") + + return current_config, len(quick_fixes), fixes_log + + +def get_state_issues_map(config_input): + """ + Get a mapping of state names to their issues for inline highlighting. + + Returns: + Dict mapping state_name -> list of issues + """ + validation = get_validation_with_fixes(config_input) + issues = validation["issues"] + + state_issues = {} + for issue in issues: + state = issue.get("state", "global") + if state not in state_issues: + state_issues[state] = [] + state_issues[state].append(issue) + + return state_issues + + +def format_issues_for_display(issues): + """ + Format issues list as markdown for Gradio display. + """ + if not issues: + return "No issues found!" + + lines = ["## Validation Issues\n"] + + # Group by type + by_type = {} + for issue in issues: + t = issue["type"] + if t not in by_type: + by_type[t] = [] + by_type[t].append(issue) + + type_icons = { + "broken_transition": "🔴", + "missing_transition": "🟠", + "dead_end": "🟡", + "orphaned_state": "⚪", + "parse_error": "❌" + } + + type_names = { + "broken_transition": "Broken Transitions", + "missing_transition": "Missing Transitions", + "dead_end": "Dead Ends", + "orphaned_state": "Orphaned States", + "parse_error": "Parse Errors" + } + + for issue_type, type_issues in by_type.items(): + icon = type_icons.get(issue_type, "⚠️") + name = type_names.get(issue_type, issue_type) + lines.append(f"### {icon} {name} ({len(type_issues)})\n") + + for issue in type_issues: + state = issue.get("state", "") + message = issue.get("message", "") + fixable = "✅ Auto-fixable" if issue.get("fixable") else "" + + lines.append(f"- **{state}**: {message} {fixable}") + + suggestions = issue.get("suggestions", []) + if suggestions: + lines.append(f" - Suggestions: {', '.join(suggestions[:3])}") + + lines.append("") + + return "\n".join(lines) diff --git a/demo_llm_loop_examples.py b/demo_llm_loop_examples.py new file mode 100644 index 0000000000000000000000000000000000000000..597d1eec4574bf43011d22db376a222cc141d5e0 --- /dev/null +++ b/demo_llm_loop_examples.py @@ -0,0 +1,935 @@ +"""Demo examples for the LLM Improvement Loop workflow. + +This module provides example configs showing the progression through +the Mystery Deepening workflow: +- Round 1: Environment (plant clues in descriptions) +- Round 2: Characters (hidden motivations) +- Round 3: Choices (investigation options) +- Round 4: Tension (red herrings and reveals) +""" + +# Original starter config (7 states, basic mystery) +DEMO_MYSTERY_STARTER = '''{ + "investigation": { + "crime_scene": { + "description": "A dimly lit office. Papers scattered everywhere. A broken window.", + "choices": ["Examine the desk", "Check the window", "Leave"], + "transitions": { + "Examine the desk": "desk_clue", + "Check the window": "window_clue", + "Leave": "hallway" + } + }, + "desk_clue": { + "description": "The desk has a half-written letter and an empty coffee cup.", + "choices": ["Read the letter", "Back to scene"], + "transitions": { + "Read the letter": "letter_reveal", + "Back to scene": "crime_scene" + } + }, + "window_clue": { + "description": "Glass shards on the inside. Someone broke in from outside.", + "choices": ["Look outside", "Back to scene"], + "transitions": { + "Look outside": "outside_view", + "Back to scene": "crime_scene" + } + }, + "letter_reveal": { + "description": "The letter mentions a meeting tonight. It's unfinished.", + "choices": ["Continue investigating"], + "transitions": { + "Continue investigating": "crime_scene" + } + }, + "outside_view": { + "description": "Footprints in the mud lead toward the parking lot.", + "choices": ["Follow the footprints", "Back inside"], + "transitions": { + "Follow the footprints": "hallway", + "Back inside": "crime_scene" + } + }, + "hallway": { + "description": "The hallway is quiet. A security guard approaches.", + "choices": ["Talk to guard", "Return to office"], + "transitions": { + "Talk to guard": "guard_talk", + "Return to office": "crime_scene" + } + }, + "guard_talk": { + "description": "The guard says he heard nothing unusual tonight.", + "choices": ["Press for details", "Thank him and leave"], + "transitions": { + "Press for details": "guard_talk", + "Thank him and leave": "hallway" + } + } + } +}''' + +# After Round 1: Environment focus - plant clues in descriptions +DEMO_AFTER_ROUND_1 = '''{ + "investigation": { + "crime_scene": { + "description": "A dimly lit corner office on the 14th floor. Papers are scattered across the mahogany desk and floor - financial reports, all dated from last month. A broken window lets in cold night air, and you notice the glass shards fell inward, onto an expensive Persian rug now stained with what looks like coffee. A framed photo lies face-down near the overturned chair.", + "choices": ["Examine the desk", "Check the window", "Look at the photo", "Leave"], + "transitions": { + "Examine the desk": "desk_clue", + "Check the window": "window_clue", + "Look at the photo": "photo_clue", + "Leave": "hallway" + } + }, + "desk_clue": { + "description": "The desk has a half-written letter addressed to 'M' - the handwriting grows increasingly shaky toward the end. An empty coffee cup sits beside it, a faint lipstick mark on the rim despite this being a man's office. The desk drawer is slightly ajar, revealing a corner of what looks like a passport.", + "choices": ["Read the letter", "Check the drawer", "Examine the coffee cup", "Back to scene"], + "transitions": { + "Read the letter": "letter_reveal", + "Check the drawer": "drawer_contents", + "Examine the coffee cup": "coffee_clue", + "Back to scene": "crime_scene" + } + }, + "window_clue": { + "description": "Glass shards cover the inside of the windowsill and floor - definitely broken from outside. But something's wrong: the window latch is still locked from the inside. On the exterior ledge, you spot a small piece of torn fabric caught on a nail, dark blue like a uniform.", + "choices": ["Look outside", "Examine the fabric", "Back to scene"], + "transitions": { + "Look outside": "outside_view", + "Examine the fabric": "fabric_clue", + "Back to scene": "crime_scene" + } + }, + "photo_clue": { + "description": "The frame's glass is cracked. The photo shows two men shaking hands at what looks like a company event - one is clearly the office's occupant, the other wears a security uniform. On the back, someone has written 'Partners - Nov 2019' in neat handwriting.", + "choices": ["Keep the photo", "Put it back", "Back to scene"], + "transitions": { + "Keep the photo": "crime_scene", + "Put it back": "crime_scene", + "Back to scene": "crime_scene" + } + }, + "letter_reveal": { + "description": "The letter reads: 'Dear M, I can't keep doing this. They're going to find out about the transfers, and when they do...' The writing stops abruptly. A single drop of dried coffee stains the page, as if the cup was knocked over mid-sentence.", + "choices": ["Continue investigating"], + "transitions": { + "Continue investigating": "crime_scene" + } + }, + "drawer_contents": { + "description": "Inside the drawer: a passport with several recent stamps to the Cayman Islands, a burner phone with a cracked screen, and a sticky note with a phone number and the words 'CALL BEFORE FRIDAY' underlined three times.", + "choices": ["Take the phone", "Note the number", "Back to desk"], + "transitions": { + "Take the phone": "desk_clue", + "Note the number": "desk_clue", + "Back to desk": "desk_clue" + } + }, + "coffee_clue": { + "description": "The lipstick shade is a distinctive coral pink. The cup still has dregs at the bottom - the coffee smells slightly bitter, different from regular office coffee. There's a faint residue around the rim that doesn't look like dried cream.", + "choices": ["Back to desk"], + "transitions": { + "Back to desk": "desk_clue" + } + }, + "fabric_clue": { + "description": "The fabric is from a security uniform - you can see part of a stitched logo. It's torn at an angle suggesting someone climbed up, not down. But why would security break in through a window?", + "choices": ["Back to window"], + "transitions": { + "Back to window": "window_clue" + } + }, + "outside_view": { + "description": "Footprints in the mud lead toward the parking lot - two sets, one coming and one going. The incoming prints are deep, as if carrying something heavy. The outgoing prints are lighter, faster, heading toward a side exit that bypasses the main security desk.", + "choices": ["Follow the footprints", "Back inside"], + "transitions": { + "Follow the footprints": "hallway", + "Back inside": "crime_scene" + } + }, + "hallway": { + "description": "The hallway is quiet, lit by flickering fluorescent lights. A security camera in the corner has a small red light - but you notice it's pointing at the wall, not the office door. A security guard approaches, his boots leaving muddy prints on the polished floor.", + "choices": ["Talk to guard", "Check the camera", "Return to office"], + "transitions": { + "Talk to guard": "guard_talk", + "Check the camera": "camera_check", + "Return to office": "crime_scene" + } + }, + "camera_check": { + "description": "The camera has been manually rotated away from the office door. The adjustment screw is loose, recently turned. Whoever did this knew exactly which camera to move.", + "choices": ["Back to hallway"], + "transitions": { + "Back to hallway": "hallway" + } + }, + "guard_talk": { + "description": "The guard - his name tag reads 'Martinez' - says he heard nothing unusual tonight. His uniform is slightly disheveled, and there's a small tear near his sleeve. He keeps glancing at his watch.", + "choices": ["Press for details", "Thank him and leave"], + "transitions": { + "Press for details": "guard_pressed", + "Thank him and leave": "hallway" + } + }, + "guard_pressed": { + "description": "'Look, I was on my rounds, okay? The 14th floor wasn't scheduled until midnight.' He shifts his weight nervously. 'Ask Jenkins at the front desk - he'll confirm my times.' But you notice his boots match the muddy prints in the hallway.", + "choices": ["Confront about the boots", "Let it go", "Ask about the photo"], + "transitions": { + "Confront about the boots": "confrontation", + "Let it go": "hallway", + "Ask about the photo": "photo_question" + } + }, + "confrontation": { + "description": "Martinez's face pales. 'Those... those aren't... look, I can explain.' His hand moves toward his radio, then stops. 'Not here. Meet me in the parking garage in ten minutes. There are things you need to know about what really happened in that office.'", + "choices": ["Agree to meet", "Detain him now"], + "transitions": { + "Agree to meet": "hallway", + "Detain him now": "hallway" + } + }, + "photo_question": { + "description": "At the mention of the photo, Martinez goes very still. 'Where did you find that? That was supposed to be... nevermind.' He takes a deep breath. 'Richardson and I go way back. But that was before I knew what he was really doing with those accounts.'", + "choices": ["Ask about the accounts", "Back to hallway"], + "transitions": { + "Ask about the accounts": "accounts_info", + "Back to hallway": "hallway" + } + }, + "accounts_info": { + "description": "'He was moving money - lots of it. Company funds to offshore accounts. I found out six months ago and... I should have reported it. But he promised me a cut.' Martinez looks defeated. 'Tonight, someone else found out too. Someone who doesn't negotiate.'", + "choices": ["Who else knew?", "Back to hallway"], + "transitions": { + "Who else knew?": "hallway", + "Back to hallway": "hallway" + } + } + } +}''' + +# After Round 2: Characters focus - hidden motivations, personality through dialogue +DEMO_AFTER_ROUND_2 = '''{ + "investigation": { + "crime_scene": { + "description": "A dimly lit corner office on the 14th floor. Papers are scattered across the mahogany desk and floor - financial reports, all dated from last month. A broken window lets in cold night air, and you notice the glass shards fell inward, onto an expensive Persian rug now stained with what looks like coffee. A framed photo lies face-down near the overturned chair. On the desk, a half-eaten sandwich suggests Richardson left in a hurry - or was interrupted mid-meal.", + "choices": ["Examine the desk", "Check the window", "Look at the photo", "Leave"], + "transitions": { + "Examine the desk": "desk_clue", + "Check the window": "window_clue", + "Look at the photo": "photo_clue", + "Leave": "hallway" + } + }, + "desk_clue": { + "description": "The desk has a half-written letter addressed to 'M' - the handwriting grows increasingly shaky toward the end, betraying Richardson's fear. An empty coffee cup sits beside it, a faint coral lipstick mark on the rim - Richardson's wife wears red, not coral. The desk drawer is slightly ajar, revealing a corner of what looks like a passport. A sticky note on the monitor reads 'MIRANDA CALLED - URGENT' in what looks like a secretary's handwriting.", + "choices": ["Read the letter", "Check the drawer", "Examine the coffee cup", "Back to scene"], + "transitions": { + "Read the letter": "letter_reveal", + "Check the drawer": "drawer_contents", + "Examine the coffee cup": "coffee_clue", + "Back to scene": "crime_scene" + } + }, + "window_clue": { + "description": "Glass shards cover the inside of the windowsill and floor - definitely broken from outside. But something's wrong: the window latch is still locked from the inside. On the exterior ledge, you spot a small piece of torn fabric caught on a nail, dark blue like a security uniform. The tear pattern suggests someone climbed up, not down - but Martinez is the only guard on duty tonight.", + "choices": ["Look outside", "Examine the fabric", "Back to scene"], + "transitions": { + "Look outside": "outside_view", + "Examine the fabric": "fabric_clue", + "Back to scene": "crime_scene" + } + }, + "photo_clue": { + "description": "The frame's glass is cracked. The photo shows two men shaking hands at the company Christmas party 2019 - Richardson in an expensive suit, Martinez in his security uniform, both grinning like old friends. On the back, someone has written 'Partners in crime - Nov 2019' in feminine handwriting, with a small heart. The same coral lipstick has left a faint kiss mark in the corner.", + "choices": ["Keep the photo", "Put it back", "Back to scene"], + "transitions": { + "Keep the photo": "crime_scene", + "Put it back": "crime_scene", + "Back to scene": "crime_scene" + } + }, + "letter_reveal": { + "description": "The letter reads: 'Dear M, I can't keep doing this. They're going to find out about the transfers, and when they do, neither of us will be safe. Elena is getting suspicious - she found the Cayman statements. I need to disappear, but I can't leave you behind. Meet me at the usual place at midnight. If I'm not there by 12:30, assume the worst and run. Don't trust Martinez anymore - he's been asking questions. All my love, R.' The writing stops mid-sentence, a single drop of dried coffee staining the page.", + "choices": ["Continue investigating"], + "transitions": { + "Continue investigating": "crime_scene" + } + }, + "drawer_contents": { + "description": "Inside the drawer: a passport with several recent stamps to the Cayman Islands - the last trip was three days ago. A burner phone with a cracked screen shows the last call was to 'M' at 11:47 PM. A sticky note reads 'CALL BEFORE FRIDAY - E knows' with 'E' underlined three times. Elena, Richardson's wife, is known for her jealous streak and family money.", + "choices": ["Take the phone", "Note the number", "Back to desk"], + "transitions": { + "Take the phone": "desk_clue", + "Note the number": "desk_clue", + "Back to desk": "desk_clue" + } + }, + "coffee_clue": { + "description": "The coral lipstick shade is distinctive - 'Sunset Kiss' by Luxe, the kind sold at high-end department stores. Richardson's secretary Miranda wears this exact shade. The cup still has dregs at the bottom - the coffee smells bitter, with an underlying chemical note. There's a faint crystalline residue around the rim. Either Miranda has unusual taste in coffee additives, or someone wanted to make sure Richardson didn't finish his drink.", + "choices": ["Back to desk"], + "transitions": { + "Back to desk": "desk_clue" + } + }, + "fabric_clue": { + "description": "The fabric is from a security uniform - you can see part of a stitched company logo. It's torn at an angle suggesting someone climbed up, not down. The thread count matches Martinez's uniform exactly, but the tear on his sleeve is on the right arm. This fabric came from a left sleeve. Either Martinez changed uniforms, or there's another security guard involved.", + "choices": ["Back to window"], + "transitions": { + "Back to window": "window_clue" + } + }, + "outside_view": { + "description": "Footprints in the mud lead toward the parking lot - two sets, one coming and one going. The incoming prints are deep, size 11 work boots, carrying something heavy. The outgoing prints are lighter, faster, size 7 women's heels, heading toward a side exit. A cigarette butt near the window smells fresh - Virginia Slims, a women's brand. Miranda smokes Virginia Slims.", + "choices": ["Follow the footprints", "Back inside"], + "transitions": { + "Follow the footprints": "hallway", + "Back inside": "crime_scene" + } + }, + "hallway": { + "description": "The hallway is quiet, lit by flickering fluorescent lights. A security camera in the corner has a small red light - but you notice it's pointing at the wall, not the office door. Martinez approaches, his boots leaving muddy prints on the polished floor. His jaw is tight, eyes darting. 'Detective. Didn't expect you so soon.' His hand rests near his radio, as if ready to call for backup - or warn someone.", + "choices": ["Talk to guard", "Check the camera", "Return to office"], + "transitions": { + "Talk to guard": "guard_talk", + "Check the camera": "camera_check", + "Return to office": "crime_scene" + } + }, + "camera_check": { + "description": "The camera has been manually rotated away from the office door. The adjustment screw is loose, recently turned - you can still see fresh scratches on the metal. Whoever did this knew exactly which camera to move. The timestamp log shows the adjustment happened at 11:42 PM, five minutes before the estimated time of the incident. Martinez clocked in at 11:30 PM.", + "choices": ["Back to hallway"], + "transitions": { + "Back to hallway": "hallway" + } + }, + "guard_talk": { + "description": "'Evening, Detective.' Martinez's voice is steady but his hands aren't - they're shaking slightly. His name tag reads 'Martinez, J.' - the same J.M. initialed on the back of the photo in Richardson's office. His uniform is disheveled, with a small tear near his left sleeve. 'I heard nothing unusual tonight. Just doing my rounds.' He keeps glancing at his watch. It's 12:15 AM.", + "choices": ["Press for details", "Thank him and leave"], + "transitions": { + "Press for details": "guard_pressed", + "Thank him and leave": "hallway" + } + }, + "guard_pressed": { + "description": "'Look, I was on my rounds, okay?' Martinez's voice rises, defensive. 'The 14th floor wasn't scheduled until midnight.' He shifts his weight, mud flaking from his boots. 'Ask Jenkins at the front desk - he'll confirm my times.' His phone buzzes. He glances at it and his face goes pale. 'I need to... excuse me.' He starts to turn away. His boots are definitely size 11.", + "choices": ["Confront about the boots", "Let it go", "Ask about the photo"], + "transitions": { + "Confront about the boots": "confrontation", + "Let it go": "hallway", + "Ask about the photo": "photo_question" + } + }, + "confrontation": { + "description": "Martinez's face drains of color. 'Those... those aren't...' He takes a shaky breath. 'Fine. Yes, I was up there tonight. But not to hurt him - to WARN him.' His voice cracks. 'Richardson and I... we were partners. In more ways than one. Someone found out about the money, about us. They called Richardson at 11:30, threatened to tell his wife everything unless he paid. I went up to help him figure out what to do. But when I got there...' He stops, unable to continue.", + "choices": ["Agree to meet", "Detain him now"], + "transitions": { + "Agree to meet": "hallway", + "Detain him now": "hallway" + } + }, + "photo_question": { + "description": "At the mention of the photo, Martinez goes rigid. 'Where did you find that? That was supposed to be... Miranda was supposed to get rid of it.' He realizes what he's said too late. 'Look, Richardson and I were close, okay? Closer than his wife knew. Miranda found out - she's been covering for us, but lately she's been acting strange. Jealous, maybe. She kept saying if she couldn't have him, no one would.'", + "choices": ["Ask about the accounts", "Back to hallway"], + "transitions": { + "Ask about the accounts": "accounts_info", + "Back to hallway": "hallway" + } + }, + "accounts_info": { + "description": "'He was moving money - lots of it. Company funds to offshore accounts. We were going to leave together, start fresh in Costa Rica.' Martinez's voice is hollow. 'I found out six months ago what he was doing. Should have reported it. But he promised we'd run away, disappear. Tonight was supposed to be the night.' He looks at his watch again - 12:28 AM. 'Someone else found out. Someone who doesn't make deals. Elena Richardson has family in organized crime. She called Richardson at 11:30 PM. I heard her voice through the door.'", + "choices": ["Who else knew?", "Back to hallway"], + "transitions": { + "Who else knew?": "hallway", + "Back to hallway": "hallway" + } + }, + "miranda_lead": { + "description": "You find Miranda Chen at her desk on the 12th floor, still typing despite the late hour. Her coral lipstick is freshly applied, and she startles when she sees you. 'Detective! I didn't hear about... is Mr. Richardson alright?' Her voice is too high, too practiced. On her desk sits an empty coffee cup - the same brand as the one in Richardson's office. A Virginia Slims pack peeks from her purse.", + "choices": ["Ask about Richardson", "Ask about the coffee", "Leave"], + "transitions": { + "Ask about Richardson": "miranda_confession", + "Ask about the coffee": "miranda_coffee", + "Leave": "hallway" + } + }, + "miranda_confession": { + "description": "'He was going to leave.' Miranda's composure cracks. 'After everything I did for him - covering up the affair with Martinez, hiding the money transfers, lying to Elena when she called. And he was going to take HIM instead of me.' Her hands tremble. 'I brought him coffee at 11:40. We talked. I told him I knew about the midnight flight. He laughed. Said I wasn't his type.' She looks at you with hollow eyes. 'I didn't mean for... the pills were just supposed to make him sleepy. Elena's man must have arrived while he was drowsy.'", + "choices": ["Continue investigating"], + "transitions": { + "Continue investigating": "hallway" + } + }, + "miranda_coffee": { + "description": "'I always bring Mr. Richardson his evening coffee. It's part of my job.' Miranda's smile is too bright. 'Tonight was no different.' But her eyes flick to her purse. Inside, you can see an empty prescription bottle - the label reads 'Alprazolam.' Enough dissolved in coffee would make anyone confused, disoriented. Easy prey.", + "choices": ["Confront about pills", "Back away"], + "transitions": { + "Confront about pills": "miranda_confession", + "Back away": "hallway" + } + } + } +}''' + +# After Round 3: Choices focus - investigation options, multiple approaches +DEMO_AFTER_ROUND_3 = '''{ + "investigation": { + "crime_scene": { + "description": "A dimly lit corner office on the 14th floor. Papers are scattered across the mahogany desk and floor - financial reports, all dated from last month. A broken window lets in cold night air, and you notice the glass shards fell inward, onto an expensive Persian rug now stained with what looks like coffee. A framed photo lies face-down near the overturned chair. On the desk, a half-eaten sandwich suggests Richardson left in a hurry - or was interrupted mid-meal.", + "choices": ["Examine the desk carefully", "Check the broken window", "Look at the fallen photo", "Search the floor for evidence", "Leave and check the hallway"], + "transitions": { + "Examine the desk carefully": "desk_clue", + "Check the broken window": "window_clue", + "Look at the fallen photo": "photo_clue", + "Search the floor for evidence": "floor_search", + "Leave and check the hallway": "hallway" + } + }, + "desk_clue": { + "description": "The desk has a half-written letter addressed to 'M' - the handwriting grows increasingly shaky toward the end, betraying Richardson's fear. An empty coffee cup sits beside it, a faint coral lipstick mark on the rim - Richardson's wife wears red, not coral. The desk drawer is slightly ajar, revealing a corner of what looks like a passport. A sticky note on the monitor reads 'MIRANDA CALLED - URGENT' in what looks like a secretary's handwriting.", + "choices": ["Read the letter", "Check the drawer", "Examine the coffee cup", "Look at the sticky note closely", "Back to scene"], + "transitions": { + "Read the letter": "letter_reveal", + "Check the drawer": "drawer_contents", + "Examine the coffee cup": "coffee_clue", + "Look at the sticky note closely": "sticky_note_clue", + "Back to scene": "crime_scene" + } + }, + "window_clue": { + "description": "Glass shards cover the inside of the windowsill and floor - definitely broken from outside. But something's wrong: the window latch is still locked from the inside. On the exterior ledge, you spot a small piece of torn fabric caught on a nail, dark blue like a security uniform. The tear pattern suggests someone climbed up, not down - but Martinez is the only guard on duty tonight.", + "choices": ["Look outside carefully", "Examine the fabric closely", "Test the lock mechanism", "Check for fingerprints", "Back to scene"], + "transitions": { + "Look outside carefully": "outside_view", + "Examine the fabric closely": "fabric_clue", + "Test the lock mechanism": "lock_test", + "Check for fingerprints": "fingerprint_search", + "Back to scene": "crime_scene" + } + }, + "photo_clue": { + "description": "The frame's glass is cracked. The photo shows two men shaking hands at the company Christmas party 2019 - Richardson in an expensive suit, Martinez in his security uniform, both grinning like old friends. On the back, someone has written 'Partners in crime - Nov 2019' in feminine handwriting, with a small heart. The same coral lipstick has left a faint kiss mark in the corner.", + "choices": ["Keep the photo as evidence", "Study the handwriting", "Compare the lipstick shade", "Put it back for now", "Back to scene"], + "transitions": { + "Keep the photo as evidence": "evidence_collected", + "Study the handwriting": "handwriting_analysis", + "Compare the lipstick shade": "lipstick_comparison", + "Put it back for now": "crime_scene", + "Back to scene": "crime_scene" + } + }, + "floor_search": { + "description": "Crouching low, you scan the Persian rug and hardwood floor. Beneath the overturned chair, you find a crumpled receipt from a pharmacy dated today - Alprazolam prescription filled at 6:15 PM, patient name obscured by a coffee stain. Near the window, a single pearl earring catches the light. Elena Richardson was photographed wearing pearls at last month's gala.", + "choices": ["Pocket the receipt", "Examine the earring", "Check under the desk", "Back to scene"], + "transitions": { + "Pocket the receipt": "receipt_clue", + "Examine the earring": "earring_clue", + "Check under the desk": "under_desk", + "Back to scene": "crime_scene" + } + }, + "letter_reveal": { + "description": "The letter reads: 'Dear M, I can't keep doing this. They're going to find out about the transfers, and when they do, neither of us will be safe. Elena is getting suspicious - she found the Cayman statements. I need to disappear, but I can't leave you behind. Meet me at the usual place at midnight. If I'm not there by 12:30, assume the worst and run. Don't trust Martinez anymore - he's been asking questions. All my love, R.' The writing stops mid-sentence, a single drop of dried coffee staining the page.", + "choices": ["Who is 'M'?", "What transfers?", "Why not trust Martinez?", "Continue investigating"], + "transitions": { + "Who is 'M'?": "m_mystery", + "What transfers?": "transfer_investigation", + "Why not trust Martinez?": "martinez_suspicion", + "Continue investigating": "crime_scene" + } + }, + "drawer_contents": { + "description": "Inside the drawer: a passport with several recent stamps to the Cayman Islands - the last trip was three days ago. A burner phone with a cracked screen shows the last call was to 'M' at 11:47 PM. A sticky note reads 'CALL BEFORE FRIDAY - E knows' with 'E' underlined three times. Elena, Richardson's wife, is known for her jealous streak and family money.", + "choices": ["Take the phone", "Check the passport dates", "Examine the sticky note", "Look for hidden compartments", "Back to desk"], + "transitions": { + "Take the phone": "phone_evidence", + "Check the passport dates": "passport_clue", + "Examine the sticky note": "friday_deadline", + "Look for hidden compartments": "secret_compartment", + "Back to desk": "desk_clue" + } + }, + "coffee_clue": { + "description": "The coral lipstick shade is distinctive - 'Sunset Kiss' by Luxe, the kind sold at high-end department stores. Richardson's secretary Miranda wears this exact shade. The cup still has dregs at the bottom - the coffee smells bitter, with an underlying chemical note. There's a faint crystalline residue around the rim. Either Miranda has unusual taste in coffee additives, or someone wanted to make sure Richardson didn't finish his drink.", + "choices": ["Smell the coffee", "Scrape the residue for analysis", "Photograph the lipstick mark", "Back to desk"], + "transitions": { + "Smell the coffee": "poison_hint", + "Scrape the residue for analysis": "residue_analysis", + "Photograph the lipstick mark": "lipstick_evidence", + "Back to desk": "desk_clue" + } + }, + "sticky_note_clue": { + "description": "The handwriting on 'MIRANDA CALLED - URGENT' is hurried, slanted - written by someone right-handed in a rush. But Richardson is left-handed. Someone else wrote this. The time stamp in the corner reads 11:35 PM. Miranda Chen is Richardson's secretary. Her desk is on the 12th floor.", + "choices": ["Go find Miranda", "Call Miranda's extension", "Back to desk"], + "transitions": { + "Go find Miranda": "miranda_lead", + "Call Miranda's extension": "miranda_call", + "Back to desk": "desk_clue" + } + }, + "fabric_clue": { + "description": "The fabric is from a security uniform - you can see part of a stitched company logo. It's torn at an angle suggesting someone climbed up, not down. The thread count matches Martinez's uniform exactly, but the tear on his sleeve is on the right arm. This fabric came from a left sleeve. Either Martinez changed uniforms, or there's another security guard involved.", + "choices": ["Check the security roster", "Confront Martinez about this", "Examine more closely", "Back to window"], + "transitions": { + "Check the security roster": "security_roster", + "Confront Martinez about this": "confrontation", + "Examine more closely": "fabric_detail", + "Back to window": "window_clue" + } + }, + "outside_view": { + "description": "Footprints in the mud lead toward the parking lot - two sets, one coming and one going. The incoming prints are deep, size 11 work boots, carrying something heavy. The outgoing prints are lighter, faster, size 7 women's heels, heading toward a side exit. A cigarette butt near the window smells fresh - Virginia Slims, a women's brand. Miranda smokes Virginia Slims.", + "choices": ["Follow the boot prints", "Follow the heel prints", "Collect the cigarette butt", "Photograph the prints", "Back inside"], + "transitions": { + "Follow the boot prints": "parking_lot", + "Follow the heel prints": "side_exit", + "Collect the cigarette butt": "cigarette_evidence", + "Photograph the prints": "print_documentation", + "Back inside": "crime_scene" + } + }, + "hallway": { + "description": "The hallway is quiet, lit by flickering fluorescent lights. A security camera in the corner has a small red light - but you notice it's pointing at the wall, not the office door. Martinez approaches, his boots leaving muddy prints on the polished floor. His jaw is tight, eyes darting. 'Detective. Didn't expect you so soon.' His hand rests near his radio, as if ready to call for backup - or warn someone.", + "choices": ["Talk to Martinez directly", "Check the security camera first", "Examine his muddy boots", "Ask about other staff on duty", "Return to office"], + "transitions": { + "Talk to Martinez directly": "guard_talk", + "Check the security camera first": "camera_check", + "Examine his muddy boots": "boot_examination", + "Ask about other staff on duty": "staff_inquiry", + "Return to office": "crime_scene" + } + }, + "camera_check": { + "description": "The camera has been manually rotated away from the office door. The adjustment screw is loose, recently turned - you can still see fresh scratches on the metal. Whoever did this knew exactly which camera to move. The timestamp log shows the adjustment happened at 11:42 PM, five minutes before the estimated time of the incident. Martinez clocked in at 11:30 PM.", + "choices": ["Check the camera footage", "Dust for prints on the camera", "Confront Martinez about timing", "Back to hallway"], + "transitions": { + "Check the camera footage": "footage_review", + "Dust for prints on the camera": "camera_prints", + "Confront Martinez about timing": "timing_confrontation", + "Back to hallway": "hallway" + } + }, + "guard_talk": { + "description": "'Evening, Detective.' Martinez's voice is steady but his hands aren't - they're shaking slightly. His name tag reads 'Martinez, J.' - the same J.M. initialed on the back of the photo in Richardson's office. His uniform is disheveled, with a small tear near his left sleeve. 'I heard nothing unusual tonight. Just doing my rounds.' He keeps glancing at his watch. It's 12:15 AM.", + "choices": ["Press for details about tonight", "Ask about his relationship with Richardson", "Point out the tear in his uniform", "Thank him and leave"], + "transitions": { + "Press for details about tonight": "guard_pressed", + "Ask about his relationship with Richardson": "relationship_question", + "Point out the tear in his uniform": "uniform_confrontation", + "Thank him and leave": "hallway" + } + }, + "guard_pressed": { + "description": "'Look, I was on my rounds, okay?' Martinez's voice rises, defensive. 'The 14th floor wasn't scheduled until midnight.' He shifts his weight, mud flaking from his boots. 'Ask Jenkins at the front desk - he'll confirm my times.' His phone buzzes. He glances at it and his face goes pale. 'I need to... excuse me.' He starts to turn away. His boots are definitely size 11.", + "choices": ["Confront about the muddy boots", "Demand to see his phone", "Let him go and follow", "Ask about the photo"], + "transitions": { + "Confront about the muddy boots": "confrontation", + "Demand to see his phone": "phone_demand", + "Let him go and follow": "follow_martinez", + "Ask about the photo": "photo_question" + } + }, + "confrontation": { + "description": "Martinez's face drains of color. 'Those... those aren't...' He takes a shaky breath. 'Fine. Yes, I was up there tonight. But not to hurt him - to WARN him.' His voice cracks. 'Richardson and I... we were partners. In more ways than one. Someone found out about the money, about us. They called Richardson at 11:30, threatened to tell his wife everything unless he paid. I went up to help him figure out what to do. But when I got there...' He stops, unable to continue.", + "choices": ["What did you find when you arrived?", "Who called Richardson?", "Tell me about Elena Richardson", "You're under arrest"], + "transitions": { + "What did you find when you arrived?": "martinez_discovery", + "Who called Richardson?": "caller_identity", + "Tell me about Elena Richardson": "elena_info", + "You're under arrest": "arrest_martinez" + } + }, + "photo_question": { + "description": "At the mention of the photo, Martinez goes rigid. 'Where did you find that? That was supposed to be... Miranda was supposed to get rid of it.' He realizes what he's said too late. 'Look, Richardson and I were close, okay? Closer than his wife knew. Miranda found out - she's been covering for us, but lately she's been acting strange. Jealous, maybe. She kept saying if she couldn't have him, no one would.'", + "choices": ["Tell me more about Miranda", "Ask about the accounts", "Was Miranda here tonight?", "Back to hallway"], + "transitions": { + "Tell me more about Miranda": "miranda_background", + "Ask about the accounts": "accounts_info", + "Was Miranda here tonight?": "miranda_whereabouts", + "Back to hallway": "hallway" + } + }, + "accounts_info": { + "description": "'He was moving money - lots of it. Company funds to offshore accounts. We were going to leave together, start fresh in Costa Rica.' Martinez's voice is hollow. 'I found out six months ago what he was doing. Should have reported it. But he promised we'd run away, disappear. Tonight was supposed to be the night.' He looks at his watch again - 12:28 AM. 'Someone else found out. Someone who doesn't make deals. Elena Richardson has family in organized crime. She called Richardson at 11:30 PM. I heard her voice through the door.'", + "choices": ["Tell me about Elena's family", "Who else knew about the money?", "What happened at 11:30?", "Back to hallway"], + "transitions": { + "Tell me about Elena's family": "elena_family", + "Who else knew about the money?": "money_knowledge", + "What happened at 11:30?": "eleven_thirty", + "Back to hallway": "hallway" + } + }, + "miranda_lead": { + "description": "You find Miranda Chen at her desk on the 12th floor, still typing despite the late hour. Her coral lipstick is freshly applied, and she startles when she sees you. 'Detective! I didn't hear about... is Mr. Richardson alright?' Her voice is too high, too practiced. On her desk sits an empty coffee cup - the same brand as the one in Richardson's office. A Virginia Slims pack peeks from her purse.", + "choices": ["Ask about Richardson directly", "Ask about the coffee", "Mention the cigarettes outside", "Ask why she's working late", "Leave quietly"], + "transitions": { + "Ask about Richardson directly": "miranda_richardson", + "Ask about the coffee": "miranda_coffee", + "Mention the cigarettes outside": "miranda_cigarettes", + "Ask why she's working late": "miranda_late", + "Leave quietly": "hallway" + } + }, + "miranda_confession": { + "description": "'He was going to leave.' Miranda's composure cracks. 'After everything I did for him - covering up the affair with Martinez, hiding the money transfers, lying to Elena when she called. And he was going to take HIM instead of me.' Her hands tremble. 'I brought him coffee at 11:40. We talked. I told him I knew about the midnight flight. He laughed. Said I wasn't his type.' She looks at you with hollow eyes. 'I didn't mean for... the pills were just supposed to make him sleepy. Elena's man must have arrived while he was drowsy.'", + "choices": ["What pills did you use?", "Who is Elena's man?", "You're under arrest", "Continue investigating"], + "transitions": { + "What pills did you use?": "pills_confession", + "Who is Elena's man?": "elenas_enforcer", + "You're under arrest": "miranda_arrest", + "Continue investigating": "hallway" + } + }, + "miranda_coffee": { + "description": "'I always bring Mr. Richardson his evening coffee. It's part of my job.' Miranda's smile is too bright. 'Tonight was no different.' But her eyes flick to her purse. Inside, you can see an empty prescription bottle - the label reads 'Alprazolam.' Enough dissolved in coffee would make anyone confused, disoriented. Easy prey.", + "choices": ["Confront about the prescription", "Ask who else visited tonight", "Mention the lipstick on the cup", "Back away slowly"], + "transitions": { + "Confront about the prescription": "miranda_confession", + "Ask who else visited tonight": "visitor_info", + "Mention the lipstick on the cup": "lipstick_confrontation", + "Back away slowly": "hallway" + } + }, + "lock_test": { + "description": "You examine the window lock mechanism carefully. It's a standard sash lock, latched from the inside. But there are faint scratches around the latch - someone jimmied it closed AFTER the glass was broken. This wasn't a break-in. It was staged to look like one. The real entrance was probably the office door.", + "choices": ["Check the office door lock", "Re-examine the glass pattern", "Back to scene"], + "transitions": { + "Check the office door lock": "door_lock", + "Re-examine the glass pattern": "glass_analysis", + "Back to scene": "crime_scene" + } + }, + "fingerprint_search": { + "description": "Dusting the window frame reveals multiple prints. Most are smudged, but you get two clean sets: one matches the security database (Martinez), and another belongs to a woman - long, manicured nails. The woman's prints are on the INSIDE of the window latch. Someone closed it from inside after the glass was broken.", + "choices": ["Run the unknown prints", "Confront Martinez", "Back to scene"], + "transitions": { + "Run the unknown prints": "print_results", + "Confront Martinez": "confrontation", + "Back to scene": "crime_scene" + } + }, + "evidence_collected": { + "description": "You slip the photo into an evidence bag. The connection between Martinez and Richardson goes deeper than a professional relationship. The feminine handwriting and lipstick mark suggest a third party - someone close enough to Richardson to know about his affairs, someone who might feel betrayed.", + "choices": ["Continue investigating"], + "transitions": { + "Continue investigating": "crime_scene" + } + }, + "handwriting_analysis": { + "description": "The handwriting on the photo back is distinctive - elegant, educated, with a slight leftward slant. Comparing it to the sticky note on Richardson's monitor, you notice similarities. Both were written by the same person. Miranda Chen is left-handed.", + "choices": ["Go find Miranda", "Back to scene"], + "transitions": { + "Go find Miranda": "miranda_lead", + "Back to scene": "crime_scene" + } + }, + "m_mystery": { + "description": "Who is 'M'? The letter suggests an intimate relationship. Martinez's first name is Jorge - that doesn't match. But Miranda Chen's last name starts with M. Or could 'M' refer to someone else entirely? Richardson's wife Elena was born Elena Morrison. Three suspects, one initial.", + "choices": ["Focus on Martinez", "Investigate Miranda", "Look into Elena", "Continue investigating"], + "transitions": { + "Focus on Martinez": "guard_talk", + "Investigate Miranda": "miranda_lead", + "Look into Elena": "elena_investigation", + "Continue investigating": "crime_scene" + } + } + } +}''' + +# After Round 4: Tension focus - time pressure, red herrings, dramatic reveals +DEMO_AFTER_ROUND_4 = '''{ + "investigation": { + "crime_scene": { + "description": "A dimly lit corner office on the 14th floor. Papers are scattered across the mahogany desk and floor - financial reports, all dated from last month. A broken window lets in cold night air, and you notice the glass shards fell inward, onto an expensive Persian rug now stained with what looks like coffee. A framed photo lies face-down near the overturned chair. On the desk, a half-eaten sandwich suggests Richardson left in a hurry - or was interrupted mid-meal. Your phone buzzes: 'URGENT - Richardson's wife just landed at the airport. ETA 45 minutes.' The clock is ticking.", + "choices": ["Examine the desk carefully", "Check the broken window", "Look at the fallen photo", "Search the floor for evidence", "Leave and check the hallway"], + "transitions": { + "Examine the desk carefully": "desk_clue", + "Check the broken window": "window_clue", + "Look at the fallen photo": "photo_clue", + "Search the floor for evidence": "floor_search", + "Leave and check the hallway": "hallway" + } + }, + "desk_clue": { + "description": "The desk has a half-written letter addressed to 'M' - the handwriting grows increasingly shaky toward the end, betraying Richardson's fear. An empty coffee cup sits beside it, a faint coral lipstick mark on the rim - Richardson's wife wears red, not coral. The desk drawer is slightly ajar, revealing a corner of what looks like a passport. A sticky note on the monitor reads 'MIRANDA CALLED - URGENT' in what looks like a secretary's handwriting. Wait - there's another note underneath: 'Elena knows. Tonight. Midnight.' Someone warned him, but he didn't run.", + "choices": ["Read the letter", "Check the drawer", "Examine the coffee cup", "Look at both sticky notes", "Back to scene"], + "transitions": { + "Read the letter": "letter_reveal", + "Check the drawer": "drawer_contents", + "Examine the coffee cup": "coffee_clue", + "Look at both sticky notes": "sticky_notes_comparison", + "Back to scene": "crime_scene" + } + }, + "window_clue": { + "description": "Glass shards cover the inside of the windowsill and floor - definitely broken from outside. But something's wrong: the window latch is still locked from the inside. RED HERRING ALERT: Your gut says this is staged. On the exterior ledge, you spot a small piece of torn fabric caught on a nail, dark blue like a security uniform. But wait - security uniforms were switched to BLACK last month. This fabric is from an OLD uniform. Someone planted this.", + "choices": ["Look outside carefully", "Examine the planted fabric", "Test the lock mechanism", "Check for fingerprints", "Back to scene"], + "transitions": { + "Look outside carefully": "outside_view", + "Examine the planted fabric": "fabric_clue", + "Test the lock mechanism": "lock_test", + "Check for fingerprints": "fingerprint_search", + "Back to scene": "crime_scene" + } + }, + "photo_clue": { + "description": "The frame's glass is cracked. The photo shows two men shaking hands at the company Christmas party 2019 - Richardson in an expensive suit, Martinez in his security uniform, both grinning like old friends. On the back, someone has written 'Partners in crime - Nov 2019' in feminine handwriting, with a small heart. The same coral lipstick has left a faint kiss mark in the corner. But here's what catches your eye: the photo has been recently handled - fresh fingerprints over old dust. Someone looked at this TONIGHT.", + "choices": ["Keep the photo as evidence", "Study the fresh fingerprints", "Compare the lipstick shade", "Check for hidden messages", "Back to scene"], + "transitions": { + "Keep the photo as evidence": "evidence_collected", + "Study the fresh fingerprints": "fresh_prints", + "Compare the lipstick shade": "lipstick_comparison", + "Check for hidden messages": "hidden_message", + "Back to scene": "crime_scene" + } + }, + "floor_search": { + "description": "Crouching low, you scan the Persian rug and hardwood floor. Beneath the overturned chair, you find a crumpled receipt from a pharmacy dated today - Alprazolam prescription filled at 6:15 PM, patient name obscured by a coffee stain. Near the window, a single pearl earring catches the light. Elena Richardson was photographed wearing pearls at last month's gala. But pearls are common - is this really hers, or another planted clue?", + "choices": ["Pocket the receipt - crucial evidence", "Examine the earring skeptically", "Check under the desk", "Look for more planted evidence", "Back to scene"], + "transitions": { + "Pocket the receipt - crucial evidence": "receipt_clue", + "Examine the earring skeptically": "earring_analysis", + "Check under the desk": "under_desk", + "Look for more planted evidence": "planted_evidence_search", + "Back to scene": "crime_scene" + } + }, + "letter_reveal": { + "description": "The letter reads: 'Dear M, I can't keep doing this. They're going to find out about the transfers, and when they do, neither of us will be safe. Elena is getting suspicious - she found the Cayman statements. I need to disappear, but I can't leave you behind. Meet me at the usual place at midnight. If I'm not there by 12:30, assume the worst and run. Don't trust Martinez anymore - he's been asking questions. All my love, R.' The writing stops mid-sentence. WAIT - the ink at the end is different. Someone ADDED that last line about Martinez. The original letter ended at 'run.'", + "choices": ["Examine the different inks", "Who is 'M'?", "Why was Martinez's name added?", "Continue investigating"], + "transitions": { + "Examine the different inks": "ink_analysis", + "Who is 'M'?": "m_mystery", + "Why was Martinez's name added?": "martinez_framed", + "Continue investigating": "crime_scene" + } + }, + "drawer_contents": { + "description": "Inside the drawer: a passport with several recent stamps to the Cayman Islands - the last trip was three days ago. A burner phone with a cracked screen shows the last call was to 'M' at 11:47 PM - just fifteen minutes before the estimated incident. A sticky note reads 'CALL BEFORE FRIDAY - E knows' with 'E' underlined three times. Your phone buzzes again: 'Elena Richardson's car just passed the highway exit. 30 minutes.' You need to solve this before she arrives and lawyers up.", + "choices": ["Grab the phone - check call history", "Check the passport dates", "Examine the 'E knows' note", "Look for hidden compartments", "Back to desk"], + "transitions": { + "Grab the phone - check call history": "phone_evidence", + "Check the passport dates": "passport_clue", + "Examine the 'E knows' note": "friday_deadline", + "Look for hidden compartments": "secret_compartment", + "Back to desk": "desk_clue" + } + }, + "coffee_clue": { + "description": "The coral lipstick shade is distinctive - 'Sunset Kiss' by Luxe, sold exclusively at Nordstrom. Richardson's secretary Miranda wears this exact shade - you've seen her wear it. But so does ELENA RICHARDSON in her Instagram photos. The cup still has dregs at the bottom - the coffee smells bitter, with an underlying chemical note. There's a faint crystalline residue around the rim. TWIST: The residue isn't a sedative. Field test shows it's artificial sweetener - the expensive kind Elena uses for her diet. Miranda takes her coffee black.", + "choices": ["This changes everything", "Test the residue again", "Photograph the evidence", "Back to desk"], + "transitions": { + "This changes everything": "elena_revelation", + "Test the residue again": "residue_confirmation", + "Photograph the evidence": "evidence_photo", + "Back to desk": "desk_clue" + } + }, + "hallway": { + "description": "The hallway is quiet, lit by flickering fluorescent lights. A security camera in the corner has a small red light - but you notice it's pointing at the wall, not the office door. Martinez approaches, his boots leaving muddy prints on the polished floor. His jaw is tight, eyes darting. 'Detective. Didn't expect you so soon.' His hand rests near his radio. Then he says something strange: 'Whatever they told you about me and Richardson - it's not the whole story. Elena's people got here first. I tried to warn him.' He looks genuinely scared.", + "choices": ["What do you mean, Elena's people?", "Check the security camera first", "Examine his muddy boots", "Ask about the warning", "Return to office"], + "transitions": { + "What do you mean, Elena's people?": "elena_people", + "Check the security camera first": "camera_check", + "Examine his muddy boots": "boot_examination", + "Ask about the warning": "warning_details", + "Return to office": "crime_scene" + } + }, + "camera_check": { + "description": "The camera has been manually rotated away from the office door. The adjustment screw is loose, recently turned - you can still see fresh scratches on the metal. Whoever did this knew exactly which camera to move. The timestamp log shows the adjustment happened at 11:42 PM. But here's the CRUCIAL DETAIL: there are TWO sets of scratches. Someone adjusted this camera TWICE. First at 11:42 PM, then again at 11:58 PM - after the incident. Someone came back to make sure it was still pointed away.", + "choices": ["Check footage from other cameras", "Who came back at 11:58?", "Confront Martinez about timing", "Back to hallway"], + "transitions": { + "Check footage from other cameras": "other_cameras", + "Who came back at 11:58?": "second_adjustment", + "Confront Martinez about timing": "timing_confrontation", + "Back to hallway": "hallway" + } + }, + "guard_talk": { + "description": "'Evening, Detective.' Martinez's voice is steady but his hands aren't - they're shaking. His name tag reads 'Martinez, J.' His uniform is disheveled, with a small tear near his left sleeve. 'I heard nothing unusual tonight. Just doing my rounds.' He keeps glancing at his watch. It's 12:15 AM. Then he leans closer and whispers: 'The woman who came up at 11:35 - it wasn't Miranda. It was Elena. She was wearing Miranda's coat, but I recognized her perfume. Chanel No. 5. I didn't say anything because... her family...' He draws a finger across his throat.", + "choices": ["Elena was HERE?", "Tell me about her family", "Why didn't you stop her?", "You're lying to protect yourself"], + "transitions": { + "Elena was HERE?": "elena_presence", + "Tell me about her family": "family_connections", + "Why didn't you stop her?": "martinez_fear", + "You're lying to protect yourself": "martinez_defense" + } + }, + "guard_pressed": { + "description": "'Look, I was on my rounds, okay?' Martinez's voice rises, then drops to a whisper. 'The 14th floor wasn't scheduled until midnight. But I went up early because Richardson texted me at 11:25 - said someone was coming, someone dangerous. When I got there at 11:40, I heard voices inside. A woman, screaming. Then silence. I... I hid. I'm not proud of it.' He shows you his phone - the text from Richardson reads: 'SHE FOUND OUT. Coming to office. Help me. Please.' Sent at 11:25 PM.", + "choices": ["Who is 'she'?", "Why did you hide?", "Show me the rest of the texts", "What happened after the silence?"], + "transitions": { + "Who is 'she'?": "she_identity", + "Why did you hide?": "hiding_reason", + "Show me the rest of the texts": "full_texts", + "What happened after the silence?": "aftermath" + } + }, + "confrontation": { + "description": "Martinez breaks down completely. 'Fine! Yes, Richardson and I were together. For two years. But I didn't hurt him - I LOVED him.' Tears stream down his face. 'When I got to the office, he was already on the floor. The coffee cup was knocked over. A woman in Miranda's coat was climbing out the window - that's why there's fabric on the ledge. But the way she moved... it wasn't Miranda. It was Elena, I swear to God. She set this whole thing up to frame Miranda, frame me. She's been planning this for months.'", + "choices": ["How do you know it was Elena?", "Where is Richardson now?", "What do you mean, planning for months?", "You're under arrest"], + "transitions": { + "How do you know it was Elena?": "elena_identification", + "Where is Richardson now?": "richardson_fate", + "What do you mean, planning for months?": "long_con", + "You're under arrest": "arrest_martinez" + } + }, + "photo_question": { + "description": "At the mention of the photo, Martinez's face changes from fear to realization. 'That photo... Elena found it last month. She came to me, showed it to me. Said she'd destroy both our careers unless I helped her.' He pulls out his phone, shows you a text thread. Elena Richardson: 'I know about you and my husband. Friday night. You will let me into the building after hours. You will turn off the camera. Or these photos go to HR, the board, and every news outlet in the city.' Dated three days ago.", + "choices": ["She blackmailed you into helping", "Did you know what she planned?", "Why didn't you warn Richardson?", "Show this to my captain"], + "transitions": { + "She blackmailed you into helping": "blackmail_reveal", + "Did you know what she planned?": "plan_knowledge", + "Why didn't you warn Richardson?": "warning_failure", + "Show this to my captain": "evidence_secured" + } + }, + "accounts_info": { + "description": "'He was moving money - lots of it. Company funds to offshore accounts.' Martinez wipes his eyes. 'We were going to leave together, start fresh. But Elena found out about everything - the money AND us. She didn't just want a divorce. She wanted revenge.' He checks his watch nervously. 'Her family has connections to people who make problems disappear. The coffee, the staged break-in - this was a professional hit made to look like a crime of passion. And I...' His voice breaks. 'I let her in. God help me, I let her in.'", + "choices": ["Where is Elena now?", "The coffee was poisoned?", "Who else helped her?", "We need to find Richardson"], + "transitions": { + "Where is Elena now?": "elena_location", + "The coffee was poisoned?": "poison_confirmation", + "Who else helped her?": "accomplices", + "We need to find Richardson": "richardson_search" + } + }, + "miranda_lead": { + "description": "You find Miranda Chen at her desk on the 12th floor, furiously packing a box. She freezes when she sees you. 'Detective! I was just... organizing.' Her coral lipstick is smeared, mascara running. 'I heard sirens. Is Mr. Richardson okay?' She's lying - her coat is hanging on the back of her chair, dry and clean. But someone wearing her coat was at the scene. On her desk, a resignation letter dated TODAY. She was planning to leave before any of this happened.", + "choices": ["Where's your other coat, Miranda?", "Who borrowed your coat tonight?", "Why are you resigning?", "Did you bring Richardson coffee tonight?"], + "transitions": { + "Where's your other coat, Miranda?": "coat_question", + "Who borrowed your coat tonight?": "coat_borrower", + "Why are you resigning?": "resignation_reason", + "Did you bring Richardson coffee tonight?": "coffee_denial" + } + }, + "miranda_confession": { + "description": "'I didn't hurt him!' Miranda sobs. 'Yes, I brought him coffee at 11:40. Yes, I've been in love with him for three years. But when I got to his office, Elena was already there. She was wearing MY coat - must have taken it from my desk earlier. She told me to leave, that this was between her and her husband. I... I ran. I was scared.' She pulls out her phone. 'Look - I called 911 at 11:52 PM. Anonymously. That's how you got here so fast. I tried to help him, I swear.'", + "choices": ["The 911 call - that was you?", "What did you see Elena doing?", "Why was she wearing your coat?", "You're coming with me"], + "transitions": { + "The 911 call - that was you?": "911_confirmation", + "What did you see Elena doing?": "elena_actions", + "Why was she wearing your coat?": "coat_explanation", + "You're coming with me": "miranda_detained" + } + }, + "miranda_coffee": { + "description": "'I always bring Mr. Richardson his evening coffee.' Miranda's hands shake as she grabs the edge of her desk. 'But tonight - tonight Elena intercepted me in the hallway. Took the cup right out of my hands. Said she wanted to make up with her husband, bring him coffee as a peace offering. I thought it was sweet.' Her face crumbles. 'Oh God. She put something in it, didn't she? She used MY cup, MY lipstick shade - she was setting me up from the start.'", + "choices": ["Elena took the coffee from you?", "She planned to frame you", "What time was this?", "I need you to come with me"], + "transitions": { + "Elena took the coffee from you?": "coffee_interception", + "She planned to frame you": "frame_job", + "What time was this?": "timeline_clarification", + "I need you to come with me": "miranda_witness" + } + }, + "ink_analysis": { + "description": "Examining the letter under your UV light, the truth becomes clear. The original letter was written with Richardson's usual fountain pen - blue-black Montblanc ink. But the last line about Martinez was added with a cheap ballpoint, common office supply. Someone added that line to redirect suspicion toward Martinez and away from the real killer. The handwriting is close to Richardson's but not exact - slightly more angular. Elena Richardson is known for her perfect forgery skills; she handles all the household documents.", + "choices": ["Elena forged this line", "Test the ballpoint ink", "Check security footage for document tampering", "Continue investigating"], + "transitions": { + "Elena forged this line": "forgery_confirmed", + "Test the ballpoint ink": "ink_test", + "Check security footage for document tampering": "document_footage", + "Continue investigating": "crime_scene" + } + }, + "final_confrontation": { + "description": "Your phone buzzes one final time: 'Elena Richardson just pulled into the parking garage. She's heading for the elevator.' You have minutes to make a decision. The evidence points to a calculated murder disguised as a crime of passion. Elena poisoned her husband's coffee, staged the break-in, framed Miranda and Martinez, and is about to walk into this building pretending to be the grieving widow. Martinez is willing to testify. Miranda is a witness. The forged letter and planted evidence tell the story. But Elena's family connections mean this case needs to be airtight.", + "choices": ["Arrest Elena in the lobby", "Gather more evidence first", "Confront her in the office", "Call for backup and wait"], + "transitions": { + "Arrest Elena in the lobby": "lobby_arrest", + "Gather more evidence first": "more_evidence", + "Confront her in the office": "office_confrontation", + "Call for backup and wait": "backup_ending" + } + }, + "true_ending": { + "description": "As Elena Richardson steps off the elevator, her face a perfect mask of concern, you step forward with your badge. 'Mrs. Richardson. We have witnesses placing you at your husband's office at 11:35 PM, wearing Miranda Chen's coat. We have the forged addition to your husband's letter. We have your artificial sweetener in the poisoned coffee. And we have your blackmail texts to Jorge Martinez.' Her mask slips, just for a moment, revealing cold fury. 'You can't prove anything. My family will—' 'Your family will read about this in tomorrow's paper,' you interrupt. 'Elena Richardson, you're under arrest for the murder of David Richardson.' As the cuffs click, Martinez finally stops shaking. Miranda stops crying. Sometimes, the truth finds a way through the lies.", + "choices": ["Case closed"], + "transitions": { + "Case closed": "crime_scene" + } + } + } +}''' + +# Demo workflow guide +DEMO_WORKFLOW_GUIDE = """ +## LLM Improvement Loop Demo: Mystery Deepening Workflow + +This demo shows how to iteratively improve a game config using the Mystery Deepening workflow. + +### Starting Config +- 7 states +- Basic mystery structure +- Short descriptions (8-15 words each) +- 2 choices per state on average + +### Workflow Steps + +**Round 1: Environment Focus** +- Click "1: environment" in Mystery Deepening column +- Generate prompt → Copy to ChatGPT/Claude +- LLM adds: environmental clues, sensory details, hidden objects +- Result: 18 states, rich atmospheric descriptions + +**Round 2: Characters Focus** +- Click "2: characters" +- Generate prompt with Round 1 config +- LLM adds: NPC motivations, dialogue revealing personality, relationships +- Result: Character depth, suspicious behaviors, backstory hints + +**Round 3: Choices Focus** +- Click "3: choices" +- Generate prompt with Round 2 config +- LLM adds: investigation options, different approaches, hidden paths +- Result: 3-4 choices per state, multiple investigation strategies + +**Round 4: Tension Focus** +- Click "4: tension" +- Generate prompt with Round 3 config +- LLM adds: time pressure, red herrings, dramatic reveals +- Result: Heightened stakes, misleading clues, climactic moments + +### Metrics Progression (Example) +| Round | States | Avg Choices | Avg Desc Length | +|-------|--------|-------------|-----------------| +| Start | 7 | 2.0 | 12 words | +| R1 | 18 | 2.8 | 45 words | +| R2 | 22 | 3.1 | 52 words | +| R3 | 25 | 3.6 | 55 words | +| R4 | 28 | 3.8 | 62 words | + +### Tips +1. Each round builds on previous improvements +2. The prompt includes round context for continuity +3. Use "Revert" if a round's changes don't work +4. Export history to save your improvement journey +""" + +if __name__ == "__main__": + import json + + # Validate all rounds + rounds = [ + ("Starter (R0)", DEMO_MYSTERY_STARTER), + ("Round 1 - Environment", DEMO_AFTER_ROUND_1), + ("Round 2 - Characters", DEMO_AFTER_ROUND_2), + ("Round 3 - Choices", DEMO_AFTER_ROUND_3), + ("Round 4 - Tension", DEMO_AFTER_ROUND_4), + ] + + print("=== Demo Config Validation ===\n") + for name, config_str in rounds: + config = json.loads(config_str) + states = config.get('investigation', {}) + total_choices = sum(len(s.get('choices', [])) for s in states.values()) + avg_choices = total_choices / len(states) if states else 0 + print(f"{name}: {len(states)} states, {avg_choices:.1f} avg choices") + + print("\n" + DEMO_WORKFLOW_GUIDE) diff --git a/demo_mystery_complete.json b/demo_mystery_complete.json new file mode 100644 index 0000000000000000000000000000000000000000..b7d8a1e8bfd62b7a4d7494729c65ff3e138189c6 --- /dev/null +++ b/demo_mystery_complete.json @@ -0,0 +1,327 @@ +{ + "investigation": { + "crime_scene": { + "description": "A dimly lit corner office on the 14th floor. Papers are scattered across the mahogany desk and floor - financial reports, all dated from last month. A broken window lets in cold night air, and you notice the glass shards fell inward, onto an expensive Persian rug now stained with what looks like coffee. A framed photo lies face-down near the overturned chair. On the desk, a half-eaten sandwich suggests Richardson left in a hurry - or was interrupted mid-meal. Your phone buzzes: 'URGENT - Richardson's wife just landed at the airport. ETA 45 minutes.' The clock is ticking.", + "choices": [ + "Examine the desk carefully", + "Check the broken window", + "Look at the fallen photo", + "Search the floor for evidence", + "Leave and check the hallway" + ], + "transitions": { + "Examine the desk carefully": "desk_clue", + "Check the broken window": "window_clue", + "Look at the fallen photo": "photo_clue", + "Search the floor for evidence": "floor_search", + "Leave and check the hallway": "hallway" + } + }, + "desk_clue": { + "description": "The desk has a half-written letter addressed to 'M' - the handwriting grows increasingly shaky toward the end, betraying Richardson's fear. An empty coffee cup sits beside it, a faint coral lipstick mark on the rim - Richardson's wife wears red, not coral. The desk drawer is slightly ajar, revealing a corner of what looks like a passport. A sticky note on the monitor reads 'MIRANDA CALLED - URGENT' in what looks like a secretary's handwriting. Wait - there's another note underneath: 'Elena knows. Tonight. Midnight.' Someone warned him, but he didn't run.", + "choices": [ + "Read the letter", + "Check the drawer", + "Examine the coffee cup", + "Look at both sticky notes", + "Back to scene" + ], + "transitions": { + "Read the letter": "letter_reveal", + "Check the drawer": "drawer_contents", + "Examine the coffee cup": "coffee_clue", + "Look at both sticky notes": "sticky_notes_comparison", + "Back to scene": "crime_scene" + } + }, + "window_clue": { + "description": "Glass shards cover the inside of the windowsill and floor - definitely broken from outside. But something's wrong: the window latch is still locked from the inside. RED HERRING ALERT: Your gut says this is staged. On the exterior ledge, you spot a small piece of torn fabric caught on a nail, dark blue like a security uniform. But wait - security uniforms were switched to BLACK last month. This fabric is from an OLD uniform. Someone planted this.", + "choices": [ + "Look outside carefully", + "Examine the planted fabric", + "Test the lock mechanism", + "Check for fingerprints", + "Back to scene" + ], + "transitions": { + "Look outside carefully": "outside_view", + "Examine the planted fabric": "fabric_clue", + "Test the lock mechanism": "lock_test", + "Check for fingerprints": "fingerprint_search", + "Back to scene": "crime_scene" + } + }, + "photo_clue": { + "description": "The frame's glass is cracked. The photo shows two men shaking hands at the company Christmas party 2019 - Richardson in an expensive suit, Martinez in his security uniform, both grinning like old friends. On the back, someone has written 'Partners in crime - Nov 2019' in feminine handwriting, with a small heart. The same coral lipstick has left a faint kiss mark in the corner. But here's what catches your eye: the photo has been recently handled - fresh fingerprints over old dust. Someone looked at this TONIGHT.", + "choices": [ + "Keep the photo as evidence", + "Study the fresh fingerprints", + "Compare the lipstick shade", + "Check for hidden messages", + "Back to scene" + ], + "transitions": { + "Keep the photo as evidence": "evidence_collected", + "Study the fresh fingerprints": "fresh_prints", + "Compare the lipstick shade": "lipstick_comparison", + "Check for hidden messages": "hidden_message", + "Back to scene": "crime_scene" + } + }, + "floor_search": { + "description": "Crouching low, you scan the Persian rug and hardwood floor. Beneath the overturned chair, you find a crumpled receipt from a pharmacy dated today - Alprazolam prescription filled at 6:15 PM, patient name obscured by a coffee stain. Near the window, a single pearl earring catches the light. Elena Richardson was photographed wearing pearls at last month's gala. But pearls are common - is this really hers, or another planted clue?", + "choices": [ + "Pocket the receipt - crucial evidence", + "Examine the earring skeptically", + "Check under the desk", + "Look for more planted evidence", + "Back to scene" + ], + "transitions": { + "Pocket the receipt - crucial evidence": "receipt_clue", + "Examine the earring skeptically": "earring_analysis", + "Check under the desk": "under_desk", + "Look for more planted evidence": "planted_evidence_search", + "Back to scene": "crime_scene" + } + }, + "letter_reveal": { + "description": "The letter reads: 'Dear M, I can't keep doing this. They're going to find out about the transfers, and when they do, neither of us will be safe. Elena is getting suspicious - she found the Cayman statements. I need to disappear, but I can't leave you behind. Meet me at the usual place at midnight. If I'm not there by 12:30, assume the worst and run. Don't trust Martinez anymore - he's been asking questions. All my love, R.' The writing stops mid-sentence. WAIT - the ink at the end is different. Someone ADDED that last line about Martinez. The original letter ended at 'run.'", + "choices": [ + "Examine the different inks", + "Who is 'M'?", + "Why was Martinez's name added?", + "Continue investigating" + ], + "transitions": { + "Examine the different inks": "ink_analysis", + "Who is 'M'?": "m_mystery", + "Why was Martinez's name added?": "martinez_framed", + "Continue investigating": "crime_scene" + } + }, + "drawer_contents": { + "description": "Inside the drawer: a passport with several recent stamps to the Cayman Islands - the last trip was three days ago. A burner phone with a cracked screen shows the last call was to 'M' at 11:47 PM - just fifteen minutes before the estimated incident. A sticky note reads 'CALL BEFORE FRIDAY - E knows' with 'E' underlined three times. Your phone buzzes again: 'Elena Richardson's car just passed the highway exit. 30 minutes.' You need to solve this before she arrives and lawyers up.", + "choices": [ + "Grab the phone - check call history", + "Check the passport dates", + "Examine the 'E knows' note", + "Look for hidden compartments", + "Back to desk" + ], + "transitions": { + "Grab the phone - check call history": "phone_evidence", + "Check the passport dates": "passport_clue", + "Examine the 'E knows' note": "friday_deadline", + "Look for hidden compartments": "secret_compartment", + "Back to desk": "desk_clue" + } + }, + "coffee_clue": { + "description": "The coral lipstick shade is distinctive - 'Sunset Kiss' by Luxe, sold exclusively at Nordstrom. Richardson's secretary Miranda wears this exact shade - you've seen her wear it. But so does ELENA RICHARDSON in her Instagram photos. The cup still has dregs at the bottom - the coffee smells bitter, with an underlying chemical note. There's a faint crystalline residue around the rim. TWIST: The residue isn't a sedative. Field test shows it's artificial sweetener - the expensive kind Elena uses for her diet. Miranda takes her coffee black.", + "choices": [ + "This changes everything", + "Test the residue again", + "Photograph the evidence", + "Back to desk" + ], + "transitions": { + "This changes everything": "elena_revelation", + "Test the residue again": "residue_confirmation", + "Photograph the evidence": "evidence_photo", + "Back to desk": "desk_clue" + } + }, + "hallway": { + "description": "The hallway is quiet, lit by flickering fluorescent lights. A security camera in the corner has a small red light - but you notice it's pointing at the wall, not the office door. Martinez approaches, his boots leaving muddy prints on the polished floor. His jaw is tight, eyes darting. 'Detective. Didn't expect you so soon.' His hand rests near his radio. Then he says something strange: 'Whatever they told you about me and Richardson - it's not the whole story. Elena's people got here first. I tried to warn him.' He looks genuinely scared.", + "choices": [ + "What do you mean, Elena's people?", + "Check the security camera first", + "Examine his muddy boots", + "Ask about the warning", + "Return to office" + ], + "transitions": { + "What do you mean, Elena's people?": "elena_people", + "Check the security camera first": "camera_check", + "Examine his muddy boots": "boot_examination", + "Ask about the warning": "warning_details", + "Return to office": "crime_scene" + } + }, + "camera_check": { + "description": "The camera has been manually rotated away from the office door. The adjustment screw is loose, recently turned - you can still see fresh scratches on the metal. Whoever did this knew exactly which camera to move. The timestamp log shows the adjustment happened at 11:42 PM. But here's the CRUCIAL DETAIL: there are TWO sets of scratches. Someone adjusted this camera TWICE. First at 11:42 PM, then again at 11:58 PM - after the incident. Someone came back to make sure it was still pointed away.", + "choices": [ + "Check footage from other cameras", + "Who came back at 11:58?", + "Confront Martinez about timing", + "Back to hallway" + ], + "transitions": { + "Check footage from other cameras": "other_cameras", + "Who came back at 11:58?": "second_adjustment", + "Confront Martinez about timing": "timing_confrontation", + "Back to hallway": "hallway" + } + }, + "guard_talk": { + "description": "'Evening, Detective.' Martinez's voice is steady but his hands aren't - they're shaking. His name tag reads 'Martinez, J.' His uniform is disheveled, with a small tear near his left sleeve. 'I heard nothing unusual tonight. Just doing my rounds.' He keeps glancing at his watch. It's 12:15 AM. Then he leans closer and whispers: 'The woman who came up at 11:35 - it wasn't Miranda. It was Elena. She was wearing Miranda's coat, but I recognized her perfume. Chanel No. 5. I didn't say anything because... her family...' He draws a finger across his throat.", + "choices": [ + "Elena was HERE?", + "Tell me about her family", + "Why didn't you stop her?", + "You're lying to protect yourself" + ], + "transitions": { + "Elena was HERE?": "elena_presence", + "Tell me about her family": "family_connections", + "Why didn't you stop her?": "martinez_fear", + "You're lying to protect yourself": "martinez_defense" + } + }, + "guard_pressed": { + "description": "'Look, I was on my rounds, okay?' Martinez's voice rises, then drops to a whisper. 'The 14th floor wasn't scheduled until midnight. But I went up early because Richardson texted me at 11:25 - said someone was coming, someone dangerous. When I got there at 11:40, I heard voices inside. A woman, screaming. Then silence. I... I hid. I'm not proud of it.' He shows you his phone - the text from Richardson reads: 'SHE FOUND OUT. Coming to office. Help me. Please.' Sent at 11:25 PM.", + "choices": [ + "Who is 'she'?", + "Why did you hide?", + "Show me the rest of the texts", + "What happened after the silence?" + ], + "transitions": { + "Who is 'she'?": "she_identity", + "Why did you hide?": "hiding_reason", + "Show me the rest of the texts": "full_texts", + "What happened after the silence?": "aftermath" + } + }, + "confrontation": { + "description": "Martinez breaks down completely. 'Fine! Yes, Richardson and I were together. For two years. But I didn't hurt him - I LOVED him.' Tears stream down his face. 'When I got to the office, he was already on the floor. The coffee cup was knocked over. A woman in Miranda's coat was climbing out the window - that's why there's fabric on the ledge. But the way she moved... it wasn't Miranda. It was Elena, I swear to God. She set this whole thing up to frame Miranda, frame me. She's been planning this for months.'", + "choices": [ + "How do you know it was Elena?", + "Where is Richardson now?", + "What do you mean, planning for months?", + "You're under arrest" + ], + "transitions": { + "How do you know it was Elena?": "elena_identification", + "Where is Richardson now?": "richardson_fate", + "What do you mean, planning for months?": "long_con", + "You're under arrest": "arrest_martinez" + } + }, + "photo_question": { + "description": "At the mention of the photo, Martinez's face changes from fear to realization. 'That photo... Elena found it last month. She came to me, showed it to me. Said she'd destroy both our careers unless I helped her.' He pulls out his phone, shows you a text thread. Elena Richardson: 'I know about you and my husband. Friday night. You will let me into the building after hours. You will turn off the camera. Or these photos go to HR, the board, and every news outlet in the city.' Dated three days ago.", + "choices": [ + "She blackmailed you into helping", + "Did you know what she planned?", + "Why didn't you warn Richardson?", + "Show this to my captain" + ], + "transitions": { + "She blackmailed you into helping": "blackmail_reveal", + "Did you know what she planned?": "plan_knowledge", + "Why didn't you warn Richardson?": "warning_failure", + "Show this to my captain": "evidence_secured" + } + }, + "accounts_info": { + "description": "'He was moving money - lots of it. Company funds to offshore accounts.' Martinez wipes his eyes. 'We were going to leave together, start fresh. But Elena found out about everything - the money AND us. She didn't just want a divorce. She wanted revenge.' He checks his watch nervously. 'Her family has connections to people who make problems disappear. The coffee, the staged break-in - this was a professional hit made to look like a crime of passion. And I...' His voice breaks. 'I let her in. God help me, I let her in.'", + "choices": [ + "Where is Elena now?", + "The coffee was poisoned?", + "Who else helped her?", + "We need to find Richardson" + ], + "transitions": { + "Where is Elena now?": "elena_location", + "The coffee was poisoned?": "poison_confirmation", + "Who else helped her?": "accomplices", + "We need to find Richardson": "richardson_search" + } + }, + "miranda_lead": { + "description": "You find Miranda Chen at her desk on the 12th floor, furiously packing a box. She freezes when she sees you. 'Detective! I was just... organizing.' Her coral lipstick is smeared, mascara running. 'I heard sirens. Is Mr. Richardson okay?' She's lying - her coat is hanging on the back of her chair, dry and clean. But someone wearing her coat was at the scene. On her desk, a resignation letter dated TODAY. She was planning to leave before any of this happened.", + "choices": [ + "Where's your other coat, Miranda?", + "Who borrowed your coat tonight?", + "Why are you resigning?", + "Did you bring Richardson coffee tonight?" + ], + "transitions": { + "Where's your other coat, Miranda?": "coat_question", + "Who borrowed your coat tonight?": "coat_borrower", + "Why are you resigning?": "resignation_reason", + "Did you bring Richardson coffee tonight?": "coffee_denial" + } + }, + "miranda_confession": { + "description": "'I didn't hurt him!' Miranda sobs. 'Yes, I brought him coffee at 11:40. Yes, I've been in love with him for three years. But when I got to his office, Elena was already there. She was wearing MY coat - must have taken it from my desk earlier. She told me to leave, that this was between her and her husband. I... I ran. I was scared.' She pulls out her phone. 'Look - I called 911 at 11:52 PM. Anonymously. That's how you got here so fast. I tried to help him, I swear.'", + "choices": [ + "The 911 call - that was you?", + "What did you see Elena doing?", + "Why was she wearing your coat?", + "You're coming with me" + ], + "transitions": { + "The 911 call - that was you?": "911_confirmation", + "What did you see Elena doing?": "elena_actions", + "Why was she wearing your coat?": "coat_explanation", + "You're coming with me": "miranda_detained" + } + }, + "miranda_coffee": { + "description": "'I always bring Mr. Richardson his evening coffee.' Miranda's hands shake as she grabs the edge of her desk. 'But tonight - tonight Elena intercepted me in the hallway. Took the cup right out of my hands. Said she wanted to make up with her husband, bring him coffee as a peace offering. I thought it was sweet.' Her face crumbles. 'Oh God. She put something in it, didn't she? She used MY cup, MY lipstick shade - she was setting me up from the start.'", + "choices": [ + "Elena took the coffee from you?", + "She planned to frame you", + "What time was this?", + "I need you to come with me" + ], + "transitions": { + "Elena took the coffee from you?": "coffee_interception", + "She planned to frame you": "frame_job", + "What time was this?": "timeline_clarification", + "I need you to come with me": "miranda_witness" + } + }, + "ink_analysis": { + "description": "Examining the letter under your UV light, the truth becomes clear. The original letter was written with Richardson's usual fountain pen - blue-black Montblanc ink. But the last line about Martinez was added with a cheap ballpoint, common office supply. Someone added that line to redirect suspicion toward Martinez and away from the real killer. The handwriting is close to Richardson's but not exact - slightly more angular. Elena Richardson is known for her perfect forgery skills; she handles all the household documents.", + "choices": [ + "Elena forged this line", + "Test the ballpoint ink", + "Check security footage for document tampering", + "Continue investigating" + ], + "transitions": { + "Elena forged this line": "forgery_confirmed", + "Test the ballpoint ink": "ink_test", + "Check security footage for document tampering": "document_footage", + "Continue investigating": "crime_scene" + } + }, + "final_confrontation": { + "description": "Your phone buzzes one final time: 'Elena Richardson just pulled into the parking garage. She's heading for the elevator.' You have minutes to make a decision. The evidence points to a calculated murder disguised as a crime of passion. Elena poisoned her husband's coffee, staged the break-in, framed Miranda and Martinez, and is about to walk into this building pretending to be the grieving widow. Martinez is willing to testify. Miranda is a witness. The forged letter and planted evidence tell the story. But Elena's family connections mean this case needs to be airtight.", + "choices": [ + "Arrest Elena in the lobby", + "Gather more evidence first", + "Confront her in the office", + "Call for backup and wait" + ], + "transitions": { + "Arrest Elena in the lobby": "lobby_arrest", + "Gather more evidence first": "more_evidence", + "Confront her in the office": "office_confrontation", + "Call for backup and wait": "backup_ending" + } + }, + "true_ending": { + "description": "As Elena Richardson steps off the elevator, her face a perfect mask of concern, you step forward with your badge. 'Mrs. Richardson. We have witnesses placing you at your husband's office at 11:35 PM, wearing Miranda Chen's coat. We have the forged addition to your husband's letter. We have your artificial sweetener in the poisoned coffee. And we have your blackmail texts to Jorge Martinez.' Her mask slips, just for a moment, revealing cold fury. 'You can't prove anything. My family will\u2014' 'Your family will read about this in tomorrow's paper,' you interrupt. 'Elena Richardson, you're under arrest for the murder of David Richardson.' As the cuffs click, Martinez finally stops shaking. Miranda stops crying. Sometimes, the truth finds a way through the lies.", + "choices": [ + "Case closed" + ], + "transitions": { + "Case closed": "crime_scene" + } + } + } +} \ No newline at end of file diff --git a/demo_mystery_starter.json b/demo_mystery_starter.json new file mode 100644 index 0000000000000000000000000000000000000000..aa553c0e435929ba324fede5caf50cd4a6d381cc --- /dev/null +++ b/demo_mystery_starter.json @@ -0,0 +1,81 @@ +{ + "investigation": { + "crime_scene": { + "description": "A dimly lit office. Papers scattered everywhere. A broken window.", + "choices": [ + "Examine the desk", + "Check the window", + "Leave" + ], + "transitions": { + "Examine the desk": "desk_clue", + "Check the window": "window_clue", + "Leave": "hallway" + } + }, + "desk_clue": { + "description": "The desk has a half-written letter and an empty coffee cup.", + "choices": [ + "Read the letter", + "Back to scene" + ], + "transitions": { + "Read the letter": "letter_reveal", + "Back to scene": "crime_scene" + } + }, + "window_clue": { + "description": "Glass shards on the inside. Someone broke in from outside.", + "choices": [ + "Look outside", + "Back to scene" + ], + "transitions": { + "Look outside": "outside_view", + "Back to scene": "crime_scene" + } + }, + "letter_reveal": { + "description": "The letter mentions a meeting tonight. It's unfinished.", + "choices": [ + "Continue investigating" + ], + "transitions": { + "Continue investigating": "crime_scene" + } + }, + "outside_view": { + "description": "Footprints in the mud lead toward the parking lot.", + "choices": [ + "Follow the footprints", + "Back inside" + ], + "transitions": { + "Follow the footprints": "hallway", + "Back inside": "crime_scene" + } + }, + "hallway": { + "description": "The hallway is quiet. A security guard approaches.", + "choices": [ + "Talk to guard", + "Return to office" + ], + "transitions": { + "Talk to guard": "guard_talk", + "Return to office": "crime_scene" + } + }, + "guard_talk": { + "description": "The guard says he heard nothing unusual tonight.", + "choices": [ + "Press for details", + "Thank him and leave" + ], + "transitions": { + "Press for details": "guard_talk", + "Thank him and leave": "hallway" + } + } + } +} \ No newline at end of file diff --git a/dnd_game_master_hfport/__init__.py b/dnd_game_master_hfport/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9717c84467919244d5c392421badb4a689c3912c --- /dev/null +++ b/dnd_game_master_hfport/__init__.py @@ -0,0 +1,34 @@ +""" +D&D AI Game Master - Python Module for Hugging Face Integration + +A complete D&D 5e game master powered by Hugging Face language models. +""" + +from .game_master import DnDGameMaster +from .hf_integration import HuggingFaceGM, create_gm +from .models.character import Character, create_blank_character, create_test_character +from .models.scene import Scene, NPC, Lighting, TimeOfDay, NPCAttitude +from .models.game_state import GameState, GamePhase, NarrativeEntry + +__version__ = "0.1.0" + +__all__ = [ + # Main classes + "DnDGameMaster", + "HuggingFaceGM", + "create_gm", + # Character + "Character", + "create_blank_character", + "create_test_character", + # Scene + "Scene", + "NPC", + "Lighting", + "TimeOfDay", + "NPCAttitude", + # Game State + "GameState", + "GamePhase", + "NarrativeEntry", +] diff --git a/dnd_game_master_hfport/engine/__init__.py b/dnd_game_master_hfport/engine/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..36c8b946789b33dad31135e8e191394375b87410 --- /dev/null +++ b/dnd_game_master_hfport/engine/__init__.py @@ -0,0 +1,28 @@ +"""D&D 5e game engine - dice, checks, and modifiers.""" + +from .dice import roll_dice, roll_d20, parse_dice_notation, DiceResult, RollMode +from .checks import skill_check, attack_roll, CheckResult +from .modifiers import ( + get_ability_modifier, + get_proficiency_bonus, + get_skill_modifier, + SKILL_ABILITIES, +) + +__all__ = [ + # Dice + "roll_dice", + "roll_d20", + "parse_dice_notation", + "DiceResult", + "RollMode", + # Checks + "skill_check", + "attack_roll", + "CheckResult", + # Modifiers + "get_ability_modifier", + "get_proficiency_bonus", + "get_skill_modifier", + "SKILL_ABILITIES", +] diff --git a/dnd_game_master_hfport/engine/checks.py b/dnd_game_master_hfport/engine/checks.py new file mode 100644 index 0000000000000000000000000000000000000000..c1840368792d2ed534618a0c34ba73d8a572c7a4 --- /dev/null +++ b/dnd_game_master_hfport/engine/checks.py @@ -0,0 +1,170 @@ +"""D&D 5e skill checks and attack rolls.""" + +from dataclasses import dataclass +from .dice import roll_d20, DiceResult, RollMode + + +@dataclass +class CheckResult: + """Result of a skill check or attack roll.""" + roll: DiceResult + dc: int + success: bool + critical_success: bool + critical_failure: bool + margin: int # How much above/below DC + + def __str__(self) -> str: + result = "SUCCESS" if self.success else "FAILURE" + if self.critical_success: + result = "CRITICAL SUCCESS" + elif self.critical_failure: + result = "CRITICAL FAILURE" + return f"{self.roll} vs DC {self.dc}: {result} (margin: {self.margin:+d})" + + +def skill_check( + modifier: int, + dc: int, + mode: RollMode = RollMode.NORMAL +) -> CheckResult: + """ + Perform a skill check against a DC. + + Args: + modifier: Total skill modifier + dc: Difficulty Class to beat + mode: RollMode for advantage/disadvantage + + Returns: + CheckResult with roll details and success/failure + """ + roll = roll_d20(modifier, mode) + + is_nat_20 = roll.natural_roll == 20 + is_nat_1 = roll.natural_roll == 1 + + # In 5e, natural 20/1 on skill checks aren't auto-success/fail RAW, + # but many tables play that way. We'll use that common house rule. + success = roll.total >= dc or is_nat_20 + if is_nat_1: + success = False + + return CheckResult( + roll=roll, + dc=dc, + success=success, + critical_success=is_nat_20, + critical_failure=is_nat_1, + margin=roll.total - dc + ) + + +def attack_roll( + attack_bonus: int, + target_ac: int, + mode: RollMode = RollMode.NORMAL +) -> CheckResult: + """ + Perform an attack roll against AC. + + Args: + attack_bonus: Total attack bonus + target_ac: Target's Armor Class + mode: RollMode for advantage/disadvantage + + Returns: + CheckResult with roll details and hit/miss + """ + roll = roll_d20(attack_bonus, mode) + + is_nat_20 = roll.natural_roll == 20 + is_nat_1 = roll.natural_roll == 1 + + # Natural 20 always hits, natural 1 always misses (RAW for attacks) + if is_nat_20: + success = True + elif is_nat_1: + success = False + else: + success = roll.total >= target_ac + + return CheckResult( + roll=roll, + dc=target_ac, + success=success, + critical_success=is_nat_20, + critical_failure=is_nat_1, + margin=roll.total - target_ac + ) + + +def saving_throw( + modifier: int, + dc: int, + mode: RollMode = RollMode.NORMAL +) -> CheckResult: + """ + Perform a saving throw against a DC. + + Args: + modifier: Total saving throw modifier + dc: Difficulty Class to beat + mode: RollMode for advantage/disadvantage + + Returns: + CheckResult with roll details and success/failure + """ + # Saving throws work like skill checks + return skill_check(modifier, dc, mode) + + +def contested_check( + modifier1: int, + modifier2: int, + mode1: RollMode = RollMode.NORMAL, + mode2: RollMode = RollMode.NORMAL +) -> tuple[CheckResult, CheckResult, int]: + """ + Perform a contested check between two parties. + + Args: + modifier1: First party's modifier + modifier2: Second party's modifier + mode1: First party's roll mode + mode2: Second party's roll mode + + Returns: + Tuple of (party1_result, party2_result, winner) + winner: 1 if party1 wins, 2 if party2 wins, 0 if tie + """ + roll1 = roll_d20(modifier1, mode1) + roll2 = roll_d20(modifier2, mode2) + + # Use each other's total as the DC + result1 = CheckResult( + roll=roll1, + dc=roll2.total, + success=roll1.total >= roll2.total, + critical_success=roll1.natural_roll == 20, + critical_failure=roll1.natural_roll == 1, + margin=roll1.total - roll2.total + ) + + result2 = CheckResult( + roll=roll2, + dc=roll1.total, + success=roll2.total > roll1.total, # Tie goes to party1 + critical_success=roll2.natural_roll == 20, + critical_failure=roll2.natural_roll == 1, + margin=roll2.total - roll1.total + ) + + if roll1.total > roll2.total: + winner = 1 + elif roll2.total > roll1.total: + winner = 2 + else: + winner = 0 # Tie + + return result1, result2, winner diff --git a/dnd_game_master_hfport/engine/dice.py b/dnd_game_master_hfport/engine/dice.py new file mode 100644 index 0000000000000000000000000000000000000000..b06058b5b2ceb4e8799c0ee9c7ad7ed947649ce4 --- /dev/null +++ b/dnd_game_master_hfport/engine/dice.py @@ -0,0 +1,136 @@ +"""Dice rolling system for D&D 5e.""" + +import random +import re +from dataclasses import dataclass +from typing import List, Tuple +from enum import Enum + + +class RollMode(Enum): + """Roll modes for advantage/disadvantage.""" + NORMAL = "normal" + ADVANTAGE = "advantage" + DISADVANTAGE = "disadvantage" + + +@dataclass +class DiceResult: + """Result of a dice roll.""" + notation: str + rolls: List[int] + modifier: int + total: int + natural_roll: int # For d20s, the die result before modifiers + + def __str__(self) -> str: + rolls_str = ", ".join(str(r) for r in self.rolls) + if self.modifier >= 0: + return f"{self.notation}: [{rolls_str}] + {self.modifier} = {self.total}" + else: + return f"{self.notation}: [{rolls_str}] - {abs(self.modifier)} = {self.total}" + + +def parse_dice_notation(notation: str) -> Tuple[int, int, int]: + """ + Parse dice notation like '2d6+3' into (count, sides, modifier). + + Args: + notation: Dice notation string (e.g., "2d6+3", "d20", "4d8-2") + + Returns: + Tuple of (count, sides, modifier) + + Raises: + ValueError: If notation is invalid + """ + pattern = r'^(\d*)d(\d+)([+-]\d+)?$' + match = re.match(pattern, notation.lower().strip()) + + if not match: + raise ValueError(f"Invalid dice notation: {notation}") + + count = int(match.group(1)) if match.group(1) else 1 + sides = int(match.group(2)) + modifier = int(match.group(3)) if match.group(3) else 0 + + return count, sides, modifier + + +def roll_dice(notation: str, mode: RollMode = RollMode.NORMAL) -> DiceResult: + """ + Roll dice with optional advantage/disadvantage. + + Args: + notation: Dice notation string (e.g., "2d6+3") + mode: RollMode for advantage/disadvantage + + Returns: + DiceResult with roll details + """ + count, sides, modifier = parse_dice_notation(notation) + + if mode == RollMode.NORMAL: + rolls = [random.randint(1, sides) for _ in range(count)] + natural = sum(rolls) + elif mode == RollMode.ADVANTAGE: + roll1 = [random.randint(1, sides) for _ in range(count)] + roll2 = [random.randint(1, sides) for _ in range(count)] + if sum(roll1) >= sum(roll2): + rolls = roll1 + else: + rolls = roll2 + natural = sum(rolls) + else: # DISADVANTAGE + roll1 = [random.randint(1, sides) for _ in range(count)] + roll2 = [random.randint(1, sides) for _ in range(count)] + if sum(roll1) <= sum(roll2): + rolls = roll1 + else: + rolls = roll2 + natural = sum(rolls) + + return DiceResult( + notation=notation, + rolls=rolls, + modifier=modifier, + total=sum(rolls) + modifier, + natural_roll=natural + ) + + +def roll_d20(modifier: int = 0, mode: RollMode = RollMode.NORMAL) -> DiceResult: + """ + Convenience function for d20 rolls. + + Args: + modifier: Modifier to add to the roll + mode: RollMode for advantage/disadvantage + + Returns: + DiceResult with roll details + """ + result = roll_dice("1d20", mode) + result.modifier = modifier + result.total = result.natural_roll + modifier + return result + + +def roll_damage(notation: str, critical: bool = False) -> DiceResult: + """ + Roll damage dice, doubling on critical hits. + + Args: + notation: Damage dice notation (e.g., "2d6+3") + critical: If True, double the dice (not modifier) + + Returns: + DiceResult with damage details + """ + count, sides, modifier = parse_dice_notation(notation) + + if critical: + count *= 2 + notation = f"{count}d{sides}{'+' if modifier >= 0 else ''}{modifier}" + + return roll_dice(notation) diff --git a/dnd_game_master_hfport/engine/modifiers.py b/dnd_game_master_hfport/engine/modifiers.py new file mode 100644 index 0000000000000000000000000000000000000000..0c751fa415625d99aee58d325191365b3f493e2c --- /dev/null +++ b/dnd_game_master_hfport/engine/modifiers.py @@ -0,0 +1,168 @@ +"""D&D 5e ability modifiers and proficiency calculations.""" + +from typing import Dict + +# Skill to ability mapping (D&D 5e) +SKILL_ABILITIES: Dict[str, str] = { + "acrobatics": "dexterity", + "animal_handling": "wisdom", + "arcana": "intelligence", + "athletics": "strength", + "deception": "charisma", + "history": "intelligence", + "insight": "wisdom", + "intimidation": "charisma", + "investigation": "intelligence", + "medicine": "wisdom", + "nature": "intelligence", + "perception": "wisdom", + "performance": "charisma", + "persuasion": "charisma", + "religion": "intelligence", + "sleight_of_hand": "dexterity", + "stealth": "dexterity", + "survival": "wisdom", +} + +# Alternate names mapping +SKILL_ALIASES: Dict[str, str] = { + "animal handling": "animal_handling", + "sleight of hand": "sleight_of_hand", +} + + +def normalize_skill_name(skill: str) -> str: + """Normalize skill name to standard format.""" + normalized = skill.lower().strip() + return SKILL_ALIASES.get(normalized, normalized.replace(" ", "_")) + + +def get_ability_modifier(score: int) -> int: + """ + Calculate ability modifier from ability score. + + Args: + score: Ability score (typically 1-30) + + Returns: + Ability modifier (score - 10) // 2 + """ + return (score - 10) // 2 + + +def get_proficiency_bonus(level: int) -> int: + """ + Get proficiency bonus based on character level. + + Args: + level: Character level (1-20) + + Returns: + Proficiency bonus (2-6) + """ + if level < 1: + return 2 + return 2 + (level - 1) // 4 + + +def get_skill_modifier( + ability_scores: Dict[str, int], + skill: str, + level: int, + is_proficient: bool = False, + has_expertise: bool = False +) -> int: + """ + Calculate total skill check modifier. + + Args: + ability_scores: Dict mapping ability names to scores + skill: Skill name (e.g., "stealth", "perception") + level: Character level + is_proficient: Whether character is proficient in this skill + has_expertise: Whether character has expertise (double proficiency) + + Returns: + Total modifier for skill checks + """ + normalized_skill = normalize_skill_name(skill) + ability = SKILL_ABILITIES.get(normalized_skill, "strength") + base_mod = get_ability_modifier(ability_scores.get(ability, 10)) + + if has_expertise: + return base_mod + (get_proficiency_bonus(level) * 2) + elif is_proficient: + return base_mod + get_proficiency_bonus(level) + return base_mod + + +def get_saving_throw_modifier( + ability_scores: Dict[str, int], + ability: str, + level: int, + is_proficient: bool = False +) -> int: + """ + Calculate saving throw modifier. + + Args: + ability_scores: Dict mapping ability names to scores + ability: Ability name (e.g., "dexterity", "wisdom") + level: Character level + is_proficient: Whether character is proficient in this save + + Returns: + Total modifier for saving throws + """ + base_mod = get_ability_modifier(ability_scores.get(ability.lower(), 10)) + + if is_proficient: + return base_mod + get_proficiency_bonus(level) + return base_mod + + +def get_attack_bonus( + ability_scores: Dict[str, int], + ability: str, + level: int, + is_proficient: bool = True +) -> int: + """ + Calculate attack bonus. + + Args: + ability_scores: Dict mapping ability names to scores + ability: Ability used for attack (usually "strength" or "dexterity") + level: Character level + is_proficient: Whether character is proficient with the weapon + + Returns: + Attack bonus + """ + base_mod = get_ability_modifier(ability_scores.get(ability.lower(), 10)) + + if is_proficient: + return base_mod + get_proficiency_bonus(level) + return base_mod + + +def get_spell_save_dc( + ability_scores: Dict[str, int], + spellcasting_ability: str, + level: int +) -> int: + """ + Calculate spell save DC. + + Args: + ability_scores: Dict mapping ability names to scores + spellcasting_ability: The spellcasting ability (e.g., "intelligence") + level: Character level + + Returns: + Spell save DC (8 + proficiency + ability modifier) + """ + ability_mod = get_ability_modifier( + ability_scores.get(spellcasting_ability.lower(), 10) + ) + return 8 + get_proficiency_bonus(level) + ability_mod diff --git a/dnd_game_master_hfport/example.py b/dnd_game_master_hfport/example.py new file mode 100644 index 0000000000000000000000000000000000000000..eb1159c3e59e63ba777caaedb7c633cedb64d587 --- /dev/null +++ b/dnd_game_master_hfport/example.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Example usage of the D&D AI Game Master module. + +Run with: + python -m dnd_game_master.example + +Or: + python example.py +""" + +import os +from dnd_game_master import ( + DnDGameMaster, + Character, + Scene, + create_test_character, +) +from dnd_game_master.models.scene import ( + Lighting, + TimeOfDay, + NPC, + NPCAttitude, + create_tavern_scene, +) +from dnd_game_master.game_master import run_game_loop + + +def main(): + """Run the D&D Game Master example.""" + + # Get API token from environment or prompt + api_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN") + + if not api_token: + print("=" * 60) + print("D&D AI Game Master - Setup") + print("=" * 60) + print("\nNo HF_TOKEN environment variable found.") + print("Get your free token at: https://huggingface.co/settings/tokens") + api_token = input("\nEnter your Hugging Face API token: ").strip() + + if not api_token: + print("\nNo token provided. Exiting.") + return + + print("\nInitializing Game Master...") + + # Create the Game Master with Hugging Face + gm = DnDGameMaster( + model_id="mistralai/Mistral-7B-Instruct-v0.3", + api_token=api_token, + use_local_model=False, # Set True to load model locally + ) + + # Use test character or create custom one + print("\nWould you like to:") + print("1. Use the pre-made character (Kira the Rogue)") + print("2. Create a quick custom character") + + choice = input("\nChoice (1/2): ").strip() + + if choice == "2": + name = input("Character name: ").strip() or "Adventurer" + print("\nClasses: Fighter, Rogue, Wizard, Cleric, Ranger, Paladin") + char_class = input("Class: ").strip() or "Fighter" + print("\nRaces: Human, Elf, Dwarf, Halfling, Half-Elf, Tiefling") + race = input("Race: ").strip() or "Human" + + character = Character( + name=name, + race=race, + character_class=char_class, + level=3, + strength=14, + dexterity=14, + constitution=14, + intelligence=12, + wisdom=12, + charisma=12, + max_hp=25, + current_hp=25, + armor_class=15, + proficient_skills=["perception", "athletics", "intimidation"], + ) + else: + character = create_test_character() + + # Create starting scene + scene = create_tavern_scene() + + # Run the game loop + run_game_loop( + gm=gm, + character=character, + scene=scene, + adventure_name="The Dusty Tankard Mystery", + ) + + +def quick_demo(): + """Run a quick non-interactive demo.""" + + api_token = os.environ.get("HF_TOKEN") + if not api_token: + print("Set HF_TOKEN environment variable to run demo") + return + + print("Initializing Game Master...") + gm = DnDGameMaster(api_token=api_token) + + character = create_test_character() + scene = create_tavern_scene() + + gm.start_adventure(character, scene, "Demo Adventure") + + # Simulate a few actions + actions = [ + "I look around the tavern, trying to spot anything unusual.", + "I approach the hooded stranger in the corner.", + "I try to pickpocket the stranger.", + ] + + for action in actions: + print(f"\n> {action}") + result = gm.process_player_action(action) + + if result["type"] == "needs_roll": + print(f"\n{result['narration']}") + print(f"[Rolling {result['skill']} DC {result['dc']}...]") + + roll_result = gm.execute_roll() + roll = roll_result["roll"] + print(f"Rolled: {roll['natural']} + {roll['modifier']} = {roll['total']}") + print(f"Result: {'Success!' if roll_result['success'] else 'Failure...'}") + print(f"\n{roll_result['narration']}") + else: + print(f"\n{result['narration']}") + + print("-" * 40) + + +if __name__ == "__main__": + import sys + + if "--demo" in sys.argv: + quick_demo() + else: + main() diff --git a/dnd_game_master_hfport/game_master.py b/dnd_game_master_hfport/game_master.py new file mode 100644 index 0000000000000000000000000000000000000000..4a591761c05b27774b80f79cb01e48578a166578 --- /dev/null +++ b/dnd_game_master_hfport/game_master.py @@ -0,0 +1,462 @@ +"""Main D&D Game Master orchestrator.""" + +from typing import Optional, Dict, Any, List +from .hf_integration import HuggingFaceGM, create_gm +from .models.character import Character, create_blank_character +from .models.scene import Scene, Lighting, TimeOfDay +from .models.game_state import GameState, GamePhase +from .engine.dice import roll_d20, roll_dice, RollMode +from .engine.checks import skill_check, attack_roll +from .engine.modifiers import get_skill_modifier, get_ability_modifier +from .prompts.gm_prompts import ( + build_gm_system_prompt, + build_scene_context, + build_roll_outcome_prompt, + build_scene_extraction_prompt, +) + + +class DnDGameMaster: + """ + Main Game Master class that orchestrates the D&D experience. + + Integrates Hugging Face models for narrative generation with + D&D 5e game mechanics. + + Example: + >>> gm = DnDGameMaster(api_token="hf_...") + >>> gm.start_adventure(character, scene) + >>> result = gm.process_player_action("I search the room") + >>> if result["type"] == "needs_roll": + ... roll_result = gm.execute_roll() + """ + + def __init__( + self, + model_id: str = "mistralai/Mistral-7B-Instruct-v0.3", + api_token: Optional[str] = None, + use_local_model: bool = False, + device: str = "auto", + ): + """ + Initialize the D&D Game Master. + + Args: + model_id: Hugging Face model to use for narrative generation + api_token: HF API token (or set HF_TOKEN environment variable) + use_local_model: If True, load model locally instead of using API + device: Device for local inference ("auto", "cuda", "cpu", "mps") + """ + self.gm = create_gm( + model_id=model_id, + api_token=api_token, + local=use_local_model, + device=device, + ) + self.state = GameState() + self.system_prompt = build_gm_system_prompt() + self.pending_roll: Optional[Dict[str, Any]] = None + + def set_character(self, character: Character): + """Set the active player character.""" + self.state.character = character + self.state.phase = GamePhase.PLAYING + + def set_scene(self, scene: Scene): + """Set the current scene.""" + self.state.scene = scene + self.state.add_narrative("scene", scene.description) + + def start_adventure( + self, + character: Character, + scene: Scene, + adventure_name: str = "", + ): + """ + Begin a new adventure. + + Args: + character: The player character + scene: The starting scene + adventure_name: Optional name for the adventure + """ + self.state.start_adventure(character, scene, adventure_name) + self.pending_roll = None + print(f"\n{'='*60}") + print(f"Adventure: {adventure_name or 'A New Journey'}") + print(f"Character: {character.name}, {character.race} {character.character_class}") + print(f"{'='*60}") + print(f"\n{scene.description}\n") + + def process_player_action(self, action: str) -> Dict[str, Any]: + """ + Process a player's described action. + + Args: + action: What the player wants to do (natural language) + + Returns: + Dict with: + - type: "impossible" | "auto_success" | "auto_fail" | "needs_roll" + - narration: The narrative response + - skill: (if needs_roll) Skill to check + - dc: (if needs_roll) Difficulty class + - advantage/disadvantage: (if needs_roll) Roll mode + - suggestions: (if impossible) Alternative actions + """ + if not self.state.character or not self.state.scene: + return { + "type": "error", + "narration": "No active character or scene. Use start_adventure() first.", + } + + # Build context for the LLM + context = build_scene_context(self.state.character, self.state.scene) + + # Get GM decision + decision = self.gm.get_gm_decision( + player_action=action, + system_prompt=self.system_prompt, + context=context, + conversation_history=self.state.get_recent_messages(), + ) + + # Log the interaction + self.state.add_message("user", action) + self.state.add_narrative("player", action) + + # Handle based on decision type + decision_type = decision.get("decision", "auto_success") + narration = decision.get("narration", "") + + if decision_type == "needs_roll": + # Store pending roll info + self.pending_roll = { + "action": action, + "skill": decision.get("skill", "perception"), + "dc": decision.get("dc", 15), + "advantage": decision.get("advantage", False), + "disadvantage": decision.get("disadvantage", False), + } + + self.state.add_narrative( + "roll_request", + f"Roll {self.pending_roll['skill'].title()} (DC {self.pending_roll['dc']})", + skill=self.pending_roll["skill"], + dc=self.pending_roll["dc"], + ) + + return { + "type": "needs_roll", + "skill": self.pending_roll["skill"], + "dc": self.pending_roll["dc"], + "advantage": self.pending_roll["advantage"], + "disadvantage": self.pending_roll["disadvantage"], + "narration": narration, + } + + # Auto-resolved (success, fail, or impossible) + self.state.add_message("assistant", narration) + self.state.add_narrative("gm_response", narration) + + result = { + "type": decision_type, + "narration": narration, + } + + if decision_type == "impossible": + result["suggestions"] = decision.get("suggestions", []) + result["reason"] = decision.get("reason", "") + + return result + + def execute_roll(self, manual_roll: Optional[int] = None) -> Dict[str, Any]: + """ + Execute a pending skill check roll. + + Args: + manual_roll: Optional manual d20 result (for physical dice) + + Returns: + Dict with: + - roll: Roll details (natural, modifier, total, dc) + - success: Whether the check succeeded + - critical_success/critical_failure: If natural 20/1 + - narration: Outcome narration + """ + if not self.pending_roll: + return {"error": "No pending roll to execute"} + + roll_info = self.pending_roll + character = self.state.character + + # Calculate modifier + modifier = get_skill_modifier( + ability_scores=character.get_ability_scores(), + skill=roll_info["skill"], + level=character.level, + is_proficient=character.is_proficient(roll_info["skill"]), + has_expertise=character.has_expertise(roll_info["skill"]), + ) + + # Determine roll mode + mode = RollMode.NORMAL + if roll_info["advantage"]: + mode = RollMode.ADVANTAGE + elif roll_info["disadvantage"]: + mode = RollMode.DISADVANTAGE + + # Roll the dice + if manual_roll is not None: + # Use manual roll + natural = manual_roll + total = natural + modifier + success = total >= roll_info["dc"] or natural == 20 + if natural == 1: + success = False + critical_success = natural == 20 + critical_failure = natural == 1 + else: + # Automatic roll + result = skill_check(modifier, roll_info["dc"], mode) + natural = result.roll.natural_roll + total = result.roll.total + success = result.success + critical_success = result.critical_success + critical_failure = result.critical_failure + + # Build outcome prompt + outcome_prompt = build_roll_outcome_prompt( + action=roll_info["action"], + skill=roll_info["skill"], + roll_total=total, + dc=roll_info["dc"], + success=success, + critical=critical_success or critical_failure, + natural_roll=natural, + ) + + # Get outcome narration from LLM + outcome = self.gm.narrate_outcome(outcome_prompt, self.system_prompt) + narration = outcome.get("narration", "The action resolves...") + + # Log the roll and outcome + roll_summary = ( + f"{roll_info['skill'].title()}: {natural} + {modifier} = {total} " + f"vs DC {roll_info['dc']} - {'SUCCESS' if success else 'FAILURE'}" + ) + self.state.add_narrative("roll", roll_summary) + self.state.add_narrative("gm_response", narration) + self.state.add_message("assistant", narration) + + # Check for location change + if outcome.get("location_changed"): + new_location = outcome.get("new_location", "Unknown") + new_description = outcome.get("new_description", "") + self.state.scene.location = new_location + self.state.scene.description = new_description + + # Clear pending roll + self.pending_roll = None + + return { + "roll": { + "natural": natural, + "modifier": modifier, + "total": total, + "dc": roll_info["dc"], + "skill": roll_info["skill"], + }, + "success": success, + "critical_success": critical_success, + "critical_failure": critical_failure, + "margin": total - roll_info["dc"], + "narration": narration, + } + + def quick_roll(self, skill: str, dc: int) -> Dict[str, Any]: + """ + Perform a quick skill check without LLM narration. + + Args: + skill: Skill name + dc: Difficulty class + + Returns: + Roll result dictionary + """ + if not self.state.character: + return {"error": "No active character"} + + modifier = get_skill_modifier( + ability_scores=self.state.character.get_ability_scores(), + skill=skill, + level=self.state.character.level, + is_proficient=self.state.character.is_proficient(skill), + has_expertise=self.state.character.has_expertise(skill), + ) + + result = skill_check(modifier, dc) + + return { + "roll": { + "natural": result.roll.natural_roll, + "modifier": modifier, + "total": result.roll.total, + "dc": dc, + "skill": skill, + }, + "success": result.success, + "critical_success": result.critical_success, + "critical_failure": result.critical_failure, + "margin": result.margin, + } + + def roll_initiative(self) -> Dict[str, Any]: + """Roll initiative for the current character.""" + if not self.state.character: + return {"error": "No active character"} + + dex_mod = get_ability_modifier(self.state.character.dexterity) + init_bonus = dex_mod + self.state.character.initiative_bonus + + roll = roll_d20(init_bonus) + + return { + "natural": roll.natural_roll, + "modifier": init_bonus, + "total": roll.total, + } + + def roll_damage(self, notation: str, critical: bool = False) -> Dict[str, Any]: + """ + Roll damage dice. + + Args: + notation: Dice notation (e.g., "2d6+3") + critical: If True, double the dice + + Returns: + Damage roll result + """ + from .engine.dice import roll_damage + result = roll_damage(notation, critical) + + return { + "notation": notation, + "rolls": result.rolls, + "modifier": result.modifier, + "total": result.total, + "critical": critical, + } + + def get_narrative_history(self, max_entries: int = 50) -> str: + """Get formatted narrative history.""" + return self.state.get_recent_context(max_entries) + + def get_character_summary(self) -> str: + """Get current character summary.""" + if not self.state.character: + return "No active character" + return self.state.character.summary() + + def get_scene_summary(self) -> str: + """Get current scene summary.""" + if not self.state.scene: + return "No active scene" + return self.state.scene.to_context_string() + + def save_game(self, filepath: str): + """Save current game state to file.""" + self.state.save_to_file(filepath) + print(f"Game saved to {filepath}") + + def load_game(self, filepath: str): + """Load game state from file.""" + self.state = GameState.load_from_file(filepath) + print(f"Game loaded from {filepath}") + print(f"Character: {self.state.character.name if self.state.character else 'None'}") + print(f"Location: {self.state.scene.location if self.state.scene else 'None'}") + + +def run_game_loop( + gm: DnDGameMaster, + character: Character, + scene: Scene, + adventure_name: str = "Adventure", +): + """ + Run an interactive game loop in the terminal. + + Args: + gm: Initialized DnDGameMaster + character: Player character + scene: Starting scene + adventure_name: Name of the adventure + """ + gm.start_adventure(character, scene, adventure_name) + + print("\nCommands: 'quit' to exit, 'stats' for character, 'look' for scene") + print("-" * 60) + + while True: + try: + action = input("\n> What do you do? ").strip() + except (KeyboardInterrupt, EOFError): + print("\n\nFarewell, adventurer!") + break + + if not action: + continue + + if action.lower() in ['quit', 'exit', 'q']: + print("\nFarewell, adventurer!") + break + + if action.lower() == 'stats': + print(f"\n{gm.get_character_summary()}") + continue + + if action.lower() in ['look', 'scene']: + print(f"\n{gm.get_scene_summary()}") + continue + + if action.lower() == 'save': + gm.save_game("savegame.json") + continue + + # Process the action + result = gm.process_player_action(action) + + if result.get("type") == "needs_roll": + print(f"\n{result.get('narration', '')}") + print(f"\n>>> Roll {result['skill'].title()} (DC {result['dc']})") + + input("Press Enter to roll...") + + roll_result = gm.execute_roll() + roll = roll_result["roll"] + + print(f"\n[{roll['natural']}] + {roll['modifier']} = {roll['total']} vs DC {roll['dc']}") + + if roll_result["critical_success"]: + print("*** CRITICAL SUCCESS! ***") + elif roll_result["critical_failure"]: + print("*** CRITICAL FAILURE! ***") + elif roll_result["success"]: + print("Success!") + else: + print("Failure...") + + print(f"\n{roll_result['narration']}") + + elif result.get("type") == "impossible": + print(f"\n{result.get('narration', 'That is not possible.')}") + if result.get("suggestions"): + print("\nYou might try:") + for s in result["suggestions"]: + print(f" - {s}") + + else: + print(f"\n{result.get('narration', '')}") diff --git a/dnd_game_master_hfport/gradio_app.py b/dnd_game_master_hfport/gradio_app.py new file mode 100644 index 0000000000000000000000000000000000000000..a2f7ec4f981d709e2bff2f31fe16ce7585f27b23 --- /dev/null +++ b/dnd_game_master_hfport/gradio_app.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +""" +Gradio web interface for D&D AI Game Master. + +For Hugging Face Spaces deployment or local web interface. + +Run with: + python -m dnd_game_master.gradio_app + +Or deploy to Hugging Face Spaces. +""" + +import os + +try: + import gradio as gr + HAS_GRADIO = True +except ImportError: + HAS_GRADIO = False + print("Gradio not installed. Install with: pip install gradio") + +from dnd_game_master import DnDGameMaster, Character, Scene +from dnd_game_master.models.scene import Lighting, TimeOfDay, NPC, NPCAttitude + + +# Global game instance +game_instance = None + + +def initialize_game( + character_name: str, + character_class: str, + character_race: str, + api_token: str, +) -> str: + """Initialize a new game session.""" + global game_instance + + if not api_token: + return "Please provide a Hugging Face API token." + + try: + # Create character + character = Character( + name=character_name or "Adventurer", + race=character_race or "Human", + character_class=character_class or "Fighter", + level=1, + strength=14, + dexterity=14, + constitution=14, + intelligence=12, + wisdom=12, + charisma=12, + max_hp=12, + current_hp=12, + armor_class=14, + proficient_skills=["perception", "athletics"], + ) + + # Create starting scene + scene = Scene( + location="The Dusty Tankard Tavern", + description=( + "You push open the weathered oak door and step into the warmth of " + "The Dusty Tankard. The tavern is dimly lit by flickering candles and " + "a crackling fireplace. The smell of ale and roasted meat fills the air. " + "A few patrons sit at scattered tables, speaking in hushed tones. " + "Behind the bar, a stern-faced woman polishes a mug, her eyes following you." + ), + lighting=Lighting.DIM, + time_of_day=TimeOfDay.EVENING, + npcs=[ + NPC("Mira the Bartender", "A stern woman with knowing eyes", NPCAttitude.NEUTRAL), + NPC("Hooded Stranger", "Sits alone in the corner, face hidden", NPCAttitude.UNFRIENDLY), + ], + objects=["Notice board", "Crackling fireplace", "Locked cellar door"], + ) + + # Initialize game master + game_instance = DnDGameMaster(api_token=api_token) + game_instance.start_adventure(character, scene, "The Dusty Tankard Mystery") + + return f"""Adventure Started! + +You are {character.name}, a {character.race} {character.character_class}. + +--- + +{scene.description} + +--- + +What do you do?""" + + except Exception as e: + return f"Error initializing game: {str(e)}" + + +def process_action(action: str, history: list) -> tuple: + """Process a player action and return the response.""" + global game_instance + + if not game_instance: + return history + [[action, "Please initialize the game first!"]], "" + + if not action.strip(): + return history, "" + + try: + result = game_instance.process_player_action(action) + + if result.get("type") == "needs_roll": + # Auto-execute the roll + narration = result.get("narration", "") + roll_result = game_instance.execute_roll() + + roll = roll_result["roll"] + roll_text = f"[{roll['natural']}] + {roll['modifier']} = {roll['total']} vs DC {roll['dc']}" + + if roll_result["critical_success"]: + roll_text += " - CRITICAL SUCCESS!" + elif roll_result["critical_failure"]: + roll_text += " - CRITICAL FAILURE!" + elif roll_result["success"]: + roll_text += " - Success!" + else: + roll_text += " - Failure..." + + response = f"{narration}\n\n**Roll {result['skill'].title()}:** {roll_text}\n\n{roll_result['narration']}" + + elif result.get("type") == "impossible": + response = result.get("narration", "That is not possible.") + if result.get("suggestions"): + response += "\n\n**You might try:**\n" + for s in result["suggestions"]: + response += f"- {s}\n" + else: + response = result.get("narration", "...") + + return history + [[action, response]], "" + + except Exception as e: + return history + [[action, f"Error: {str(e)}"]], "" + + +def get_character_info() -> str: + """Get current character information.""" + global game_instance + if not game_instance or not game_instance.state.character: + return "No active character" + return game_instance.get_character_summary() + + +def get_scene_info() -> str: + """Get current scene information.""" + global game_instance + if not game_instance or not game_instance.state.scene: + return "No active scene" + return game_instance.get_scene_summary() + + +def create_interface(): + """Create the Gradio interface.""" + + with gr.Blocks(title="D&D AI Game Master", theme=gr.themes.Soft()) as demo: + gr.Markdown(""" + # D&D AI Game Master + + An AI-powered Dungeons & Dragons Game Master using Hugging Face models. + + --- + """) + + with gr.Tab("Setup"): + gr.Markdown("### Character Creation") + + with gr.Row(): + name_input = gr.Textbox( + label="Character Name", + placeholder="Enter your character's name", + value="Adventurer" + ) + class_input = gr.Dropdown( + choices=["Fighter", "Rogue", "Wizard", "Cleric", "Ranger", "Paladin", "Bard"], + label="Class", + value="Fighter" + ) + race_input = gr.Dropdown( + choices=["Human", "Elf", "Dwarf", "Halfling", "Half-Elf", "Tiefling", "Dragonborn"], + label="Race", + value="Human" + ) + + token_input = gr.Textbox( + label="Hugging Face API Token", + placeholder="hf_...", + type="password", + info="Get your free token at huggingface.co/settings/tokens" + ) + + start_btn = gr.Button("Start Adventure", variant="primary") + setup_output = gr.Markdown() + + start_btn.click( + initialize_game, + inputs=[name_input, class_input, race_input, token_input], + outputs=setup_output + ) + + with gr.Tab("Play"): + chatbot = gr.Chatbot( + label="Adventure", + height=400, + show_copy_button=True, + ) + + with gr.Row(): + action_input = gr.Textbox( + label="What do you do?", + placeholder="Describe your action...", + scale=4 + ) + submit_btn = gr.Button("Submit", variant="primary", scale=1) + + submit_btn.click( + process_action, + inputs=[action_input, chatbot], + outputs=[chatbot, action_input] + ) + action_input.submit( + process_action, + inputs=[action_input, chatbot], + outputs=[chatbot, action_input] + ) + + with gr.Tab("Status"): + with gr.Row(): + with gr.Column(): + gr.Markdown("### Character") + char_info = gr.Textbox( + label="Character Stats", + lines=6, + interactive=False + ) + refresh_char_btn = gr.Button("Refresh") + refresh_char_btn.click(get_character_info, outputs=char_info) + + with gr.Column(): + gr.Markdown("### Current Scene") + scene_info = gr.Textbox( + label="Scene Details", + lines=6, + interactive=False + ) + refresh_scene_btn = gr.Button("Refresh") + refresh_scene_btn.click(get_scene_info, outputs=scene_info) + + gr.Markdown(""" + --- + **Tips:** + - Describe your actions naturally: "I search the room for hidden doors" + - The GM will decide if you need to roll dice + - Use the Status tab to check your character and surroundings + """) + + return demo + + +def main(): + """Launch the Gradio app.""" + if not HAS_GRADIO: + print("Please install gradio: pip install gradio") + return + + demo = create_interface() + + # Get port from environment (for Spaces) or use default + port = int(os.environ.get("PORT", 7860)) + + demo.launch( + server_name="0.0.0.0", + server_port=port, + share=False, # Set True for public link + ) + + +if __name__ == "__main__": + main() diff --git a/dnd_game_master_hfport/hf_integration.py b/dnd_game_master_hfport/hf_integration.py new file mode 100644 index 0000000000000000000000000000000000000000..95777cf0bd3fb69fd02b618eceba3e2680ef78a6 --- /dev/null +++ b/dnd_game_master_hfport/hf_integration.py @@ -0,0 +1,395 @@ +"""Hugging Face integration for the D&D Game Master.""" + +from typing import List, Dict, Optional, Any, Union +import json +import re +import os + +# Support multiple import methods +try: + from transformers import pipeline, AutoModelForCausalLM, AutoTokenizer + HAS_TRANSFORMERS = True +except ImportError: + HAS_TRANSFORMERS = False + +try: + from huggingface_hub import InferenceClient + HAS_HF_HUB = True +except ImportError: + HAS_HF_HUB = False + + +class HuggingFaceGM: + """Hugging Face-powered Game Master for D&D.""" + + # Recommended models for narrative generation + RECOMMENDED_MODELS = { + "mistralai/Mistral-7B-Instruct-v0.3": { + "name": "Mistral 7B Instruct", + "size": "7B", + "description": "Good balance of speed and quality", + "speed": "fast", + }, + "meta-llama/Llama-3.2-3B-Instruct": { + "name": "Llama 3.2 3B", + "size": "3B", + "description": "Fast responses, good for quick gameplay", + "speed": "very_fast", + }, + "google/gemma-2-9b-it": { + "name": "Gemma 2 9B", + "size": "9B", + "description": "High quality narrative generation", + "speed": "medium", + }, + "HuggingFaceH4/zephyr-7b-beta": { + "name": "Zephyr 7B", + "size": "7B", + "description": "Strong instruction following", + "speed": "fast", + }, + "microsoft/Phi-3-mini-4k-instruct": { + "name": "Phi-3 Mini", + "size": "3.8B", + "description": "Compact but capable", + "speed": "very_fast", + }, + } + + def __init__( + self, + model_id: str = "mistralai/Mistral-7B-Instruct-v0.3", + use_api: bool = True, + api_token: Optional[str] = None, + device: str = "auto", + ): + """ + Initialize the Hugging Face Game Master. + + Args: + model_id: Hugging Face model ID + use_api: If True, use Inference API. If False, load model locally. + api_token: Hugging Face API token (required for API, optional for local) + device: Device for local inference ("auto", "cuda", "cpu", "mps") + """ + self.model_id = model_id + self.use_api = use_api + self.api_token = api_token or os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN") + self.device = device + + # Model and tokenizer for local inference + self.model = None + self.tokenizer = None + self.generator = None + self.client = None + + if use_api: + self._init_api_client() + else: + self._init_local_model() + + def _init_api_client(self): + """Initialize the Hugging Face Inference API client.""" + if not HAS_HF_HUB: + raise ImportError( + "huggingface_hub is required for API inference. " + "Install with: pip install huggingface_hub" + ) + + self.client = InferenceClient( + model=self.model_id, + token=self.api_token, + ) + print(f"Initialized HF Inference API with model: {self.model_id}") + + def _init_local_model(self): + """Load model locally for inference.""" + if not HAS_TRANSFORMERS: + raise ImportError( + "transformers and torch are required for local inference. " + "Install with: pip install transformers torch" + ) + + print(f"Loading model {self.model_id}... This may take a few minutes.") + + self.tokenizer = AutoTokenizer.from_pretrained( + self.model_id, + token=self.api_token, + ) + + self.model = AutoModelForCausalLM.from_pretrained( + self.model_id, + device_map=self.device, + torch_dtype="auto", + token=self.api_token, + ) + + self.generator = pipeline( + "text-generation", + model=self.model, + tokenizer=self.tokenizer, + ) + + print(f"Model loaded successfully on device: {self.device}") + + def chat( + self, + messages: List[Dict[str, str]], + max_tokens: int = 1024, + temperature: float = 0.8, + top_p: float = 0.95, + ) -> str: + """ + Send a chat request to the model. + + Args: + messages: List of {"role": "system/user/assistant", "content": "..."} + max_tokens: Maximum tokens to generate + temperature: Sampling temperature (higher = more creative) + top_p: Nucleus sampling parameter + + Returns: + Generated text response + """ + if self.use_api: + return self._chat_api(messages, max_tokens, temperature, top_p) + else: + return self._chat_local(messages, max_tokens, temperature, top_p) + + def _chat_api( + self, + messages: List[Dict[str, str]], + max_tokens: int, + temperature: float, + top_p: float, + ) -> str: + """Use Hugging Face Inference API.""" + try: + response = self.client.chat_completion( + messages=messages, + max_tokens=max_tokens, + temperature=temperature, + top_p=top_p, + ) + return response.choices[0].message.content + except Exception as e: + print(f"API Error: {e}") + raise RuntimeError(f"Failed to get response from HF API: {e}") + + def _chat_local( + self, + messages: List[Dict[str, str]], + max_tokens: int, + temperature: float, + top_p: float, + ) -> str: + """Use local model inference.""" + # Format messages using the tokenizer's chat template if available + if hasattr(self.tokenizer, 'apply_chat_template'): + prompt = self.tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True + ) + else: + prompt = self._format_messages_fallback(messages) + + outputs = self.generator( + prompt, + max_new_tokens=max_tokens, + temperature=temperature, + top_p=top_p, + do_sample=True, + pad_token_id=self.tokenizer.eos_token_id, + ) + + generated = outputs[0]["generated_text"] + + # Extract only the new generation (after the prompt) + if generated.startswith(prompt): + response = generated[len(prompt):].strip() + else: + # Try to find where the response starts + response = generated.strip() + + return response + + def _format_messages_fallback(self, messages: List[Dict[str, str]]) -> str: + """Fallback message formatting for models without chat templates.""" + parts = [] + for msg in messages: + role = msg["role"].upper() + content = msg["content"] + if role == "SYSTEM": + parts.append(f"### System:\n{content}") + elif role == "USER": + parts.append(f"### User:\n{content}") + elif role == "ASSISTANT": + parts.append(f"### Assistant:\n{content}") + parts.append("### Assistant:\n") + return "\n\n".join(parts) + + def parse_json_response(self, response: str) -> Dict[str, Any]: + """ + Parse JSON from the model response. + + Handles various formats including markdown code blocks. + + Args: + response: Raw model response + + Returns: + Parsed JSON dictionary + """ + # Remove markdown code blocks if present + cleaned = response.strip() + + # Try to extract JSON from markdown code block + json_block_match = re.search(r'```(?:json)?\s*([\s\S]*?)```', cleaned) + if json_block_match: + cleaned = json_block_match.group(1).strip() + + # Try to find JSON object boundaries + json_match = re.search(r'\{[\s\S]*\}', cleaned) + if json_match: + try: + return json.loads(json_match.group()) + except json.JSONDecodeError: + pass + + # Try parsing the whole thing + try: + return json.loads(cleaned) + except json.JSONDecodeError: + pass + + # Fallback: treat as pure narration + return { + "decision": "auto_success", + "narration": response.strip(), + "parse_error": True, + "raw_response": response, + } + + def get_gm_decision( + self, + player_action: str, + system_prompt: str, + context: str, + conversation_history: Optional[List[Dict[str, str]]] = None, + ) -> Dict[str, Any]: + """ + Get the GM's decision for a player action. + + Args: + player_action: What the player wants to do + system_prompt: The GM system prompt + context: Current scene/character context + conversation_history: Previous conversation messages + + Returns: + Parsed GM decision dictionary with keys like: + - decision: "impossible" | "auto_success" | "auto_fail" | "needs_roll" + - narration: The narrative text + - skill: (if needs_roll) The skill to check + - dc: (if needs_roll) The difficulty class + """ + messages = [{"role": "system", "content": system_prompt}] + + if conversation_history: + messages.extend(conversation_history[-10:]) # Limit context + + user_message = f"{context}\n\n---\n\nPlayer action: {player_action}" + messages.append({"role": "user", "content": user_message}) + + response = self.chat(messages) + return self.parse_json_response(response) + + def narrate_outcome( + self, + outcome_prompt: str, + system_prompt: str, + ) -> Dict[str, Any]: + """ + Get narration for a roll outcome. + + Args: + outcome_prompt: The outcome prompt with roll results + system_prompt: The GM system prompt + + Returns: + Parsed response with narration and any scene updates + """ + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": outcome_prompt}, + ] + + response = self.chat(messages) + return self.parse_json_response(response) + + def extract_scene( + self, + extraction_prompt: str, + ) -> Dict[str, Any]: + """ + Extract scene details from narrative. + + Args: + extraction_prompt: The scene extraction prompt + + Returns: + Parsed scene data + """ + messages = [{"role": "user", "content": extraction_prompt}] + response = self.chat(messages, max_tokens=512) + return self.parse_json_response(response) + + @classmethod + def list_recommended_models(cls) -> List[Dict[str, str]]: + """Get list of recommended models with descriptions.""" + return [ + {"id": model_id, **info} + for model_id, info in cls.RECOMMENDED_MODELS.items() + ] + + +def create_gm( + model_id: str = "mistralai/Mistral-7B-Instruct-v0.3", + api_token: Optional[str] = None, + local: bool = False, + device: str = "auto", +) -> HuggingFaceGM: + """ + Create a Hugging Face Game Master instance. + + This is a convenience function for quick setup. + + Args: + model_id: Hugging Face model ID + api_token: Your HF API token (or set HF_TOKEN env var) + local: If True, load model locally instead of using API + device: Device for local inference ("auto", "cuda", "cpu", "mps") + + Returns: + HuggingFaceGM instance ready for use + + Example: + >>> gm = create_gm(api_token="hf_...") + >>> response = gm.chat([{"role": "user", "content": "Hello!"}]) + """ + return HuggingFaceGM( + model_id=model_id, + use_api=not local, + api_token=api_token, + device=device, + ) + + +# For backwards compatibility and simpler imports +__all__ = [ + "HuggingFaceGM", + "create_gm", + "HAS_TRANSFORMERS", + "HAS_HF_HUB", +] diff --git a/dnd_game_master_hfport/models/__init__.py b/dnd_game_master_hfport/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..30c71e417e502afb4b099d084b906d03461a432c --- /dev/null +++ b/dnd_game_master_hfport/models/__init__.py @@ -0,0 +1,22 @@ +"""Data models for D&D game state.""" + +from .character import Character, create_blank_character, create_test_character +from .scene import Scene, NPC, Lighting, TimeOfDay, NPCAttitude +from .game_state import GameState, GamePhase, NarrativeEntry + +__all__ = [ + # Character + "Character", + "create_blank_character", + "create_test_character", + # Scene + "Scene", + "NPC", + "Lighting", + "TimeOfDay", + "NPCAttitude", + # Game State + "GameState", + "GamePhase", + "NarrativeEntry", +] diff --git a/dnd_game_master_hfport/models/character.py b/dnd_game_master_hfport/models/character.py new file mode 100644 index 0000000000000000000000000000000000000000..93c10ae5a52a58fcd3c1017fb046f2318a5eb809 --- /dev/null +++ b/dnd_game_master_hfport/models/character.py @@ -0,0 +1,344 @@ +"""Character data model for D&D 5e.""" + +from dataclasses import dataclass, field +from typing import Dict, List, Optional + + +@dataclass +class Character: + """A D&D 5e player character.""" + + # Basic info + name: str + race: str + character_class: str + level: int = 1 + background: str = "" + alignment: str = "Neutral" + + # Ability scores + strength: int = 10 + dexterity: int = 10 + constitution: int = 10 + intelligence: int = 10 + wisdom: int = 10 + charisma: int = 10 + + # Combat stats + max_hp: int = 10 + current_hp: int = 10 + temp_hp: int = 0 + armor_class: int = 10 + speed: int = 30 + initiative_bonus: int = 0 + + # Proficiencies + proficient_skills: List[str] = field(default_factory=list) + expertise_skills: List[str] = field(default_factory=list) + proficient_saves: List[str] = field(default_factory=list) + + # Equipment and features + equipment: List[str] = field(default_factory=list) + features: List[str] = field(default_factory=list) + + # Status + conditions: List[str] = field(default_factory=list) + death_saves_success: int = 0 + death_saves_failure: int = 0 + + def get_ability_scores(self) -> Dict[str, int]: + """Get all ability scores as a dictionary.""" + return { + "strength": self.strength, + "dexterity": self.dexterity, + "constitution": self.constitution, + "intelligence": self.intelligence, + "wisdom": self.wisdom, + "charisma": self.charisma, + } + + def is_proficient(self, skill: str) -> bool: + """Check if character is proficient in a skill.""" + normalized = skill.lower().replace(" ", "_") + return normalized in [ + s.lower().replace(" ", "_") for s in self.proficient_skills + ] + + def has_expertise(self, skill: str) -> bool: + """Check if character has expertise in a skill.""" + normalized = skill.lower().replace(" ", "_") + return normalized in [ + s.lower().replace(" ", "_") for s in self.expertise_skills + ] + + def is_save_proficient(self, ability: str) -> bool: + """Check if character is proficient in a saving throw.""" + return ability.lower() in [s.lower() for s in self.proficient_saves] + + def take_damage(self, amount: int) -> int: + """ + Apply damage to the character. + + Args: + amount: Damage amount + + Returns: + Actual damage taken after temp HP + """ + # Temp HP absorbs damage first + if self.temp_hp > 0: + if amount <= self.temp_hp: + self.temp_hp -= amount + return 0 + else: + amount -= self.temp_hp + self.temp_hp = 0 + + self.current_hp = max(0, self.current_hp - amount) + return amount + + def heal(self, amount: int) -> int: + """ + Heal the character. + + Args: + amount: Healing amount + + Returns: + Actual HP restored + """ + old_hp = self.current_hp + self.current_hp = min(self.max_hp, self.current_hp + amount) + return self.current_hp - old_hp + + def is_unconscious(self) -> bool: + """Check if character is at 0 HP.""" + return self.current_hp <= 0 + + def is_dead(self) -> bool: + """Check if character has failed 3 death saves.""" + return self.death_saves_failure >= 3 + + def is_stable(self) -> bool: + """Check if character has succeeded 3 death saves.""" + return self.death_saves_success >= 3 + + def add_condition(self, condition: str): + """Add a condition to the character.""" + if condition.lower() not in [c.lower() for c in self.conditions]: + self.conditions.append(condition) + + def remove_condition(self, condition: str): + """Remove a condition from the character.""" + self.conditions = [ + c for c in self.conditions if c.lower() != condition.lower() + ] + + def has_condition(self, condition: str) -> bool: + """Check if character has a condition.""" + return condition.lower() in [c.lower() for c in self.conditions] + + def to_dict(self) -> Dict: + """Convert character to dictionary for serialization.""" + return { + "name": self.name, + "race": self.race, + "character_class": self.character_class, + "level": self.level, + "background": self.background, + "alignment": self.alignment, + "ability_scores": self.get_ability_scores(), + "max_hp": self.max_hp, + "current_hp": self.current_hp, + "temp_hp": self.temp_hp, + "armor_class": self.armor_class, + "speed": self.speed, + "proficient_skills": self.proficient_skills, + "expertise_skills": self.expertise_skills, + "proficient_saves": self.proficient_saves, + "equipment": self.equipment, + "features": self.features, + "conditions": self.conditions, + } + + def summary(self) -> str: + """Get a brief character summary.""" + return ( + f"{self.name}, Level {self.level} {self.race} {self.character_class}\n" + f"HP: {self.current_hp}/{self.max_hp} | AC: {self.armor_class}\n" + f"STR: {self.strength} DEX: {self.dexterity} CON: {self.constitution}\n" + f"INT: {self.intelligence} WIS: {self.wisdom} CHA: {self.charisma}" + ) + + +def create_blank_character(name: str = "Adventurer") -> Character: + """Create a blank character with default stats.""" + return Character( + name=name, + race="Human", + character_class="Fighter", + level=1, + ) + + +def create_test_character() -> Character: + """Create a pre-built test character (Kira the Rogue).""" + return Character( + name="Kira", + race="Half-Elf", + character_class="Rogue", + level=3, + background="Criminal", + alignment="Chaotic Good", + strength=10, + dexterity=16, + constitution=12, + intelligence=14, + wisdom=10, + charisma=14, + max_hp=21, + current_hp=21, + armor_class=14, + speed=30, + proficient_skills=[ + "stealth", "perception", "deception", + "sleight_of_hand", "acrobatics", "investigation" + ], + expertise_skills=["stealth", "sleight_of_hand"], + proficient_saves=["dexterity", "intelligence"], + equipment=[ + "Shortsword", "Shortbow", "Leather Armor", + "Thieves' Tools", "Burglar's Pack" + ], + features=["Sneak Attack (2d6)", "Cunning Action", "Expertise"], + ) + + +def create_fighter_character() -> Character: + """Create a pre-built Fighter character (Thorin the Dwarf).""" + return Character( + name="Thorin", + race="Dwarf", + character_class="Fighter", + level=3, + background="Soldier", + alignment="Lawful Good", + strength=16, + dexterity=12, + constitution=16, + intelligence=10, + wisdom=12, + charisma=8, + max_hp=31, + current_hp=31, + armor_class=18, + speed=25, + proficient_skills=["athletics", "intimidation", "perception", "survival"], + expertise_skills=[], + proficient_saves=["strength", "constitution"], + equipment=[ + "Longsword", "Shield", "Chain Mail", + "Handaxe (2)", "Explorer's Pack" + ], + features=["Fighting Style (Defense)", "Second Wind", "Action Surge"], + ) + + +def create_wizard_character() -> Character: + """Create a pre-built Wizard character (Elara the Elf).""" + return Character( + name="Elara", + race="High Elf", + character_class="Wizard", + level=3, + background="Sage", + alignment="Neutral Good", + strength=8, + dexterity=14, + constitution=12, + intelligence=17, + wisdom=13, + charisma=10, + max_hp=17, + current_hp=17, + armor_class=12, + speed=30, + proficient_skills=["arcana", "history", "investigation", "insight"], + expertise_skills=[], + proficient_saves=["intelligence", "wisdom"], + equipment=[ + "Quarterstaff", "Spellbook", "Component Pouch", + "Scholar's Pack", "Dagger" + ], + features=["Arcane Recovery", "Evocation Savant", "Sculpt Spells"], + ) + + +def create_cleric_character() -> Character: + """Create a pre-built Cleric character (Brother Marcus).""" + return Character( + name="Brother Marcus", + race="Human", + character_class="Cleric", + level=3, + background="Acolyte", + alignment="Lawful Good", + strength=14, + dexterity=10, + constitution=14, + intelligence=10, + wisdom=16, + charisma=12, + max_hp=24, + current_hp=24, + armor_class=18, + speed=30, + proficient_skills=["medicine", "religion", "insight", "persuasion"], + expertise_skills=[], + proficient_saves=["wisdom", "charisma"], + equipment=[ + "Mace", "Shield", "Chain Mail", + "Holy Symbol", "Priest's Pack" + ], + features=["Spellcasting", "Channel Divinity", "Turn Undead"], + ) + + +def create_ranger_character() -> Character: + """Create a pre-built Ranger character (Sylvan the Wood Elf).""" + return Character( + name="Sylvan", + race="Wood Elf", + character_class="Ranger", + level=3, + background="Outlander", + alignment="Neutral Good", + strength=12, + dexterity=16, + constitution=14, + intelligence=10, + wisdom=14, + charisma=10, + max_hp=28, + current_hp=28, + armor_class=15, + speed=35, + proficient_skills=["nature", "survival", "stealth", "perception", "animal_handling"], + expertise_skills=[], + proficient_saves=["strength", "dexterity"], + equipment=[ + "Longbow", "Shortsword (2)", "Studded Leather", + "Explorer's Pack", "Quiver (20 arrows)" + ], + features=["Favored Enemy (Beasts)", "Natural Explorer (Forest)", "Fighting Style (Archery)"], + ) + + +# Dictionary of all preset characters +PRESET_CHARACTERS = { + "Kira (Rogue)": create_test_character, + "Thorin (Fighter)": create_fighter_character, + "Elara (Wizard)": create_wizard_character, + "Brother Marcus (Cleric)": create_cleric_character, + "Sylvan (Ranger)": create_ranger_character, +} diff --git a/dnd_game_master_hfport/models/game_state.py b/dnd_game_master_hfport/models/game_state.py new file mode 100644 index 0000000000000000000000000000000000000000..09ebeb61d116471b4702f28733e2e138be3d8e65 --- /dev/null +++ b/dnd_game_master_hfport/models/game_state.py @@ -0,0 +1,288 @@ +"""Game state management for D&D sessions.""" + +from dataclasses import dataclass, field +from typing import List, Optional, Dict, Any +from enum import Enum +from datetime import datetime +from .character import Character +from .scene import Scene + + +class GamePhase(Enum): + """Current phase of the game.""" + SETUP = "setup" + ADVENTURE_SELECTION = "adventure_selection" + CHARACTER_CREATION = "character_creation" + PLAYING = "playing" + COMBAT = "combat" + PAUSED = "paused" + ENDED = "ended" + + +class NarrativeType(Enum): + """Types of narrative entries.""" + SCENE = "scene" + PLAYER = "player" + GM_RESPONSE = "gm_response" + NPC = "npc" + ROLL = "roll" + ROLL_REQUEST = "roll_request" + ROLL_OUTCOME = "roll_outcome" + DAMAGE = "damage" + SYSTEM = "system" + + +@dataclass +class NarrativeEntry: + """A single entry in the narrative log.""" + entry_type: str + content: str + timestamp: str = field(default_factory=lambda: datetime.now().isoformat()) + metadata: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary.""" + return { + "entry_type": self.entry_type, + "content": self.content, + "timestamp": self.timestamp, + "metadata": self.metadata, + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "NarrativeEntry": + """Create from dictionary.""" + return cls( + entry_type=data.get("entry_type", "system"), + content=data.get("content", ""), + timestamp=data.get("timestamp", datetime.now().isoformat()), + metadata=data.get("metadata", {}), + ) + + +@dataclass +class CombatState: + """State for combat encounters.""" + is_active: bool = False + round_number: int = 0 + turn_order: List[str] = field(default_factory=list) + current_turn_index: int = 0 + combatants: Dict[str, Dict[str, Any]] = field(default_factory=dict) + + def start_combat(self, initiative_order: List[tuple]): + """ + Start combat with initiative order. + + Args: + initiative_order: List of (name, initiative_roll) tuples + """ + self.is_active = True + self.round_number = 1 + self.current_turn_index = 0 + + # Sort by initiative (descending) + sorted_order = sorted(initiative_order, key=lambda x: x[1], reverse=True) + self.turn_order = [name for name, _ in sorted_order] + + # Initialize combatant tracking + for name, initiative in sorted_order: + self.combatants[name] = { + "initiative": initiative, + "conditions": [], + "has_acted": False, + } + + def next_turn(self) -> Optional[str]: + """Advance to next combatant's turn.""" + if not self.is_active: + return None + + # Mark current combatant as having acted + current = self.get_current_combatant() + if current and current in self.combatants: + self.combatants[current]["has_acted"] = True + + # Move to next + self.current_turn_index += 1 + + # Check for new round + if self.current_turn_index >= len(self.turn_order): + self.current_turn_index = 0 + self.round_number += 1 + # Reset has_acted for all + for combatant in self.combatants.values(): + combatant["has_acted"] = False + + return self.get_current_combatant() + + def get_current_combatant(self) -> Optional[str]: + """Get the name of the current combatant.""" + if not self.turn_order or self.current_turn_index >= len(self.turn_order): + return None + return self.turn_order[self.current_turn_index] + + def remove_combatant(self, name: str): + """Remove a combatant (defeated, fled, etc.).""" + if name in self.turn_order: + idx = self.turn_order.index(name) + self.turn_order.remove(name) + if idx < self.current_turn_index: + self.current_turn_index -= 1 + if name in self.combatants: + del self.combatants[name] + + def end_combat(self): + """End the combat encounter.""" + self.is_active = False + self.round_number = 0 + self.turn_order = [] + self.current_turn_index = 0 + self.combatants = {} + + +@dataclass +class GameState: + """Complete game state for a D&D session.""" + # Session info + session_id: str = "" + session_name: str = "New Adventure" + created_at: str = field(default_factory=lambda: datetime.now().isoformat()) + + # Game phase + phase: GamePhase = GamePhase.SETUP + + # Character and scene + character: Optional[Character] = None + scene: Optional[Scene] = None + + # Combat + combat: CombatState = field(default_factory=CombatState) + + # Narrative history + narrative_history: List[NarrativeEntry] = field(default_factory=list) + + # LLM conversation history (for context) + conversation_history: List[Dict[str, str]] = field(default_factory=list) + + # Adventure metadata + adventure_name: str = "" + adventure_description: str = "" + + def add_narrative( + self, + entry_type: str, + content: str, + **metadata + ) -> NarrativeEntry: + """Add a narrative entry to the history.""" + entry = NarrativeEntry( + entry_type=entry_type, + content=content, + metadata=metadata + ) + self.narrative_history.append(entry) + return entry + + def add_message(self, role: str, content: str): + """Add a message to the LLM conversation history.""" + self.conversation_history.append({ + "role": role, + "content": content + }) + + def get_recent_context(self, max_entries: int = 10) -> str: + """Get recent narrative for context building.""" + recent = self.narrative_history[-max_entries:] + return "\n".join([ + f"[{e.entry_type.upper()}] {e.content}" + for e in recent + ]) + + def get_recent_messages(self, max_messages: int = 20) -> List[Dict[str, str]]: + """Get recent conversation messages for LLM context.""" + return self.conversation_history[-max_messages:] + + def clear_conversation(self): + """Clear conversation history (but keep narrative).""" + self.conversation_history = [] + + def start_adventure( + self, + character: Character, + scene: Scene, + adventure_name: str = "", + ): + """Initialize a new adventure.""" + self.character = character + self.scene = scene + self.adventure_name = adventure_name + self.phase = GamePhase.PLAYING + self.narrative_history = [] + self.conversation_history = [] + + # Add initial scene to narrative + self.add_narrative("scene", scene.description) + + def to_dict(self) -> Dict[str, Any]: + """Serialize game state to dictionary.""" + return { + "session_id": self.session_id, + "session_name": self.session_name, + "created_at": self.created_at, + "phase": self.phase.value, + "character": self.character.to_dict() if self.character else None, + "scene": self.scene.to_dict() if self.scene else None, + "narrative_history": [e.to_dict() for e in self.narrative_history], + "adventure_name": self.adventure_name, + "adventure_description": self.adventure_description, + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "GameState": + """Deserialize game state from dictionary.""" + state = cls( + session_id=data.get("session_id", ""), + session_name=data.get("session_name", "New Adventure"), + created_at=data.get("created_at", datetime.now().isoformat()), + phase=GamePhase(data.get("phase", "setup")), + adventure_name=data.get("adventure_name", ""), + adventure_description=data.get("adventure_description", ""), + ) + + # Load character if present + if data.get("character"): + from .character import Character + char_data = data["character"] + state.character = Character( + name=char_data.get("name", "Adventurer"), + race=char_data.get("race", "Human"), + character_class=char_data.get("character_class", "Fighter"), + level=char_data.get("level", 1), + # Add other fields as needed + ) + + # Load scene if present + if data.get("scene"): + state.scene = Scene.from_dict(data["scene"]) + + # Load narrative history + state.narrative_history = [ + NarrativeEntry.from_dict(e) + for e in data.get("narrative_history", []) + ] + + return state + + def save_to_file(self, filepath: str): + """Save game state to a JSON file.""" + import json + with open(filepath, 'w') as f: + json.dump(self.to_dict(), f, indent=2) + + @classmethod + def load_from_file(cls, filepath: str) -> "GameState": + """Load game state from a JSON file.""" + import json + with open(filepath, 'r') as f: + data = json.load(f) + return cls.from_dict(data) diff --git a/dnd_game_master_hfport/models/scene.py b/dnd_game_master_hfport/models/scene.py new file mode 100644 index 0000000000000000000000000000000000000000..8711ec8bf9b5281576fc540a8a4e3a27d3770860 --- /dev/null +++ b/dnd_game_master_hfport/models/scene.py @@ -0,0 +1,235 @@ +"""Scene and environment data models for D&D.""" + +from dataclasses import dataclass, field +from typing import List, Optional, Dict, Any +from enum import Enum + + +class TimeOfDay(Enum): + """Time of day for scene atmosphere.""" + DAWN = "dawn" + MORNING = "morning" + MIDDAY = "midday" + AFTERNOON = "afternoon" + DUSK = "dusk" + EVENING = "evening" + NIGHT = "night" + + +class Lighting(Enum): + """Lighting conditions affecting visibility.""" + BRIGHT = "bright" + DIM = "dim" + DARK = "dark" + + +class NPCAttitude(Enum): + """NPC disposition toward the party.""" + HOSTILE = "hostile" + UNFRIENDLY = "unfriendly" + NEUTRAL = "neutral" + FRIENDLY = "friendly" + HELPFUL = "helpful" + + +@dataclass +class NPC: + """A non-player character in the scene.""" + name: str + description: str = "" + attitude: NPCAttitude = NPCAttitude.NEUTRAL + notes: str = "" + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary.""" + return { + "name": self.name, + "description": self.description, + "attitude": self.attitude.value, + "notes": self.notes, + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "NPC": + """Create NPC from dictionary.""" + return cls( + name=data.get("name", "Unknown"), + description=data.get("description", ""), + attitude=NPCAttitude(data.get("attitude", "neutral")), + notes=data.get("notes", ""), + ) + + +@dataclass +class Scene: + """Current scene/location in the game world.""" + location: str + description: str + lighting: Lighting = Lighting.BRIGHT + time_of_day: TimeOfDay = TimeOfDay.MIDDAY + npcs: List[NPC] = field(default_factory=list) + objects: List[str] = field(default_factory=list) + exits: List[str] = field(default_factory=list) + notes: str = "" + + def to_context_string(self) -> str: + """Format scene as context for the LLM.""" + parts = [ + f"Location: {self.location}", + f"Time: {self.time_of_day.value.title()}, {self.lighting.value} lighting", + f"Description: {self.description}", + ] + + if self.npcs: + npc_list = ", ".join([ + f"{n.name} ({n.attitude.value})" for n in self.npcs + ]) + parts.append(f"NPCs present: {npc_list}") + + if self.objects: + parts.append(f"Notable objects: {', '.join(self.objects)}") + + if self.exits: + parts.append(f"Exits: {', '.join(self.exits)}") + + return "\n".join(parts) + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for serialization.""" + return { + "location": self.location, + "description": self.description, + "lighting": self.lighting.value, + "time_of_day": self.time_of_day.value, + "npcs": [npc.to_dict() for npc in self.npcs], + "objects": self.objects, + "exits": self.exits, + "notes": self.notes, + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Scene": + """Create Scene from dictionary.""" + npcs = [NPC.from_dict(n) for n in data.get("npcs", [])] + return cls( + location=data.get("location", "Unknown"), + description=data.get("description", ""), + lighting=Lighting(data.get("lighting", "bright")), + time_of_day=TimeOfDay(data.get("time_of_day", "midday")), + npcs=npcs, + objects=data.get("objects", []), + exits=data.get("exits", []), + notes=data.get("notes", ""), + ) + + def add_npc(self, npc: NPC): + """Add an NPC to the scene.""" + self.npcs.append(npc) + + def remove_npc(self, name: str): + """Remove an NPC by name.""" + self.npcs = [n for n in self.npcs if n.name.lower() != name.lower()] + + def get_npc(self, name: str) -> Optional[NPC]: + """Get an NPC by name.""" + for npc in self.npcs: + if npc.name.lower() == name.lower(): + return npc + return None + + def add_object(self, obj: str): + """Add a notable object to the scene.""" + if obj not in self.objects: + self.objects.append(obj) + + def remove_object(self, obj: str): + """Remove an object from the scene.""" + self.objects = [o for o in self.objects if o.lower() != obj.lower()] + + def update_lighting(self, lighting: Lighting): + """Update scene lighting.""" + self.lighting = lighting + + def advance_time(self): + """Advance to the next time of day.""" + times = list(TimeOfDay) + current_idx = times.index(self.time_of_day) + next_idx = (current_idx + 1) % len(times) + self.time_of_day = times[next_idx] + + +# Premade adventure scenes +def create_tavern_scene() -> Scene: + """Create a classic tavern starting scene.""" + return Scene( + location="The Dusty Tankard Tavern", + description=( + "A dimly lit tavern filled with the murmur of hushed conversations. " + "Wooden beams creak overhead, and the smell of ale and roasted meat " + "hangs thick in the air. A crackling fireplace provides warmth against " + "the chill outside." + ), + lighting=Lighting.DIM, + time_of_day=TimeOfDay.EVENING, + npcs=[ + NPC( + "Mira the Bartender", + "A stern woman with knowing eyes and calloused hands", + NPCAttitude.NEUTRAL + ), + NPC( + "Hooded Stranger", + "Sits alone in the corner, nursing a drink", + NPCAttitude.UNFRIENDLY + ), + ], + objects=["Notice board", "Crackling fireplace", "Locked cellar door"], + exits=["Front door to the street", "Back door to the alley", "Stairs to rooms above"], + ) + + +def create_dungeon_entrance_scene() -> Scene: + """Create a dungeon entrance scene.""" + return Scene( + location="Ancient Crypt Entrance", + description=( + "Crumbling stone steps descend into darkness. The air grows cold and " + "damp as you approach the weathered entrance. Faded runes are carved " + "into the archway, their meaning long forgotten. A foul smell drifts " + "up from below." + ), + lighting=Lighting.DIM, + time_of_day=TimeOfDay.AFTERNOON, + npcs=[], + objects=[ + "Faded warning runes", "Scattered bones", "Rusty iron gate", + "Extinguished torch sconces" + ], + exits=["Stone steps descending into darkness", "Forest path back to town"], + ) + + +def create_forest_clearing_scene() -> Scene: + """Create a forest clearing scene.""" + return Scene( + location="Moonlit Forest Clearing", + description=( + "Silver moonlight filters through the canopy, illuminating a small " + "clearing in the ancient forest. Fireflies dance among the ferns, " + "and the distant hoot of an owl echoes through the trees. A moss-covered " + "stone circle stands at the center." + ), + lighting=Lighting.DIM, + time_of_day=TimeOfDay.NIGHT, + npcs=[], + objects=["Ancient stone circle", "Fairy ring of mushrooms", "Clear spring"], + exits=["Overgrown path north", "Game trail east", "Dense thicket west"], + ) + + +# Dictionary of all preset scenes +PRESET_SCENES = { + "The Dusty Tankard (Tavern)": create_tavern_scene, + "Ancient Crypt Entrance (Dungeon)": create_dungeon_entrance_scene, + "Moonlit Forest Clearing": create_forest_clearing_scene, +} diff --git a/dnd_game_master_hfport/prompts/__init__.py b/dnd_game_master_hfport/prompts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6c83685ce4d16b36b3d92524fbd850a349d8b936 --- /dev/null +++ b/dnd_game_master_hfport/prompts/__init__.py @@ -0,0 +1,19 @@ +"""System prompts for the D&D Game Master LLM.""" + +from .gm_prompts import ( + build_gm_system_prompt, + build_scene_context, + build_roll_outcome_prompt, + build_scene_extraction_prompt, + build_combat_prompt, + GM_PERSONALITY, +) + +__all__ = [ + "build_gm_system_prompt", + "build_scene_context", + "build_roll_outcome_prompt", + "build_scene_extraction_prompt", + "build_combat_prompt", + "GM_PERSONALITY", +] diff --git a/dnd_game_master_hfport/prompts/gm_prompts.py b/dnd_game_master_hfport/prompts/gm_prompts.py new file mode 100644 index 0000000000000000000000000000000000000000..748488c766809268301f949e720af906ccf76c1f --- /dev/null +++ b/dnd_game_master_hfport/prompts/gm_prompts.py @@ -0,0 +1,288 @@ +"""Prompt templates for the D&D Game Master LLM.""" + +from typing import Optional +from ..models.character import Character +from ..models.scene import Scene + + +# Base GM personality and rules +GM_PERSONALITY = """You are an expert Dungeons & Dragons 5th Edition Game Master with decades of experience running immersive campaigns. Your storytelling is vivid and engaging, bringing scenes to life with sensory details. You are fair but challenging, creating tension while respecting player agency. + +Your core principles: +- Describe environments with rich sensory details (sights, sounds, smells) +- Give NPCs distinct personalities and motivations +- Maintain internal world consistency +- Reward creative problem-solving +- Apply D&D 5e rules accurately +- Keep the narrative moving forward +- Balance challenge with fun""" + + +def build_gm_system_prompt() -> str: + """Build the main GM system prompt for action adjudication.""" + return f"""{GM_PERSONALITY} + +## Your Role +When a player describes an action, you must decide how to resolve it and respond with a JSON object. + +## Decision Types +- "impossible": The action cannot physically happen or breaks world logic +- "auto_success": Trivial action that any adventurer could accomplish +- "auto_fail": Action that would certainly fail regardless of luck +- "needs_roll": Outcome is uncertain and requires a skill check + +## Response Format +You MUST respond with valid JSON only, in this exact structure: + +For needs_roll: +{{ + "decision": "needs_roll", + "skill": "stealth", + "dc": 15, + "advantage": false, + "disadvantage": false, + "narration": "Brief setup for what happens as you attempt this...", + "reason": "Why this requires a roll" +}} + +For auto_success: +{{ + "decision": "auto_success", + "narration": "Vivid description of what happens as you succeed...", + "reason": "Why this automatically succeeds" +}} + +For auto_fail: +{{ + "decision": "auto_fail", + "narration": "Description of why this fails...", + "reason": "Why this cannot succeed" +}} + +For impossible: +{{ + "decision": "impossible", + "narration": "Explanation of why this cannot be attempted...", + "reason": "Why this is impossible", + "suggestions": ["alternative action 1", "alternative action 2"] +}} + +## Skill Check Guidelines +Common DCs: +- DC 5: Very easy (climb a knotted rope) +- DC 10: Easy (hear an approaching guard) +- DC 15: Medium (pick a typical lock) +- DC 20: Hard (leap across a 20-foot chasm) +- DC 25: Very hard (pick an amazing lock) +- DC 30: Nearly impossible (leap across a 30-foot chasm) + +Match skills to actions: +- Stealth: Moving quietly, hiding +- Perception: Noticing things, searching +- Investigation: Deducing, examining closely +- Persuasion: Convincing through charm +- Deception: Lying convincingly +- Intimidation: Threatening, coercing +- Athletics: Climbing, jumping, swimming +- Acrobatics: Balance, tumbling, dodging +- Sleight of Hand: Pickpocketing, hiding objects +- Insight: Reading intentions, detecting lies + +IMPORTANT: Respond with JSON only. No markdown, no explanation outside the JSON.""" + + +def build_scene_context(character: Character, scene: Scene) -> str: + """Build context string with current scene and character info.""" + context_parts = [ + "=== CURRENT SCENE ===", + scene.to_context_string(), + "", + "=== ACTIVE CHARACTER ===", + f"Name: {character.name}", + f"Race/Class: {character.race} {character.character_class} (Level {character.level})", + f"HP: {character.current_hp}/{character.max_hp} | AC: {character.armor_class}", + ] + + if character.conditions: + context_parts.append(f"Conditions: {', '.join(character.conditions)}") + + return "\n".join(context_parts) + + +def build_roll_outcome_prompt( + action: str, + skill: str, + roll_total: int, + dc: int, + success: bool, + critical: bool = False, + natural_roll: int = 0, +) -> str: + """Build prompt for narrating roll outcomes.""" + if critical and success: + result = "CRITICAL SUCCESS (Natural 20)" + elif critical and not success: + result = "CRITICAL FAILURE (Natural 1)" + elif success: + result = "SUCCESS" + else: + result = "FAILURE" + + margin = roll_total - dc + margin_str = f"+{margin}" if margin >= 0 else str(margin) + + return f"""The player attempted: "{action}" + +Skill check: {skill.title()} +Roll: {natural_roll} + modifier = {roll_total} vs DC {dc} (margin: {margin_str}) +Result: {result} + +Narrate the outcome vividly, describing what happens based on the result. Be specific and cinematic. +{"The critical success should feel especially rewarding and impactful." if critical and success else ""} +{"The critical failure should have dramatic (but not campaign-ending) consequences." if critical and not success else ""} + +After narrating, if the scene has changed significantly, note the new scene state. + +Respond with JSON: +{{ + "narration": "Your vivid narration of what happens...", + "location_changed": false, + "new_location": null, + "new_description": null, + "npcs_changed": [], + "objects_changed": [] +}}""" + + +def build_scene_extraction_prompt(narrative: str) -> str: + """Build prompt for extracting scene details from narrative.""" + return f"""Analyze this narrative and extract the current scene details: + +--- +{narrative} +--- + +Extract and respond with JSON: +{{ + "location": "Name of the current location", + "description": "Detailed description of the scene (2-3 sentences)", + "lighting": "bright" | "dim" | "dark", + "time_of_day": "dawn" | "morning" | "midday" | "afternoon" | "dusk" | "evening" | "night", + "npcs": [ + {{"name": "NPC name", "description": "Brief description", "attitude": "hostile" | "unfriendly" | "neutral" | "friendly" | "helpful"}} + ], + "objects": ["notable object 1", "notable object 2"], + "exits": ["exit 1", "exit 2"] +}} + +Only include NPCs and objects that are clearly present in the narrative. If unsure, omit them.""" + + +def build_combat_prompt( + combatants: list, + current_turn: str, + round_number: int, + scene_context: str, +) -> str: + """Build prompt for combat encounters.""" + combatant_list = "\n".join([ + f"- {c['name']}: HP {c.get('hp', '?')}, AC {c.get('ac', '?')}" + for c in combatants + ]) + + return f"""{GM_PERSONALITY} + +## COMBAT ENCOUNTER +Round: {round_number} +Current Turn: {current_turn} + +### Combatants: +{combatant_list} + +### Scene: +{scene_context} + +When describing combat actions: +1. Be visceral and exciting +2. Describe the physical action vividly +3. React to hits and misses dramatically +4. Track positioning and tactics +5. Give enemies personality in their fighting style + +For enemy turns, describe their action and respond with: +{{ + "action": "attack" | "spell" | "ability" | "movement" | "other", + "target": "target name", + "narration": "Description of what they do", + "attack_roll": null, // For attacks: the roll to make + "damage_roll": null, // For damage: the dice to roll + "save_dc": null, // For saves: the DC + "save_type": null // For saves: which save +}}""" + + +def build_npc_dialogue_prompt( + npc_name: str, + npc_description: str, + npc_attitude: str, + player_said: str, + context: str, +) -> str: + """Build prompt for NPC dialogue responses.""" + return f"""You are roleplaying as {npc_name}. + +Description: {npc_description} +Attitude toward party: {npc_attitude} + +Context: +{context} + +The player says: "{player_said}" + +Respond in character as {npc_name}. Stay true to their personality and attitude. +Consider: +- Their current mood and disposition +- What they know vs. don't know +- Their goals and motivations +- Their speech patterns and mannerisms + +Respond with JSON: +{{ + "dialogue": "What {npc_name} says in response", + "action": "Optional physical action or expression", + "attitude_change": null // or "more_friendly" / "more_hostile" if attitude shifts +}}""" + + +def build_exploration_prompt( + action: str, + scene: Scene, + character: Character, +) -> str: + """Build prompt for exploration and investigation actions.""" + return f"""{GM_PERSONALITY} + +The player is exploring and wants to: {action} + +Current Scene: +{scene.to_context_string()} + +Character: +{character.name} - {character.race} {character.character_class} (Level {character.level}) + +Describe what they discover or experience. If they're searching or investigating: +- Reward thoroughness with details +- Hide secrets behind appropriate skill checks +- Connect discoveries to the larger world/story +- Foreshadow dangers or opportunities + +If no roll is needed, describe what they find directly. If a roll would reveal more, ask for one. + +Respond with JSON: +{{ + "decision": "auto_success" | "needs_roll", + "skill": "perception", // if needs_roll + "dc": 12, // if needs_roll + "narration": "What they discover or experience...", + "hidden_info": "Additional info revealed on success" // if needs_roll +}}""" diff --git a/dnd_game_master_hfport/requirements.txt b/dnd_game_master_hfport/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..4f23901d6888787b0106e3c68f8f15ba0b1c0f36 --- /dev/null +++ b/dnd_game_master_hfport/requirements.txt @@ -0,0 +1,17 @@ +# D&D AI Game Master - Python Dependencies + +# Required for Hugging Face Inference API +huggingface_hub>=0.20.0 + +# Required for local model inference (optional) +transformers>=4.40.0 +torch>=2.0.0 + +# Optional: for faster inference on NVIDIA GPUs +# accelerate>=0.27.0 + +# Optional: for quantized models (reduced memory) +# bitsandbytes>=0.42.0 + +# Optional: for Gradio web interface +# gradio>=4.0.0 diff --git a/dnd_scenarios.py b/dnd_scenarios.py new file mode 100644 index 0000000000000000000000000000000000000000..18e0e452848c2f6c7aa22c5a8bef2e3b6a888ad7 --- /dev/null +++ b/dnd_scenarios.py @@ -0,0 +1,229 @@ +"""D&D Adventure scenario data and sequence generation. + +This module provides: +- DND_ENCOUNTERS: Dictionary of encounter templates by type +- generate_dnd_sequence: Function to generate adventure sequences +""" + +import json +import random + +# Encounter templates organized by type +DND_ENCOUNTERS = { + "combat": [ + ("Goblin Ambush", "A band of goblin raiders springs from hiding, blocking the path ahead", ["Goblin", "Goblin Boss"]), + ("Wolf Pack Hunt", "Hungry wolves circle the party, eyes gleaming in the darkness", ["Wolf", "Dire Wolf"]), + ("Orc War Party", "War drums echo as an orc raiding party charges forward", ["Orc", "Orc War Chief"]), + ("Undead Rising", "The ground trembles as skeletal warriors claw their way to the surface", ["Skeleton", "Zombie"]), + ("Bandit Roadblock", "Rough-looking bandits demand toll or blood", ["Bandit", "Bandit Captain"]), + ("Giant Spider Lair", "Webs glisten in the torchlight as massive spiders descend", ["Giant Spider", "Phase Spider"]), + ("Troll Bridge", "A massive troll guards the only crossing, demanding tribute", ["Troll"]), + ("Dragon Wyrmling", "A young dragon defends its growing hoard with fierce determination", ["Dragon Wyrmling"]), + ("Cultist Ritual", "Robed figures chant around a dark altar, summoning something terrible", ["Cultist", "Cult Fanatic"]), + ("Elemental Fury", "An elemental creature of pure destructive force bars the way", ["Fire Elemental", "Earth Elemental"]), + ], + "social": [ + ("Tavern Rumors", "Locals share whispered tales over ale in a dimly lit tavern", ["Commoner", "Noble"]), + ("Noble's Request", "A wealthy patron offers gold for a dangerous task", ["Noble", "Knight"]), + ("Merchant Dispute", "Two traders argue over goods, seeking an arbiter", ["Merchant", "Guard"]), + ("Temple Audience", "A high priest offers guidance... for a price", ["Priest", "Acolyte"]), + ("Guild Meeting", "The local guild has work, but demands loyalty first", ["Guild Master", "Apprentice"]), + ("Royal Court", "The party gains audience with local nobility", ["King", "Advisor"]), + ("Mysterious Stranger", "A hooded figure offers cryptic warnings and hidden knowledge", ["Spy", "Assassin"]), + ("Festival Celebration", "The town celebrates, but something lurks beneath the merriment", ["Commoner", "Entertainer"]), + ], + "exploration": [ + ("Ancient Ruins", "Crumbling stone walls hint at a civilization long forgotten", ["trap", "puzzle"]), + ("Dark Cave", "The cave mouth yawns before you, promising secrets in the darkness", ["hazard", "treasure"]), + ("Haunted Manor", "The abandoned estate creaks and groans with supernatural presence", ["ghost", "trap"]), + ("Underground River", "A subterranean waterway offers passage deeper into the earth", ["hazard", "discovery"]), + ("Forest Clearing", "Fey magic permeates this impossibly perfect glade", ["fey", "mystery"]), + ("Mountain Pass", "Treacherous cliffs and howling winds test every step", ["hazard", "environmental"]), + ("Sunken Temple", "Flooded chambers hide ancient treasures and guardians", ["trap", "treasure"]), + ("Wizard's Tower", "Arcane energy crackles through abandoned magical laboratories", ["magic", "puzzle"]), + ], + "puzzle": [ + ("The Riddle Door", "Ancient text poses a riddle - only the wise may pass", ["intelligence", "lore"]), + ("Pressure Plates", "The floor is a grid of stone plates, some safe, some deadly", ["dexterity", "perception"]), + ("Mirror Maze", "Reflections shift and change, hiding the true path", ["wisdom", "investigation"]), + ("Elemental Locks", "Four elements must be combined in the correct order", ["arcana", "elements"]), + ("Ancestor's Trial", "Spirits of the past judge worth through tests of virtue", ["charisma", "history"]), + ("Clockwork Mechanism", "Gears and levers control something vital within", ["intelligence", "mechanics"]), + ("Symbol Sequence", "Ancient symbols must be activated in the prophesied order", ["religion", "arcana"]), + ("Weight Balance", "Treasures and counterweights must be perfectly balanced", ["wisdom", "mathematics"]), + ], + "boss": [ + ("Dragon's Lair", "The great wyrm awakens, fire building in its throat", ["Adult Dragon"]), + ("Lich's Sanctum", "The undead sorcerer rises from its throne of bones", ["Lich"]), + ("Beholder's Den", "Multiple eyes swivel toward you, each crackling with deadly magic", ["Beholder"]), + ("Giant King", "The massive ruler of giants hefts a weapon the size of a tree", ["Giant King"]), + ("Vampire Lord", "The ancient vampire regards you with cold, eternal hunger", ["Vampire"]), + ("Mind Flayer Colony", "Alien intellects probe your mind as tentacled horrors advance", ["Mind Flayer"]), + ("Demon Portal", "A rift to the Abyss spews forth a demon of terrible power", ["Demon Lord"]), + ("Corrupted Paladin", "Once a hero, now fallen to darkness and seeking to spread it", ["Death Knight"]), + ], +} + +# Difficulty modifiers +DIFFICULTY_LEVELS = { + "easy": { + "adjective": "manageable", + "enemies": "few", + "treasure": "modest", + }, + "medium": { + "adjective": "challenging", + "enemies": "several", + "treasure": "valuable", + }, + "hard": { + "adjective": "dangerous", + "enemies": "many", + "treasure": "significant", + }, + "deadly": { + "adjective": "deadly", + "enemies": "overwhelming", + "treasure": "legendary", + }, +} + +# Location flavors +LOCATIONS = [ + "dungeon depths", "ancient forest", "mountain fortress", "coastal caves", + "desert tomb", "frozen tundra", "volcanic lair", "feywild crossing", + "shadowfell edge", "planar rift", "underwater ruins", "floating citadel", +] + + +def generate_dnd_sequence(encounter_count, difficulty, include_combat, include_social, + include_exploration, include_puzzle, include_boss, + force_boss_end=True): + """Generate a D&D adventure sequence. + + Args: + encounter_count: Number of encounters (3-10) + difficulty: One of 'easy', 'medium', 'hard', 'deadly' + include_combat: Include combat encounters + include_social: Include social encounters + include_exploration: Include exploration encounters + include_puzzle: Include puzzle encounters + include_boss: Include boss encounters + force_boss_end: Force sequence to end with a boss fight + + Returns: + Tuple of (list_output, json_output, prompts_output) + """ + difficulty_info = DIFFICULTY_LEVELS.get(difficulty, DIFFICULTY_LEVELS["medium"]) + location = random.choice(LOCATIONS) + + # Build available encounters pool + available = [] + if include_combat: + available.extend([(e[0], e[1], "combat", e[2]) for e in DND_ENCOUNTERS["combat"]]) + if include_social: + available.extend([(e[0], e[1], "social", e[2]) for e in DND_ENCOUNTERS["social"]]) + if include_exploration: + available.extend([(e[0], e[1], "exploration", e[2]) for e in DND_ENCOUNTERS["exploration"]]) + if include_puzzle: + available.extend([(e[0], e[1], "puzzle", e[2]) for e in DND_ENCOUNTERS["puzzle"]]) + + if not available and not include_boss: + return "Select at least one encounter type!", "{}", "" + + sequence = [] + + # Reserve boss for end if requested + boss_encounter = None + if force_boss_end and include_boss: + boss_encounter = random.choice(DND_ENCOUNTERS["boss"]) + encounter_count -= 1 + + # Fill with random encounters + random.shuffle(available) + for enc in available[:encounter_count]: + sequence.append(enc) + + # Add boss at end + if boss_encounter: + sequence.append((boss_encounter[0], boss_encounter[1], "boss", boss_encounter[2])) + + if not sequence: + return "No encounters available!", "{}", "" + + # Format as list + list_output = f"## D&D Adventure in the {location.title()}\n" + list_output += f"**Difficulty:** {difficulty.title()} ({difficulty_info['adjective']})\n\n" + + for i, (name, desc, enc_type, extras) in enumerate(sequence, 1): + extras_str = ", ".join(extras) if isinstance(extras, list) else str(extras) + list_output += f"**{i}. {name}** [{enc_type}]\n{desc}\n*Features: {extras_str}*\n\n" + + # Format as config JSON + config = {"adventure": {}} + for i, (name, desc, enc_type, extras) in enumerate(sequence, 1): + state_name = name.lower().replace(" ", "_").replace("'", "").replace("-", "_") + current_state_id = f"encounter_{i}_{state_name}" + + extras_str = ", ".join(extras) if isinstance(extras, list) else str(extras) + + # Determine next state + if i < len(sequence): + next_name = sequence[i][0] + next_state_name = next_name.lower().replace(" ", "_").replace("'", "").replace("-", "_") + next_state_id = f"encounter_{i+1}_{next_state_name}" + else: + next_state_id = "adventure_complete" + + # Create choices based on encounter type + if enc_type == "combat": + choices = ["Fight", "Attempt Diplomacy", "Try to Sneak Past"] + elif enc_type == "social": + choices = ["Engage in Conversation", "Observe Silently", "Make an Offer"] + elif enc_type == "exploration": + choices = ["Investigate Carefully", "Rush Forward", "Search for Traps"] + elif enc_type == "puzzle": + choices = ["Study the Puzzle", "Try a Solution", "Look for Clues"] + elif enc_type == "boss": + choices = ["Prepare for Battle", "Attempt Negotiation", "Launch Surprise Attack"] + else: + choices = ["Continue"] + + # All choices lead to same next state for simplicity + transitions = {choice: next_state_id for choice in choices} + + config["adventure"][current_state_id] = { + "description": f"[{enc_type.upper()} - {difficulty.upper()}] {desc}\n\nYou notice: {extras_str}", + "media_prompt": f"Fantasy RPG scene in {location}: {name} - {desc}, {difficulty_info['adjective']} atmosphere, featuring {extras_str}", + "choices": choices, + "transitions": transitions + } + + # Add completion state + first_enc = sequence[0][0].lower().replace(" ", "_").replace("'", "").replace("-", "_") + config["adventure"]["adventure_complete"] = { + "description": f"Victory! The {location} has been conquered. {difficulty_info['treasure'].title()} treasures await the brave adventurers.", + "choices": ["Embark on New Adventure"], + "transitions": { + "Embark on New Adventure": f"encounter_1_{first_enc}" + } + } + + json_output = json.dumps(config, indent=2) + + # Format prompts + prompts_output = f"## D&D Adventure Prompts ({location.title()})\n\n" + for i, (name, desc, enc_type, extras) in enumerate(sequence, 1): + extras_str = ", ".join(extras) if isinstance(extras, list) else str(extras) + prompts_output += f"Fantasy RPG scene: {name} - {desc}, featuring {extras_str}\n" + + return list_output, json_output, prompts_output + + +# Quick test +if __name__ == "__main__": + list_out, json_out, prompts_out = generate_dnd_sequence( + 5, "medium", True, True, True, False, True + ) + print(list_out) + print(json_out) diff --git a/docs/hf_auth_integration_notes.md b/docs/hf_auth_integration_notes.md new file mode 100644 index 0000000000000000000000000000000000000000..0f5c2bdaf44dc3deaead2e7fe5ba3443dfe64403 --- /dev/null +++ b/docs/hf_auth_integration_notes.md @@ -0,0 +1,168 @@ +# HuggingFace Authentication Integration Notes + +## Purpose +Allow users to save their Big RPG Scale entity databases to their own HuggingFace datasets (avoiding data collection concerns). + +## Requirements +- Space-level OAuth configuration +- User authentication via `gr.LoginButton()` +- Save/load to user's own HF datasets + +## README.md Changes Required +```yaml +--- +title: GameConfigIdeaEditBrainstorm +hf_oauth: true +hf_oauth_scopes: + - read-repos + - write-repos +--- +``` + +## Gradio OAuth Integration + +### Login Button +```python +import gradio as gr + +with gr.Blocks() as demo: + gr.LoginButton(variant="huggingface", size="sm") +``` + +### Access User Profile +```python +def my_function(profile: gr.OAuthProfile | None): + if profile: + username = profile.username + # Can now save to user's repos + else: + return "Please log in first" +``` + +### Access User Token +```python +def save_to_user_dataset( + profile: gr.OAuthProfile | None, + token: gr.OAuthToken | None, + data: str +): + if not profile or not token: + return "Login required" + + # Use token.token for API calls +``` + +## Implementation Functions + +### Save Database +```python +from huggingface_hub import HfApi, create_repo + +def save_database_to_hf( + db: EntityDatabase, + profile: gr.OAuthProfile, + token: gr.OAuthToken, + dataset_name: str = "rpg-entity-database" +) -> str: + username = profile.username + repo_id = f"{username}/{dataset_name}" + + api = HfApi(token=token.token) + + # Create repo if needed + create_repo(repo_id, repo_type="dataset", exist_ok=True, private=True) + + # Export and upload + db_json = db.to_json() + api.upload_file( + path_or_fileobj=db_json.encode('utf-8'), + path_in_repo="entity_database.json", + repo_id=repo_id, + repo_type="dataset" + ) + + return f"Saved to: https://huggingface.co/datasets/{repo_id}" +``` + +### Load Database +```python +from huggingface_hub import hf_hub_download + +def load_database_from_hf( + db: EntityDatabase, + profile: gr.OAuthProfile, + token: gr.OAuthToken, + dataset_name: str = "rpg-entity-database" +) -> str: + username = profile.username + repo_id = f"{username}/{dataset_name}" + + file_path = hf_hub_download( + repo_id=repo_id, + filename="entity_database.json", + repo_type="dataset", + token=token.token + ) + + with open(file_path, 'r') as f: + json_str = f.read() + + success, message = db.from_json(json_str) + return message +``` + +## UI Integration + +### Entity Database Tab Addition +```python +with gr.Row(): + login_btn = gr.LoginButton(variant="huggingface", size="sm") + login_status = gr.Markdown("*Not logged in*") + +with gr.Accordion("Cloud Save/Load (HuggingFace)", open=False): + gr.Markdown("Save your entity database to your HuggingFace account.") + + hf_dataset_name = gr.Textbox( + label="Dataset Name", + value="rpg-entity-database" + ) + + with gr.Row(): + save_to_hf_btn = gr.Button("Save to HuggingFace", variant="primary") + load_from_hf_btn = gr.Button("Load from HuggingFace") + + hf_status = gr.Markdown("") +``` + +## Graceful Degradation +- All local features work without login +- Show `gr.Info("Please log in...")` when save/load attempted without auth +- Display current login status prominently + +## Alternative: CommitScheduler (Auto-Save) +For session logging (like D&D GM event logs): +```python +from huggingface_hub import CommitScheduler + +scheduler = CommitScheduler( + folder_path="./user_data", + repo_id=f"{username}/game-sessions", + token=token.token, + private=True +) +``` +- Runs background thread, pushes every ~1 minute +- Good for append-only data (logs, feedback) +- Not ideal for frequently-updated files + +## Resources +- [Gradio OAuth Docs](https://huggingface.co/docs/hub/spaces-oauth) +- [Gradio OAuth Example Space](https://huggingface.co/spaces/gradio/oauth-example) +- [HuggingFace Hub Upload Guide](https://huggingface.co/docs/huggingface_hub/guides/upload) +- [Spaces Persistent Storage](https://huggingface.co/docs/hub/spaces-storage) + +## TODO +- [ ] Test OAuth flow locally with `huggingface-cli login` +- [ ] Decide on dataset structure (single JSON vs multiple files) +- [ ] Consider versioning/backup strategy +- [ ] Investigate Space-level persistent storage options diff --git a/exporters/__init__.py b/exporters/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e24a4ee37db63f073caf8bcbd3a92266e5b0fee0 --- /dev/null +++ b/exporters/__init__.py @@ -0,0 +1,97 @@ +"""Game Config IDE - Platform Exporters Package. + +This package provides exporters for converting game configs to various +platforms and engines. + +Supported platforms: +- Game Engines: GameMaker, Flutter +- Narrative Engines: Twine, Ren'Py, Ink, Yarn Spinner +- Game Platforms: Roblox, RPG Maker MZ, Minecraft, Papyrus (Skyrim/Fallout), VRChat +- AR/XR Platforms: Snap Lens Studio, Meta Spark, 8th Wall, TikTok Effect House, Apple Reality +- Interactive Fiction: Inform 7, Godot Dialogue Manager +- Playable HTML: 2D Map, MovingDotSpace +- Terminal: Python Terminal Game +""" + +# Individual exporters +from .gamemaker_exporter import export_to_gamemaker +from .flutter_exporter import export_to_flutter +from .twine_exporter import export_to_twine +from .renpy_exporter import export_to_renpy +from .ink_exporter import export_to_ink +from .yarn_exporter import export_to_yarn +from .roblox_exporter import export_to_roblox +from .rpgmaker_exporter import export_to_rpgmaker +from .terminal_exporter import export_to_terminal + +# AR/XR exporters +from .ar_exporters import ( + export_to_lensstudio, + export_to_metaspark, + export_to_8thwall, + export_to_tiktok, + export_to_reality +) + +# Game platform exporters +from .game_platform_exporters import ( + export_to_minecraft, + export_to_papyrus, + export_to_vrchat +) + +# Interactive fiction exporters +from .fiction_exporters import ( + export_to_inform7, + export_to_godot_dialogue +) + +# HTML exporters +from .html_exporters import ( + export_to_2d_map, + export_to_movingdotspace +) + +# PlayCanvas exporter (from root level) +try: + from playcanvas_exporter import export_to_playcanvas, export_to_playcanvas_html +except ImportError: + export_to_playcanvas = None + export_to_playcanvas_html = None + +# Router +from .router import ConfigConversionforExporttoPlatform + +__all__ = [ + # Individual exporters + 'export_to_gamemaker', + 'export_to_flutter', + 'export_to_twine', + 'export_to_renpy', + 'export_to_ink', + 'export_to_yarn', + 'export_to_roblox', + 'export_to_rpgmaker', + 'export_to_terminal', + # AR/XR + 'export_to_lensstudio', + 'export_to_metaspark', + 'export_to_8thwall', + 'export_to_tiktok', + 'export_to_reality', + # Game platforms + 'export_to_minecraft', + 'export_to_papyrus', + 'export_to_vrchat', + # Fiction + 'export_to_inform7', + 'export_to_godot_dialogue', + # HTML + 'export_to_2d_map', + 'export_to_movingdotspace', + # PlayCanvas + 'export_to_playcanvas', + 'export_to_playcanvas_html', + # Router + 'ConfigConversionforExporttoPlatform', +] diff --git a/exporters/ar_exporters.py b/exporters/ar_exporters.py new file mode 100644 index 0000000000000000000000000000000000000000..19477d95567816a5a9f12ea1563030d096604c7b --- /dev/null +++ b/exporters/ar_exporters.py @@ -0,0 +1,843 @@ +"""AR/XR platform exporters for game configs. + +Includes exporters for: +- Snap Lens Studio +- Meta Spark AR (Instagram/Facebook) +- 8th Wall WebAR +- TikTok Effect House +- Apple Reality Composer / visionOS +""" + +import json + + +def export_to_lensstudio(config_json): + """Export game config to Snap Lens Studio JavaScript format.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + js_code = '''// Snap Lens Studio - Interactive Story Experience +// Generated from Game Config IDE +// +// Setup in Lens Studio: +// 1. Create a Script component +// 2. Add @input bindings for UI elements +// 3. Paste this code + +// @input Component.Text descriptionText {"label":"Description Text"} +// @input Component.ScriptComponent[] choiceButtons {"label":"Choice Buttons"} +// @input Asset.AudioTrackAsset[] audioTracks {"label":"Audio Tracks"} + +// Story States Configuration +var STORY_STATES = { +''' + + first_state = None + + # Generate states + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_name = f"{location_key}_{state_key}" + if first_state is None: + first_state = state_name + + description = state_data.get('description', 'No description').replace('"', '\\"').replace('\n', '\\n') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + media = state_data.get('media', {}) + + js_code += f' "{state_name}": {{\n' + js_code += f' description: "{description}",\n' + js_code += f' location: "{location_key}",\n' + + # Choices + js_code += ' choices: [' + if choices: + js_code += ', '.join([f'"{c}"' for c in choices]) + js_code += '],\n' + + # Transitions + js_code += ' transitions: {\n' + if isinstance(transitions, dict): + for choice, next_state in transitions.items(): + if next_state: + if '_' not in next_state: + next_state = f"{location_key}_{next_state}" + js_code += f' "{choice}": "{next_state}",\n' + js_code += ' },\n' + + # Media hints + if media.get('image') or media.get('audio'): + js_code += ' media: {\n' + if media.get('image'): + js_code += f' image: "{media["image"]}",\n' + if media.get('audio'): + js_code += f' audio: "{media["audio"]}",\n' + js_code += ' },\n' + + js_code += ' },\n' + + js_code += f'''}}; + +var currentState = "{first_state or "start"}"; + +// Initialize the story +function initialize() {{ + showState(currentState); +}} + +// Display current state +function showState(stateName) {{ + var state = STORY_STATES[stateName]; + if (!state) {{ + print("State not found: " + stateName); + return; + }} + + // Update description text + if (script.descriptionText) {{ + script.descriptionText.text = state.description; + }} + + // Update choice buttons + for (var i = 0; i < script.choiceButtons.length; i++) {{ + var btn = script.choiceButtons[i]; + if (i < state.choices.length) {{ + btn.getSceneObject().enabled = true; + // Assuming button has a text child + var textComponent = btn.getSceneObject().getComponent("Component.Text"); + if (textComponent) {{ + textComponent.text = state.choices[i]; + }} + }} else {{ + btn.getSceneObject().enabled = false; + }} + }} + + currentState = stateName; + print("Now at state: " + stateName); +}} + +// Handle choice selection (call this from button tap events) +function makeChoice(choiceIndex) {{ + var state = STORY_STATES[currentState]; + if (!state || choiceIndex >= state.choices.length) return; + + var choiceText = state.choices[choiceIndex]; + var nextState = state.transitions[choiceText]; + + if (nextState) {{ + showState(nextState); + }} else {{ + print("No transition for choice: " + choiceText); + }} +}} + +// Reset to beginning +function resetStory() {{ + showState("{first_state or "start"}"); +}} + +// Export functions for external use +script.api.makeChoice = makeChoice; +script.api.resetStory = resetStory; +script.api.getCurrentState = function() {{ return currentState; }}; + +// Auto-initialize on start +var onStartEvent = script.createEvent("OnStartEvent"); +onStartEvent.bind(initialize); +''' + + explanation = """Snap Lens Studio Export Generated! + +Setup Instructions: +1. Download Lens Studio: https://lensstudio.snapchat.com/ +2. Create new project (World or Face lens) +3. Add Script component to scene +4. Paste this code +5. Set up @input bindings in Inspector + +Required Scene Setup: +- Text component for description (link to descriptionText) +- Button objects with tap handlers (link to choiceButtons array) +- Call script.api.makeChoice(0), makeChoice(1), etc. from buttons + +Button Tap Setup: +1. Select button object +2. Add "Interaction Component" +3. On Tap event -> call Script.makeChoice(index) + +Testing: +- Use Preview panel in Lens Studio +- Send to Snapchat for device testing +- Use Lens Studio's built-in simulator + +Publishing: +- Submit via My Lenses portal +- Review typically takes 1-3 days +- 300M+ daily Snapchat AR users + +Tips: +- Use World Tracking for immersive stories +- Add 3D objects that appear with state changes +- Use audio for atmosphere +""" + + return explanation, js_code + + +def export_to_metaspark(config_json): + """Export game config to Meta Spark AR (Instagram/Facebook) format.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + js_code = '''// Meta Spark AR - Interactive Story Effect +// Generated from Game Config IDE +// +// Setup in Meta Spark Studio: +// 1. Create new effect project +// 2. Add Script to Assets +// 3. Paste this code and connect to scene objects + +const Scene = require('Scene'); +const Diagnostics = require('Diagnostics'); +const Patches = require('Patches'); +const Time = require('Time'); +const Reactive = require('Reactive'); + +// Story States Configuration +const STORY_STATES = { +''' + + first_state = None + + # Generate states + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_name = f"{location_key}_{state_key}" + if first_state is None: + first_state = state_name + + description = state_data.get('description', 'No description').replace('"', '\\"').replace('\n', '\\n') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + js_code += f' "{state_name}": {{\n' + js_code += f' description: "{description}",\n' + js_code += f' location: "{location_key}",\n' + js_code += ' choices: [' + if choices: + js_code += ', '.join([f'"{c}"' for c in choices]) + js_code += '],\n' + js_code += ' transitions: {\n' + if isinstance(transitions, dict): + for choice, next_state in transitions.items(): + if next_state: + if '_' not in next_state: + next_state = f"{location_key}_{next_state}" + js_code += f' "{choice}": "{next_state}",\n' + js_code += ' }\n' + js_code += ' },\n' + + js_code += f'''}}; + +let currentState = "{first_state or "start"}"; + +(async function() {{ + // Find scene objects (adjust names to match your scene) + const [descriptionText, choice1Btn, choice2Btn, choice3Btn] = await Promise.all([ + Scene.root.findFirst('descriptionText'), + Scene.root.findFirst('choice1Button'), + Scene.root.findFirst('choice2Button'), + Scene.root.findFirst('choice3Button') + ]); + + const choiceButtons = [choice1Btn, choice2Btn, choice3Btn].filter(b => b); + + // Update UI with current state + function showState(stateName) {{ + const state = STORY_STATES[stateName]; + if (!state) {{ + Diagnostics.log("State not found: " + stateName); + return; + }} + + // Update description + if (descriptionText) {{ + descriptionText.text = state.description; + }} + + // Update choice buttons + choiceButtons.forEach((btn, i) => {{ + if (btn) {{ + const hasChoice = i < state.choices.length; + btn.hidden = Reactive.val(!hasChoice); + if (hasChoice && btn.findFirst) {{ + btn.findFirst('text').then(t => {{ + if (t) t.text = state.choices[i]; + }}); + }} + }} + }}); + + currentState = stateName; + Diagnostics.log("State: " + stateName); + + // Send to Patch Editor + Patches.inputs.setString('currentState', stateName); + Patches.inputs.setString('description', state.description); + }} + + // Handle choice (connect via Patch Editor pulse inputs) + async function setupChoiceHandlers() {{ + try {{ + const choice1Pulse = await Patches.outputs.getPulse('choice1Selected'); + const choice2Pulse = await Patches.outputs.getPulse('choice2Selected'); + const choice3Pulse = await Patches.outputs.getPulse('choice3Selected'); + + choice1Pulse.subscribe(() => makeChoice(0)); + choice2Pulse.subscribe(() => makeChoice(1)); + choice3Pulse.subscribe(() => makeChoice(2)); + }} catch(e) {{ + Diagnostics.log("Patch outputs not connected: " + e); + }} + }} + + function makeChoice(index) {{ + const state = STORY_STATES[currentState]; + if (!state || index >= state.choices.length) return; + + const choiceText = state.choices[index]; + const nextState = state.transitions[choiceText]; + + if (nextState) {{ + showState(nextState); + }} + }} + + // Initialize + showState(currentState); + setupChoiceHandlers(); + +}})(); +''' + + explanation = """Meta Spark AR Export Generated! + +Setup Instructions: +1. Download Meta Spark Studio: https://spark.meta.com/ +2. Create new project (Target Tracker, World, or Face effect) +3. Add Script asset and paste this code +4. Create UI elements and name them to match script + +Scene Object Names: +- descriptionText: Text object for story description +- choice1Button, choice2Button, choice3Button: Button objects + +Patch Editor Integration: +1. Open Patch Editor +2. Create pulse outputs: choice1Selected, choice2Selected, choice3Selected +3. Connect tap events to these pulses +4. Script outputs: currentState, description (string) + +Testing: +- Use device preview in Meta Spark +- Send to Instagram app for testing +- Test on both iOS and Android + +Publishing: +- Submit via Meta Spark Hub +- Publish to Instagram and/or Facebook +- 600M+ Instagram AR filter users + +Tips: +- Use Face Tracker for selfie-style stories +- Use World Effects for immersive AR +- Add 3D assets that change with states +""" + + return explanation, js_code + + +def export_to_8thwall(config_json): + """Export game config to 8th Wall WebAR (A-Frame) format.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + html_code = ''' + + + + AR Story Experience + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Loading story...
+
+
+ + + + +''' + + explanation = """8th Wall WebAR Export Generated! + +Setup Instructions: +1. Create account at https://www.8thwall.com/ +2. Create new Web project +3. Get your App Key from dashboard +4. Replace YOUR_APP_KEY in the HTML +5. Upload to 8th Wall hosting or your own server + +Key Features: +- Works in mobile browser (no app needed!) +- A-Frame for 3D content +- Responsive story UI overlay +- World tracking enabled + +Customization: +- Add 3D models in tags +- Modify CSS for different UI styles +- Add sounds with elements +- Use xrextras components for AR features + +Testing: +- Use 8th Wall's built-in hosting +- Scan QR code on mobile device +- Test on iOS Safari and Android Chrome + +Hosting Options: +1. 8th Wall hosting (included with subscription) +2. Your own HTTPS server +3. GitHub Pages (free, needs HTTPS) + +Requirements: +- 8th Wall subscription for App Key +- HTTPS hosting (required for camera access) +- Mobile device with camera + +Alternative (Free Testing): +- Remove 8th Wall script +- Use basic A-Frame only +- Won't have advanced AR features + +Reach: 1B+ WebAR experiences served +""" + + return explanation, html_code + + +def export_to_tiktok(config_json): + """Export game config to TikTok Effect House JavaScript format.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + js_code = '''// TikTok Effect House - Interactive Story Effect +// Generated from Game Config IDE +// +// Setup in Effect House: +// 1. Create new project +// 2. Add Visual Scripting or Script component +// 3. Paste this code + +const Scene = require('Scene'); +const Time = require('Time'); +const Patches = require('Patches'); +const Diagnostics = require('Diagnostics'); +const TouchGestures = require('TouchGestures'); + +const STORY_STATES = { +''' + + first_state = None + + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_name = f"{location_key}_{state_key}" + if first_state is None: + first_state = state_name + + description = state_data.get('description', '').replace('"', '\\"').replace('\n', '\\n') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + js_code += f' "{state_name}": {{\n' + js_code += f' description: "{description}",\n' + js_code += ' choices: [' + if choices: + js_code += ', '.join([f'"{c}"' for c in choices]) + js_code += '],\n' + js_code += ' transitions: {' + if isinstance(transitions, dict): + trans_items = [] + for choice, next_state in transitions.items(): + if next_state: + if '_' not in next_state: + next_state = f"{location_key}_{next_state}" + trans_items.append(f'"{choice}": "{next_state}"') + js_code += ', '.join(trans_items) + js_code += '}\n' + js_code += ' },\n' + + js_code += f'''}}; + +let currentState = "{first_state or "start"}"; +let choiceIndex = 0; + +(async function() {{ + const storyText = await Scene.root.findFirst('storyText'); + const choiceTexts = await Promise.all([ + Scene.root.findFirst('choice1'), + Scene.root.findFirst('choice2'), + Scene.root.findFirst('choice3') + ]); + + function showState(stateName) {{ + const state = STORY_STATES[stateName]; + if (!state) return; + if (storyText) storyText.text = state.description; + choiceTexts.forEach((t, i) => {{ + if (t) t.text = i < state.choices.length ? state.choices[i] : ''; + }}); + currentState = stateName; + choiceIndex = 0; + Diagnostics.log('State: ' + stateName); + }} + + let lastTap = 0; + TouchGestures.onTap().subscribe(() => {{ + const now = Date.now(); + const state = STORY_STATES[currentState]; + if (now - lastTap < 300 && state.choices.length > 0) {{ + const next = state.transitions[state.choices[choiceIndex]]; + if (next) showState(next); + }} else if (state.choices.length > 0) {{ + choiceIndex = (choiceIndex + 1) % state.choices.length; + Patches.inputs.setScalar('currentChoice', choiceIndex); + }} + lastTap = now; + }}); + + showState(currentState); +}})(); +''' + + explanation = """TikTok Effect House Export Generated! + +Setup: +1. Download Effect House: https://effecthouse.tiktok.com/ +2. Create new project +3. Add Script component +4. Paste this code + +Interaction: +- Single tap: Cycle through choices +- Double tap: Select current choice + +Scene Objects: +- storyText: 2D Text for description +- choice1, choice2, choice3: Choice text objects + +Testing: +- Preview in Effect House +- Send to TikTok app + +Publishing: +- Submit via Effect House +- 1B+ TikTok users reach +""" + + return explanation, js_code + + +def export_to_reality(config_json): + """Export game config to Apple Reality Composer Pro / RealityKit Swift format.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + swift_code = '''import SwiftUI +import RealityKit + +// Story Adventure for visionOS / iOS AR +// Generated from Game Config IDE + +struct StoryState: Identifiable { + let id: String + let description: String + let choices: [String] + let transitions: [String: String] +} + +class StoryManager: ObservableObject { + @Published var currentStateId: String + let states: [String: StoryState] + + init() { + var stateDict: [String: StoryState] = [:] +''' + + first_state = None + + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_name = f"{location_key}_{state_key}" + if first_state is None: + first_state = state_name + + description = state_data.get('description', '').replace('"', '\\"') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + choices_str = ', '.join([f'"{c}"' for c in choices]) + trans_items = [] + if isinstance(transitions, dict): + for choice, next_state in transitions.items(): + if next_state: + if '_' not in next_state: + next_state = f"{location_key}_{next_state}" + trans_items.append(f'"{choice}": "{next_state}"') + + swift_code += f'\n stateDict["{state_name}"] = StoryState(id: "{state_name}", description: "{description}", choices: [{choices_str}], transitions: [{", ".join(trans_items)}])\n' + + swift_code += f''' + self.states = stateDict + self.currentStateId = "{first_state or "start"}" + }} + + var currentState: StoryState? {{ states[currentStateId] }} + + func makeChoice(_ choice: String) {{ + guard let next = currentState?.transitions[choice] else {{ return }} + currentStateId = next + }} +}} + +struct StoryView: View {{ + @StateObject private var manager = StoryManager() + + var body: some View {{ + VStack(spacing: 20) {{ + if let state = manager.currentState {{ + Text(state.description) + .font(.title3) + .padding() + ForEach(state.choices, id: \\.self) {{ choice in + Button(choice) {{ manager.makeChoice(choice) }} + .buttonStyle(.borderedProminent) + }} + }} + }} + .padding() + }} +}} + +#Preview {{ StoryView() }} +''' + + explanation = """Apple Reality / visionOS Export Generated! + +Setup: +1. Create visionOS or iOS project in Xcode 15+ +2. Add this Swift file +3. Use StoryView in your app + +Requirements: +- Xcode 15+ +- iOS 17+ / visionOS 1.0+ +- Swift 5.9+ + +For Vision Pro: +- Wrap in RealityView for spatial +- Add 3D content per state + +Testing: +- visionOS Simulator +- iOS device with AR + +Distribution: +- App Store +""" + + return explanation, swift_code diff --git a/exporters/fiction_exporters.py b/exporters/fiction_exporters.py new file mode 100644 index 0000000000000000000000000000000000000000..6d752dac3b6fd96eb6bcbed29ba228323e740069 --- /dev/null +++ b/exporters/fiction_exporters.py @@ -0,0 +1,140 @@ +"""Interactive fiction exporters for game configs. + +Includes exporters for: +- Inform 7 natural language format +- Godot Dialogue Manager format +""" + +import json + + +def export_to_inform7(config_json): + """Export game config to Inform 7 natural language format.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + i7_code = '''"Story Adventure" by "Game Config IDE" + +''' + + locations = {} + first_room = None + + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + room_name = location_key.replace('_', ' ').title() + if first_room is None: + first_room = room_name + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + desc = state_data.get('description', f'You are in {room_name}.').replace('"', "'") + locations[room_name] = desc + break + + for room_name, desc in locations.items(): + i7_code += f'{room_name} is a room. "{desc}"\n\n' + + if first_room: + i7_code += f"The player is in {first_room}.\n\n" + + # Generate actions for choices + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + for choice in choices: + action_name = choice.lower().replace(' ', '-')[:20] + next_state = transitions.get(choice, '') if isinstance(transitions, dict) else '' + + i7_code += f'{action_name.replace("-", " ")}ing is an action applying to nothing.\n' + i7_code += f'Understand "{choice.lower()}" as {action_name}ing.\n' + i7_code += f'Carry out {action_name}ing: say "You chose: {choice}."\n\n' + + explanation = """Inform 7 Interactive Fiction Export Generated! + +Setup: +1. Download Inform 7: http://inform7.com/ +2. Create new project +3. Paste code in Source panel +4. Click "Go!" to compile + +Online Testing: +- Borogove: https://borogove.app/ + +Distribution: +- Compile to .z8 or .gblorb +- Upload to IFDB +""" + + return explanation, i7_code + + +def export_to_godot_dialogue(config_json): + """Export game config to Godot Dialogue Manager format.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + dialogue_code = '# Godot Dialogue Manager - story.dialogue\n\n' + + first_title = None + + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + title = f"{location_key}_{state_key}" + if first_title is None: + first_title = title + + description = state_data.get('description', '') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + dialogue_code += f"~ {title}\n" + dialogue_code += f"{description}\n" + + if choices: + for choice in choices: + next_state = transitions.get(choice, '') if isinstance(transitions, dict) else '' + if next_state: + if '_' not in next_state: + next_state = f"{location_key}_{next_state}" + dialogue_code += f"- {choice} => {next_state}\n" + else: + dialogue_code += f"- {choice} => END\n" + else: + dialogue_code += "=> END\n" + + dialogue_code += "\n" + + explanation = f"""Godot Dialogue Manager Export Generated! + +Setup: +1. Install "Dialogue Manager" addon from AssetLib +2. Save as story.dialogue +3. Use in GDScript: + +DialogueManager.show_dialogue_balloon( + preload("res://story.dialogue"), + "{first_title or "start"}" +) + +Syntax: +- ~ title : Node start +- Text : Dialogue line +- - Choice => target : Player choice +- => END : End dialogue + +Requirements: +- Godot 4.0+ +- Dialogue Manager addon +""" + + return explanation, dialogue_code diff --git a/exporters/flutter_exporter.py b/exporters/flutter_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..39924953dc1b80f5beaf464c8fcb1010859578a4 --- /dev/null +++ b/exporters/flutter_exporter.py @@ -0,0 +1,124 @@ +"""Flutter/Dart exporter for game configs.""" + +import json + + +def export_to_flutter(config_json): + """Export config to Flutter/Dart code.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except: + return "Error: Invalid JSON", "" + + dart = '''// Flutter Text Adventure Game +// Add to your lib/main.dart or create lib/game_state.dart + +import 'package:flutter/material.dart'; + +// ========== GAME CONFIG ========== +final Map> gameConfig = ''' + json.dumps(config, indent=2).replace('true', 'true').replace('false', 'false').replace('null', 'null') + '''; + +// ========== GAME STATE ========== +class GameState extends ChangeNotifier { + String currentLocation = "''' + list(config.keys())[0] + '''"; + String currentState = "''' + list(config[list(config.keys())[0]].keys())[0] + '''"; + List inventory = []; + int money = 0; + Map flags = {}; + + Map? get currentStateData { + return gameConfig[currentLocation]?[currentState]; + } + + String get description => currentStateData?['description'] ?? 'No description'; + + List get choices => + List.from(currentStateData?['choices'] ?? []); + + void makeChoice(int index) { + if (index < 0 || index >= choices.length) return; + + String choice = choices[index]; + var transitions = currentStateData?['transitions'] as Map?; + + if (transitions != null && transitions.containsKey(choice)) { + String nextState = transitions[choice].toString(); + if (nextState.contains('/')) { + var parts = nextState.split('/'); + currentLocation = parts[0]; + currentState = parts[1]; + } else { + currentState = nextState; + } + notifyListeners(); + } + } +} + +// ========== GAME SCREEN WIDGET ========== +class GameScreen extends StatelessWidget { + final GameState gameState; + + const GameScreen({Key? key, required this.gameState}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: gameState, + builder: (context, child) { + return Scaffold( + appBar: AppBar(title: Text(gameState.currentLocation)), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + gameState.description, + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 24), + const Text('What do you do?', + style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + ...gameState.choices.asMap().entries.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ElevatedButton( + onPressed: () => gameState.makeChoice(entry.key), + child: Text(entry.value), + ), + ); + }).toList(), + ], + ), + ), + ); + }, + ); + } +} + +// ========== MAIN APP ========== +void main() { + runApp(MaterialApp( + home: GameScreen(gameState: GameState()), + theme: ThemeData.dark(), + )); +} +''' + + explanation = """Flutter/Dart Export: + +1. Create new Flutter project: flutter create text_adventure +2. Replace lib/main.dart with this code +3. Run: flutter run + +Features: +- Uses ChangeNotifier for state management +- Responsive button-based choices +- Dark theme by default + +For mobile: flutter build apk (Android) or flutter build ios""" + + return explanation, dart diff --git a/exporters/game_platform_exporters.py b/exporters/game_platform_exporters.py new file mode 100644 index 0000000000000000000000000000000000000000..f6df72aa42ace17a6934a63b7c4a35bcbf3d9421 --- /dev/null +++ b/exporters/game_platform_exporters.py @@ -0,0 +1,633 @@ +"""Game platform exporters for game configs. + +Includes exporters for: +- Minecraft Datapack +- Skyrim/Fallout 4 Papyrus +- VRChat UdonSharp +""" + +import json + + +def export_to_minecraft(config_json): + """Export game config to Minecraft Datapack format (mcfunction + JSON).""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + # Generate datapack structure as a combined output + output = '''# ======================================== +# MINECRAFT DATAPACK - Story Adventure +# Generated from Game Config IDE +# ======================================== +# +# FOLDER STRUCTURE: +# story_adventure/ +# ├── pack.mcmeta +# ├── data/ +# │ └── story/ +# │ ├── functions/ +# │ │ ├── init.mcfunction +# │ │ ├── tick.mcfunction +# │ │ └── states/ +# │ │ └── [state].mcfunction +# │ └── advancements/ +# │ └── root.json +# +# INSTALLATION: +# 1. Create folder: .minecraft/saves/[world]/datapacks/story_adventure/ +# 2. Create the folder structure above +# 3. Copy each file section below to the corresponding file +# ======================================== + +### FILE: pack.mcmeta ### +{ + "pack": { + "pack_format": 26, + "description": "Story Adventure - Interactive narrative datapack" + } +} + +### FILE: data/story/functions/init.mcfunction ### +# Initialize story system +# Run once: /function story:init + +# Create scoreboard objectives +scoreboard objectives add story_state dummy "Current State" +scoreboard objectives add story_choice dummy "Player Choice" + +# Set initial state (0 = first state) +scoreboard players set @a story_state 0 +scoreboard players set @a story_choice 0 + +# Notify players +tellraw @a {"text":"[Story] Adventure initialized! Use /trigger story_choice to make choices.","color":"gold"} + +# Enable trigger for choices +scoreboard objectives add story_trigger trigger "Make Choice" +scoreboard players enable @a story_trigger + +### FILE: data/story/functions/tick.mcfunction ### +# Main tick function - checks for player choices +# Add to tick.json or run periodically + +# Check if any player made a choice +execute as @a[scores={story_trigger=1..}] run function story:process_choice +scoreboard players set @a story_trigger 0 +scoreboard players enable @a story_trigger + +### FILE: data/story/functions/process_choice.mcfunction ### +# Process player choice based on current state +# Called when player uses /trigger story_trigger set [choice_number] + +''' + + # Generate state functions + state_index = 0 + state_map = {} # Map state names to indices + + # First pass: assign indices to states + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_name = f"{location_key}_{state_key}" + state_map[state_name] = state_index + state_index += 1 + + # Second pass: generate state functions + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_name = f"{location_key}_{state_key}" + state_idx = state_map[state_name] + description = state_data.get('description', 'No description').replace('"', '\\"') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + output += f"\n### FILE: data/story/functions/states/{state_name}.mcfunction ###\n" + output += f"# State: {state_name} (index {state_idx})\n\n" + + # Display description + output += f'tellraw @s {{"text":"\\n=== {location_key.upper()} ===\\n","color":"yellow","bold":true}}\n' + output += f'tellraw @s {{"text":"{description}\\n","color":"white"}}\n' + + # Display choices + if choices: + output += f'tellraw @s {{"text":"\\nChoices:","color":"green"}}\n' + for i, choice in enumerate(choices): + choice_escaped = choice.replace('"', '\\"') + output += f'tellraw @s {{"text":"[{i+1}] {choice_escaped}","color":"aqua","clickEvent":{{"action":"run_command","value":"/trigger story_trigger set {i+1}"}}}}\n' + + output += "\n" + + # Generate process_choice logic + output += "\n### FILE: data/story/functions/process_choice.mcfunction (continued) ###\n" + output += "# Add state transition logic here\n\n" + + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_name = f"{location_key}_{state_key}" + state_idx = state_map[state_name] + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + output += f"# From state: {state_name} ({state_idx})\n" + for i, choice in enumerate(choices): + next_state = transitions.get(choice, '') if isinstance(transitions, dict) else '' + if next_state: + if '_' not in next_state: + next_state = f"{location_key}_{next_state}" + if next_state in state_map: + next_idx = state_map[next_state] + output += f"execute as @s[scores={{story_state={state_idx},story_trigger={i+1}}}] run scoreboard players set @s story_state {next_idx}\n" + output += f"execute as @s[scores={{story_state={next_idx}}}] run function story:states/{next_state}\n" + + # Add start command + output += ''' + +### FILE: data/story/functions/start.mcfunction ### +# Start the story - run with /function story:start +function story:init +''' + + first_state = list(state_map.keys())[0] if state_map else "start" + output += f"function story:states/{first_state}\n" + + explanation = """Minecraft Datapack Export Generated! + +Installation: +1. Navigate to: .minecraft/saves/[world]/datapacks/ +2. Create folder: story_adventure +3. Create the folder structure shown above +4. Copy each ### FILE: ### section to its file +5. Run: /reload in-game + +Commands: +- /function story:start - Begin the adventure +- /function story:init - Reset/initialize +- Click choices in chat or use /trigger story_trigger set [number] + +Requirements: +- Minecraft Java Edition 1.20.4+ +- pack_format 26 (adjust for your version) +- Command blocks enabled (for automation) + +Testing: +- Use /reload after changes +- Check /datapack list to verify loaded +- Use F3+T to reload resources + +Multiplayer: +- Each player has independent state +- Uses scoreboards for tracking +- Works on servers with datapacks enabled + +Customization: +- Add sounds with /playsound +- Add particles with /particle +- Spawn entities for NPCs +- Use /title for dramatic text +""" + + return explanation, output + + +def export_to_papyrus(config_json): + """Export game config to Skyrim/Fallout 4 Papyrus script format.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + psc_code = '''Scriptname StoryAdventure extends Quest +{Story Adventure System - Generated from Game Config IDE} + +; ======================================== +; PROPERTIES - Set these in Creation Kit +; ======================================== +Message Property StartMessage Auto +{Initial message to show player} + +Message[] Property StateMessages Auto +{Array of messages for each state - create in CK} + +GlobalVariable Property CurrentStateIndex Auto +{Tracks current state - create GlobalVariable in CK} + +; ======================================== +; STATE DATA +; ======================================== + +''' + + # Generate state data + states = [] + state_index = 0 + state_map = {} + + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_name = f"{location_key}_{state_key}" + state_map[state_name] = state_index + states.append({ + 'name': state_name, + 'index': state_index, + 'description': state_data.get('description', ''), + 'choices': state_data.get('choices', []), + 'transitions': state_data.get('transitions', {}), + 'location': location_key + }) + state_index += 1 + + # Add state constants + for state in states: + psc_code += f"int Property STATE_{state['name'].upper()} = {state['index']} AutoReadOnly\n" + + psc_code += f''' +int Property TOTAL_STATES = {len(states)} AutoReadOnly + +; ======================================== +; FUNCTIONS +; ======================================== + +Function StartStory() + {{Begin the story from the first state}} + CurrentStateIndex.SetValue(0) + ShowCurrentState() +EndFunction + +Function ShowCurrentState() + {{Display the current state's message and choices}} + int stateIdx = CurrentStateIndex.GetValueInt() + + If stateIdx >= 0 && stateIdx < StateMessages.Length + int result = StateMessages[stateIdx].Show() + ProcessChoice(stateIdx, result) + EndIf +EndFunction + +Function ProcessChoice(int fromState, int choiceIndex) + {{Handle the player's choice and transition to next state}} + + int nextState = -1 + +''' + + # Generate choice processing + for state in states: + psc_code += f" ; From {state['name']}\n" + psc_code += f" If fromState == {state['index']}\n" + + transitions = state['transitions'] + choices = state['choices'] + + for i, choice in enumerate(choices): + next_state = transitions.get(choice, '') if isinstance(transitions, dict) else '' + if next_state: + if '_' not in next_state: + next_state = f"{state['location']}_{next_state}" + if next_state in state_map: + psc_code += f" If choiceIndex == {i}\n" + psc_code += f" nextState = {state_map[next_state]} ; -> {next_state}\n" + psc_code += f" EndIf\n" + + psc_code += f" EndIf\n\n" + + psc_code += ''' + ; Transition to next state + If nextState >= 0 + CurrentStateIndex.SetValue(nextState) + ShowCurrentState() + Else + Debug.Notification("Story: No valid transition") + EndIf +EndFunction + +Function ResetStory() + {Reset to beginning} + CurrentStateIndex.SetValue(0) +EndFunction + +; ======================================== +; CREATION KIT MESSAGE TEMPLATES +; ======================================== +; Create these Message forms in Creation Kit: +; +''' + + # Add message templates as comments + for state in states: + desc = state['description'][:100].replace('\n', ' ') + psc_code += f"; Message: {state['name']}_MSG\n" + psc_code += f"; Message Text: \"{desc}...\"\n" + psc_code += f"; Buttons:\n" + for i, choice in enumerate(state['choices']): + psc_code += f"; [{i}] {choice}\n" + psc_code += ";\n" + + explanation = """Skyrim/Fallout 4 Papyrus Export Generated! + +Setup in Creation Kit: +1. Create new Quest (or use existing) +2. Attach this script to the Quest +3. Create Message forms for each state (see bottom of script) +4. Create GlobalVariable for CurrentStateIndex +5. Link properties in Quest script properties + +Message Form Setup: +- Create one Message per state +- Add buttons matching the choices +- Link to StateMessages array property + +Triggering the Story: +- Call StartStory() from dialogue, trigger, or spell +- Example: StoryQuest.StartStory() + +Testing: +- Use console: cqf StoryQuest StartStory +- Or create test spell/item + +Requirements: +- Creation Kit (free with game) +- Papyrus compiler +- Basic CK knowledge + +Mod Distribution: +- Package as .esp + .psc/.pex files +- Upload to Nexus Mods +- 10M+ active Skyrim modders +""" + + return explanation, psc_code + + +def export_to_vrchat(config_json): + """Export game config to VRChat UdonSharp format.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + cs_code = '''using UdonSharp; +using UnityEngine; +using UnityEngine.UI; +using VRC.SDKBase; +using VRC.Udon; +using TMPro; + +/// +/// VRChat Story Adventure System +/// Generated from Game Config IDE +/// +/// Setup: +/// 1. Create UdonSharpProgramAsset +/// 2. Attach to GameObject in world +/// 3. Link UI elements in Inspector +/// +public class StoryAdventure : UdonSharpBehaviour +{ + [Header("UI References")] + [SerializeField] private TextMeshProUGUI descriptionText; + [SerializeField] private Button[] choiceButtons; + [SerializeField] private TextMeshProUGUI[] choiceButtonTexts; + + [Header("Audio (Optional)")] + [SerializeField] private AudioSource audioSource; + [SerializeField] private AudioClip[] stateAudioClips; + + [Header("Settings")] + [SerializeField] private bool syncAcrossPlayers = false; + + // State tracking + [UdonSynced] private int currentStateIndex = 0; + private string currentStateName = ""; + + // ======================================== + // STATE DATA + // ======================================== + +''' + + # Generate state data + states = [] + state_index = 0 + state_map = {} + + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_name = f"{location_key}_{state_key}" + state_map[state_name] = state_index + states.append({ + 'name': state_name, + 'index': state_index, + 'description': state_data.get('description', '').replace('"', '\\"').replace('\n', '\\n'), + 'choices': state_data.get('choices', []), + 'transitions': state_data.get('transitions', {}), + 'location': location_key + }) + state_index += 1 + + # State name constants + for state in states: + cs_code += f' private const int STATE_{state["name"].upper()} = {state["index"]};\n' + + cs_code += f''' + private const int TOTAL_STATES = {len(states)}; + + // State descriptions + private string[] stateDescriptions = new string[] + {{ +''' + + for state in states: + cs_code += f' "{state["description"]}",\n' + + cs_code += ''' }; + + // State choices (jagged array workaround for Udon) + private string[][] stateChoices; + + // ======================================== + // INITIALIZATION + // ======================================== + + void Start() + { + InitializeChoices(); + ShowState(currentStateIndex); + } + + private void InitializeChoices() + { + stateChoices = new string[TOTAL_STATES][]; + +''' + + for state in states: + choices_str = ', '.join([f'"{c}"' for c in state['choices']]) + cs_code += f' stateChoices[{state["index"]}] = new string[] {{ {choices_str} }};\n' + + cs_code += ''' } + + // ======================================== + // STATE DISPLAY + // ======================================== + + private void ShowState(int stateIndex) + { + if (stateIndex < 0 || stateIndex >= TOTAL_STATES) return; + + currentStateIndex = stateIndex; + + // Update description + if (descriptionText != null) + { + descriptionText.text = stateDescriptions[stateIndex]; + } + + // Update choice buttons + string[] choices = stateChoices[stateIndex]; + for (int i = 0; i < choiceButtons.Length; i++) + { + if (i < choices.Length) + { + choiceButtons[i].gameObject.SetActive(true); + if (choiceButtonTexts[i] != null) + { + choiceButtonTexts[i].text = choices[i]; + } + } + else + { + choiceButtons[i].gameObject.SetActive(false); + } + } + + // Play audio if available + if (audioSource != null && stateIndex < stateAudioClips.Length && stateAudioClips[stateIndex] != null) + { + audioSource.PlayOneShot(stateAudioClips[stateIndex]); + } + + // Sync if enabled + if (syncAcrossPlayers) + { + RequestSerialization(); + } + } + + // ======================================== + // CHOICE HANDLING + // ======================================== + + // Call these from button OnClick events + public void OnChoice0() { ProcessChoice(0); } + public void OnChoice1() { ProcessChoice(1); } + public void OnChoice2() { ProcessChoice(2); } + public void OnChoice3() { ProcessChoice(3); } + + private void ProcessChoice(int choiceIndex) + { + int nextState = GetNextState(currentStateIndex, choiceIndex); + if (nextState >= 0) + { + ShowState(nextState); + } + } + + private int GetNextState(int fromState, int choiceIndex) + { +''' + + # Generate transition logic + for state in states: + cs_code += f" // From: {state['name']}\n" + cs_code += f" if (fromState == {state['index']})\n" + cs_code += " {\n" + + transitions = state['transitions'] + choices = state['choices'] + + for i, choice in enumerate(choices): + next_state = transitions.get(choice, '') if isinstance(transitions, dict) else '' + if next_state: + if '_' not in next_state: + next_state = f"{state['location']}_{next_state}" + if next_state in state_map: + cs_code += f" if (choiceIndex == {i}) return {state_map[next_state]}; // -> {next_state}\n" + + cs_code += " }\n\n" + + cs_code += ''' + return -1; // No valid transition + } + + // ======================================== + // PUBLIC API + // ======================================== + + public void ResetStory() + { + ShowState(0); + } + + public override void OnDeserialization() + { + // Sync state when variable is updated + if (syncAcrossPlayers) + { + ShowState(currentStateIndex); + } + } +} +''' + + explanation = """VRChat UdonSharp Export Generated! + +Setup in Unity: +1. Create new UdonSharpProgramAsset +2. Copy this code to the .cs file +3. Create GameObject in scene +4. Add UdonBehaviour component +5. Link the UdonSharpProgramAsset +6. Set up UI in Inspector + +UI Setup: +- Canvas with TextMeshPro for description +- Button array for choices (4 recommended) +- Link to descriptionText, choiceButtons, choiceButtonTexts + +Button Events: +- Button 0: OnClick -> UdonBehaviour.SendCustomEvent("OnChoice0") +- Button 1: OnClick -> UdonBehaviour.SendCustomEvent("OnChoice1") +- etc. + +Multiplayer Sync: +- Enable syncAcrossPlayers for shared experience +- Uses UdonSynced for state synchronization + +Requirements: +- Unity 2019.4.31f1 (VRChat current) +- VRChat SDK3 - Worlds +- UdonSharp package + +Testing: +- Use VRChat SDK control panel +- Build & Test locally first +- Check Udon console for errors + +Publishing: +- Build and publish via VRChat SDK +- 30K+ concurrent VR users +""" + + return explanation, cs_code diff --git a/exporters/gamemaker_exporter.py b/exporters/gamemaker_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..d169cbde547f8b92706f580c223f002876b2e61f --- /dev/null +++ b/exporters/gamemaker_exporter.py @@ -0,0 +1,103 @@ +"""GameMaker Studio 2 exporter for game configs.""" + +import json + + +def export_to_gamemaker(config_json): + """Export config to GameMaker GML script.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except: + return "Error: Invalid JSON", "" + + gml = '''// GameMaker Studio 2 - Text Adventure Game +// Create this in a persistent controller object + +// ========== GAME CONFIG ========== +global.game_config = ds_map_create(); +''' + + # Add states to config + for location, states in config.items(): + for state_name, state_data in states.items(): + state_key = f"{location}_{state_name}" + gml += f'\n// State: {state_key}\n' + gml += f'var state_{state_key} = ds_map_create();\n' + gml += f'ds_map_add(state_{state_key}, "description", "{state_data.get("description", "")[:100]}");\n' + + choices = state_data.get("choices", []) + gml += f'var choices_{state_key} = ds_list_create();\n' + for choice in choices: + choice_escaped = str(choice).replace('"', '\\"') + gml += f'ds_list_add(choices_{state_key}, "{choice_escaped}");\n' + gml += f'ds_map_add_list(state_{state_key}, "choices", choices_{state_key});\n' + gml += f'ds_map_add_map(global.game_config, "{state_key}", state_{state_key});\n' + + gml += ''' +// ========== GAME STATE ========== +global.current_location = "''' + list(config.keys())[0] + '''"; +global.current_state = "''' + list(config[list(config.keys())[0]].keys())[0] + '''"; +global.inventory = ds_list_create(); +global.money = 0; + +// ========== GAME FUNCTIONS ========== +function get_current_state() { + var state_key = global.current_location + "_" + global.current_state; + return ds_map_find_value(global.game_config, state_key); +} + +function get_description() { + var state = get_current_state(); + if (state != undefined) { + return ds_map_find_value(state, "description"); + } + return "No description"; +} + +function get_choices() { + var state = get_current_state(); + if (state != undefined) { + return ds_map_find_value(state, "choices"); + } + return undefined; +} + +function make_choice(choice_index) { + var choices = get_choices(); + if (choices != undefined && choice_index < ds_list_size(choices)) { + // Handle transition logic here + show_debug_message("Choice made: " + ds_list_find_value(choices, choice_index)); + } +} + +// ========== DRAW EVENT (for obj_game_controller) ========== +// In Draw GUI event: +/* +draw_set_font(fnt_game); +draw_set_color(c_white); + +// Draw description +draw_text_ext(50, 50, get_description(), 20, 700); + +// Draw choices +var choices = get_choices(); +if (choices != undefined) { + for (var i = 0; i < ds_list_size(choices); i++) { + var choice_text = string(i + 1) + ". " + ds_list_find_value(choices, i); + draw_text(50, 200 + (i * 30), choice_text); + } +} +*/ +''' + + explanation = """GameMaker Studio 2 Export: + +1. Create a new object: obj_game_controller +2. Mark it as Persistent +3. In Create event: paste the config and state code +4. In Draw GUI event: use the drawing code +5. In keyboard events: call make_choice() with player input + +The code uses ds_maps and ds_lists for state management.""" + + return explanation, gml diff --git a/exporters/html_exporters.py b/exporters/html_exporters.py new file mode 100644 index 0000000000000000000000000000000000000000..40ced0e0dc8a7d77a37644ab429e3a4c8b6f4517 --- /dev/null +++ b/exporters/html_exporters.py @@ -0,0 +1,596 @@ +"""HTML-based exporters for game configs. + +Includes exporters for: +- 2D Map SVG visualization +- MovingDotSpace interactive HTML game +""" + +import json +import os +import math + + +def export_to_2d_map(config_json): + """Export config to 2D map layout visualization.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except: + return "Error: Invalid JSON", "" + + # Generate SVG map + locations = list(config.keys()) + num_locations = len(locations) + + svg_width = 800 + svg_height = 600 + + svg = f''' + + +''' + + # Calculate positions in a circle + center_x, center_y = svg_width // 2, svg_height // 2 + radius = min(svg_width, svg_height) // 3 + + positions = {} + for i, location in enumerate(locations): + angle = (2 * math.pi * i) / num_locations - math.pi / 2 + x = center_x + radius * math.cos(angle) + y = center_y + radius * math.sin(angle) + positions[location] = (x, y) + + # Draw connections based on transitions + connections_drawn = set() + for location, states in config.items(): + for state_name, state_data in states.items(): + transitions = state_data.get("transitions", {}) + for choice, target in transitions.items(): + if isinstance(target, str) and "/" in target: + target_loc = target.split("/")[0] + if target_loc in positions: + conn_key = tuple(sorted([location, target_loc])) + if conn_key not in connections_drawn: + x1, y1 = positions[location] + x2, y2 = positions[target_loc] + svg += f' \n' + connections_drawn.add(conn_key) + + # Draw location nodes + for location, (x, y) in positions.items(): + num_states = len(config[location]) + node_size = 30 + num_states * 5 + svg += f' \n' + svg += f' {location}\n' + + svg += '' + + # Also generate a text-based map + text_map = "=== 2D MAP LAYOUT ===\n\n" + text_map += "Locations and their states:\n" + for location, states in config.items(): + text_map += f"\n[{location}]\n" + for state_name in states.keys(): + text_map += f" - {state_name}\n" + + text_map += "\nConnections:\n" + for conn in connections_drawn: + text_map += f" {conn[0]} <---> {conn[1]}\n" + + explanation = """2D Map Export: + +The SVG shows your game's location layout: +- Each circle is a location +- Circle size = number of states in that location +- Lines show connections between locations + +You can: +1. Copy the SVG and open in a browser +2. Use as a reference for level design +3. Import into vector editors (Figma, Inkscape)""" + + return explanation, svg + + +def export_to_movingdotspace(config_json, autotravel=True): + """Export game config to a playable MovingDotSpace HTML file. + + Converts the game config into an interactive 2D exploration game + where players use arrow keys to move and collide with targets to + trigger state-machine modals. Locations are locked until the player + transitions to them via game choices. + + Args: + config_json: The game config JSON string or dict + autotravel: If True, teleport player to new locations on transition. + If False, just unlock the location and player must walk there. + """ + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + # === EXTRACT STATS DATA FROM CONFIG === + extracted_objectives = [] + extracted_inventory = [] + extracted_skills = [] + starting_money = 1000 # Default + + # Check for MovingDotSpace metadata (from Big RPG Scale export) + mds_meta = config.get("_movingdotspace_meta", {}) + if mds_meta: + # Use metadata if available + extracted_inventory = mds_meta.get("inventory", []) + extracted_skills = mds_meta.get("skills", []) + extracted_objectives = mds_meta.get("objectives", []) + starting_money = mds_meta.get("startingMoney", 100) + + # Also parse through config to find additional quests, items, skills, and money from on_enter effects + for location_key, location_data in config.items(): + if location_key.startswith("_"): # Skip metadata keys + continue + if not isinstance(location_data, dict): + continue + + for state_key, state_data in location_data.items(): + if not isinstance(state_data, dict): + continue + + # Check on_enter effects for money and quests + on_enter = state_data.get("on_enter", {}) + if isinstance(on_enter, dict): + # Extract starting money (override if found) + if "set_money" in on_enter: + starting_money = on_enter["set_money"] + + # Extract quests as objectives (add to existing) + if "start_mission" in on_enter: + missions = on_enter["start_mission"] + if isinstance(missions, list): + for mission in missions: + if not any(obj["id"] == mission for obj in extracted_objectives): + extracted_objectives.append({ + "id": mission.replace("_", " ").title(), + "name": f"Complete: {mission.replace('_', ' ').title()}", + "complete": False + }) + elif isinstance(missions, str): + if not any(obj["id"] == missions for obj in extracted_objectives): + extracted_objectives.append({ + "id": missions.replace("_", " ").title(), + "name": f"Complete: {missions.replace('_', ' ').title()}", + "complete": False + }) + + # Extract items added to inventory (add to existing) + if "add_item" in on_enter: + items = on_enter["add_item"] + if isinstance(items, list): + for item in items: + if isinstance(item, dict): + extracted_inventory.append(item) + elif isinstance(item, str): + extracted_inventory.append({ + "type": "item", + "name": item.replace("_", " ").title(), + "description": f"A {item.replace('_', ' ')}" + }) + elif isinstance(items, str): + extracted_inventory.append({ + "type": "item", + "name": items.replace("_", " ").title(), + "description": f"A {items.replace('_', ' ')}" + }) + + # If no objectives found, add default exploration objective + if not extracted_objectives: + extracted_objectives.append({ + "id": "Main", + "name": "Explore all locations", + "complete": False + }) + + # Generate targets from game config locations (exclude metadata and special UI locations) + # Special locations that are UI/menu states, not map targets: + # - game_start: intro state + # - inventory: player inventory UI + # - quest_journal: quest tracking UI + special_locations = {"_movingdotspace_meta", "game_start", "inventory", "quest_journal"} + targets = [] + locations = [k for k in config.keys() if k not in special_locations and not k.startswith("_")] + first_location = locations[0] if locations else None + + # Build a map of location keys to their display names for JS + location_map = {} + + for i, location_key in enumerate(locations): + location_data = config[location_key] + display_name = location_key.replace("_", " ").title() + location_map[location_key] = display_name + + # Calculate grid position (4 columns) + x = 150 + (i % 4) * 350 + y = 150 + (i // 4) * 250 + + # Build modalStates from game config states + modal_states = {} + first_state = None + + for state_key, state_data in location_data.items(): + if not isinstance(state_data, dict): + continue + + if first_state is None: + first_state = state_key + + # Convert choices to items with actions + items = [] + transitions = state_data.get("transitions", {}) + for choice in state_data.get("choices", []): + # Get transition target for this choice + target_ref = transitions.get(choice, "close") + + # Parse target reference - check if it's a cross-location transition + if isinstance(target_ref, str): + if "/" in target_ref: + # Format: "location/state" - check if location is different + parts = target_ref.split("/", 1) + target_location = parts[0] + target_state = parts[1] if len(parts) > 1 else "arrive" + + if target_location != location_key: + # Cross-location transition - mark with crossloc prefix + action = f"crossloc:{target_ref}" + else: + # Same location - just use the state name + action = target_state + else: + action = target_ref + else: + action = "close" + + items.append({ + "label": choice, + "action": action + }) + + # Get image from media + media = state_data.get("media", {}) + modal_img = media.get("image", "") if isinstance(media, dict) else "" + + modal_states[state_key] = { + "title": state_key.replace("_", " ").title(), + "content": state_data.get("description", ""), + "items": items, + "consequences": [], + "modalimg": modal_img + } + + # Create target with smmodal collision type + # Include locationKey for lock/unlock tracking + target = { + "name": display_name, + "locationKey": location_key, + "x": x, + "y": y, + "collisionType": "smmodal", + "modalStates": modal_states, + "locked": location_key != first_location # Only first location unlocked + } + targets.append(target) + + # Build the MovingDotSpace config structure with extracted stats + movingdot_config = { + "themes": { + "Exported Game": { + "background": "#2d3436", + "backgroundColor": "#2d3436", + "inventory": extracted_inventory, + "skills": extracted_skills, + "startingMoney": starting_money, + "autotravel": autotravel, + "persistentTargets": [], + "story": [{ + "part": 0, + "objectives": extracted_objectives, + "targets": targets + }], + "actions": {} + } + } + } + + # Read the HTML template + template_path = os.path.join(os.path.dirname(__file__), "..", "MovingDotSpaceExport", "MovingDotSpaceStandalone.html") + + try: + with open(template_path, 'r', encoding='utf-8') as f: + html_template = f.read() + except FileNotFoundError: + return "Error: MovingDotSpaceStandalone.html template not found", "" + + # Convert config to JSON and create the initialization code + config_json_str = json.dumps(movingdot_config, indent=2) + location_map_json = json.dumps(location_map) + + # Custom JS to handle location locking and cross-location transitions + autotravel_js = "true" if autotravel else "false" + init_code = f""" + // Auto-generated config from Game Config IDE + // This overrides the default themes after initial load + (function() {{ + const exportedConfig = {config_json_str}; + const locationMap = {location_map_json}; + + // Track unlocked locations + const unlockedLocations = new Set(['{first_location}']); + + // Autotravel setting - if true, teleport to new locations; if false, just unlock them + let autotravelEnabled = {autotravel_js}; + + // Use setTimeout to ensure this runs after the default init() + setTimeout(function() {{ + initMovingDotSpace(exportedConfig); + + // Explicitly set stats from config after theme is loaded + const themeConfig = exportedConfig.themes['Exported Game']; + if (themeConfig) {{ + // Set starting money + if (themeConfig.startingMoney !== undefined) {{ + gameState.playerStats.money = themeConfig.startingMoney; + }} + // Ensure inventory is loaded + if (themeConfig.inventory && themeConfig.inventory.length > 0) {{ + gameState.inventory = themeConfig.inventory; + }} + // Ensure skills are loaded + if (themeConfig.skills && themeConfig.skills.length > 0) {{ + gameState.skills = themeConfig.skills; + }} + // Load objectives from story part 0 (they should already be there, but ensure) + const storyPart = themeConfig.story && themeConfig.story[0]; + if (storyPart && storyPart.objectives) {{ + gameState.objectives = storyPart.objectives; + }} + // Re-render stats to show updated values + renderStats(); + console.log('Stats loaded:', {{ + money: gameState.playerStats.money, + inventory: gameState.inventory.length, + skills: gameState.skills.length, + objectives: gameState.objectives.length + }}); + }} + + // Create autotravel toggle button + const controlsDiv = document.createElement('div'); + controlsDiv.id = 'autotravel-controls'; + controlsDiv.style.cssText = 'position: absolute; top: 10px; right: 10px; z-index: 1000; background: rgba(0,0,0,0.7); padding: 8px 12px; border-radius: 5px; color: white; font-size: 12px;'; + controlsDiv.innerHTML = ` + + `; + document.getElementById('game-container').appendChild(controlsDiv); + + document.getElementById('autotravel-toggle').addEventListener('change', function(e) {{ + autotravelEnabled = e.target.checked; + const status = autotravelEnabled ? 'ON - Will teleport to new locations' : 'OFF - Walk to unlocked locations manually'; + elements.overlayText.textContent = 'Autotravel: ' + status; + }}); + + // Override the handleSMModalAction to support cross-location transitions + const originalHandleSMModalAction = window.handleSMModalAction || function() {{}}; + + window.handleSMModalActionCustom = function(item) {{ + const action = item.action; + + // Check for cross-location transition + if (action.startsWith('crossloc:')) {{ + const fullRef = action.substring(9); // Remove "crossloc:" + const parts = fullRef.split('/'); + const targetLocation = parts[0]; + const targetState = parts[1]; + + // Unlock the target location + unlockedLocations.add(targetLocation); + + // Update target visibility + updateTargetVisibility(); + + // Find the target element for that location + const targetData = gameState.targets.find(t => t.locationKey === targetLocation); + if (targetData) {{ + // Close current modal first + closeSMModal(); + + if (autotravelEnabled) {{ + // Autotravel ON: Teleport player near the target + gameState.dotPosition.x = targetData.x + 50; + gameState.dotPosition.y = targetData.y; + renderDot(); + + // After a brief delay, open the new location's modal at the target state + setTimeout(function() {{ + if (targetData.modalStates && targetData.modalStates[targetState]) {{ + gameState.currentSMModalStates = targetData.modalStates; + gameState.currentSMModalState = targetState; + gameState.isSMModalOpen = true; + renderSMModalState(); + elements.smModal.classList.add('open'); + }} + }}, 300); + }} else {{ + // Autotravel OFF: Just show message, player must walk there + const locationName = locationMap[targetLocation] || targetLocation; + elements.overlayText.textContent = `Unlocked: ${{locationName}}! Walk there using arrow keys.`; + }} + }} + return; + }} + + // Check if action is a state transition within current modal + if (gameState.currentSMModalStates && gameState.currentSMModalStates[action]) {{ + gameState.currentSMModalState = action; + renderSMModalState(); + }} else if (action === 'close' || !gameState.currentSMModalStates) {{ + // Explicit close or no modal states - just close + closeSMModal(); + }} else {{ + // Execute as action and close + executeAction(action); + closeSMModal(); + }} + }}; + + // Override renderSMModalState to use our custom handler + const originalRenderSMModalState = window.renderSMModalState; + window.renderSMModalState = function() {{ + const states = gameState.currentSMModalStates; + if (!states) {{ + console.error("No modal states available"); + return; + }} + const currentState = states[gameState.currentSMModalState]; + + if (!currentState) {{ + console.error("State not found:", gameState.currentSMModalState); + return; + }} + + elements.smModalTitle.textContent = currentState.title || ''; + elements.smModalContent.textContent = currentState.content || ''; + + // Render actions with custom handler + elements.smModalActions.innerHTML = ''; + (currentState.items || []).forEach(item => {{ + const btn = document.createElement('button'); + btn.textContent = item.label; + btn.addEventListener('click', () => handleSMModalActionCustom(item)); + elements.smModalActions.appendChild(btn); + }}); + + // Handle image + if (currentState.modalimg) {{ + elements.smModalImage.src = currentState.modalimg; + elements.smModalImage.style.display = 'block'; + }} else {{ + elements.smModalImage.style.display = 'none'; + }} + + // Execute consequences + (currentState.consequences || []).forEach(consequence => {{ + executeAction(consequence); + }}); + }}; + + // Function to update target visibility based on unlocked locations + function updateTargetVisibility() {{ + const targetsContainer = document.getElementById('targets-container'); + const targetElements = targetsContainer.querySelectorAll('.target'); + const labelElements = targetsContainer.querySelectorAll('.target-label'); + + gameState.targets.forEach((target, index) => {{ + const isUnlocked = unlockedLocations.has(target.locationKey); + target.locked = !isUnlocked; + + // Find corresponding DOM elements + const targetEl = targetElements[index]; + const labelEl = labelElements[index]; + + if (targetEl) {{ + if (isUnlocked) {{ + targetEl.style.backgroundColor = 'red'; + targetEl.style.opacity = '1'; + targetEl.style.pointerEvents = 'auto'; + }} else {{ + targetEl.style.backgroundColor = '#555'; + targetEl.style.opacity = '0.4'; + }} + }} + if (labelEl) {{ + labelEl.style.opacity = isUnlocked ? '1' : '0.4'; + if (!isUnlocked) {{ + labelEl.textContent = target.name + ' (Locked)'; + }} else {{ + labelEl.textContent = target.name; + }} + }} + }}); + }} + + // Override handleCollision to check if location is unlocked + const originalHandleCollision = window.handleCollision; + window.handleCollision = function(target) {{ + // Check if target is locked + if (target.locked) {{ + elements.overlayText.textContent = 'This location is locked. Find a way to unlock it first!'; + return; + }} + // Call original handler + originalHandleCollision(target); + }}; + + // Initial visibility update + setTimeout(updateTargetVisibility, 100); + + }}, 50); + }})(); + """ + + html_output = html_template.replace("/* CONFIG_PLACEHOLDER */", init_code) + + # Build stats summary for explanation + stats_summary = [] + if extracted_objectives: + stats_summary.append(f"{len(extracted_objectives)} objectives") + if extracted_inventory: + stats_summary.append(f"{len(extracted_inventory)} starting items") + stats_summary.append(f"{starting_money} starting gold") + + autotravel_status = "ON (teleport to locations)" if autotravel else "OFF (walk manually)" + + explanation = f"""MovingDotSpace Export: + +Created a playable 2D exploration game with {len(targets)} location targets. + +**Location Locking System:** +- Only the first location ("{first_location}") is unlocked at start +- Other locations appear grayed out with "(Locked)" label +- When you select a choice that transitions to another location, it unlocks + +**Autotravel:** {autotravel_status} +- Toggle checkbox in top-right corner to switch modes +- ON: Automatically teleport to new locations when unlocked +- OFF: Location unlocks but you must walk there with arrow keys + +**Stats Panel (bottom):** +- {', '.join(stats_summary)} +- Objectives extracted from quests/missions in your config +- Inventory populated from on_enter add_item effects +- Player stats with starting money from config + +**How to Play:** +1. Save the HTML file and open in a browser +2. Click the game area to focus it +3. Use arrow keys to move the green dot +4. Collide with red targets to trigger interactions +5. Choose options that lead to new locations to unlock them +6. Toggle autotravel checkbox to control movement style + +**Features:** +- {len(targets)} interactive locations from your config +- Progressive unlocking based on game choices +- Cross-location transitions (e.g., "village/hub") handled automatically +- State machine modals with your choices/transitions +- Live stats panel with objectives, inventory, and player stats + +The HTML file is self-contained and can be shared directly.""" + + return explanation, html_output diff --git a/exporters/ink_exporter.py b/exporters/ink_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..d20b99ae4c34e9314f0851a3c95ec8d7203bf78a --- /dev/null +++ b/exporters/ink_exporter.py @@ -0,0 +1,86 @@ +"""Ink interactive fiction exporter for game configs.""" + +import json + + +def export_to_ink(config_json): + """Export game config to Ink interactive fiction format (.ink).""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + ink_code = '''// Ink Interactive Story +// Generated from Game Config IDE +// Test at: https://www.inklestudios.com/ink/ + +VAR current_location = "" + +''' + + first_knot = None + + # Generate knots (Ink's term for passages/labels) + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + knot_name = f"{location_key}_{state_key}" + if first_knot is None: + first_knot = knot_name + + description = state_data.get('description', 'No description') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + ink_code += f"=== {knot_name} ===\n" + ink_code += f"~ current_location = \"{location_key}\"\n" + ink_code += f"{description}\n\n" + + if choices: + for choice in choices: + next_state = transitions.get(choice, '') if isinstance(transitions, dict) else '' + + if next_state: + if '_' not in next_state: + next_state = f"{location_key}_{next_state}" + ink_code += f"* [{choice}] -> {next_state}\n" + else: + ink_code += f"* [{choice}]\n" + ink_code += f" You chose: {choice}\n" + ink_code += f" -> END\n" + else: + ink_code += "-> END\n" + + ink_code += "\n" + + # Add start redirect at the top + if first_knot: + ink_code = f"-> {first_knot}\n\n" + ink_code + + explanation = """Ink Interactive Fiction Export Generated! + +Setup Instructions: +1. Install Inky editor: https://github.com/inkle/inky/releases +2. Paste this code into Inky +3. Click Play to test +4. Export to JSON for game integration + +Online Testing: +- Ink Playground: https://www.inklestudios.com/ink/ +- Paste code and click "Play" + +Integration Options: +- Unity: Use ink-unity-integration package +- Web: Use inkjs library +- Godot: Use inkgd plugin + +Ink Syntax Reference: +- === knot === : Named passage +- * [choice] : Player choice +- -> target : Redirect/jump +- ~ var = value : Set variable +- { condition: text } : Conditional text +""" + + return explanation, ink_code diff --git a/exporters/renpy_exporter.py b/exporters/renpy_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..868396bf9d2c09247745971b6ae222c03a2c25c1 --- /dev/null +++ b/exporters/renpy_exporter.py @@ -0,0 +1,112 @@ +"""Ren'Py visual novel exporter for game configs.""" + +import json + + +def export_to_renpy(config_json): + """Export game config to Ren'Py visual novel format (.rpy).""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + rpy_code = '''# Ren'Py Visual Novel Export +# Generated from Game Config IDE + +init python: + # Game state tracking + game_flags = {} + +''' + + # Track all labels for jump validation + all_labels = [] + first_label = None + + # First pass: collect all state names + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + label_name = f"{location_key}_{state_key}" + all_labels.append(label_name) + if first_label is None: + first_label = label_name + + # Add start label + rpy_code += f'''label start: + jump {first_label or "game_start"} + +''' + + # Second pass: generate labels + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + label_name = f"{location_key}_{state_key}" + description = state_data.get('description', 'No description').replace('"', '\\"') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + media = state_data.get('media', {}) + + rpy_code += f"label {label_name}:\n" + + # Add scene/background if media exists + if media.get('image'): + img_name = media['image'].replace('saved_media/', '').replace('.', '_') + rpy_code += f' scene bg {img_name}\n' + + # Add description as narration + rpy_code += f' "{description}"\n\n' + + # Add choices menu + if choices: + rpy_code += " menu:\n" + for choice in choices: + choice_escaped = choice.replace('"', '\\"') + next_state = transitions.get(choice, '') if isinstance(transitions, dict) else '' + + rpy_code += f' "{choice_escaped}":\n' + + if next_state: + # Handle state references + if '_' not in next_state: + next_state = f"{location_key}_{next_state}" + if next_state in all_labels: + rpy_code += f' jump {next_state}\n' + else: + rpy_code += f' # Target not found: {next_state}\n' + rpy_code += f' "This path is not yet implemented."\n' + else: + rpy_code += f' "You chose: {choice_escaped}"\n' + else: + rpy_code += " return\n" + + rpy_code += "\n" + + explanation = """Ren'Py Visual Novel Export Generated! + +Setup Instructions: +1. Download Ren'Py from https://www.renpy.org/ +2. Create a new project +3. Replace game/script.rpy with this code +4. Add images to game/images/ folder +5. Run the project + +Image Setup: +- Place background images in game/images/ +- Name them to match the scene declarations +- Supported formats: .png, .jpg, .webp + +Testing: +- Use Ren'Py launcher to test +- Press Shift+R to reload scripts +- Use developer console for debugging + +Web Export: +- Ren'Py can export to web (HTML5) +- Use "Build Distributions" > "Web" +""" + + return explanation, rpy_code diff --git a/exporters/roblox_exporter.py b/exporters/roblox_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..f8fe49ed7fbc70135aee4b2944df36aca6fae353 --- /dev/null +++ b/exporters/roblox_exporter.py @@ -0,0 +1,192 @@ +"""Roblox Lua exporter for game configs.""" + +import json + + +def export_to_roblox(config_json): + """Export game config to Roblox Lua format (ModuleScript + LocalScript).""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + lua_code = '''--[[ + Roblox Story System Export + Generated from Game Config IDE + + Setup: + 1. Create a ModuleScript in ReplicatedStorage named "StoryModule" + 2. Paste this code + 3. Create UI elements and connect to the module +--]] + +local StoryModule = {} + +-- Story States Configuration +StoryModule.States = { +''' + + first_state = None + + # Generate states table + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_name = f"{location_key}_{state_key}" + if first_state is None: + first_state = state_name + + description = state_data.get('description', 'No description').replace('"', '\\"').replace('\n', '\\n') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + media = state_data.get('media', {}) + + lua_code += f' ["{state_name}"] = {{\n' + lua_code += f' Description = "{description}",\n' + lua_code += f' Location = "{location_key}",\n' + + # Choices array + lua_code += ' Choices = {' + if choices: + choice_strs = [f'"{c}"' for c in choices] + lua_code += ', '.join(choice_strs) + lua_code += '},\n' + + # Transitions table + lua_code += ' Transitions = {\n' + if isinstance(transitions, dict): + for choice, next_state in transitions.items(): + if next_state: + if '_' not in next_state: + next_state = f"{location_key}_{next_state}" + lua_code += f' ["{choice}"] = "{next_state}",\n' + lua_code += ' },\n' + + # Media table + lua_code += ' Media = {\n' + if media.get('image'): + lua_code += f' Image = "{media["image"]}",\n' + if media.get('audio'): + lua_code += f' Audio = "{media["audio"]}",\n' + lua_code += ' },\n' + + lua_code += ' },\n' + + lua_code += f'''}} + +-- Starting state +StoryModule.StartState = "{first_state or "start"}" + +-- Current state tracker +local currentState = StoryModule.StartState + +-- Get current state data +function StoryModule.GetCurrentState() + return StoryModule.States[currentState] +end + +-- Get state by name +function StoryModule.GetState(stateName) + return StoryModule.States[stateName] +end + +-- Make a choice and transition to next state +function StoryModule.MakeChoice(choiceText) + local state = StoryModule.States[currentState] + if state and state.Transitions[choiceText] then + currentState = state.Transitions[choiceText] + return true, StoryModule.States[currentState] + end + return false, nil +end + +-- Reset to start +function StoryModule.Reset() + currentState = StoryModule.StartState +end + +-- Get all available choices for current state +function StoryModule.GetChoices() + local state = StoryModule.States[currentState] + return state and state.Choices or {{}} +end + +return StoryModule + +--[[ + EXAMPLE LOCALSCRIPT (put in StarterPlayerScripts): + + local ReplicatedStorage = game:GetService("ReplicatedStorage") + local StoryModule = require(ReplicatedStorage:WaitForChild("StoryModule")) + local Players = game:GetService("Players") + local player = Players.LocalPlayer + local playerGui = player:WaitForChild("PlayerGui") + + -- Create simple UI + local screenGui = Instance.new("ScreenGui") + screenGui.Parent = playerGui + + local descLabel = Instance.new("TextLabel") + descLabel.Size = UDim2.new(0.8, 0, 0.3, 0) + descLabel.Position = UDim2.new(0.1, 0, 0.1, 0) + descLabel.TextWrapped = true + descLabel.Parent = screenGui + + local function updateUI() + local state = StoryModule.GetCurrentState() + descLabel.Text = state.Description + + -- Clear old buttons + for _, child in pairs(screenGui:GetChildren()) do + if child:IsA("TextButton") then + child:Destroy() + end + end + + -- Create choice buttons + for i, choice in ipairs(state.Choices) do + local btn = Instance.new("TextButton") + btn.Size = UDim2.new(0.3, 0, 0.08, 0) + btn.Position = UDim2.new(0.35, 0, 0.5 + (i-1) * 0.1, 0) + btn.Text = choice + btn.Parent = screenGui + + btn.MouseButton1Click:Connect(function() + StoryModule.MakeChoice(choice) + updateUI() + end) + end + end + + updateUI() +--]] +''' + + explanation = """Roblox Lua Export Generated! + +Setup Instructions: +1. Open Roblox Studio +2. Create a ModuleScript in ReplicatedStorage named "StoryModule" +3. Paste this code into the ModuleScript +4. Create a LocalScript in StarterPlayerScripts for UI +5. Use the example code at the bottom as a starting point + +Testing: +- Use Play Solo (F5) to test +- The example creates basic text UI +- Customize with proper GUI elements + +API Reference: +- StoryModule.GetCurrentState() : Get current state data +- StoryModule.MakeChoice(choice) : Make a choice, returns success + new state +- StoryModule.GetChoices() : Get available choices array +- StoryModule.Reset() : Reset to start state + +Publishing: +- Test in Studio first +- Publish as Experience +- Share via Roblox platform +""" + + return explanation, lua_code diff --git a/exporters/router.py b/exporters/router.py new file mode 100644 index 0000000000000000000000000000000000000000..14284cc67b9a1f5f941cfe65d2548b959c713958 --- /dev/null +++ b/exporters/router.py @@ -0,0 +1,127 @@ +"""Router for config-to-platform exports. + +This module provides the main routing function that dispatches +to the appropriate platform-specific exporter based on the +platform identifier. +""" + +import json + +# Import all exporters +from .gamemaker_exporter import export_to_gamemaker +from .flutter_exporter import export_to_flutter +from .twine_exporter import export_to_twine +from .renpy_exporter import export_to_renpy +from .ink_exporter import export_to_ink +from .yarn_exporter import export_to_yarn +from .roblox_exporter import export_to_roblox +from .rpgmaker_exporter import export_to_rpgmaker +from .ar_exporters import ( + export_to_lensstudio, + export_to_metaspark, + export_to_8thwall, + export_to_tiktok, + export_to_reality +) +from .game_platform_exporters import ( + export_to_minecraft, + export_to_papyrus, + export_to_vrchat +) +from .fiction_exporters import ( + export_to_inform7, + export_to_godot_dialogue +) +from .html_exporters import ( + export_to_2d_map, + export_to_movingdotspace +) +from .terminal_exporter import export_to_terminal + + +def ConfigConversionforExporttoPlatform(platform, config): + """Route config export to the appropriate platform-specific exporter. + + Args: + platform: Platform identifier string (e.g., 'gamemaker', 'flutter', 'twine') + config: Game config JSON string or dict + + Returns: + Tuple of (explanation, output_code) + """ + FinalOutput = "" + FinalOutputExplanation = "" + + # Import engine exporters from main module (these live in my_text_game_engine_attempt.py) + try: + from my_text_game_engine_attempt import ( + export_to_playcanvas, + export_to_godot, + export_to_unreal + ) + has_engine_exporters = True + except ImportError: + has_engine_exporters = False + + if platform == "playcanvas" and has_engine_exporters: + FinalOutputExplanation, FinalOutput = export_to_playcanvas(config) + elif platform == "godot" and has_engine_exporters: + FinalOutputExplanation, FinalOutput = export_to_godot(config) + elif platform == "unreal" and has_engine_exporters: + FinalOutputExplanation, FinalOutput = export_to_unreal(config) + elif platform == "gamemaker": + FinalOutputExplanation, FinalOutput = export_to_gamemaker(config) + elif platform == "flutter": + FinalOutputExplanation, FinalOutput = export_to_flutter(config) + elif platform == "twine": + FinalOutputExplanation, FinalOutput = export_to_twine(config) + elif platform == "renpy": + FinalOutputExplanation, FinalOutput = export_to_renpy(config) + elif platform == "ink": + FinalOutputExplanation, FinalOutput = export_to_ink(config) + elif platform == "yarn": + FinalOutputExplanation, FinalOutput = export_to_yarn(config) + elif platform == "roblox": + FinalOutputExplanation, FinalOutput = export_to_roblox(config) + elif platform == "rpgmaker": + FinalOutputExplanation, FinalOutput = export_to_rpgmaker(config) + elif platform == "lensstudio": + FinalOutputExplanation, FinalOutput = export_to_lensstudio(config) + elif platform == "metaspark": + FinalOutputExplanation, FinalOutput = export_to_metaspark(config) + elif platform == "8thwall": + FinalOutputExplanation, FinalOutput = export_to_8thwall(config) + elif platform == "minecraft": + FinalOutputExplanation, FinalOutput = export_to_minecraft(config) + elif platform == "papyrus": + FinalOutputExplanation, FinalOutput = export_to_papyrus(config) + elif platform == "vrchat": + FinalOutputExplanation, FinalOutput = export_to_vrchat(config) + elif platform == "tiktok": + FinalOutputExplanation, FinalOutput = export_to_tiktok(config) + elif platform == "reality": + FinalOutputExplanation, FinalOutput = export_to_reality(config) + elif platform == "inform7": + FinalOutputExplanation, FinalOutput = export_to_inform7(config) + elif platform == "godot_dialogue": + FinalOutputExplanation, FinalOutput = export_to_godot_dialogue(config) + elif platform == "movingdotspace" or platform == "2d map related space": + FinalOutputExplanation, FinalOutput = export_to_movingdotspace(config) + elif platform == "terminal": + FinalOutputExplanation, FinalOutput = export_to_terminal(config) + elif platform == "existing game": + FinalOutput = """// Config for importing into existing game mod systems +// Copy this JSON and use your game's import feature + +""" + (config if isinstance(config, str) else json.dumps(json.loads(config) if isinstance(config, str) else config, indent=2)) + FinalOutputExplanation = """Existing Game Integration: + +Popular games with mod/custom content support: +- Skyrim: Use Creation Kit, save as quest mod +- GTA V: Use Script Hook V, convert to mission script +- Sims 4: Use Mod Constructor, create story mod +- Roblox: Use Roblox Studio, create experience + +The config JSON is provided above - convert it to your game's format.""" + + return FinalOutputExplanation, FinalOutput diff --git a/exporters/rpgmaker_exporter.py b/exporters/rpgmaker_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..7fa3e8b6f6c7e0155edb8d5bd9f520467cba0bff --- /dev/null +++ b/exporters/rpgmaker_exporter.py @@ -0,0 +1,247 @@ +"""RPG Maker MZ JavaScript plugin exporter for game configs.""" + +import json + + +def export_to_rpgmaker(config_json): + """Export game config to RPG Maker MZ JavaScript plugin format.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + js_code = '''//============================================================================= +// StoryAdventure.js - RPG Maker MZ Plugin +// Generated from Game Config IDE +//============================================================================= + +/*: + * @target MZ + * @plugindesc Story Adventure System - Branching narrative plugin + * @author Game Config IDE Export + * + * @command StartStory + * @text Start Story + * @desc Begin the story from the starting state + * + * @command ShowCurrentState + * @text Show Current State + * @desc Display the current state with choices + * + * @help + * This plugin provides a branching narrative system. + * + * Plugin Commands: + * - StartStory: Initialize and show first state + * - ShowCurrentState: Redisplay current state + * + * Script Calls: + * - $gameStory.setState("state_name") + * - $gameStory.makeChoice(index) + * - $gameStory.getCurrentState() + */ + +(() => { + const pluginName = "StoryAdventure"; + + // Story States Data + const STORY_STATES = { +''' + + first_state = None + + # Generate states + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_name = f"{location_key}_{state_key}" + if first_state is None: + first_state = state_name + + description = state_data.get('description', 'No description').replace('"', '\\"').replace('\n', '\\n') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + js_code += f' "{state_name}": {{\n' + js_code += f' description: "{description}",\n' + js_code += f' location: "{location_key}",\n' + + # Choices array + js_code += ' choices: [' + if choices: + choice_strs = [f'"{c}"' for c in choices] + js_code += ', '.join(choice_strs) + js_code += '],\n' + + # Transitions object + js_code += ' transitions: {\n' + if isinstance(transitions, dict): + for choice, next_state in transitions.items(): + if next_state: + if '_' not in next_state: + next_state = f"{location_key}_{next_state}" + choice_escaped = choice.replace('"', '\\"') + js_code += f' "{choice_escaped}": "{next_state}",\n' + js_code += ' }\n' + js_code += ' },\n' + + js_code += f''' }}; + + const START_STATE = "{first_state or "start"}"; + + // Game Story Manager + class Game_Story {{ + constructor() {{ + this.initialize(); + }} + + initialize() {{ + this._currentState = START_STATE; + this._visitedStates = []; + }} + + getCurrentState() {{ + return STORY_STATES[this._currentState]; + }} + + getStateName() {{ + return this._currentState; + }} + + setState(stateName) {{ + if (STORY_STATES[stateName]) {{ + this._currentState = stateName; + this._visitedStates.push(stateName); + return true; + }} + return false; + }} + + makeChoice(choiceIndex) {{ + const state = this.getCurrentState(); + if (state && state.choices[choiceIndex]) {{ + const choiceText = state.choices[choiceIndex]; + const nextState = state.transitions[choiceText]; + if (nextState) {{ + return this.setState(nextState); + }} + }} + return false; + }} + + getChoices() {{ + const state = this.getCurrentState(); + return state ? state.choices : []; + }} + + hasVisited(stateName) {{ + return this._visitedStates.includes(stateName); + }} + + reset() {{ + this.initialize(); + }} + }} + + // Create global instance + window.$gameStory = null; + + // Initialize on new game + const _DataManager_createGameObjects = DataManager.createGameObjects; + DataManager.createGameObjects = function() {{ + _DataManager_createGameObjects.call(this); + $gameStory = new Game_Story(); + }}; + + // Save story state + const _DataManager_makeSaveContents = DataManager.makeSaveContents; + DataManager.makeSaveContents = function() {{ + const contents = _DataManager_makeSaveContents.call(this); + contents.story = {{ + currentState: $gameStory._currentState, + visitedStates: $gameStory._visitedStates + }}; + return contents; + }}; + + // Load story state + const _DataManager_extractSaveContents = DataManager.extractSaveContents; + DataManager.extractSaveContents = function(contents) {{ + _DataManager_extractSaveContents.call(this, contents); + if (contents.story) {{ + $gameStory._currentState = contents.story.currentState; + $gameStory._visitedStates = contents.story.visitedStates; + }} + }}; + + // Show story state as message with choices + function showStoryState() {{ + const state = $gameStory.getCurrentState(); + if (!state) return; + + // Show description + $gameMessage.add(state.description); + + // Set up choices if available + if (state.choices.length > 0) {{ + $gameMessage.setChoices(state.choices, 0, -1); + $gameMessage.setChoiceCallback(n => {{ + if (n >= 0) {{ + $gameStory.makeChoice(n); + // Optionally auto-continue + // showStoryState(); + }} + }}); + }} + }} + + // Plugin Commands + PluginManager.registerCommand(pluginName, "StartStory", args => {{ + $gameStory.reset(); + showStoryState(); + }}); + + PluginManager.registerCommand(pluginName, "ShowCurrentState", args => {{ + showStoryState(); + }}); + + // Export for script calls + window.Game_Story = Game_Story; + window.showStoryState = showStoryState; + +}})(); +''' + + explanation = """RPG Maker MZ Plugin Export Generated! + +Setup Instructions: +1. Save this as js/plugins/StoryAdventure.js +2. Open RPG Maker MZ project +3. Go to Plugin Manager (gear icon) +4. Add "StoryAdventure" plugin +5. Use Plugin Commands in events + +Plugin Commands: +- StartStory: Begin from first state +- ShowCurrentState: Show current state again + +Script Calls (in Event > Script): +- $gameStory.setState("state_name") +- $gameStory.makeChoice(0) // choice index +- $gameStory.getCurrentState() +- $gameStory.getChoices() +- $gameStory.hasVisited("state_name") +- showStoryState() // Show with message window + +Event Integration: +- Use Conditional Branch to check states +- Script: $gameStory.getStateName() === "village_start" +- Combine with map transfers for location changes + +Saving: +- Story progress auto-saves with game +- Visited states tracked for conditionals +""" + + return explanation, js_code diff --git a/exporters/terminal_exporter.py b/exporters/terminal_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..755f0280014bd5fcbbc4efe84cf65697209ff190 --- /dev/null +++ b/exporters/terminal_exporter.py @@ -0,0 +1,131 @@ +"""Python terminal game exporter for game configs.""" + +import json + + +def export_to_terminal(config_json): + """Export game config to standalone Python terminal game.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + code = '''#!/usr/bin/env python3 +""" +Auto-generated Terminal Text Adventure Game +Run with: python game.py +""" + +import json + +# Game configuration +CONFIG = ''' + json.dumps(config, indent=2) + ''' + +def get_state(location, state_name): + """Get state data from config.""" + if location in CONFIG and state_name in CONFIG[location]: + return CONFIG[location][state_name] + return None + +def play_game(): + """Main game loop.""" + # Find starting location and state + current_location = list(CONFIG.keys())[0] + current_state = list(CONFIG[current_location].keys())[0] + + inventory = [] + flags = {} + money = 0 + + print("=" * 50) + print("TERMINAL TEXT ADVENTURE") + print("=" * 50) + print() + + while True: + state = get_state(current_location, current_state) + if not state: + print(f"\\nGame Over - No state found: {current_location}/{current_state}") + break + + # Display description + description = state.get("description", "No description") + print(f"\\n[{current_location.upper()} - {current_state}]") + print("-" * 40) + print(description) + print() + + # Get choices + choices = state.get("choices", []) + if not choices: + print("\\n*** THE END ***") + break + + # Display choices + print("What do you do?") + for i, choice in enumerate(choices, 1): + print(f" {i}. {choice}") + print(f" 0. Quit game") + + # Get player input + try: + choice_input = input("\\nEnter choice: ").strip() + if choice_input == "0": + print("\\nThanks for playing!") + break + choice_idx = int(choice_input) - 1 + if 0 <= choice_idx < len(choices): + chosen = choices[choice_idx] + print(f"\\n> {chosen}") + + # Handle transitions + transitions = state.get("transitions", {}) + if chosen in transitions: + next_state = transitions[chosen] + if "/" in str(next_state): + parts = next_state.split("/") + current_location = parts[0] + current_state = parts[1] + else: + current_state = next_state + else: + print("Invalid choice, try again.") + except ValueError: + print("Please enter a number.") + except KeyboardInterrupt: + print("\\n\\nGame interrupted. Goodbye!") + break + +if __name__ == "__main__": + play_game() +''' + + explanation = """Python Terminal Game Export Generated! + +How to Run: +1. Save the code as 'game.py' +2. Open terminal/command prompt +3. Run: python game.py + +Requirements: +- Python 3.6+ +- No external dependencies + +Features: +- Text-based adventure gameplay +- Choice-based navigation +- State transitions from your config +- Quit option (enter 0) + +Mobile Options: +- Pydroid 3 (Android) - Run Python on phone +- Pythonista (iOS) - Python IDE for iPhone/iPad +- Replit Mobile - Cloud-based Python + +Web Hosting: +- Replit.com - Host and share Python apps +- PythonAnywhere - Free Python hosting +- Google Colab - Run in browser (add input handling) +""" + + return explanation, code diff --git a/exporters/twine_exporter.py b/exporters/twine_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..2853871cbda4c98b4f79cae2880518352182fd79 --- /dev/null +++ b/exporters/twine_exporter.py @@ -0,0 +1,127 @@ +"""Twine/SugarCube exporter for game configs.""" + +import json + + +def export_to_twine(config_json): + """Export game config to Twine/SugarCube HTML format.""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + # Build Twine passages + passages = [] + first_passage = None + + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + passage_name = f"{location_key}_{state_key}" + if first_passage is None: + first_passage = passage_name + + description = state_data.get('description', 'No description') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + + # Build passage content + content = f"{description}\n\n" + + # Add choices as links + for choice in choices: + if isinstance(transitions, dict): + next_state = transitions.get(choice, '') + if next_state: + # Handle slash notation (location/state) -> convert to underscore + if '/' in next_state: + next_state = next_state.replace('/', '_') + # Handle state-only references (add current location prefix) + elif '_' not in next_state or not any(next_state.startswith(loc) for loc in config.keys()): + # Check if it's just a state name within current location + if next_state in location_data: + next_state = f"{location_key}_{next_state}" + content += f"[[{choice}|{next_state}]]\n" + else: + content += f"[[{choice}]]\n" + else: + content += f"[[{choice}]]\n" + + passages.append((passage_name, content.strip())) + + # Generate Twine HTML + twine_html = f''' + + + + Exported Story + + + +
+
+
+ + + +''' + + explanation = """Twine/Interactive Fiction Export Generated! + +Instructions: +1. Save this as an .html file +2. Open in any web browser to play +3. Or import into Twine (twinery.org): + - Open Twine + - Import > From File + - Select the HTML file + +Alternative - Use Twinery.org online: +1. Go to https://twinery.org/ +2. Click "Use it online" +3. Create new story +4. Copy passages manually or import HTML + +Features: +- Self-contained HTML file +- No dependencies required +- Works offline +- Easy to customize CSS +""" + + return explanation, twine_html diff --git a/exporters/yarn_exporter.py b/exporters/yarn_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..e6004ad3791a2fc5eee5ee8b65ea5a16c56ab783 --- /dev/null +++ b/exporters/yarn_exporter.py @@ -0,0 +1,108 @@ +"""Yarn Spinner dialogue exporter for game configs.""" + +import json + + +def export_to_yarn(config_json): + """Export game config to Yarn Spinner dialogue format (.yarn).""" + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + except json.JSONDecodeError as e: + return f"JSON Error: {str(e)}", "" + + yarn_code = '''title: Start +tags: +--- +<> +=== + +''' + + first_node = None + nodes = [] + + # Generate nodes + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + node_name = f"{location_key}_{state_key}" + if first_node is None: + first_node = node_name + + description = state_data.get('description', 'No description') + choices = state_data.get('choices', []) + transitions = state_data.get('transitions', {}) + media = state_data.get('media', {}) + + node_code = f"title: {node_name}\n" + node_code += f"tags: location:{location_key}\n" + node_code += "---\n" + + # Add media commands if present + if media.get('image'): + node_code += f"<>\n" + if media.get('audio'): + node_code += f"<>\n" + + # Add description + node_code += f"{description}\n\n" + + # Add choices + if choices: + for choice in choices: + next_state = transitions.get(choice, '') if isinstance(transitions, dict) else '' + + if next_state: + if '_' not in next_state: + next_state = f"{location_key}_{next_state}" + node_code += f"-> {choice}\n" + node_code += f" <>\n" + else: + node_code += f"-> {choice}\n" + node_code += f" You selected: {choice}\n" + node_code += f" <>\n" + else: + node_code += "<>\n" + + node_code += "===\n\n" + nodes.append(node_code) + + # Replace placeholder in start node + yarn_code = yarn_code.replace("{first_node}", first_node or "Start") + + # Add all nodes + yarn_code += "\n".join(nodes) + + explanation = """Yarn Spinner Dialogue Export Generated! + +Setup Instructions: +1. Install Yarn Spinner for Unity: https://yarnspinner.dev/ +2. Create a new .yarn file in your project +3. Paste this code +4. Add a Dialogue Runner component to your scene + +Online Testing: +- Try Yarn Spinner: https://try.yarnspinner.dev/ +- Paste code and click "Run" + +Unity Integration: +1. Import Yarn Spinner package from Unity Asset Store (free) +2. Create YarnProject asset +3. Add .yarn files to project +4. Set up DialogueRunner with your UI + +Custom Commands (implement in C#): +- <> : Change background +- <> : Play sound effect +- <> : End dialogue + +Yarn Syntax Reference: +- title: NodeName : Start of node +- -> Choice text : Player option +- <> : Go to node +- <> : Set variable +- <> ... <> : Conditional +""" + + return explanation, yarn_code diff --git a/file_explorer_and_upload.py b/file_explorer_and_upload.py index f6dc1975fa0823d956b9f0ff9387c7af69e3e2ad..55cbb72b9ec303dc9cda8c5454ec303add4f8ee4 100644 --- a/file_explorer_and_upload.py +++ b/file_explorer_and_upload.py @@ -16,7 +16,8 @@ os.makedirs(SAVE_DIR, exist_ok=True) SUPPORTED_EXTENSIONS = { "image": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"], "audio": [".mp3", ".wav", ".ogg"], - "video": [".mp4", ".avi", ".mov", ".webm"] + "video": [".mp4", ".avi", ".mov", ".webm"], + "model3d": [".glb", ".gltf", ".obj"] } def save_file(file): @@ -50,24 +51,61 @@ def save_file(file): return f"Error saving file: {str(e)}", gr.update(value=SAVE_DIR), gr.update() def view_file(file_path): + """View a file and return appropriate outputs for each media type. + Returns: (image, audio, video, message, model3d) + """ if not file_path: - return None, None, None, "No file selected." + return None, None, None, "No file selected.", None try: - full_path = os.path.join(SAVE_DIR, file_path) + # FileExplorer returns relative path from root_dir, or could be full path + # Handle both cases + if os.path.isabs(file_path): + full_path = file_path + elif file_path.startswith(SAVE_DIR): + full_path = file_path + else: + full_path = os.path.join(SAVE_DIR, file_path) + + # Normalize the path + full_path = os.path.normpath(full_path) + + if not os.path.exists(full_path): + return None, None, None, f"File not found: {full_path}", None + _, extension = os.path.splitext(full_path) extension = extension.lower() if extension in SUPPORTED_EXTENSIONS["image"]: - return Image.open(full_path), None, None, None + return Image.open(full_path), None, None, f"Viewing: {os.path.basename(full_path)}", None elif extension in SUPPORTED_EXTENSIONS["audio"]: - return None, full_path, None, None + return None, full_path, None, f"Viewing: {os.path.basename(full_path)}", None elif extension in SUPPORTED_EXTENSIONS["video"]: - return None, None, full_path, None + return None, None, full_path, f"Viewing: {os.path.basename(full_path)}", None + elif extension in SUPPORTED_EXTENSIONS["model3d"]: + return None, None, None, f"Viewing 3D: {os.path.basename(full_path)}", full_path else: - return None, None, None, f"Unsupported file type: {extension}" + return None, None, None, f"Unsupported file type: {extension}", None + except Exception as e: + return None, None, None, f"Error viewing file: {str(e)}", None + + +def get_all_media_files(): + """Get all media files from saved_media folder for use in dropdowns""" + try: + if not os.path.exists(SAVE_DIR): + return [] + files = os.listdir(SAVE_DIR) + # Filter to only supported media files + media_files = [] + for f in files: + ext = os.path.splitext(f)[1].lower() + if any(ext in exts for exts in SUPPORTED_EXTENSIONS.values()): + media_files.append(f) + return sorted(media_files) except Exception as e: - return None, None, None, f"Error viewing file: {str(e)}" + print(f"Error listing media files: {e}") + return [] def refresh_file_explorer(): files = os.listdir(SAVE_DIR) @@ -81,6 +119,117 @@ def delete_file(file_path): else: return f"{file_path} does not exist." + +def delete_file_and_refresh(filename): + """Delete file and return updated dropdown choices""" + if not filename: + return gr.update(choices=get_all_media_files()), "No file selected" + + full_path = os.path.join(SAVE_DIR, filename) + if os.path.exists(full_path): + os.remove(full_path) + return gr.update(choices=get_all_media_files(), value=None), f"Deleted: {filename}" + return gr.update(choices=get_all_media_files()), f"File not found: {filename}" + + +def save_file_and_refresh(file): + """Save file and return updated dropdown choices for Browse tab""" + if file is None: + return "No file uploaded.", gr.update(), gr.update(value=None) + + try: + original_filename = os.path.basename(file.name) + _, extension = os.path.splitext(original_filename) + + if not any(extension.lower() in exts for exts in SUPPORTED_EXTENSIONS.values()): + return f"Unsupported file type: {extension}", gr.update(), gr.update() + + base_name = os.path.splitext(original_filename)[0] + counter = 1 + new_filename = f"{base_name}{extension}" + while os.path.exists(os.path.join(SAVE_DIR, new_filename)): + new_filename = f"{base_name}_{counter}{extension}" + counter += 1 + + dest_path = os.path.join(SAVE_DIR, new_filename) + shutil.copy2(file.name, dest_path) + + # Return updated dropdown choices + return f"File saved: {new_filename}", gr.update(choices=get_all_media_files()), gr.update(value=None) + except Exception as e: + return f"Error saving file: {str(e)}", gr.update(), gr.update() + +def upload_file_or_bundle(file): + """ + Unified upload handler that handles both media files and zip bundles. + Returns: (status, dropdown_update, file_input_clear, config_json, extracted_files) + """ + if file is None: + return "No file uploaded.", gr.update(), gr.update(value=None), None, "" + + try: + original_filename = os.path.basename(file.name) + _, extension = os.path.splitext(original_filename) + ext_lower = extension.lower() + + # Check if it's a zip bundle + if ext_lower == ".zip": + try: + config, extracted_files = import_config_with_media(file.name) + config_json = json.dumps(config, indent=2) + files_str = "\n".join(extracted_files) if extracted_files else "No new files extracted (all already existed)" + status = f"Bundle imported! Config loaded, {len(extracted_files)} new media files extracted." + return ( + status, + gr.update(choices=get_all_media_files()), + gr.update(value=None), + config_json, + files_str + ) + except FileNotFoundError as e: + return f"Error: {str(e)}", gr.update(), gr.update(), None, "" + except Exception as e: + return f"Error importing bundle: {str(e)}", gr.update(), gr.update(), None, "" + + # Handle regular media files + if not any(ext_lower in exts for exts in SUPPORTED_EXTENSIONS.values()): + return f"Unsupported file type: {extension}", gr.update(), gr.update(), None, "" + + base_name = os.path.splitext(original_filename)[0] + counter = 1 + new_filename = f"{base_name}{extension}" + while os.path.exists(os.path.join(SAVE_DIR, new_filename)): + new_filename = f"{base_name}_{counter}{extension}" + counter += 1 + + dest_path = os.path.join(SAVE_DIR, new_filename) + shutil.copy2(file.name, dest_path) + + return ( + f"File saved: {new_filename}", + gr.update(choices=get_all_media_files()), + gr.update(value=None), + None, + "" + ) + except Exception as e: + return f"Error: {str(e)}", gr.update(), gr.update(), None, "" + + +def export_config_with_media_wrapper(config_json): + """Wrapper for export that provides status message along with the file""" + from my_text_game_engine_attempt import export_config_with_media + if not config_json or not config_json.strip(): + return None, "Please paste a config JSON to export" + try: + zip_path = export_config_with_media(config_json) + return zip_path, f"Export successful! Bundle ready for download." + except json.JSONDecodeError as e: + return None, f"Invalid JSON: {str(e)}" + except Exception as e: + return None, f"Export failed: {str(e)}" + + def import_config_with_media(zip_path): global SAVE_DIR target_folder = SAVE_DIR @@ -100,21 +249,27 @@ def import_config_with_media(zip_path): with zipfile.ZipFile(zip_path, 'r') as zip_ref: zip_ref.extractall(temp_dir) - # Load the config + # Load the config (check both config.json and game_config.json for compatibility) config_path = os.path.join(temp_dir, 'config.json') + alt_config_path = os.path.join(temp_dir, 'game_config.json') + if os.path.exists(config_path): with open(config_path, 'r') as f: config = json.load(f) + elif os.path.exists(alt_config_path): + with open(alt_config_path, 'r') as f: + config = json.load(f) else: - raise FileNotFoundError("config.json not found in the zip file") + raise FileNotFoundError("config.json (or game_config.json) not found in the zip file") # Create the target folder if it doesn't exist os.makedirs(target_folder, exist_ok=True) # Copy media files that don't already exist in the target folder + config_files = {'config.json', 'game_config.json'} for root, _, files in os.walk(temp_dir): for file in files: - if file != 'config.json': + if file not in config_files: src_path = os.path.join(root, file) dst_path = os.path.join(target_folder, file) if not os.path.exists(dst_path): diff --git a/film_scenarios.py b/film_scenarios.py new file mode 100644 index 0000000000000000000000000000000000000000..17ec5680f8ecf9bd90f76ee8b8c109f8b0dda4a2 --- /dev/null +++ b/film_scenarios.py @@ -0,0 +1,206 @@ +"""Film scenario data and sequence generation. + +This module provides: +- FILM_SCENARIOS: Dictionary of categorized film scenario templates +- generate_scenario_sequence: Function to generate random sequences of scenarios +""" + +import json +import random + +FILM_SCENARIOS = { + "openings": [ + ("Dawn Patrol", "Character wakes up, morning routine reveals personality/situation"), + ("The Arrival", "Protagonist enters new location for first time"), + ("In Medias Res", "Opens mid-action, audience catches up"), + ("The Funeral", "Character death sets story in motion, mourners gathered"), + ("Voiceover Reflection", "Older character narrates past events"), + ], + "tension": [ + ("The Ticking Clock", "Countdown visible/mentioned, deadline approaching"), + ("Hiding in Plain Sight", "Character conceals identity among enemies"), + ("The Stakeout", "Characters wait and watch from vehicle/building"), + ("Parallel Editing Chase", "Cut between pursuer and pursued"), + ("The Interrogation", "Character questioned under pressure"), + ("Eavesdropping", "Character overhears crucial conversation"), + ("The Setup/Double-Cross", "Trusted ally reveals betrayal"), + ("Trapped in Enclosed Space", "Elevator, room, vehicle - no escape"), + ("The Hostage Situation", "Loved one held, demands made"), + ("Walking Into a Trap", "Audience knows danger character doesn't"), + ], + "action": [ + ("The Standoff", "Multiple parties aim weapons, no one shoots first"), + ("The Bar Fight", "Violence erupts in drinking establishment"), + ("Car Chase Through City", "Vehicles weave through traffic, obstacles"), + ("Rooftop Confrontation", "Final battle on building top, city below"), + ("The Heist Execution", "Plan unfolds step by step with complications"), + ("Training Montage", "Character improves skills over compressed time"), + ("The Last Stand", "Outnumbered defenders hold position"), + ("One vs Many", "Single fighter takes on multiple opponents"), + ("The Duel", "Two opponents face off, formal or informal"), + ("Escape Sequence", "Character flees captivity/danger"), + ], + "emotional": [ + ("The Confession", "Character admits truth - love, crime, secret"), + ("Deathbed Scene", "Dying character's final words/wishes"), + ("The Reunion", "Long-separated characters meet again"), + ("Breaking the News", "Character learns devastating information"), + ("The Sacrifice", "Character gives up something precious for others"), + ("Saying Goodbye", "Characters part ways, possibly forever"), + ("The Breakdown", "Character's composure finally cracks"), + ("Reconciliation", "Estranged characters make peace"), + ("The Proposal", "Marriage or significant commitment offered"), + ("Visiting the Grave", "Character speaks to deceased at burial site"), + ], + "discovery": [ + ("Finding the Body", "Character discovers corpse"), + ("The Evidence Room", "Character finds proof of conspiracy/truth"), + ("Flashback Reveal", "Past event recontextualizes everything"), + ("The Twist Revealed", "Major plot revelation changes understanding"), + ("Reading the Letter/Document", "Written words deliver crucial info"), + ("Surveillance Footage", "Character watches recording of key event"), + ("The Photograph", "Image reveals connection or identity"), + ("Decoding the Message", "Cipher/puzzle solved, meaning clear"), + ], + "social": [ + ("The Dinner Party", "Tension beneath polite social gathering"), + ("The Job Interview", "Character proves worth or fails to"), + ("Meeting the Parents", "Romantic partner meets family"), + ("The Courtroom", "Legal proceedings, testimony, verdict"), + ("Press Conference", "Public statement, reporters' questions"), + ("The Gala/Ball", "Formal event, everyone dressed up, intrigue"), + ("The Wake/Reception", "Social gathering after significant event"), + ], + "endings": [ + ("Walking Into the Sunset", "Hero departs, back to camera, horizon ahead"), + ("The Circular Return", "Final scene mirrors opening, showing change"), + ("Freeze Frame", "Action stops, often with voiceover or text"), + ("The Bittersweet Victory", "Won but at great cost, hollow celebration"), + ("New Dawn", "Sun rises on changed world/character, hope restored"), + ("The Cliffhanger", "Unresolved tension, question left hanging"), + ("Where Are They Now", "Text/montage showing characters' futures"), + ("The Twist Ending", "Final revelation reframes entire story"), + ("Full Circle Reunion", "Characters gather one last time"), + ("The Long Walk Away", "Character leaves location, camera lingers"), + ("Passing the Torch", "Legacy/knowledge transferred to next generation"), + ("The Final Confrontation", "Last face-to-face with antagonist"), + ("Quiet Moment After Storm", "Calm after climax, processing events"), + ("The Sacrifice Payoff", "Earlier sacrifice proven worthwhile"), + ("Open Road/New Beginning", "Character sets off on next journey"), + ], +} + + +def generate_scenario_sequence(count, include_opening, include_tension, include_action, + include_emotional, include_discovery, include_social, include_ending, + force_opening, force_ending): + """Generate a random sequence of film scenarios. + + Args: + count: Number of scenarios to generate + include_opening: Include opening scenarios + include_tension: Include tension scenarios + include_action: Include action scenarios + include_emotional: Include emotional scenarios + include_discovery: Include discovery scenarios + include_social: Include social scenarios + include_ending: Include ending scenarios + force_opening: Force sequence to start with an opening + force_ending: Force sequence to end with an ending + + Returns: + Tuple of (list_output, json_output, prompts_output) + """ + available = [] + if include_opening: + available.extend([(s[0], s[1], "opening") for s in FILM_SCENARIOS["openings"]]) + if include_tension: + available.extend([(s[0], s[1], "tension") for s in FILM_SCENARIOS["tension"]]) + if include_action: + available.extend([(s[0], s[1], "action") for s in FILM_SCENARIOS["action"]]) + if include_emotional: + available.extend([(s[0], s[1], "emotional") for s in FILM_SCENARIOS["emotional"]]) + if include_discovery: + available.extend([(s[0], s[1], "discovery") for s in FILM_SCENARIOS["discovery"]]) + if include_social: + available.extend([(s[0], s[1], "social") for s in FILM_SCENARIOS["social"]]) + if include_ending: + available.extend([(s[0], s[1], "ending") for s in FILM_SCENARIOS["endings"]]) + + if not available: + return "Select at least one category!", "{}", "" + + sequence = [] + + # Force opening if requested + if force_opening and include_opening: + opening = random.choice(FILM_SCENARIOS["openings"]) + sequence.append((opening[0], opening[1], "opening")) + count -= 1 + + # Force ending if requested + ending_scene = None + if force_ending and include_ending: + ending_scene = random.choice(FILM_SCENARIOS["endings"]) + count -= 1 + + # Fill middle with random selections + middle_available = [s for s in available if s[2] not in (["opening"] if force_opening else []) + and s[2] not in (["ending"] if force_ending else [])] + if not middle_available: + middle_available = available + + random.shuffle(middle_available) + sequence.extend(middle_available[:max(0, count)]) + + # Add ending at the end + if ending_scene: + sequence.append((ending_scene[0], ending_scene[1], "ending")) + + # Format as list + list_output = "## Generated Sequence\n\n" + for i, (name, desc, cat) in enumerate(sequence, 1): + list_output += f"**{i}. {name}** [{cat}]\n{desc}\n\n" + + # Format as config JSON (using correct engine format) + config = {"story_location": {}} + for i, (name, desc, cat) in enumerate(sequence, 1): + state_name = name.lower().replace(" ", "_").replace("/", "_").replace("-", "_") + current_state_id = f"scene_{i}_{state_name}" + + # Determine next state + if i < len(sequence): + next_name = sequence[i][0] # Get next scene's name + next_state_name = next_name.lower().replace(" ", "_").replace("/", "_").replace("-", "_") + next_state_id = f"scene_{i+1}_{next_state_name}" + else: + next_state_id = "ending" + + # Use proper format: choices as list of strings, transitions as dict + config["story_location"][current_state_id] = { + "description": f"[{cat.upper()}] {desc}", + "media_prompt": f"Cinematic shot: {name} - {desc}", + "choices": ["Continue"], + "transitions": { + "Continue": next_state_id + } + } + + # Add ending state + first_state_name = sequence[0][0].lower().replace(" ", "_").replace("/", "_").replace("-", "_") + config["story_location"]["ending"] = { + "description": "The story concludes here.", + "choices": ["Start Over"], + "transitions": { + "Start Over": f"scene_1_{first_state_name}" + } + } + + json_output = json.dumps(config, indent=2) + + # Format prompts only (for queue) + prompts_output = "## Video Prompts (copy to queue)\n\n" + for i, (name, desc, cat) in enumerate(sequence, 1): + prompts_output += f"Cinematic shot: {name} - {desc}\n" + + return list_output, json_output, prompts_output diff --git a/game_configs.py b/game_configs.py new file mode 100644 index 0000000000000000000000000000000000000000..0f94b0f0fd73eb4dd31943415f34fd537a3f7c24 --- /dev/null +++ b/game_configs.py @@ -0,0 +1,3199 @@ +""" +Game configurations, states, and example configs +""" + +# Default configuration template +default_config = { + 'background': '/AutoGameBackgrounds/1stGameLoc123.png', + 'inventory': [], + 'skills': [], + 'objectives': [], + 'targets': [], + 'story': [], + 'actions': {} +} + +#-------------------------#-------------------------#-------------------------#------------------------- + +# List of player engagement UI elements +player_engagement_items = [ + "Health Bar", "Mana/Energy Bar", "Experience Bar", "Mini-Map", "Quest Tracker", + "Inventory Quick Access Slots", "Skill Cooldown Indicators", "Currency Display", + "Compass", "Timer/Clock", "Action Prompts", "Reticle/Crosshair", "Enemy Health Bars", + "Score Display", "Inventory Grid", "Item Tooltip", "Item Categories/Tabs", + "Equip/Unequip Buttons", "Item Sorting Options", "Character Stats Panel", + "Character Model Viewer", "Quick Equip Slots", "Dialogue Box", "Dialogue Choices", + "Character Portraits", "Character Name Display", "Subtitle Text", "World Map", + "Local Map", "Fast Travel Points", "Map Markers", "Map Legend", "Crafting Recipe List", + "Ingredient Slots", "Craft Button", "Crafting Progress Bar", "Skill Nodes", + "Skill Descriptions", "Skill Point Counter", "Unlock Button", "Skill Path Highlight", + "Quest List", "Quest Details Panel", "Quest Objectives", "Quest Rewards", + "Quest Status Indicators", "Chat Box", "Player List", "Voice Chat Indicators", + "Ping/Latency Indicator", "Party/Team UI", "Scoreboard", "Matchmaking Screen", + "Pop-up Notifications", "Achievement Unlocks", "System Messages", + "On-screen Key Prompts", "Environmental Interaction Icons", "Save/Auto-Save Indicators", + "Credits Screen" +] + +# List of character-based story events +story_events = [ + 'exploreLocation', 'discoverClue', 'meetCharacter', 'solveRiddle', 'findItem', + 'faceChallenge', 'makeDecision', 'engageBattle', 'unlockAbility', 'learnSecret', + 'completeMission', 'experienceSetback', 'earnReward', 'uncoverMystery', 'formAlliance', + 'faceBetrayal', 'confrontNemesis', 'makeDiscovery', 'overcomeLoss', 'achieveVictory' +] + + +#-------------------------#-------------------------#-------------------------#------------------------- + + +# Define the states +all_states = { + 'village': { + 'start': { + "description": "You wake up in a small village. You hear a rumor about a lost treasure.", + "choices": ['explore village', 'gather supplies', 'rest'], + "transitions": {'explore village': 'village_rumor', 'gather supplies': 'village_supplies', 'rest': 'village_start'}, + "consequences": { + 'gather supplies': lambda player: player.add_item('basic supplies') + }, + "media": [] + }, + 'rumor': { + "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", + "choices": ['decide to go', 'ignore'], + "transitions": {'decide to go': 'village_supplies', 'ignore': 'village_start'}, + "consequences": { + 'decide to go': lambda player: player.update_knowledge('treasure location') + }, + "media": [] + }, + 'supplies': { + "description": "You gather supplies for your journey.", + "choices": ['head to forest', 'stay in village'], + "transitions": {'head to forest': 'forest_forest', 'stay in village': 'village_start'}, + "media": [] + }, + }, + 'forest': { + 'forest': { + "description": "You enter the dense forest, heading towards the ruins.", + "choices": ['travel further', 'return to village'], + "transitions": {'travel further': 'ruins_ruins', 'return to village': 'village_start'}, + "media": [] + }, + }, + 'ruins': { + 'ruins': { + "description": "You reach the ancient ruins. The entrance is dark and eerie.", + "choices": ['enter ruins', 'return to forest'], + "transitions": {'enter ruins': 'ruins_explore', 'return to forest': 'forest_forest'}, + "media": [] + }, + 'explore': { + "description": "You explore the ruins, encountering traps and puzzles.", + "choices": ['solve puzzle', 'avoid traps'], + "transitions": {'solve puzzle': 'ruins_hiddenPassage', 'avoid traps': 'ruins_ruins'}, + "media": [] + }, + 'hiddenPassage': { + "description": "You solve a challenging puzzle and unlock a hidden passage.", + "choices": ['enter passage', 'go back'], + "transitions": {'enter passage': 'ruins_treasureRoom', 'go back': 'ruins_explore'}, + "media": [] + }, + 'treasureRoom': { + "description": "You enter the treasure room and find the treasure chest.", + "choices": ['take treasure', 'leave'], + "transitions": {'take treasure': 'ruins_celebrate', 'leave': 'ruins_ruins'}, + "consequences": { + 'take treasure': lambda player: player.add_item('treasure') + }, + "media": [] + }, + 'celebrate': { + "description": "You celebrate your discovery and decide to bring the treasure back to the village.", + "choices": ['return to village'], + "transitions": {'return to village': 'village_return_return'}, + "media": [] + }, + }, + 'village_return': { + 'return': { + "description": "You return to the village with the treasure and share it with the villagers.", + "choices": ['end adventure'], + "transitions": {'end adventure': 'end_end'}, + "media": [] + }, + }, + 'end': { + 'end': { + "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", + "choices": [], + "transitions": {}, + "media": [] + }, + } +} + +#-------------------------#-------------------------#-------------------------#------------------------- + +#actual is not screen ready + +originalconfigatbeinningofthisspace = """{ + "village": { + "start": { + "description": "You wake up in a small village. You hear a rumor about a lost treasure.", + "choices": ['explore village', 'gather supplies', 'rest'], + "transitions": {'explore village': 'village_rumor', 'gather supplies': 'village_supplies', 'rest': 'village_start'}, + "consequences": { + 'gather supplies': lambda player: player.add_item('basic supplies') + }, + "media": [] + }, + "rumor": { + "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", + "choices": ['decide to go', 'ignore'], + "transitions": {'decide to go': 'village_supplies', 'ignore': 'village_start'}, + "consequences": { + 'decide to go': lambda player: player.update_knowledge('treasure location') + }, + "media": [] + }, + "supplies": { + "description": "You gather supplies for your journey.", + "choices": ['head to forest', 'stay in village'], + "transitions": {'head to forest': 'forest_forest', 'stay in village': 'village_start'}, + "media": [] + }, + }, + "forest": { + "forest": { + "description": "You enter the dense forest, heading towards the ruins.", + "choices": ['travel further', 'return to village'], + "transitions": {'travel further': 'ruins_ruins', 'return to village': 'village_start'}, + "media": [] + }, + }, + "ruins": { + "ruins": { + "description": "You reach the ancient ruins. The entrance is dark and eerie.", + "choices": ['enter ruins', 'return to forest'], + "transitions": {'enter ruins': 'ruins_explore', 'return to forest': 'forest_forest'}, + "media": [] + }, + "explore": { + "description": "You explore the ruins, encountering traps and puzzles.", + "choices": ['solve puzzle', 'avoid traps'], + "transitions": {'solve puzzle': 'ruins_hiddenPassage', 'avoid traps': 'ruins_ruins'}, + "media": [] + }, + "hiddenPassage": { + "description": "You solve a challenging puzzle and unlock a hidden passage.", + "choices": ['enter passage', 'go back'], + "transitions": {'enter passage': 'ruins_treasureRoom', 'go back': 'ruins_explore'}, + "media": [] + }, + "treasureRoom": { + "description": "You enter the treasure room and find the treasure chest.", + "choices": ['take treasure', 'leave'], + "transitions": {'take treasure': 'ruins_celebrate', 'leave': 'ruins_ruins'}, + "consequences": { + 'take treasure': lambda player: player.add_item('treasure') + }, + "media": [] + }, + "celebrate": { + "description": "You celebrate your discovery and decide to bring the treasure back to the village.", + "choices": ['return to village'], + "transitions": {'return to village': 'village_return_return'}, + "media": [] + }, + }, + "village_return": { + "return": { + "description": "You return to the village with the treasure and share it with the villagers.", + "choices": ['end adventure'], + "transitions": {'end adventure': 'end_end'}, + "media": [] + }, + }, + "end": { + "end": { + "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", + "choices": [], + "transitions": {}, + "media": [] + }, + } +}""" + #Analyzing Game Configuration Metrics - was the thread name of where the +claude3_5_06072024configtips = """Example Config Development Tab - Claude 3.5 Sonnet as the LLM Assitant + +This config is for a basic text based game engine I made with help many llms including you. +Thanks to all the help it loads which presented another problem, I dont have any structural metrics to assess the quality of the config. +What JSON things can we look at to see if it may be too bland for a person testing the game + + +Text-Based Game Configuration Analysis +1. State Variety + +Total number of states: 13 +Number of unique locations: 5 (village, forest, ruins, village_return, end) + +2. Choice Diversity + +Average choices per state: 1.92 +States with no choices: 1 (end state) +States with most choices: 3 (village_start with 3 choices) +Unique choices across all states: 17 + +3. Transition Complexity + +Total number of transitions: 25 +Average transitions per state: 1.92 +States with most transitions: 3 (village_start with 3 transitions) + +4. Consequence Implementation + +States with consequences: 3 +Total number of consequences: 3 +Types of consequences: Adding items, Updating knowledge + +5. Description Length + +Average description length: 13.15 words +Longest description: 24 words (village_return) +Shortest description: 4 words (village_supplies) + +6. Cyclic Paths + +Presence of loops: Yes (e.g., village_start -> village_supplies -> village_start) + +7. Dead Ends + +Number of dead ends: 1 (end state) + +8. Media Integration + +States with media: 0 +Total media elements: 0 + +9. Narrative Progression + +Clear beginning (village_start) and end (end) states +Multiple paths to reach the end + +10. Player Agency + +Choices affect game progression +Some choices have consequences (3 instances) + +11. Game Length + +Minimum steps to complete: 6 (village_start -> village_supplies -> forest_forest -> ruins_ruins -> ruins_explore -> ruins_hiddenPassage -> ruins_treasureRoom -> ruins_celebrate -> village_return -> end) +Maximum steps (including loops): Potentially infinite due to cycles + +12. Replayability + +Multiple paths through the game +Some variability in outcomes based on choices + +#============================================= + +To assess the quality and potential blandness of your game configuration, we can analyze several structural metrics within the JSON. Here are some aspects we can examine: +Text-Based Game Configuration AnalysisClick to open document +Based on this analysis, here are some observations about the potential blandness of the game: + +Limited state variety: With only 13 states across 5 locations, the game might feel short and lacking in diversity. Consider adding more locations or states within existing locations. +Low average choices: An average of 1.92 choices per state might make the game feel linear. Increasing the number of choices in more states could improve player engagement. +Limited consequences: Only 3 states have consequences, which might make player choices feel less impactful. Adding more consequences could increase the sense of agency. +Short descriptions: The average description length of 13.15 words might not provide enough detail to immerse players. Consider expanding descriptions to create a richer narrative. +Lack of media: No states currently use media elements, which could make the game feel less engaging. Adding images, sound effects, or other media could enhance the player experience. +Limited narrative branching: While there are some loops and choices, the overall structure is relatively linear. Adding more branching paths could increase replayability and player interest. + +To make the game less bland, consider: + +Adding more states and locations +Increasing the number of choices in each state +Implementing more consequences for player actions +Expanding descriptions to create a richer narrative +Incorporating media elements +Creating more diverse paths through the game + +#=============================================== + + +# Text-Based Game Improvement Suggestions + +## 1. Expand State Variety +- Add a new location: "Mountain Pass" + - Include states like "steep_climb", "avalanche_risk", "hidden_cave" +- Expand the "forest" location: + - Add states like "dense_thicket", "mysterious_clearing", "ancient_tree" + +## 2. Increase Choice Diversity +- Add more choices to existing states. For example, in "village_start": + - "talk to elder" + - "visit market" + - "investigate strange noises" +- Ensure most states have at least 3 choices + +## 3. Enhance Transition Complexity +- Create more interconnected states. For example: + - Allow travel between "forest" and "mountain_pass" + - Add shortcuts or secret passages between non-adjacent locations + +## 4. Implement More Consequences +- Add consequences to more choices. For example: + - In "forest_forest": + - "forage for food" -> `lambda player: player.add_item('wild berries')` + - "take shortcut" -> `lambda player: player.update_status('lost')` + +## 5. Enrich Descriptions +- Expand existing descriptions with more vivid details +- Example for "village_start": + ```python + "description": "You wake up in a small village nestled between rolling hills. The air is filled with the scent of freshly baked bread and the sound of a distant waterfall. Villagers bustle about, whispering excitedly about a rumor of lost treasure." + ``` + +## 6. Introduce Cyclic Content with Variations +- Add time-based changes to revisited locations +- Example: + ```python + 'village_start': { + "description": lambda player: f"You return to the village square. It's now {player.time_of_day()}. " + + ("The market is bustling with activity." if player.time_of_day() == "day" else + "The streets are quiet, lit by flickering torches.") + } + ``` + +## 7. Create Meaningful Dead Ends +- Add optional quests or side adventures +- Example: "help_lost_child" state in the village that doesn't progress main story but provides rewards + +## 8. Integrate Media Elements +- Add placeholder media to enhance immersion: + ```python + "media": [ + {"type": "image", "url": "/api/placeholder/400/300", "alt": "A picturesque village at dawn"}, + {"type": "sound", "url": "village_ambience.mp3"} + ] + ``` + +## 9. Enhance Narrative Progression +- Add a main quest storyline with multiple stages +- Introduce side quests for additional depth + +## 10. Increase Player Agency +- Add more impactful choices that significantly alter the game's direction +- Implement a simple inventory or skill system that affects available choices + +## 11. Extend Game Length +- Add more locations and states to create a longer, more involved adventure +- Implement quest chains that span multiple locations + +## 12. Improve Replayability +- Add random events that can occur in each location +- Implement multiple endings based on player choices and achievements + + +#=============================================== + +# Updated Text-Based Game Configuration with Side Quest + +## New State: help_lost_child + +Add this new state to the 'village' section of your `all_states` dictionary: + +```python +'village': { + # ... existing states ... + + 'help_lost_child': { + "description": "You encounter a crying child in the village square. They seem to have lost their way home.", + "choices": ['help child', 'ignore and continue'], + "transitions": { + 'help child': 'village_child_quest', + 'ignore and continue': 'village_start' + }, + "consequences": { + 'help child': lambda player: player.update_status('helping_child') + }, + "media": [] + }, + 'child_quest': { + "description": "You decide to help the lost child. After asking around and searching the village, you finally locate their home.", + "choices": ['return child', 'ask for reward'], + "transitions": { + 'return child': 'village_child_reunion', + 'ask for reward': 'village_child_reward' + }, + "media": [] + }, + 'child_reunion': { + "description": "You return the child to their grateful parents. They thank you profusely for your kindness.", + "choices": ['accept thanks', 'request reward'], + "transitions": { + 'accept thanks': 'village_start', + 'request reward': 'village_child_reward' + }, + "consequences": { + 'accept thanks': lambda player: player.update_status('local_hero') + }, + "media": [] + }, + 'child_reward': { + "description": "The child's parents offer you a small pouch of coins as a reward for your help.", + "choices": ['accept reward', 'decline reward'], + "transitions": { + 'accept reward': 'village_start', + 'decline reward': 'village_start' + }, + "consequences": { + 'accept reward': lambda player: player.add_item('coin pouch'), + 'decline reward': lambda player: player.update_status('selfless_hero') + }, + "media": [] + }, +} +``` + +## Updated Transitions + +To incorporate this side quest into the existing game flow, we need to update some transitions in the 'village_start' state: + +```python +'village': { + 'start': { + "description": "You're in the village square. The usual bustle of village life surrounds you.", + "choices": ['explore village', 'gather supplies', 'rest', 'look around'], # Added 'look around' + "transitions": { + 'explore village': 'village_rumor', + 'gather supplies': 'village_supplies', + 'rest': 'village_start', + 'look around': 'village_help_lost_child' # New transition + }, + "consequences": { + 'gather supplies': lambda player: player.add_item('basic supplies') + }, + "media": [] + }, + # ... other states ... +} +``` + +## Explanation of Changes + +1. New States: + - `help_lost_child`: Introduces the side quest + - `child_quest`: Represents the process of helping the child + - `child_reunion`: Concludes the quest if you don't ask for a reward + - `child_reward`: Offers a choice to accept or decline a reward + +2. Updated `village_start`: + - Added a new choice: 'look around' + - Added a new transition to `village_help_lost_child` + +3. Consequences: + - Added status updates: 'helping_child', 'local_hero', 'selfless_hero' + - Added item reward: 'coin pouch' + +These changes create a meaningful dead end by: +1. Providing an optional quest that doesn't directly progress the main treasure hunt story +2. Offering multiple choices within the side quest, allowing for different outcomes +3. Implementing consequences that affect the player's status or inventory, providing rewards for engagement +4. Allowing the player to return to the main story flow after completing (or ignoring) the side quest + +This side quest adds depth to the village location, gives the player more to do, and provides character-building opportunities without derailing the main narrative. It also demonstrates how you can implement similar side quests or meaningful dead ends in other locations of your game. + +#================================================================================ + +Certainly! Let's implement the "help_lost_child" side quest as a meaningful dead end in the village. This will add depth to the game without necessarily progressing the main story. Here's how we can incorporate this into your current configuration: +Updated Text-Based Game Configuration with Side QuestClick to open document +To further improve this side quest and make it even more engaging, you could consider: + +Time sensitivity: Make the quest available only at certain times or for a limited number of turns. +Skill checks: If your game has a skill system, you could add checks for persuasion or perception that affect the outcome. +Long-term consequences: The status changes (like 'local_hero') could affect how NPCs interact with the player later in the game. +Branching outcomes: You could add more complex branches, such as the child being in actual danger, leading to a mini-adventure. + +Would you like me to elaborate on any of these ideas or show how to implement them in your configuration? + +""" + + +tipsupdatedconfigatbeinningofthisspace = """{ + "village": { + "start": { + "description": "You wake up in a small village. You hear a rumor about a lost treasure.", + "choices": ['explore village', 'gather supplies', 'rest', 'look around'], + "transitions": {'explore village': 'village_rumor', 'gather supplies': 'village_supplies', 'rest': 'village_start', 'look around': 'village_help_lost_child'}, + "consequences": { + 'gather supplies': lambda player: player.add_item('basic supplies') + }, + "media": [] + }, + "rumor": { + "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", + "choices": ['decide to go', 'ignore'], + "transitions": {'decide to go': 'village_supplies', 'ignore': 'village_start'}, + "consequences": { + 'decide to go': lambda player: player.update_knowledge('treasure location') + }, + "media": [] + }, + "supplies": { + "description": "You gather supplies for your journey.", + "choices": ['head to forest', 'stay in village'], + "transitions": {'head to forest': 'forest_forest', 'stay in village': 'village_start'}, + "media": [] + }, + "help_lost_child": { + "description": "You encounter a crying child in the village square. They seem to have lost their way home.", + "choices": ['help child', 'ignore and continue'], + "transitions": { + 'help child': 'village_child_quest', + 'ignore and continue': 'village_start' + }, + "consequences": { + 'help child': lambda player: player.update_status('helping_child') + }, + "media": [] + }, + "child_quest": { + "description": "You decide to help the lost child. After asking around and searching the village, you finally locate their home.", + "choices": ['return child', 'ask for reward'], + "transitions": { + 'return child': 'village_child_reunion', + 'ask for reward': 'village_child_reward' + }, + "media": [] + }, + "child_reunion": { + "description": "You return the child to their grateful parents. They thank you profusely for your kindness.", + "choices": ['accept thanks', 'request reward'], + "transitions": { + 'accept thanks': 'village_start', + 'request reward': 'village_child_reward' + }, + "consequences": { + 'accept thanks': lambda player: player.update_status('local_hero') + }, + "media": [] + }, + "child_reward": { + "description": "The child's parents offer you a small pouch of coins as a reward for your help.", + "choices": ['accept reward', 'decline reward'], + "transitions": { + 'accept reward': 'village_start', + 'decline reward': 'village_start' + }, + "consequences": { + 'accept reward': lambda player: player.add_item('coin pouch'), + 'decline reward': lambda player: player.update_status('selfless_hero') + }, + "media": [] + }, + }, + "forest": { + "forest": { + "description": "You enter the dense forest, heading towards the ruins.", + "choices": ['travel further', 'return to village'], + "transitions": {'travel further': 'ruins_ruins', 'return to village': 'village_start'}, + "media": [] + }, + }, + "ruins": { + "ruins": { + "description": "You reach the ancient ruins. The entrance is dark and eerie.", + "choices": ['enter ruins', 'return to forest'], + "transitions": {'enter ruins': 'ruins_explore', 'return to forest': 'forest_forest'}, + "media": [] + }, + "explore": { + "description": "You explore the ruins, encountering traps and puzzles.", + "choices": ['solve puzzle', 'avoid traps'], + "transitions": {'solve puzzle': 'ruins_hiddenPassage', 'avoid traps': 'ruins_ruins'}, + "media": [] + }, + "hiddenPassage": { + "description": "You solve a challenging puzzle and unlock a hidden passage.", + "choices": ['enter passage', 'go back'], + "transitions": {'enter passage': 'ruins_treasureRoom', 'go back': 'ruins_explore'}, + "media": [] + }, + "treasureRoom": { + "description": "You enter the treasure room and find the treasure chest.", + "choices": ['take treasure', 'leave'], + "transitions": {'take treasure': 'ruins_celebrate', 'leave': 'ruins_ruins'}, + "consequences": { + 'take treasure': lambda player: player.add_item('treasure') + }, + "media": [] + }, + 'celebrate': { + "description": "You celebrate your discovery and decide to bring the treasure back to the village.", + "choices": ['return to village'], + "transitions": {'return to village': 'village_return_return'}, + "media": [] + }, + }, + "village_return": { + "return": { + "description": "You return to the village with the treasure and share it with the villagers.", + "choices": ['end adventure'], + "transitions": {'end adventure': 'end_end'}, + "media": [] + }, + }, + "end": { + "end": { + "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", + "choices": [], + "transitions": {}, + "media": [] + }, + } +}""" + +#-------------------------#-------------------------#-------------------------#------------------------- + + +ExampleGameConfigs = { + "Racinggametest": { + "pitLane": { + "start": { + "description": "You're in the pit lane of a famous racing circuit. There's buzz about a legendary race coming up.", + "choices": [ + "check car", + "talk to crew", + "rest" + ], + "transitions": { + "check car": "pitLane_carPrep", + "talk to crew": "pitLane_crewInfo", + "rest": "pitLane_start" + }, + "consequences": { + "check car": {} + }, + "media": [] + }, + "crewInfo": { + "description": "Your crew tells you about the challenging track and the fierce competition.", + "choices": [ + "start preparations", + "ignore advice" + ], + "transitions": { + "start preparations": "pitLane_carPrep", + "ignore advice": "pitLane_start" + }, + "consequences": { + "start preparations": {} + }, + "media": [] + }, + "carPrep": { + "description": "You and your crew prepare the car for the race.", + "choices": [ + "head to grid", + "more tuning" + ], + "transitions": { + "head to grid": "track_startingGrid", + "more tuning": "pitLane_start" + }, + "media": [] + } + }, + "track": { + "startingGrid": { + "description": "You're on the starting grid. The atmosphere is tense as drivers prepare for the race.", + "choices": [ + "start race", + "return to pit" + ], + "transitions": { + "start race": "track_firstLap", + "return to pit": "pitLane_start" + }, + "media": [] + }, + "firstLap": { + "description": "The race begins! You navigate through the first lap, jostling for position.", + "choices": [ + "overtake", + "maintain position" + ], + "transitions": { + "overtake": "track_midRace", + "maintain position": "track_midRace" + }, + "media": [] + }, + "midRace": { + "description": "You're in the middle of the race, facing challenges and making split-second decisions.", + "choices": [ + "push harder", + "conserve tires" + ], + "transitions": { + "push harder": "track_finalLaps", + "conserve tires": "track_finalLaps" + }, + "media": [] + }, + "finalLaps": { + "description": "The final laps approach. Victory is within reach, but so is the risk of a mistake.", + "choices": [ + "give it all", + "play it safe" + ], + "transitions": { + "give it all": "track_finishLine", + "play it safe": "track_finishLine" + }, + "consequences": { + "give it all": {} + }, + "media": [] + }, + "finishLine": { + "description": "You cross the finish line! The results are in.", + "choices": [ + "check results", + "return to pit" + ], + "transitions": { + "check results": "podium_ceremony", + "return to pit": "pitLane_start" + }, + "media": [] + } + }, + "podium": { + "ceremony": { + "description": "The podium ceremony begins. Your performance is recognized.", + "choices": [ + "celebrate", + "prepare for next race" + ], + "transitions": { + "celebrate": "end_celebration", + "prepare for next race": "pitLane_start" + }, + "consequences": { + "celebrate": {} + }, + "media": [] + } + }, + "end": { + "celebration": { + "description": "You celebrate your achievement. Your team and fans are overjoyed.", + "choices": [ + "end race day" + ], + "transitions": { + "end race day": "end_end" + }, + "media": [] + }, + "end": { + "description": "Your racing day ends here. Your performance has impacted your standing in the championship.", + "choices": [], + "transitions": {}, + "media": [] + } + } + }, + + "Eventplanningstortytest": { + "tavern": { + "start": { + "description": "The air crackles with anticipation as you step into the bustling event planning expo. Caterers offer tantalizing samples, florists boast breathtaking arrangements, and a charismatic DJ promises an unforgettable party atmosphere.", + "choices": ["approach the DJ", "sample the catering", "admire the floral displays"], + "transitions": { + "approach the DJ": "tavern_rumor", + "sample the catering": "tavern_grog", + "admire the floral displays": "tavern_start" + }, + "consequences": {}, + "media": [] + }, + "rumor": { + "description": "The DJ, a master of hype, spins tales of legendary events he's orchestrated. He mentions a client, a mysterious socialite, planning an extravagant masked ball with a budget that could make your career.", + "choices": ["express your interest", "dismiss him as a showman"], + "transitions": { + "express your interest": "tavern_map", + "dismiss him as a showman": "tavern_start" + }, + "consequences": {}, + "media": [] + }, + "grog": { + "description": "You savor the exquisite flavors of miniature gourmet dishes. The caterer, a true artist, describes their vision for a multi-sensory dining experience.", + "choices": ["inquire about their rates", "network with other attendees"], + "transitions": { + "inquire about their rates": "tavern_grog", + "network with other attendees": "tavern_start" + }, + "media": [] + }, + "map": { + "description": "The DJ, sensing your ambition, offers a tantalizing clue: the socialite is looking for a fresh perspective. He suggests you submit a proposal, highlighting your unique vision.", + "choices": ["propose a themed experience", "focus on logistics and budget"], + "transitions": { + "propose a themed experience": "docks_prepare", + "focus on logistics and budget": "tavern_haggle" + }, + "consequences": {}, + "media": [] + }, + "haggle": { + "description": "The DJ warns you that the socialite is notoriously demanding. \"She expects perfection,\" he cautions.", + "choices": ["assure him you deliver excellence", "reconsider your options"], + "transitions": { + "assure him you deliver excellence": "docks_prepare", + "reconsider your options": "tavern_start" + }, + "consequences": {}, + "media": [] + }, + "fight": { + "description": "You overhear another event planner bragging about securing the masked ball contract. Jealousy burns, and you're tempted to challenge their claim.", + "choices": ["confront the planner", "stay focused on your goals"], + "transitions": { + "confront the planner": "tavern_victory", + "stay focused on your goals": "tavern_defeat" + }, + "consequences": {}, + "media": [] + }, + "victory": { + "description": "You confront the planner, exposing their lies. The truth prevails, and word of your integrity reaches the socialite.", + "choices": ["prepare your proposal"], + "transitions": { + "prepare your proposal": "docks_prepare" + }, + "consequences": {}, + "media": [] + }, + "defeat": { + "description": "You decide not to stoop to their level. Trusting your instincts, you continue networking and refining your pitch.", + "choices": ["continue networking"], + "transitions": { + "continue networking": "tavern_start" + }, + "consequences": {}, + "media": [] + } + }, + "docks": { + "prepare": { + "description": "Armed with a killer concept, you retreat to your studio to craft the perfect proposal. It's time to gather your team: the best florists, caterers, entertainers...", + "choices": ["contact your florist", "secure a caterer"], + "transitions": { + "contact your florist": "docks_ship", + "secure a caterer": "docks_crew" + }, + "media": [] + }, + "ship": { + "description": "Your trusted florist presents a stunning array of options, from elegant orchids to whimsical fairy lights. You envision transforming the venue into a magical landscape.", + "choices": ["finalize the floral arrangements", "explore additional decor"], + "transitions": { + "finalize the floral arrangements": "docks_captain", + "explore additional decor": "docks_prepare" + }, + "media": [] + }, + "crew": { + "description": "You browse through portfolios of caterers, each offering a unique culinary experience. The decision is crucial: the food must be as unforgettable as the event itself.", + "choices": ["choose a gourmet caterer", "opt for a themed buffet"], + "transitions": { + "choose a gourmet caterer": "docks_captain", + "opt for a themed buffet": "docks_captain" + }, + "consequences": {}, + "media": [] + }, + "captain": { + "description": "With each element falling into place, your vision for the masked ball comes alive. The proposal is complete - a masterpiece of creativity and meticulous planning.", + "choices": ["submit your proposal"], + "transitions": { + "submit your proposal": "openSea_start" + }, + "media": [] + } + }, + "openSea": { + "start": { + "description": "Days turn into nights as you await a response. The uncertainty is agonizing, your hopes rising and falling with every ring of the phone.", + "choices": ["check your emails obsessively", "distract yourself with another project"], + "transitions": { + "check your emails obsessively": "openSea_storm", + "distract yourself with another project": "openSea_mutiny" + }, + "media": [] + }, + "storm": { + "description": "A wave of despair crashes over you as you receive a generic rejection email. Your carefully crafted proposal, your dreams... dashed.", + "choices": ["analyze the email for feedback", "move on to the next opportunity"], + "transitions": { + "analyze the email for feedback": "openSea_ghostShip", + "move on to the next opportunity": "openSea_start" + }, + "media": [] + }, + "mutiny": { + "description": "Your phone vibrates - an unknown number. Your heart races as you answer. It's the socialite's assistant, and they're impressed.", + "choices": ["accept their invitation to meet", "negotiate your terms"], + "transitions": { + "accept their invitation to meet": "openSea_ghostShip", + "negotiate your terms": "openSea_start" + }, + "consequences": {}, + "media": [] + }, + "ghostShip": { + "description": "You meet the socialite - an enigmatic figure radiating elegance and power. They're intrigued by your vision, your passion.", + "choices": ["present your ideas in detail", "listen to their expectations"], + "transitions": { + "present your ideas in detail": "ghostShip_deck", + "listen to their expectations": "openSea_start" + }, + "media": [] + } + }, + "ghostShip": { + "deck": { + "description": "The socialite listens intently as you unveil your plans, their eyes sparkling with approval. They love your innovative concepts, your attention to detail.", + "choices": ["discuss the budget and timeline", "negotiate your fee"], + "transitions": { + "discuss the budget and timeline": "ghostShip_brig", + "negotiate your fee": "ghostShip_quarters" + }, + "media": [] + }, + "brig": { + "description": "Negotiations are swift and decisive. The socialite appreciates your professionalism and agrees to your terms. The masked ball is yours to create!", + "choices": ["celebrate your success", "start planning immediately"], + "transitions": { + "celebrate your success": "ghostShip_clue", + "start planning immediately": "ghostShip_treasure" + }, + "media": [] + }, + "quarters": { + "description": "You secure a contract that exceeds your wildest dreams. The masked ball, a testament to your talent and dedication, will be your masterpiece.", + "choices": ["assemble your dream team", "order the first batch of champagne"], + "transitions": { + "assemble your dream team": "ghostShip_clue", + "order the first batch of champagne": "ghostShip_treasure" + }, + "media": [] + }, + "clue": { + "description": "Excitement courses through you as you share the good news with your team. The best in the business, they're ready to help you execute this extraordinary event.", + "choices": ["begin the preparations"], + "transitions": { + "begin the preparations": "ghostShip_treasure" + }, + "media": [] + }, + "treasure": { + "description": "Weeks of tireless work culminate in the grand opening of the masked ball. It's a triumph - a symphony of elegance, enchantment, and pure joy.", + "choices": ["bask in the glory of your success", "network with potential clients"], + "transitions": { + "bask in the glory of your success": "ghostShip_victory", + "network with potential clients": "openSea_return" + }, + "consequences": {}, + "media": [] + }, + "victory": { + "description": "The masked ball becomes legendary, exceeding all expectations. Your name is on everyone's lips, synonymous with unforgettable events.", + "choices": ["claim your place as an event icon"], + "transitions": { + "claim your place as an event icon": "openSea_return" + }, + "consequences": {}, + "media": [] + } + }, + "openSea_return": { + "return": { + "description": "Your star continues to rise as you orchestrate one successful event after another. Your creativity knows no bounds, your reputation solidified.", + "choices": ["embrace your destiny as an event maestro"], + "transitions": { + "embrace your destiny as an event maestro": "end_end" + }, + "media": [] + } + }, + "end": { + "end": { + "description": "From humble beginnings at an industry expo, you've become an icon, transforming dreams into dazzling reality. The world is your canvas, and every event is a masterpiece.", + "choices": [], + "transitions": {}, + "media": [] + } + } + }, + "TimelineStructurePlanningAttempt": { + "actualtimeline": { + "A1": { + "description": "You arrive at the Windfall Orchard. Tall, swaying fruit trees dance in the breeze, and birds chirp secrets you can’t quite decipher.", + "events": [ + "arrived" + ], + "choices": [ + "continue actual", + "explore orchard story (N3)", + "investigate orchard hidden (H1)" + ], + "transitions": { + "continue actual": "actualtimeline_A2", + "explore orchard story (N3)": "narrativetimeline_N3", + "investigate orchard hidden (H1)": "hiddentimeline_H1" + }, + "media": [], + "developernotes": [ + "Location 1 in Actual Timeline", + "Connects to Narrative N3 and Hidden H1" + ] + }, + "A2": { + "description": "You travel to the Misty Bazaar. Aromas of exotic spices fill the air as merchants barter, and you sense secrets around every corner.", + "events": [ + "traveled", + "observed" + ], + "choices": [ + "continue actual", + "uncover bazaar story (N1)", + "probe bazaar hidden (H2)" + ], + "transitions": { + "continue actual": "actualtimeline_A3", + "uncover bazaar story (N1)": "narrativetimeline_N1", + "probe bazaar hidden (H2)": "hiddentimeline_H2" + }, + "media": [], + "developernotes": [ + "Location 2 in Actual Timeline", + "Connects to Narrative N1 and Hidden H2" + ] + }, + "A3": { + "description": "You stand upon the Northern Cliffs. Sea winds roar beneath the clouds as distant waves crash, calling you to reflect on deeper mysteries.", + "events": [ + "arrived", + "reflected" + ], + "choices": [ + "continue actual", + "witness cliffside story (N4)", + "examine cliffside hidden (H3)" + ], + "transitions": { + "continue actual": "actualtimeline_A4", + "witness cliffside story (N4)": "narrativetimeline_N4", + "examine cliffside hidden (H3)": "hiddentimeline_H3" + }, + "media": [], + "developernotes": [ + "Location 3 in Actual Timeline", + "Connects to Narrative N4 and Hidden H3" + ] + }, + "A4": { + "description": "You approach the Heartwood Temple, feeling an otherworldly calm. The walls glow with faint runes that hint at ancient truths.", + "events": [ + "approached", + "pondered" + ], + "choices": [ + "loop actual", + "unearth temple story (N2)", + "search temple hidden (H4)" + ], + "transitions": { + "loop actual": "actualtimeline_A4", + "unearth temple story (N2)": "narrativetimeline_N2", + "search temple hidden (H4)": "hiddentimeline_H4" + }, + "media": [], + "developernotes": [ + "Location 4 in Actual Timeline", + "Connects to Narrative N2 and Hidden H4", + "Loops to itself for demonstration purposes" + ] + } + }, + "narrativetimeline": { + "N1": { + "description": "Narrative Event 1: In the bazaar, you hear a whispered rumor about a powerful relic hidden beyond the cliffs.", + "events": [ + "heard", + "questioned" + ], + "choices": [ + "continue narrative", + "return to actual A2", + "peek hidden (H2)" + ], + "transitions": { + "continue narrative": "narrativetimeline_N2", + "return to actual A2": "actualtimeline_A2", + "peek hidden (H2)": "hiddentimeline_H2" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A2" + ] + }, + "N2": { + "description": "Narrative Event 2: Within the temple, a monk reveals a crucial secret that ties orchard, bazaar, and cliffs together.", + "events": [ + "uncovered" + ], + "choices": [ + "continue narrative", + "return to actual A4", + "peek hidden (H4)" + ], + "transitions": { + "continue narrative": "narrativetimeline_N3", + "return to actual A4": "actualtimeline_A4", + "peek hidden (H4)": "hiddentimeline_H4" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A4" + ] + }, + "N3": { + "description": "Narrative Event 3: Back at the orchard, a forgotten memory surfaces—hinting all is connected by a hidden thread.", + "events": [ + "remembered" + ], + "choices": [ + "continue narrative", + "return to actual A1", + "peek hidden (H1)" + ], + "transitions": { + "continue narrative": "narrativetimeline_N4", + "return to actual A1": "actualtimeline_A1", + "peek hidden (H1)": "hiddentimeline_H1" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A1" + ] + }, + "N4": { + "description": "Narrative Event 4: At the cliffside, you discover the final piece of the story. Only by uniting all locations can true revelation be found.", + "events": [ + "realized" + ], + "choices": [ + "finish narrative", + "return to actual A3", + "peek hidden (H3)" + ], + "transitions": { + "finish narrative": "narrativetimeline_N4", + "return to actual A3": "actualtimeline_A3", + "peek hidden (H3)": "hiddentimeline_H3" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A3", + "Loops to itself for demonstration of an end state" + ] + } + }, + "hiddentimeline": { + "H1": { + "description": "Hidden Event 1: A hushed whisper in the orchard suggests all paths are part of a larger design.", + "events": [ + "listened" + ], + "choices": [ + "continue hidden", + "return to A1", + "narrative check (N3)" + ], + "transitions": { + "continue hidden": "hiddentimeline_H2", + "return to A1": "actualtimeline_A1", + "narrative check (N3)": "narrativetimeline_N3" + }, + "media": [], + "developernotes": [ + "Hidden event connected to orchard (A1) and story event (N3)" + ] + }, + "H2": { + "description": "Hidden Event 2: Faint footprints in the bazaar’s back alley reveal someone else is guiding your steps.", + "events": [ + "noticed" + ], + "choices": [ + "continue hidden", + "return to A2", + "narrative check (N1)" + ], + "transitions": { + "continue hidden": "hiddentimeline_H3", + "return to A2": "actualtimeline_A2", + "narrative check (N1)": "narrativetimeline_N1" + }, + "media": [], + "developernotes": [ + "Hidden event connected to bazaar (A2) and story event (N1)" + ] + }, + "H3": { + "description": "Hidden Event 3: You decipher ancient runes carved under the cliffs, learning that orchard, bazaar, and cliffs are intertwined.", + "events": [ + "deciphered" + ], + "choices": [ + "continue hidden", + "return to A3", + "narrative check (N4)" + ], + "transitions": { + "continue hidden": "hiddentimeline_H4", + "return to A3": "actualtimeline_A3", + "narrative check (N4)": "narrativetimeline_N4" + }, + "media": [], + "developernotes": [ + "Hidden event connected to cliffs (A3) and story event (N4)" + ] + }, + "H4": { + "description": "Hidden Event 4: In the temple’s deepest chamber, you comprehend the final revelation—everything converges at Heartwood.", + "events": [ + "comprehended" + ], + "choices": [ + "close hidden", + "return to A4", + "narrative check (N2)" + ], + "transitions": { + "close hidden": "hiddentimeline_H4", + "return to A4": "actualtimeline_A4", + "narrative check (N2)": "narrativetimeline_N2" + }, + "media": [], + "developernotes": [ + "Hidden event connected to temple (A4) and story event (N2)", + "Loops to itself for demonstration of an end state" + ] + } + } + }, + "MemoryFragments": { + "mind": { + "awakening": { + "description": "Your eyes open. White ceiling. Hospital? No... somewhere else. Your head throbs. Memories float just out of reach, fragmented and hazy. You sense four distinct moments trying to surface...", + "choices": [ + "focus on the burning smell", + "focus on the sound of waves", + "focus on the face in shadows", + "focus on the cold metal" + ], + "transitions": { + "focus on the burning smell": "memory_fire", + "focus on the sound of waves": "memory_boat", + "focus on the face in shadows": "memory_betrayal", + "focus on the cold metal": "memory_safe" + }, + "media": [] + }, + "hub": { + "description": "The memory fades, leaving you gasping. More fragments swirl in the darkness of your mind. What else happened that night?", + "choices": [ + "focus on the burning smell", + "focus on the sound of waves", + "focus on the face in shadows", + "focus on the cold metal", + "try to piece it all together", + "sink deeper into the darkness" + ], + "transitions": { + "focus on the burning smell": "memory_fire", + "focus on the sound of waves": "memory_boat", + "focus on the face in shadows": "memory_betrayal", + "focus on the cold metal": "memory_safe", + "try to piece it all together": "mind_convergence", + "sink deeper into the darkness": "deeper_nightmare" + }, + "media": [] + }, + "convergence": { + "description": "The fragments align. You remember now: The safe in Marcus's office. The documents proving the conspiracy. The boat waiting at the dock. Marcus's betrayal. The fire he set to cover his tracks. And you... you escaped with the evidence. It's still in your coat pocket.", + "choices": [ + "check your coat pocket", + "rest and recover more" + ], + "transitions": { + "check your coat pocket": "reality_evidence", + "rest and recover more": "mind_hub" + }, + "media": [] + } + }, + "memory": { + "fire": { + "description": "MEMORY: Flames lick the walls of the old warehouse. Smoke burns your lungs. You're running, papers clutched to your chest. Behind you, a voice shouts - angry, desperate. The heat is unbearable. You see a window, already cracked from the heat...", + "choices": [ + "remember jumping through", + "remember who was shouting", + "let the memory fade" + ], + "transitions": { + "remember jumping through": "memory_fire_detail", + "remember who was shouting": "memory_fire_voice", + "let the memory fade": "mind_hub" + }, + "media": [] + }, + "fire_detail": { + "description": "Glass shatters around you. You hit water - cold, shockingly cold. The canal. You surface, gasping, the papers still somehow in your grip. The warehouse is an inferno now. No one could survive inside...", + "choices": [ + "return to the present" + ], + "transitions": { + "return to the present": "mind_hub" + }, + "media": [] + }, + "fire_voice": { + "description": "The voice... it was Marcus. Your partner. Your friend. He was screaming for you to stop, to give back what you'd taken. But you knew what those documents meant. You couldn't let him destroy them.", + "choices": [ + "return to the present" + ], + "transitions": { + "return to the present": "mind_hub" + }, + "media": [] + }, + "boat": { + "description": "MEMORY: Salt air. The creak of ropes. A small boat rocks gently at a hidden dock. Someone helped you here - a woman with kind eyes and a captain's hat. 'You'll be safe,' she said. 'Sleep now.'", + "choices": [ + "remember her face clearly", + "remember what she said next", + "let the memory fade" + ], + "transitions": { + "remember her face clearly": "memory_boat_face", + "remember what she said next": "memory_boat_words", + "let the memory fade": "mind_hub" + }, + "media": [] + }, + "boat_face": { + "description": "Elena. Her name surfaces with her face. She's the journalist you contacted weeks ago. She believed you when no one else would. She arranged the boat, the safe house... she saved your life.", + "choices": [ + "return to the present" + ], + "transitions": { + "return to the present": "mind_hub" + }, + "media": [] + }, + "boat_words": { + "description": "'When you wake, everything will hurt. But you'll have the proof. That's all that matters now. The world needs to know what they did.' She pressed something into your hands - a phone. 'Call this number when you're ready.'", + "choices": [ + "return to the present" + ], + "transitions": { + "return to the present": "mind_hub" + }, + "media": [] + }, + "betrayal": { + "description": "MEMORY: Marcus's office. Late at night. You weren't supposed to be there. The safe was open - he'd forgotten to close it. Inside: documents, transfers, names. A conspiracy bigger than you imagined. And Marcus... at the center of it all.", + "choices": [ + "remember taking the documents", + "remember Marcus finding you", + "let the memory fade" + ], + "transitions": { + "remember taking the documents": "memory_betrayal_theft", + "remember Marcus finding you": "memory_betrayal_caught", + "let the memory fade": "mind_hub" + }, + "media": [] + }, + "betrayal_theft": { + "description": "Your hands shook as you photographed each page, then took the originals. Years of friendship, partnership, trust - all built on lies. He'd been using the company to launder money for people who destroyed lives. And you'd signed off on it, unknowing.", + "choices": [ + "return to the present" + ], + "transitions": { + "return to the present": "mind_hub" + }, + "media": [] + }, + "betrayal_caught": { + "description": "The light flicked on. Marcus stood in the doorway, his face cycling through surprise, fear, and finally cold fury. 'I wish you hadn't seen that,' he said, reaching for his phone. You ran. He chased. The warehouse. The fire. Everything that followed.", + "choices": [ + "return to the present" + ], + "transitions": { + "return to the present": "mind_hub" + }, + "media": [] + }, + "safe": { + "description": "MEMORY: Cold steel under your fingertips. The click of a combination lock. Numbers you overheard Marcus mumble in his sleep during a conference trip: 7-23-91. His mother's birthday. The door swings open...", + "choices": [ + "remember what was inside", + "remember the smell of the room", + "let the memory fade" + ], + "transitions": { + "remember what was inside": "memory_safe_contents", + "remember the smell of the room": "memory_safe_smell", + "let the memory fade": "mind_hub" + }, + "media": [] + }, + "safe_contents": { + "description": "Stacks of cash. Foreign passports with Marcus's face but different names. And the documents - transaction records, shell companies, names of officials on the take. Enough to bring down not just Marcus, but everyone connected to him.", + "choices": [ + "return to the present" + ], + "transitions": { + "return to the present": "mind_hub" + }, + "media": [] + }, + "safe_smell": { + "description": "Cigar smoke and old paper. Marcus's office always smelled like success and secrets. You never questioned it before. Now the smell makes you sick, knowing what that 'success' was built on.", + "choices": [ + "return to the present" + ], + "transitions": { + "return to the present": "mind_hub" + }, + "media": [] + } + }, + "reality": { + "evidence": { + "description": "Your fingers find paper. Crumpled, water-damaged, but legible. The documents. The proof. It's all here. On the nightstand: a phone you don't recognize. Elena's phone. A number already saved: 'Press Contact - NYT'. Your head still pounds, but your mind is clear now. You know what happened. You know what you have to do.", + "choices": [ + "make the call", + "review the documents first", + "wait until you're stronger", + "confront Marcus yourself" + ], + "transitions": { + "make the call": "reality_call", + "review the documents first": "investigation_review", + "wait until you're stronger": "ending_wait", + "confront Marcus yourself": "ending_confrontation" + }, + "media": [] + }, + "call": { + "description": "The phone rings twice. 'This is Sarah Chen, New York Times.' You take a breath. 'I have documents proving a money laundering conspiracy involving a dozen city officials. I have names, amounts, dates. And I have proof that my former partner tried to kill me to keep it quiet.'", + "choices": [ + "tell her everything" + ], + "transitions": { + "tell her everything": "ending_expose" + }, + "media": [] + } + }, + "ending": { + "expose": { + "description": "Three weeks later, you watch from a safe house as Marcus is led away in handcuffs. Twelve officials have resigned. Four more are under investigation. The story is everywhere. Elena sits beside you, smiling. 'You remembered,' she says. 'When it mattered most, you remembered everything.' The memories still hurt. But now they mean something.", + "choices": [], + "transitions": {}, + "media": [] + }, + "wait": { + "description": "You set the phone down. Not yet. You need to be sure, to remember everything clearly. You close your eyes and drift back into the fragments, searching for more details, more proof, more certainty. The truth can wait a little longer. It has to be perfect.", + "choices": [ + "rest and remember" + ], + "transitions": { + "rest and remember": "mind_hub" + }, + "media": [] + }, + "confrontation": { + "description": "You chose to confront Marcus yourself. Standing in the shadows of the rebuilt warehouse, you wait. He arrives alone, as you demanded. His face is gaunt, aged years in weeks. 'You should have died in that fire,' he says quietly.", + "choices": [ + "demand answers", + "offer him a choice", + "signal Elena to call the police" + ], + "transitions": { + "demand answers": "ending_answers", + "offer him a choice": "ending_mercy", + "signal Elena to call the police": "ending_justice" + }, + "media": [] + }, + "answers": { + "description": "'Why?' you ask. He laughs bitterly. 'You think I wanted this? They approached me ten years ago. One favor became ten, became a hundred. By the time I realized what I'd become, I was in too deep. You were never supposed to find out. I was protecting you.' His words ring hollow. Protection isn't built on lies.", + "choices": [ + "turn him in", + "let him run" + ], + "transitions": { + "turn him in": "ending_justice", + "let him run": "ending_escape" + }, + "media": [] + }, + "mercy": { + "description": "'I have the documents,' you say. 'I could destroy them. You could disappear. Start over somewhere far away.' Marcus stares at you, hope flickering in his eyes. 'You would do that? After everything?' The question hangs in the air. Would you?", + "choices": [ + "yes, for our old friendship", + "no, the victims deserve justice" + ], + "transitions": { + "yes, for our old friendship": "ending_escape", + "no, the victims deserve justice": "ending_justice" + }, + "media": [] + }, + "justice": { + "description": "Blue and red lights fill the warehouse. Marcus doesn't run. He stands still as they cuff him, meeting your eyes one last time. 'I hope it was worth it,' he says. Six months later, he's sentenced to twenty-five years. The network is dismantled. The victims' families send you letters of thanks. You read them when the guilt gets heavy. It was worth it. It has to be.", + "choices": [], + "transitions": {}, + "media": [] + }, + "escape": { + "description": "You watch him drive away into the night. The documents burn in a metal barrel behind you. Elena finds you an hour later, staring at the ashes. 'What did you do?' she asks. You don't answer. Some truths are too heavy to speak. The conspiracy continues, smaller now but alive. And you carry the weight of everyone who will never see justice.", + "choices": [], + "transitions": {}, + "media": [] + } + }, + "investigation": { + "review": { + "description": "Before making the call, you spread the documents across the bed. Water-stained but legible. You need to understand the full scope of what you're about to unleash. Where do you start?", + "choices": [ + "study the financial records", + "read the list of names", + "examine the photographs", + "just make the call already" + ], + "transitions": { + "study the financial records": "investigation_finances", + "read the list of names": "investigation_names", + "examine the photographs": "investigation_photos", + "just make the call already": "reality_call" + }, + "media": [] + }, + "finances": { + "description": "The numbers are staggering. Millions moved through shell companies, laundered through real estate, funneled into offshore accounts. Each transaction is a crime. Each signature is a confession. Marcus's initials appear on dozens of documents. But so do others - people you've seen on television, in newspapers, at charity galas. This goes higher than you imagined.", + "choices": [ + "continue investigating", + "this is enough - make the call" + ], + "transitions": { + "continue investigating": "investigation_review", + "this is enough - make the call": "reality_call" + }, + "media": [] + }, + "names": { + "description": "A list of names. Some you recognize - city council members, police captains, a federal judge. Others mean nothing to you, but they will soon. Red lines connect them in a web of corruption. At the center: a name you've never seen before. Someone who stayed completely hidden. The real architect. You photograph the page three times to be sure.", + "choices": [ + "continue investigating", + "this is enough - make the call" + ], + "transitions": { + "continue investigating": "investigation_review", + "this is enough - make the call": "reality_call" + }, + "media": [] + }, + "photos": { + "description": "Surveillance photos. Meetings in parking garages, exchanges in restaurants, handshakes at private clubs. Timestamps and locations carefully noted. Insurance, you realize. Marcus was keeping records in case they ever turned on him. These photos could protect you too, or make you a target. Knowledge is dangerous. You've seen too much to go back now.", + "choices": [ + "continue investigating", + "this is enough - make the call", + "hide the photos separately as insurance" + ], + "transitions": { + "continue investigating": "investigation_review", + "this is enough - make the call": "reality_call", + "hide the photos separately as insurance": "investigation_insurance" + }, + "media": [] + }, + "insurance": { + "description": "You photograph the surveillance images with Elena's phone, then upload them to three different cloud services under fake names. If something happens to you, these images will surface. It's not much, but it's something. A dead man's switch. You hope you never need it.", + "choices": [ + "now make the call" + ], + "transitions": { + "now make the call": "reality_call" + }, + "media": [] + } + }, + "deeper": { + "nightmare": { + "description": "You're not awake. Not truly. The memories have pulled you under, into something darker. The warehouse burns around you, but this time you can't move. Marcus stands over you, face shifting between the friend you knew and the stranger he became. 'You shouldn't have looked,' he says, and his voice echoes infinitely.", + "choices": [ + "fight to wake up", + "let the nightmare show you more", + "try to talk to dream-Marcus" + ], + "transitions": { + "fight to wake up": "mind_hub", + "let the nightmare show you more": "deeper_vision", + "try to talk to dream-Marcus": "deeper_confrontation" + }, + "media": [] + }, + "vision": { + "description": "The fire fades. You stand in a vast office, looking down at the city through floor-to-ceiling windows. A desk behind you holds a nameplate you can't quite read. This is the future, you realize. Or a possible one. You could take Marcus's place. Use the network for good, or for yourself. The power is intoxicating, even in dreams.", + "choices": [ + "reject this future", + "explore what you could become" + ], + "transitions": { + "reject this future": "mind_hub", + "explore what you could become": "deeper_temptation" + }, + "media": [] + }, + "temptation": { + "description": "In the dream, you become the one behind the desk. Years pass in moments. The network grows under your control. You tell yourself it's different when you're in charge. But the faces of the people you step on to climb... they look familiar. They look like you. You wake gasping, cold sweat soaking the sheets. Some paths should never be walked.", + "choices": [ + "shake off the nightmare" + ], + "transitions": { + "shake off the nightmare": "mind_hub" + }, + "media": [] + }, + "confrontation": { + "description": "Dream-Marcus tilts his head, considering you. 'You want to understand,' he says. 'Fine. I'll show you.' The warehouse dissolves. You're in his memory now - the first meeting, the first compromise, the first step down a road with no return. You feel what he felt: the fear, the greed, the slow erosion of everything he thought he was.", + "choices": [ + "pull away from his perspective", + "see it through to the end" + ], + "transitions": { + "pull away from his perspective": "mind_hub", + "see it through to the end": "deeper_understanding" + }, + "media": [] + }, + "understanding": { + "description": "You live his choices. Every justification, every small betrayal that led to larger ones. You feel the moment he decided you had to die - not hate, but cold calculation. Survival. By the time you wake, you understand him completely. It doesn't make you forgive him. But you know now: anyone can become a monster, one small choice at a time. The question is whether you're strong enough to choose differently.", + "choices": [ + "wake with new resolve" + ], + "transitions": { + "wake with new resolve": "reality_evidence" + }, + "media": [] + } + } + }, + + # ============================================================ + # LOGIC GATES EXAMPLE - Demonstrates conditional choices, + # dynamic transitions, effects, and encounter injection + # ============================================================ + "LogicGatesDemo": { + "tavern": { + "entrance": { + "description": "You push open the heavy oak door and step into the Rusty Tankard. The smell of ale and roasted meat fills your nostrils.", + "choices": [ + "approach the bar", + "sit at a corner table", + "talk to the mysterious stranger", + "use the back door" + ], + "choice_config": { + "talk to the mysterious stranger": { + "condition": {"flag": "noticed_stranger"}, + "visible_text": "Approach the hooded figure", + "hidden_text": "[Look around more carefully first]" + }, + "use the back door": { + "condition": { + "or": [ + {"has_item": "back_door_key"}, + {"flag": "knows_shortcut"} + ] + } + } + }, + "transitions": { + "approach the bar": "tavern_bar", + "sit at a corner table": { + "random": [ + ["tavern_quiet_corner", 0.7], + ["tavern_eavesdrop", 0.3] + ] + }, + "talk to the mysterious stranger": { + "if": {"flag": "stranger_trusts_you"}, + "then": "tavern_stranger_mission", + "else": "tavern_stranger_suspicious" + }, + "use the back door": "alley_escape" + }, + "effects": { + "approach the bar": { + "increment": {"tavern_visits": 1} + }, + "sit at a corner table": { + "set_flag": "observed_tavern" + } + }, + "on_enter": { + "set_flag": "visited_tavern", + "add_location": "tavern" + }, + "encounter_chance": { + "probability": 0.15, + "pool": ["tavern_brawl", "tavern_drunk"], + "bypass_conditions": {"flag": "tavern_regular"} + }, + "media": [] + }, + "bar": { + "description": "The bartender, a burly woman with a knowing smile, polishes a mug while watching you approach.", + "choices": [ + "order a drink", + "ask about rumors", + "bribe for information" + ], + "choice_config": { + "bribe for information": { + "condition": {"money": {"gte": 20}}, + "visible_text": "Slip her some coins (20 gold)", + "hidden_text": "[Not enough gold - need 20]" + } + }, + "transitions": { + "order a drink": "tavern_drinking", + "ask about rumors": { + "conditions": [ + {"if": {"counter": {"bartender_trust": {"gte": 3}}}, "then": "tavern_secret_rumor"}, + {"if": {"flag": "bought_drink"}, "then": "tavern_common_rumor"}, + {"default": "tavern_no_rumor"} + ] + }, + "bribe for information": "tavern_bribed_info" + }, + "effects": { + "order a drink": { + "remove_money": 2, + "set_flag": "bought_drink", + "increment": {"bartender_trust": 1} + }, + "bribe for information": { + "remove_money": 20, + "increment": {"bartender_trust": 2}, + "set_flag": "noticed_stranger" + } + }, + "media": [] + }, + "drinking": { + "description": "The ale is strong and warming. You notice a hooded figure in the corner watching you.", + "choices": ["return to entrance", "keep drinking"], + "transitions": { + "return to entrance": "tavern_entrance", + "keep drinking": "tavern_bar" + }, + "on_enter": { + "set_flag": "noticed_stranger" + }, + "media": [] + }, + "quiet_corner": { + "description": "You find a quiet corner to observe the room. The usual patrons go about their business.", + "choices": ["return to entrance"], + "transitions": {"return to entrance": "tavern_entrance"}, + "media": [] + }, + "eavesdrop": { + "description": "From your corner seat, you overhear a conversation about a secret meeting happening tonight.", + "choices": ["investigate further", "ignore and leave"], + "transitions": { + "investigate further": "tavern_stranger_suspicious", + "ignore and leave": "tavern_entrance" + }, + "on_enter": { + "set_flag": "knows_secret_meeting", + "set_knowledge": {"overheard": "secret meeting tonight"} + }, + "media": [] + }, + "no_rumor": { + "description": "The bartender shrugs. 'Buy a drink first, then we can talk.'", + "choices": ["return to bar"], + "transitions": {"return to bar": "tavern_bar"}, + "media": [] + }, + "common_rumor": { + "description": "The bartender leans in. 'There's talk of bandits on the north road. Be careful if you're traveling.'", + "choices": ["return to bar"], + "transitions": {"return to bar": "tavern_bar"}, + "on_enter": { + "set_knowledge": {"bandit_warning": True} + }, + "media": [] + }, + "secret_rumor": { + "description": "The bartender whispers conspiratorially. 'The hooded stranger in the corner? He's looking for someone with... special skills. Pays well, they say.'", + "choices": ["return to bar"], + "transitions": {"return to bar": "tavern_bar"}, + "on_enter": { + "set_flag": "knows_stranger_job", + "set_flag": "noticed_stranger" + }, + "media": [] + }, + "bribed_info": { + "description": "The bartender pockets the coins smoothly. 'The stranger? Name's Vex. Looking for someone to retrieve something from the old ruins. Dangerous work, but the pay...' She trails off with a knowing look.", + "choices": ["return to bar"], + "transitions": {"return to bar": "tavern_bar"}, + "on_enter": { + "add_person": "vex", + "set_knowledge": {"vex_job": "ruins retrieval"}, + "set_flag": "knows_stranger_job" + }, + "media": [] + }, + "stranger_suspicious": { + "description": "The hooded figure eyes you warily. 'I don't know you. Why should I trust a stranger?'", + "choices": [ + "mention the bartender sent you", + "offer to prove yourself", + "leave them alone" + ], + "choice_config": { + "mention the bartender sent you": { + "condition": {"flag": "knows_stranger_job"} + } + }, + "transitions": { + "mention the bartender sent you": "tavern_stranger_mission", + "offer to prove yourself": "tavern_stranger_test", + "leave them alone": "tavern_entrance" + }, + "on_enter": { + "add_person": "mysterious_stranger" + }, + "media": [] + }, + "stranger_test": { + "description": "The stranger slides a small lockbox across the table. 'Open this without the key. Prove you have useful skills.'", + "choices": [ + "try to pick the lock", + "admit you can't", + "offer gold instead" + ], + "choice_config": { + "try to pick the lock": { + "condition": {"has_item": "lockpicks"} + }, + "offer gold instead": { + "condition": {"money": {"gte": 50}} + } + }, + "transitions": { + "try to pick the lock": "tavern_stranger_mission", + "admit you can't": "tavern_entrance", + "offer gold instead": "tavern_stranger_mission" + }, + "effects": { + "try to pick the lock": { + "set_flag": "stranger_trusts_you" + }, + "offer gold instead": { + "remove_money": 50, + "set_flag": "stranger_trusts_you" + } + }, + "media": [] + }, + "stranger_mission": { + "description": "The stranger nods approvingly. 'I am Vex. I need someone to retrieve an artifact from the old ruins. The pay is 500 gold. Are you interested?'", + "choices": ["accept the job", "decline"], + "transitions": { + "accept the job": "tavern_mission_accepted", + "decline": "tavern_entrance" + }, + "on_enter": { + "add_person": "vex", + "set_flag": "stranger_trusts_you" + }, + "media": [] + }, + "mission_accepted": { + "description": "Vex hands you a worn map. 'The ruins are north of town. Bring me the Obsidian Amulet, and the gold is yours. Don't fail me.'", + "choices": ["head to the ruins", "prepare first"], + "transitions": { + "head to the ruins": "ruins_entrance", + "prepare first": "tavern_entrance" + }, + "on_enter": { + "add_item": "worn_map", + "start_mission": {"ruins_artifact": {"target": "obsidian_amulet", "reward": 500}}, + "add_location": "ruins" + }, + "media": [] + }, + "brawl": { + "description": "A fight breaks out! Mugs fly and patrons scramble for cover.", + "choices": ["join the fight", "duck and cover", "slip out the back"], + "choice_config": { + "slip out the back": { + "condition": {"flag": "knows_shortcut"} + } + }, + "transitions": { + "join the fight": "tavern_entrance", + "duck and cover": "tavern_entrance", + "slip out the back": "alley_escape" + }, + "effects": { + "join the fight": { + "increment": {"reputation_tough": 1}, + "remove_money": 5 + } + }, + "media": [] + }, + "drunk": { + "description": "A drunk patron stumbles into you, spilling ale on your clothes. 'Sorry, friend! Let me make it up to ya...' He presses something into your hand.", + "choices": ["accept the gift", "push him away"], + "transitions": { + "accept the gift": "tavern_entrance", + "push him away": "tavern_entrance" + }, + "effects": { + "accept the gift": { + "add_item": "lucky_coin", + "set_flag": "knows_shortcut" + } + }, + "media": [] + }, + "mission_complete": { + "description": "Vex examines the amulet, a rare smile crossing their face. 'You've done well.' They hand you a heavy pouch of gold. 'Perhaps we'll work together again.'", + "choices": ["celebrate your success", "ask about more work"], + "transitions": { + "celebrate your success": "tavern_entrance", + "ask about more work": "tavern_entrance" + }, + "effects": { + "celebrate your success": { + "add_money": 500, + "remove_item": "obsidian_amulet", + "adjust_reputation": {"vex": 10} + }, + "ask about more work": { + "add_money": 500, + "remove_item": "obsidian_amulet", + "adjust_reputation": {"vex": 15}, + "set_flag": "vex_ally" + } + }, + "media": [] + } + }, + "alley": { + "escape": { + "description": "You slip through the back door into a narrow alley. The night air is cool and quiet.", + "choices": ["return to tavern", "explore the alley"], + "transitions": { + "return to tavern": "tavern_entrance", + "explore the alley": "alley_escape" + }, + "on_enter": { + "set_flag": "used_back_door" + }, + "media": [] + } + }, + "ruins": { + "entrance": { + "description": "The ancient ruins loom before you, crumbling stone covered in ivy. The entrance is dark and foreboding.", + "choices": ["enter the ruins", "return to town"], + "choice_config": { + "enter the ruins": { + "condition": {"has_item": "worn_map"}, + "hidden_text": "[Need a map to navigate]" + } + }, + "transitions": { + "enter the ruins": "ruins_interior", + "return to town": "tavern_entrance" + }, + "on_enter": { + "visit_location": "ruins" + }, + "media": [] + }, + "interior": { + "description": "Inside the ruins, faded murals line the walls. In the center of the chamber, you see a pedestal with an obsidian amulet.", + "choices": ["take the amulet", "examine the murals", "leave"], + "transitions": { + "take the amulet": "ruins_success", + "examine the murals": "ruins_interior", + "leave": "ruins_entrance" + }, + "effects": { + "take the amulet": { + "add_item": "obsidian_amulet", + "complete_mission": "ruins_artifact" + } + }, + "media": [] + }, + "success": { + "description": "You hold the Obsidian Amulet in your hands. Time to return to Vex and collect your reward.", + "choices": ["return to the tavern"], + "transitions": { + "return to the tavern": "tavern_mission_complete" + }, + "media": [] + } + } + }, + + # ============================================================ + # COMPREHENSIVE STATE SYSTEM REFERENCE + # Demonstrates ALL condition types, effect types, and transition patterns + # Use this as a reference for the full capability of the state system + # ============================================================ + "StateSystemReference": { + "hub": { + # ============================================================ + # HUB: Central navigation to all feature demonstrations + # ============================================================ + "start": { + "description": "Welcome to the State System Reference! This comprehensive demo showcases all available conditions, effects, and transition patterns. Choose a category to explore.", + "choices": [ + "explore conditions", + "explore effects", + "explore transitions", + "explore state features", + "view current state" + ], + "transitions": { + "explore conditions": "conditions_menu", + "explore effects": "effects_menu", + "explore transitions": "transitions_menu", + "explore state features": "features_menu", + "view current state": "hub_state_viewer" + }, + "on_enter": { + "set_flag": "tutorial_started", + "add_location": "hub" + }, + "media": [] + }, + "state_viewer": { + "description": "=== CURRENT STATE ===\n\n📍 Location: {current_location} / {current_state}\n💰 Money: {money}\n🎒 Inventory: {inventory}\n🚩 Flags: {flags}\n🔢 Counters: {counters}\n👥 People Met: {people_met}\n🗺️ Visited: {locations_visited}\n📋 Active Missions: {missions_active}\n✅ Completed: {missions_completed}", + "choices": ["return to hub"], + "transitions": {"return to hub": "hub_start"}, + "on_enter": { + "increment": {"hub_views": 1} + }, + "media": [] + } + }, + + "conditions": { + # ============================================================ + # CONDITIONS MENU + # ============================================================ + "menu": { + "description": "CONDITION TYPES REFERENCE - Conditions determine when choices are available and how transitions resolve. Select a category:", + "choices": [ + "inventory conditions", + "flag conditions", + "numeric conditions", + "social conditions", + "location conditions", + "mission conditions", + "compound conditions", + "return to hub" + ], + "transitions": { + "inventory conditions": "conditions_inventory", + "flag conditions": "conditions_flags", + "numeric conditions": "conditions_numeric", + "social conditions": "conditions_social", + "location conditions": "conditions_location", + "mission conditions": "conditions_mission", + "compound conditions": "conditions_compound", + "return to hub": "hub_start" + }, + "media": [] + }, + + "inventory": { + "description": "INVENTORY CONDITIONS: has_item checks if player has item, not_has_item checks if missing. Try getting the key to see choice visibility change.", + "choices": [ + "pick up demo key", + "use demo key", + "check for missing item", + "back to conditions menu" + ], + "choice_config": { + "use demo key": { + "condition": {"has_item": "demo_key"}, + "visible_text": "Use the demo key (has_item passed)", + "hidden_text": "[Requires: demo_key]" + }, + "check for missing item": { + "condition": {"not_has_item": "nonexistent_item"}, + "visible_text": "Confirm missing item (not_has_item passed)" + } + }, + "transitions": { + "pick up demo key": "conditions_inventory", + "use demo key": "conditions_inventory_used", + "check for missing item": "conditions_inventory", + "back to conditions menu": "conditions_menu" + }, + "effects": { + "pick up demo key": {"add_item": "demo_key"}, + "use demo key": {"remove_item": "demo_key"} + }, + "media": [] + }, + "inventory_used": { + "description": "Success! The has_item condition passed. Key was consumed.", + "choices": ["return"], + "transitions": {"return": "conditions_inventory"}, + "media": [] + }, + + "flags": { + "description": "FLAG CONDITIONS: flag checks if true, not_flag checks if false/unset. Flags are boolean switches.", + "choices": [ + "set demo flag", + "action requiring flag", + "action requiring no flag", + "clear demo flag", + "back to conditions menu" + ], + "choice_config": { + "action requiring flag": { + "condition": {"flag": "demo_flag_set"}, + "visible_text": "Proceed (flag check passed)", + "hidden_text": "[Requires: demo_flag_set]" + }, + "action requiring no flag": { + "condition": {"not_flag": "demo_flag_set"}, + "visible_text": "Alternative path (not_flag passed)" + } + }, + "transitions": { + "set demo flag": "conditions_flags", + "action requiring flag": "conditions_flags_ok", + "action requiring no flag": "conditions_flags_ok", + "clear demo flag": "conditions_flags", + "back to conditions menu": "conditions_menu" + }, + "effects": { + "set demo flag": {"set_flag": "demo_flag_set"}, + "clear demo flag": {"clear_flag": "demo_flag_set"} + }, + "media": [] + }, + "flags_ok": { + "description": "Condition passed!", + "choices": ["return"], + "transitions": {"return": "conditions_flags"}, + "media": [] + }, + + "numeric": { + "description": "NUMERIC CONDITIONS: money and counter checks with operators gte(>=), lte(<=), gt(>), lt(<), eq(==), neq(!=)", + "choices": [ + "get 100 gold", + "spend 50 gold", + "buy expensive item", + "increment demo counter", + "action at counter >= 3", + "back to conditions menu" + ], + "choice_config": { + "spend 50 gold": { + "condition": {"money": {"gte": 50}}, + "visible_text": "Spend 50 gold (money >= 50)", + "hidden_text": "[Need 50 gold]" + }, + "buy expensive item": { + "condition": {"money": {"gte": 200}}, + "visible_text": "Buy rare artifact (200 gold)", + "hidden_text": "[Need 200 gold]" + }, + "action at counter >= 3": { + "condition": {"counter": {"demo_counter": {"gte": 3}}}, + "visible_text": "Unlock reward (counter >= 3)", + "hidden_text": "[demo_counter must be >= 3]" + } + }, + "transitions": { + "get 100 gold": "conditions_numeric", + "spend 50 gold": "conditions_numeric", + "buy expensive item": "conditions_numeric", + "increment demo counter": "conditions_numeric", + "action at counter >= 3": "conditions_numeric_reward", + "back to conditions menu": "conditions_menu" + }, + "effects": { + "get 100 gold": {"add_money": 100}, + "spend 50 gold": {"remove_money": 50}, + "buy expensive item": {"remove_money": 200, "add_item": "rare_artifact"}, + "increment demo counter": {"increment": {"demo_counter": 1}} + }, + "media": [] + }, + "numeric_reward": { + "description": "Counter check passed! Reward granted.", + "choices": ["continue"], + "transitions": {"continue": "conditions_numeric"}, + "on_enter": {"add_item": "counter_reward"}, + "media": [] + }, + + "social": { + "description": "SOCIAL CONDITIONS: met_person, not_met_person, reputation checks. Track NPCs and relationships.", + "choices": [ + "meet the merchant", + "talk to merchant", + "increase merchant reputation", + "get merchant discount", + "back to conditions menu" + ], + "choice_config": { + "talk to merchant": { + "condition": {"met_person": "demo_merchant"}, + "visible_text": "Chat with merchant (met_person)", + "hidden_text": "[Haven't met merchant]" + }, + "get merchant discount": { + "condition": {"reputation": {"npc": "demo_merchant", "gte": 5}}, + "visible_text": "Ask for discount (rep >= 5)", + "hidden_text": "[Need reputation >= 5]" + } + }, + "transitions": { + "meet the merchant": "conditions_social", + "talk to merchant": "conditions_social", + "increase merchant reputation": "conditions_social", + "get merchant discount": "conditions_social_discount", + "back to conditions menu": "conditions_menu" + }, + "effects": { + "meet the merchant": {"add_person": "demo_merchant"}, + "increase merchant reputation": {"adjust_reputation": {"demo_merchant": 2}} + }, + "media": [] + }, + "social_discount": { + "description": "Discount granted! Reputation check passed.", + "choices": ["continue"], + "transitions": {"continue": "conditions_social"}, + "on_enter": {"add_item": "discount_token"}, + "media": [] + }, + + "location": { + "description": "LOCATION CONDITIONS: visited (been there), discovered (know about), visit_count (times visited)", + "choices": [ + "discover secret cave", + "visit the cave", + "tell about cave", + "veteran check (visit 3+ times)", + "back to conditions menu" + ], + "choice_config": { + "visit the cave": { + "condition": {"discovered": "secret_cave"}, + "visible_text": "Enter cave (discovered)", + "hidden_text": "[Discover first]" + }, + "tell about cave": { + "condition": {"visited": "secret_cave"}, + "visible_text": "Share stories (visited)", + "hidden_text": "[Must have visited]" + }, + "veteran check (visit 3+ times)": { + "condition": {"visit_count": {"state": "conditions_location", "gte": 3}}, + "visible_text": "Veteran reward (3+ visits)", + "hidden_text": "[Visit 3+ times]" + } + }, + "transitions": { + "discover secret cave": "conditions_location", + "visit the cave": "conditions_location_cave", + "tell about cave": "conditions_location", + "veteran check (visit 3+ times)": "conditions_location_veteran", + "back to conditions menu": "conditions_menu" + }, + "effects": { + "discover secret cave": {"add_location": "secret_cave"} + }, + "media": [] + }, + "location_cave": { + "description": "You enter the crystal cave!", + "choices": ["return"], + "transitions": {"return": "conditions_location"}, + "on_enter": {"visit_location": "secret_cave"}, + "media": [] + }, + "location_veteran": { + "description": "Veteran explorer badge earned!", + "choices": ["continue"], + "transitions": {"continue": "conditions_location"}, + "on_enter": {"add_item": "veteran_badge"}, + "media": [] + }, + + "mission": { + "description": "MISSION CONDITIONS: mission_active, mission_complete, mission_failed - track quest states", + "choices": [ + "start demo quest", + "report progress", + "complete the quest", + "fail the quest", + "collect reward", + "back to conditions menu" + ], + "choice_config": { + "report progress": { + "condition": {"mission_active": "demo_quest"}, + "visible_text": "Report progress (active)", + "hidden_text": "[No active quest]" + }, + "complete the quest": { + "condition": {"mission_active": "demo_quest"}, + "visible_text": "Turn in quest" + }, + "fail the quest": { + "condition": {"mission_active": "demo_quest"}, + "visible_text": "Abandon quest" + }, + "collect reward": { + "condition": {"mission_complete": "demo_quest"}, + "visible_text": "Collect reward (complete)", + "hidden_text": "[Complete quest first]" + } + }, + "transitions": { + "start demo quest": "conditions_mission", + "report progress": "conditions_mission", + "complete the quest": "conditions_mission", + "fail the quest": "conditions_mission", + "collect reward": "conditions_mission_reward", + "back to conditions menu": "conditions_menu" + }, + "effects": { + "start demo quest": {"start_mission": "demo_quest"}, + "complete the quest": {"complete_mission": "demo_quest"}, + "fail the quest": {"fail_mission": "demo_quest"} + }, + "media": [] + }, + "mission_reward": { + "description": "Quest reward: 100 gold!", + "choices": ["continue"], + "transitions": {"continue": "conditions_mission"}, + "on_enter": {"add_money": 100}, + "media": [] + }, + + "compound": { + "description": "COMPOUND CONDITIONS: and (all true), or (any true), not (invert). Can be nested!", + "choices": [ + "get key and 50 gold", + "locked door (AND)", + "vip or wealthy (OR)", + "not tourist (NOT)", + "complex nested", + "back to conditions menu" + ], + "choice_config": { + "locked door (AND)": { + "condition": {"and": [{"has_item": "compound_key"}, {"money": {"gte": 50}}]}, + "visible_text": "Open door (key AND 50g)", + "hidden_text": "[Need key AND 50g]" + }, + "vip or wealthy (OR)": { + "condition": {"or": [{"flag": "vip_status"}, {"money": {"gte": 500}}]}, + "visible_text": "VIP area (VIP OR 500g)", + "hidden_text": "[Need VIP OR 500g]" + }, + "not tourist (NOT)": { + "condition": {"not": {"flag": "tourist_mode"}}, + "visible_text": "Locals only (NOT tourist)" + }, + "complex nested": { + "condition": {"and": [{"has_item": "compound_key"}, {"or": [{"flag": "vip_status"}, {"money": {"gte": 100}}]}]}, + "visible_text": "Ultimate (key AND (VIP OR 100g))", + "hidden_text": "[Need key AND (VIP OR 100g)]" + } + }, + "transitions": { + "get key and 50 gold": "conditions_compound", + "locked door (AND)": "conditions_compound_ok", + "vip or wealthy (OR)": "conditions_compound_ok", + "not tourist (NOT)": "conditions_compound_ok", + "complex nested": "conditions_compound_ok", + "back to conditions menu": "conditions_menu" + }, + "effects": { + "get key and 50 gold": {"add_item": "compound_key", "add_money": 50} + }, + "media": [] + }, + "compound_ok": { + "description": "Compound condition passed!", + "choices": ["return"], + "transitions": {"return": "conditions_compound"}, + "media": [] + } + }, + + "effects": { + # ============================================================ + # EFFECTS MENU + # ============================================================ + "menu": { + "description": "EFFECT TYPES REFERENCE - Effects modify game state. Select a category:", + "choices": [ + "inventory effects", + "money effects", + "flag effects", + "counter effects", + "social effects", + "location effects", + "mission effects", + "knowledge effects", + "return to hub" + ], + "transitions": { + "inventory effects": "effects_inventory", + "money effects": "effects_money", + "flag effects": "effects_flags", + "counter effects": "effects_counters", + "social effects": "effects_social", + "location effects": "effects_location", + "mission effects": "effects_mission", + "knowledge effects": "effects_knowledge", + "return to hub": "hub_start" + }, + "media": [] + }, + + "inventory": { + "description": "INVENTORY EFFECTS: add_item (single or list), remove_item (single or list)", + "choices": [ + "add single item", + "add multiple items", + "remove single item", + "remove multiple items", + "back to effects menu" + ], + "transitions": { + "add single item": "effects_inventory", + "add multiple items": "effects_inventory", + "remove single item": "effects_inventory", + "remove multiple items": "effects_inventory", + "back to effects menu": "effects_menu" + }, + "effects": { + "add single item": {"add_item": "demo_sword"}, + "add multiple items": {"add_item": ["demo_shield", "demo_potion", "demo_scroll"]}, + "remove single item": {"remove_item": "demo_sword"}, + "remove multiple items": {"remove_item": ["demo_shield", "demo_potion"]} + }, + "media": [] + }, + + "money": { + "description": "MONEY EFFECTS: add_money, remove_money, set_money", + "choices": [ + "add 100 gold", + "add 500 gold", + "remove 50 gold", + "set to 1000", + "reset to zero", + "back to effects menu" + ], + "transitions": { + "add 100 gold": "effects_money", + "add 500 gold": "effects_money", + "remove 50 gold": "effects_money", + "set to 1000": "effects_money", + "reset to zero": "effects_money", + "back to effects menu": "effects_menu" + }, + "effects": { + "add 100 gold": {"add_money": 100}, + "add 500 gold": {"add_money": 500}, + "remove 50 gold": {"remove_money": 50}, + "set to 1000": {"set_money": 1000}, + "reset to zero": {"set_money": 0} + }, + "media": [] + }, + + "flags": { + "description": "FLAG EFFECTS: set_flag (single/list/dict), clear_flag, toggle_flag", + "choices": [ + "set single flag", + "set multiple flags", + "set with dict", + "clear flag", + "toggle flag", + "back to effects menu" + ], + "transitions": { + "set single flag": "effects_flags", + "set multiple flags": "effects_flags", + "set with dict": "effects_flags", + "clear flag": "effects_flags", + "toggle flag": "effects_flags", + "back to effects menu": "effects_menu" + }, + "effects": { + "set single flag": {"set_flag": "flag_a"}, + "set multiple flags": {"set_flag": ["flag_b", "flag_c"]}, + "set with dict": {"set_flag": {"flag_explicit": True, "flag_false": False}}, + "clear flag": {"clear_flag": "flag_a"}, + "toggle flag": {"toggle_flag": "flag_toggle"} + }, + "media": [] + }, + + "counters": { + "description": "COUNTER EFFECTS: set_counter, increment, decrement", + "choices": [ + "set to 10", + "increment by 1", + "increment by 5", + "decrement by 2", + "reset to 0", + "back to effects menu" + ], + "transitions": { + "set to 10": "effects_counters", + "increment by 1": "effects_counters", + "increment by 5": "effects_counters", + "decrement by 2": "effects_counters", + "reset to 0": "effects_counters", + "back to effects menu": "effects_menu" + }, + "effects": { + "set to 10": {"set_counter": {"score": 10}}, + "increment by 1": {"increment": {"score": 1}}, + "increment by 5": {"increment": {"score": 5}}, + "decrement by 2": {"decrement": {"score": 2}}, + "reset to 0": {"set_counter": {"score": 0}} + }, + "media": [] + }, + + "social": { + "description": "SOCIAL EFFECTS: add_person (single/list), adjust_reputation, set_reputation", + "choices": [ + "meet single NPC", + "meet multiple NPCs", + "increase reputation", + "decrease reputation", + "set reputation to 50", + "back to effects menu" + ], + "transitions": { + "meet single NPC": "effects_social", + "meet multiple NPCs": "effects_social", + "increase reputation": "effects_social", + "decrease reputation": "effects_social", + "set reputation to 50": "effects_social", + "back to effects menu": "effects_menu" + }, + "effects": { + "meet single NPC": {"add_person": "blacksmith"}, + "meet multiple NPCs": {"add_person": ["baker", "guard", "innkeeper"]}, + "increase reputation": {"adjust_reputation": {"blacksmith": 5}}, + "decrease reputation": {"adjust_reputation": {"guard": -3}}, + "set reputation to 50": {"set_reputation": {"innkeeper": 50}} + }, + "media": [] + }, + + "location": { + "description": "LOCATION EFFECTS: add_location (discover), visit_location (mark visited)", + "choices": [ + "discover single", + "discover multiple", + "visit location", + "back to effects menu" + ], + "transitions": { + "discover single": "effects_location", + "discover multiple": "effects_location", + "visit location": "effects_location", + "back to effects menu": "effects_menu" + }, + "effects": { + "discover single": {"add_location": "temple"}, + "discover multiple": {"add_location": ["forest", "lake", "mountain"]}, + "visit location": {"visit_location": "temple"} + }, + "media": [] + }, + + "mission": { + "description": "MISSION EFFECTS: start_mission (simple/with data), complete_mission, fail_mission, update_mission", + "choices": [ + "start simple mission", + "start with data", + "update mission", + "complete mission", + "fail mission", + "back to effects menu" + ], + "transitions": { + "start simple mission": "effects_mission", + "start with data": "effects_mission", + "update mission": "effects_mission", + "complete mission": "effects_mission", + "fail mission": "effects_mission", + "back to effects menu": "effects_menu" + }, + "effects": { + "start simple mission": {"start_mission": "simple_quest"}, + "start with data": {"start_mission": {"complex_quest": {"objective": "Find artifact", "reward": 500}}}, + "update mission": {"update_mission": {"complex_quest": {"progress": 50}}}, + "complete mission": {"complete_mission": "simple_quest"}, + "fail mission": {"fail_mission": "complex_quest"} + }, + "media": [] + }, + + "knowledge": { + "description": "KNOWLEDGE EFFECTS: set_knowledge (any JSON value), remove_knowledge", + "choices": [ + "learn boolean", + "learn string", + "learn object", + "forget single", + "forget multiple", + "back to effects menu" + ], + "transitions": { + "learn boolean": "effects_knowledge", + "learn string": "effects_knowledge", + "learn object": "effects_knowledge", + "forget single": "effects_knowledge", + "forget multiple": "effects_knowledge", + "back to effects menu": "effects_menu" + }, + "effects": { + "learn boolean": {"set_knowledge": {"secret_known": True}}, + "learn string": {"set_knowledge": {"villain_name": "Lord Darkmore"}}, + "learn object": {"set_knowledge": {"coords": {"x": 100, "y": 250}}}, + "forget single": {"remove_knowledge": "secret_known"}, + "forget multiple": {"remove_knowledge": ["villain_name", "coords"]} + }, + "media": [] + } + }, + + "transitions": { + # ============================================================ + # TRANSITIONS MENU + # ============================================================ + "menu": { + "description": "TRANSITION PATTERNS - Different ways to route between states. Select a pattern:", + "choices": [ + "deterministic", + "weighted random", + "random from pool", + "conditional if/then/else", + "chained conditions", + "return to hub" + ], + "transitions": { + "deterministic": "transitions_deterministic", + "weighted random": "transitions_weighted", + "random from pool": "transitions_pool", + "conditional if/then/else": "transitions_conditional", + "chained conditions": "transitions_chained", + "return to hub": "hub_start" + }, + "media": [] + }, + + "deterministic": { + "description": "DETERMINISTIC: Simple string targets - always goes to same place", + "choices": [ + "go to result A", + "go to result B", + "back to menu" + ], + "transitions": { + "go to result A": "transitions_det_a", + "go to result B": "transitions_det_b", + "back to menu": "transitions_menu" + }, + "media": [] + }, + "det_a": { + "description": "Result A - deterministic transition", + "choices": ["return"], + "transitions": {"return": "transitions_deterministic"}, + "media": [] + }, + "det_b": { + "description": "Result B - deterministic transition", + "choices": ["return"], + "transitions": {"return": "transitions_deterministic"}, + "media": [] + }, + + "weighted": { + "description": "WEIGHTED RANDOM: {random: [[state, prob], ...]} - Try multiple times to see different outcomes!", + "choices": [ + "roll dice (70/30)", + "rare attempt (90/9/1)", + "back to menu" + ], + "transitions": { + "roll dice (70/30)": { + "random": [["transitions_common", 0.7], ["transitions_rare", 0.3]] + }, + "rare attempt (90/9/1)": { + "random": [["transitions_common", 0.90], ["transitions_uncommon", 0.09], ["transitions_legendary", 0.01]] + }, + "back to menu": "transitions_menu" + }, + "media": [] + }, + "common": { + "description": "COMMON outcome (highest probability)", + "choices": ["try again"], + "transitions": {"try again": "transitions_weighted"}, + "media": [] + }, + "rare": { + "description": "RARE outcome (30% chance)", + "choices": ["try again"], + "transitions": {"try again": "transitions_weighted"}, + "on_enter": {"add_item": "rare_drop"}, + "media": [] + }, + "uncommon": { + "description": "UNCOMMON outcome (9% chance)", + "choices": ["try again"], + "transitions": {"try again": "transitions_weighted"}, + "on_enter": {"add_item": "uncommon_drop"}, + "media": [] + }, + "legendary": { + "description": "LEGENDARY outcome (1% chance)! Extremely lucky!", + "choices": ["try again"], + "transitions": {"try again": "transitions_weighted"}, + "on_enter": {"add_item": "legendary_drop", "set_flag": "got_legendary"}, + "media": [] + }, + + "pool": { + "description": "RANDOM FROM POOL: {random_from: [states]} - Equal probability for each", + "choices": [ + "enter mystery door", + "back to menu" + ], + "transitions": { + "enter mystery door": { + "random_from": ["transitions_treasure", "transitions_trap", "transitions_monster", "transitions_empty"] + }, + "back to menu": "transitions_menu" + }, + "media": [] + }, + "treasure": { + "description": "TREASURE! (1/4 equal chance)", + "choices": ["try again"], + "transitions": {"try again": "transitions_pool"}, + "on_enter": {"add_money": 50}, + "media": [] + }, + "trap": { + "description": "TRAP! (1/4 equal chance)", + "choices": ["try again"], + "transitions": {"try again": "transitions_pool"}, + "on_enter": {"remove_money": 10}, + "media": [] + }, + "monster": { + "description": "MONSTER! (1/4 equal chance)", + "choices": ["try again"], + "transitions": {"try again": "transitions_pool"}, + "on_enter": {"increment": {"monsters": 1}}, + "media": [] + }, + "empty": { + "description": "EMPTY room (1/4 equal chance)", + "choices": ["try again"], + "transitions": {"try again": "transitions_pool"}, + "media": [] + }, + + "conditional": { + "description": "CONDITIONAL: {if: cond, then: target, else: fallback} - Routes based on state", + "choices": [ + "set vip flag", + "clear vip flag", + "try enter club", + "back to menu" + ], + "transitions": { + "set vip flag": "transitions_conditional", + "clear vip flag": "transitions_conditional", + "try enter club": { + "if": {"flag": "vip_access"}, + "then": "transitions_vip_ok", + "else": "transitions_vip_denied" + }, + "back to menu": "transitions_menu" + }, + "effects": { + "set vip flag": {"set_flag": "vip_access"}, + "clear vip flag": {"clear_flag": "vip_access"} + }, + "media": [] + }, + "vip_ok": { + "description": "VIP lounge! Condition was TRUE", + "choices": ["return"], + "transitions": {"return": "transitions_conditional"}, + "media": [] + }, + "vip_denied": { + "description": "Bounced! Condition was FALSE", + "choices": ["return"], + "transitions": {"return": "transitions_conditional"}, + "media": [] + }, + + "chained": { + "description": "CHAINED CONDITIONS: {conditions: [{if, then}, ..., {default}]} - First match wins", + "choices": [ + "become wealthy (500g)", + "become VIP", + "become both", + "reset status", + "check status", + "back to menu" + ], + "transitions": { + "become wealthy (500g)": "transitions_chained", + "become VIP": "transitions_chained", + "become both": "transitions_chained", + "reset status": "transitions_chained", + "check status": { + "conditions": [ + {"if": {"and": [{"flag": "chain_vip"}, {"money": {"gte": 500}}]}, "then": "transitions_elite"}, + {"if": {"flag": "chain_vip"}, "then": "transitions_vip_only"}, + {"if": {"money": {"gte": 500}}, "then": "transitions_wealthy_only"}, + {"default": "transitions_commoner"} + ] + }, + "back to menu": "transitions_menu" + }, + "effects": { + "become wealthy (500g)": {"set_money": 500}, + "become VIP": {"set_flag": "chain_vip"}, + "become both": {"set_money": 500, "set_flag": "chain_vip"}, + "reset status": {"set_money": 0, "clear_flag": "chain_vip"} + }, + "media": [] + }, + "elite": { + "description": "ELITE! First condition: VIP AND wealthy", + "choices": ["return"], + "transitions": {"return": "transitions_chained"}, + "media": [] + }, + "vip_only": { + "description": "VIP only - second condition matched", + "choices": ["return"], + "transitions": {"return": "transitions_chained"}, + "media": [] + }, + "wealthy_only": { + "description": "WEALTHY only - third condition matched", + "choices": ["return"], + "transitions": {"return": "transitions_chained"}, + "media": [] + }, + "commoner": { + "description": "COMMONER - default case", + "choices": ["return"], + "transitions": {"return": "transitions_chained"}, + "media": [] + } + }, + + "features": { + # ============================================================ + # STATE FEATURES MENU + # ============================================================ + "menu": { + "description": "SPECIAL STATE FEATURES - Beyond choices and transitions. Select a feature:", + "choices": [ + "choice configuration", + "on_enter effects", + "encounter chances", + "return to hub" + ], + "transitions": { + "choice configuration": "features_choice_config", + "on_enter effects": "features_on_enter", + "encounter chances": "features_encounters", + "return to hub": "hub_start" + }, + "media": [] + }, + + "choice_config": { + "description": "CHOICE CONFIG: condition (when available), visible_text (override), hidden_text (locked text)", + "choices": [ + "always available", + "requires key", + "requires 100 gold", + "get key", + "get 100 gold", + "back to features" + ], + "choice_config": { + "requires key": { + "condition": {"has_item": "feature_key"}, + "visible_text": "Use key to unlock", + "hidden_text": "[Locked - Need key]" + }, + "requires 100 gold": { + "condition": {"money": {"gte": 100}}, + "visible_text": "Pay 100g for access", + "hidden_text": "[Need 100 gold]" + } + }, + "transitions": { + "always available": "features_choice_config", + "requires key": "features_cc_ok", + "requires 100 gold": "features_cc_ok", + "get key": "features_choice_config", + "get 100 gold": "features_choice_config", + "back to features": "features_menu" + }, + "effects": { + "get key": {"add_item": "feature_key"}, + "get 100 gold": {"add_money": 100}, + "requires 100 gold": {"remove_money": 100} + }, + "media": [] + }, + "cc_ok": { + "description": "Access granted! Condition passed.", + "choices": ["return"], + "transitions": {"return": "features_choice_config"}, + "media": [] + }, + + "on_enter": { + "description": "ON_ENTER EFFECTS: Automatically run when entering state. This state increments visit counter each entry.", + "choices": [ + "leave and return", + "back to features" + ], + "transitions": { + "leave and return": "features_on_enter_away", + "back to features": "features_menu" + }, + "on_enter": { + "increment": {"on_enter_visits": 1}, + "set_flag": "saw_on_enter_demo" + }, + "media": [] + }, + "on_enter_away": { + "description": "Step outside...", + "choices": ["return to on_enter demo"], + "transitions": {"return to on_enter demo": "features_on_enter"}, + "media": [] + }, + + "encounters": { + "description": "ENCOUNTER CHANCE: probability, pool of states, bypass_conditions. 30% chance here (bypassed by safe_passage flag).", + "choices": [ + "explore area", + "get safe passage", + "remove safe passage", + "back to features" + ], + "transitions": { + "explore area": "features_encounters", + "get safe passage": "features_encounters", + "remove safe passage": "features_encounters", + "back to features": "features_menu" + }, + "effects": { + "get safe passage": {"set_flag": "safe_passage"}, + "remove safe passage": {"clear_flag": "safe_passage"} + }, + "encounter_chance": { + "probability": 0.30, + "pool": ["features_enc_bandit", "features_enc_merchant"], + "bypass_conditions": {"flag": "safe_passage"} + }, + "media": [] + }, + "enc_bandit": { + "description": "ENCOUNTER: Bandits appeared! (30% chance triggered)", + "choices": ["flee"], + "transitions": {"flee": "features_encounters"}, + "on_enter": {"increment": {"bandits_met": 1}}, + "media": [] + }, + "enc_merchant": { + "description": "ENCOUNTER: Traveling merchant! (30% chance triggered)", + "choices": ["continue"], + "transitions": {"continue": "features_encounters"}, + "on_enter": {"add_person": "random_merchant"}, + "media": [] + } + } + } + + +} + +#-------------------------#-------------------------#-------------------------#------------------------- + + +finished_product_demo = { + 'village': { + 'start': { + "description": "You wake up in a small village. You hear a rumor about a lost treasure.", + "choices": ['explore village', 'gather supplies', 'rest'], + "transitions": {'explore village': 'village_rumor', 'gather supplies': 'village_supplies', 'rest': 'village_start'}, + "consequences": { + 'gather supplies': lambda player: player.add_item('basic supplies') + }, + "media": ["testmedia/fpe_small_village.webp", "testmedia/fpe_rumor_about_a_lost_treasure.webp", "testmedia/fpe_gathering_supplies_in_a_village.webp"] + }, + 'rumor': { + "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", + "choices": ['decide to go', 'ignore'], + "transitions": {'decide to go': 'village_supplies', 'ignore': 'village_start'}, + "consequences": { + 'decide to go': lambda player: player.update_knowledge('treasure location') + }, + "media": ["testmedia/fpe_rumor_about_a_lost_treasure.webp"] + }, + 'supplies': { + "description": "You gather supplies for your journey.", + "choices": ['head to forest', 'stay in village'], + "transitions": {'head to forest': 'forest_forest', 'stay in village': 'village_start'}, + "media": ["testmedia/fpe_person_looking_away_Listening_I_cant_hear_you_pose.webp", "testmedia/fpr_person_looking_away_Listening_hand_over_ear.webp", "testmedia/fpe_You_gather_supplies_for_your_journey.webp", "testmedia/fpe_head_to_forest.webp"] + }, + }, + 'forest': { + 'forest': { + "description": "You enter the dense forest, heading towards the ruins.", + "choices": ['travel further', 'return to village'], + "transitions": {'travel further': 'ruins_ruins', 'return to village': 'village_start'}, + "media": ["testmedia/fpe_village_start_collage.png", ] + }, + }, + 'ruins': { + 'ruins': { + "description": "You reach the ancient ruins. The entrance is dark and eerie.", + "choices": ['enter ruins', 'return to forest'], + "transitions": {'enter ruins': 'ruins_explore', 'return to forest': 'forest_forest'}, + "media": [] + }, + 'explore': { + "description": "You explore the ruins, encountering traps and puzzles.", + "choices": ['solve puzzle', 'avoid traps'], + "transitions": {'solve puzzle': 'ruins_hiddenPassage', 'avoid traps': 'ruins_ruins'}, + "media": [] + }, + 'hiddenPassage': { + "description": "You solve a challenging puzzle and unlock a hidden passage.", + "choices": ['enter passage', 'go back'], + "transitions": {'enter passage': 'ruins_treasureRoom', 'go back': 'ruins_explore'}, + "media": [] + }, + 'treasureRoom': { + "description": "You enter the treasure room and find the treasure chest.", + "choices": ['take treasure', 'leave'], + "transitions": {'take treasure': 'ruins_celebrate', 'leave': 'ruins_ruins'}, + "consequences": { + 'take treasure': lambda player: player.add_item('treasure') + }, + "media": [] + }, + 'celebrate': { + "description": "You celebrate your discovery and decide to bring the treasure back to the village.", + "choices": ['return to village'], + "transitions": {'return to village': 'village_return_return'}, + "media": [] + }, + }, + 'village_return': { + 'return': { + "description": "You return to the village with the treasure and share it with the villagers.", + "choices": ['end adventure'], + "transitions": {'end adventure': 'end_end'}, + "media": [] + }, + }, + 'end': { + 'end': { + "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", + "choices": [], + "transitions": {}, + "media": [] + }, + } +} + + +#-------------------------#-------------------------#-------------------------#------------------------- + +# Convenience exports for commonly used configs +LogicGatesDemo = ExampleGameConfigs.get("LogicGatesDemo", {}) +StateSystemReference = ExampleGameConfigs.get("StateSystemReference", {}) diff --git a/game_state.py b/game_state.py new file mode 100644 index 0000000000000000000000000000000000000000..cb15dac0ce3bfede9662af5ca8cdaa21d7a5889d --- /dev/null +++ b/game_state.py @@ -0,0 +1,377 @@ +""" +GameState - Rich state tracking for game configs with logic gates. + +Provides comprehensive tracking beyond the basic Player class: +- Inventory, money, knowledge (existing) +- People met, locations discovered +- Missions/quests (active, completed, failed) +- Boolean flags and numeric counters +- Choice history for analytics + +JSON-serializable for save/load functionality. +""" + +from typing import Dict, List, Set, Any, Optional +from dataclasses import dataclass, field +import json +import time + + +@dataclass +class GameState: + """ + Comprehensive game state tracking. + All fields are JSON-serializable for save/load. + """ + + # ==================== Character Resources (from existing Player) ==================== + inventory: List[str] = field(default_factory=list) + money: int = 0 + knowledge: Dict[str, Any] = field(default_factory=dict) + + # ==================== People/NPCs ==================== + people_met: Set[str] = field(default_factory=set) + npc_reputation: Dict[str, int] = field(default_factory=dict) # NPC -> affinity score + + # ==================== Locations ==================== + locations_visited: Set[str] = field(default_factory=set) + locations_discovered: Set[str] = field(default_factory=set) # Known but not visited + current_location: str = "" + current_state: str = "" + + # ==================== Missions/Quests ==================== + missions_active: Dict[str, Dict] = field(default_factory=dict) # id -> {status, progress, data} + missions_completed: Set[str] = field(default_factory=set) + missions_failed: Set[str] = field(default_factory=set) + + # ==================== Flags (boolean switches) ==================== + flags: Dict[str, bool] = field(default_factory=dict) + + # ==================== Counters (numeric trackers) ==================== + counters: Dict[str, int] = field(default_factory=dict) + + # ==================== History Tracking ==================== + choice_history: List[Dict] = field(default_factory=list) # [{state, choice, timestamp}] + state_visit_counts: Dict[str, int] = field(default_factory=dict) + + # ==================== Metadata ==================== + game_start_time: float = field(default_factory=time.time) + total_choices_made: int = 0 + + # ==================== Item Methods ==================== + + def add_item(self, item: str) -> None: + """Add an item to inventory (no duplicates).""" + if item not in self.inventory: + self.inventory.append(item) + + def add_items(self, items: List[str]) -> None: + """Add multiple items to inventory.""" + for item in items: + self.add_item(item) + + def remove_item(self, item: str) -> bool: + """Remove an item from inventory. Returns True if successful.""" + if item in self.inventory: + self.inventory.remove(item) + return True + return False + + def has_item(self, item: str) -> bool: + """Check if player has an item.""" + return item in self.inventory + + def item_count(self, item: str) -> int: + """Count occurrences of an item (for stackable items).""" + return self.inventory.count(item) + + # ==================== Money Methods ==================== + + def add_money(self, amount: int) -> None: + """Add money to player's balance.""" + self.money += amount + + def remove_money(self, amount: int) -> bool: + """Remove money. Returns True if player had enough.""" + if self.money >= amount: + self.money -= amount + return True + return False + + def has_money(self, amount: int) -> bool: + """Check if player has at least this much money.""" + return self.money >= amount + + # ==================== People Methods ==================== + + def meet_person(self, person: str) -> None: + """Mark an NPC as met.""" + self.people_met.add(person) + + def has_met(self, person: str) -> bool: + """Check if player has met an NPC.""" + return person in self.people_met + + def adjust_reputation(self, npc: str, change: int) -> None: + """Adjust reputation with an NPC.""" + self.npc_reputation[npc] = self.npc_reputation.get(npc, 0) + change + + def get_reputation(self, npc: str) -> int: + """Get reputation with an NPC (default 0).""" + return self.npc_reputation.get(npc, 0) + + # ==================== Location Methods ==================== + + def visit_location(self, location: str) -> None: + """Mark a location as visited.""" + self.locations_visited.add(location) + self.locations_discovered.discard(location) # No longer just discovered + + def discover_location(self, location: str) -> None: + """Mark a location as discovered (but not visited).""" + if location not in self.locations_visited: + self.locations_discovered.add(location) + + def has_visited(self, location: str) -> bool: + """Check if player has visited a location.""" + return location in self.locations_visited + + def has_discovered(self, location: str) -> bool: + """Check if player knows about a location (visited or discovered).""" + return location in self.locations_visited or location in self.locations_discovered + + # ==================== Mission Methods ==================== + + def start_mission(self, mission_id: str, data: Dict = None) -> None: + """Start a new mission/quest.""" + self.missions_active[mission_id] = data or {"status": "active", "progress": 0} + + def update_mission(self, mission_id: str, updates: Dict) -> None: + """Update mission data.""" + if mission_id in self.missions_active: + self.missions_active[mission_id].update(updates) + + def complete_mission(self, mission_id: str) -> None: + """Mark a mission as completed.""" + if mission_id in self.missions_active: + del self.missions_active[mission_id] + self.missions_completed.add(mission_id) + + def fail_mission(self, mission_id: str) -> None: + """Mark a mission as failed.""" + if mission_id in self.missions_active: + del self.missions_active[mission_id] + self.missions_failed.add(mission_id) + + def is_mission_complete(self, mission_id: str) -> bool: + """Check if a mission is completed.""" + return mission_id in self.missions_completed + + def is_mission_active(self, mission_id: str) -> bool: + """Check if a mission is currently active.""" + return mission_id in self.missions_active + + def is_mission_failed(self, mission_id: str) -> bool: + """Check if a mission has failed.""" + return mission_id in self.missions_failed + + def get_mission_data(self, mission_id: str) -> Optional[Dict]: + """Get data for an active mission.""" + return self.missions_active.get(mission_id) + + # ==================== Flag Methods ==================== + + def set_flag(self, flag: str, value: bool = True) -> None: + """Set a boolean flag.""" + self.flags[flag] = value + + def clear_flag(self, flag: str) -> None: + """Clear (set to False) a flag.""" + self.flags[flag] = False + + def has_flag(self, flag: str) -> bool: + """Check if a flag is set (True).""" + return self.flags.get(flag, False) + + def toggle_flag(self, flag: str) -> bool: + """Toggle a flag and return new value.""" + new_value = not self.flags.get(flag, False) + self.flags[flag] = new_value + return new_value + + # ==================== Counter Methods ==================== + + def set_counter(self, name: str, value: int) -> None: + """Set a counter to a specific value.""" + self.counters[name] = value + + def increment_counter(self, name: str, amount: int = 1) -> int: + """Increment a counter and return new value.""" + self.counters[name] = self.counters.get(name, 0) + amount + return self.counters[name] + + def decrement_counter(self, name: str, amount: int = 1) -> int: + """Decrement a counter and return new value.""" + return self.increment_counter(name, -amount) + + def get_counter(self, name: str, default: int = 0) -> int: + """Get a counter value.""" + return self.counters.get(name, default) + + # ==================== Knowledge Methods ==================== + + def update_knowledge(self, key: str, value: Any = True) -> None: + """Add or update knowledge.""" + self.knowledge[key] = value + + def has_knowledge(self, key: str) -> bool: + """Check if player has a knowledge entry.""" + return key in self.knowledge + + def get_knowledge(self, key: str, default: Any = None) -> Any: + """Get a knowledge value.""" + return self.knowledge.get(key, default) + + def remove_knowledge(self, key: str) -> None: + """Remove a knowledge entry.""" + self.knowledge.pop(key, None) + + # ==================== History Methods ==================== + + def record_choice(self, state: str, choice: str) -> None: + """Record a player choice for history tracking.""" + self.choice_history.append({ + "state": state, + "choice": choice, + "timestamp": time.time() + }) + self.total_choices_made += 1 + self.state_visit_counts[state] = self.state_visit_counts.get(state, 0) + 1 + + def get_visit_count(self, state: str) -> int: + """Get number of times a state was visited.""" + return self.state_visit_counts.get(state, 0) + + def get_last_choices(self, n: int = 5) -> List[Dict]: + """Get the last N choices made.""" + return self.choice_history[-n:] if self.choice_history else [] + + # ==================== Serialization ==================== + + def to_dict(self) -> Dict: + """Convert to JSON-serializable dict for saving.""" + return { + "inventory": self.inventory, + "money": self.money, + "knowledge": self.knowledge, + "people_met": list(self.people_met), + "npc_reputation": self.npc_reputation, + "locations_visited": list(self.locations_visited), + "locations_discovered": list(self.locations_discovered), + "current_location": self.current_location, + "current_state": self.current_state, + "missions_active": self.missions_active, + "missions_completed": list(self.missions_completed), + "missions_failed": list(self.missions_failed), + "flags": self.flags, + "counters": self.counters, + "choice_history": self.choice_history, + "state_visit_counts": self.state_visit_counts, + "game_start_time": self.game_start_time, + "total_choices_made": self.total_choices_made + } + + @classmethod + def from_dict(cls, data: Dict) -> 'GameState': + """Reconstruct GameState from dict (for loading saves).""" + state = cls() + state.inventory = data.get("inventory", []) + state.money = data.get("money", 0) + state.knowledge = data.get("knowledge", {}) + state.people_met = set(data.get("people_met", [])) + state.npc_reputation = data.get("npc_reputation", {}) + state.locations_visited = set(data.get("locations_visited", [])) + state.locations_discovered = set(data.get("locations_discovered", [])) + state.current_location = data.get("current_location", "") + state.current_state = data.get("current_state", "") + state.missions_active = data.get("missions_active", {}) + state.missions_completed = set(data.get("missions_completed", [])) + state.missions_failed = set(data.get("missions_failed", [])) + state.flags = data.get("flags", {}) + state.counters = data.get("counters", {}) + state.choice_history = data.get("choice_history", []) + state.state_visit_counts = data.get("state_visit_counts", {}) + state.game_start_time = data.get("game_start_time", time.time()) + state.total_choices_made = data.get("total_choices_made", 0) + return state + + def to_json(self) -> str: + """Serialize to JSON string.""" + return json.dumps(self.to_dict(), indent=2) + + @classmethod + def from_json(cls, json_str: str) -> 'GameState': + """Deserialize from JSON string.""" + return cls.from_dict(json.loads(json_str)) + + def reset(self) -> None: + """Reset state to initial values (for new game).""" + self.inventory.clear() + self.money = 0 + self.knowledge.clear() + self.people_met.clear() + self.npc_reputation.clear() + self.locations_visited.clear() + self.locations_discovered.clear() + self.current_location = "" + self.current_state = "" + self.missions_active.clear() + self.missions_completed.clear() + self.missions_failed.clear() + self.flags.clear() + self.counters.clear() + self.choice_history.clear() + self.state_visit_counts.clear() + self.game_start_time = time.time() + self.total_choices_made = 0 + + +class Player: + """ + Backwards-compatible Player class that wraps GameState. + Existing configs using lambda consequences will continue to work. + """ + + def __init__(self, game_state: GameState = None): + self._state = game_state or GameState() + + # Existing API (unchanged for backwards compatibility) + @property + def inventory(self) -> List[str]: + return self._state.inventory + + @property + def money(self) -> int: + return self._state.money + + @money.setter + def money(self, value: int): + self._state.money = value + + @property + def knowledge(self) -> Dict[str, Any]: + return self._state.knowledge + + def add_item(self, item: str) -> None: + self._state.add_item(item) + + def has_item(self, item: str) -> bool: + return self._state.has_item(item) + + def update_knowledge(self, topic: str) -> None: + self._state.update_knowledge(topic, True) + + # NEW: Expose full state for advanced usage + @property + def game_state(self) -> GameState: + return self._state diff --git a/gemma-2-2b_12-gemmascope-res-16k_4667.json b/gemma-2-2b_12-gemmascope-res-16k_4667.json new file mode 100644 index 0000000000000000000000000000000000000000..0605a909e9afba082f6eb60a13798600c78a008a --- /dev/null +++ b/gemma-2-2b_12-gemmascope-res-16k_4667.json @@ -0,0 +1,2313 @@ +{ + "modelId": "gemma-2-2b", + "layer": "12-gemmascope-res-16k", + "index": "4667", + "vectorLabel": "", + "vector": [ + 0.0223388671875, + 0.057373046875, + 0.1640625, + 0.1611328125, + -0.031494140625, + -0.00127410888671875, + 0.058349609375, + -0.029052734375, + -0.02490234375, + -0.052490234375, + -0.00115203857421875, + 0.0281982421875, + -0.0556640625, + -0.045654296875, + -0.0791015625, + 0.05322265625, + 0.0888671875, + 0.09228515625, + -0.032470703125, + 0.099609375, + 0.03515625, + 0.0038909912109375, + -0.04931640625, + 0.04541015625, + 0.07763671875, + -0.08203125, + 0.08984375, + -0.017578125, + -0.0986328125, + 0.049072265625, + 0.02880859375, + 0.140625, + -0.12158203125, + -0.00185394287109375, + -0.036376953125, + -0.02197265625, + -0.0859375, + 0.0244140625, + -0.08984375, + 0.032958984375, + 0.025634765625, + 0.029296875, + 0.162109375, + 0.029052734375, + -0.006011962890625, + -0.083984375, + 0.060546875, + -0.08935546875, + -0.035400390625, + 0.10009765625, + -0.0203857421875, + -0.047607421875, + -0.064453125, + -0.059326171875, + -0.10205078125, + -0.064453125, + -0.1962890625, + 0.0167236328125, + 0.05810546875, + -0.1298828125, + 0.00958251953125, + -0.09423828125, + 0.008544921875, + 0.1171875, + 0.05322265625, + 0.177734375, + -0.080078125, + -0.021728515625, + 0.00885009765625, + -0.138671875, + -0.0458984375, + -0.002105712890625, + 0.1083984375, + -0.06396484375, + -0.05029296875, + 0.0228271484375, + 0.0771484375, + 0.1337890625, + -0.1865234375, + -0.09716796875, + -0.020751953125, + -0.003509521484375, + 0.0233154296875, + -0.00823974609375, + 0.052001953125, + 0.08203125, + -0.00836181640625, + -0.052490234375, + 0.08740234375, + -0.00823974609375, + 0.00933837890625, + 0.03271484375, + -0.0556640625, + 0.0220947265625, + -0.08447265625, + -0.00897216796875, + 0.09375, + -0.0751953125, + 0.0125732421875, + 0.037353515625, + -0.047607421875, + 0.0286865234375, + -0.1025390625, + -0.12060546875, + 0.2265625, + 0.09375, + -0.049072265625, + -0.0888671875, + 0.0169677734375, + 0.07275390625, + 0.055908203125, + -0.1376953125, + 0.003997802734375, + 0.12451171875, + -0.004852294921875, + -0.035400390625, + -0.021240234375, + 0.0029754638671875, + 0.10498046875, + -0.0087890625, + 0.0869140625, + -0.05859375, + 0.03466796875, + -0.051025390625, + -0.08984375, + -0.1328125, + -0.0908203125, + 0.0262451171875, + -0.0255126953125, + 0.1357421875, + -0.050048828125, + -0.08740234375, + -0.0595703125, + 0.052001953125, + 0.0001239776611328125, + -0.09033203125, + -0.08056640625, + -0.017333984375, + -0.0299072265625, + 0.00176239013671875, + 0.142578125, + 0.034912109375, + 0.10205078125, + -0.07666015625, + -0.1328125, + 0.06884765625, + 0.087890625, + 0.09423828125, + -0.08349609375, + -0.11376953125, + -0.08154296875, + -0.046875, + -0.03271484375, + 0.13671875, + 0.07470703125, + 0.0556640625, + 0.09130859375, + 0.01348876953125, + 0.061279296875, + -0.044189453125, + 0.0673828125, + -0.0322265625, + -0.0478515625, + 0.00628662109375, + -0.0284423828125, + -0.1181640625, + -0.052001953125, + -0.007171630859375, + -0.00982666015625, + -0.07666015625, + 0.05322265625, + -0.029541015625, + -0.047119140625, + 0.0269775390625, + 0.095703125, + 0.1298828125, + 0.072265625, + -0.0111083984375, + 0.173828125, + 0.0301513671875, + 0.0654296875, + -0.017333984375, + 0.0458984375, + -0.0189208984375, + 0.07763671875, + -0.0069580078125, + -0.07861328125, + -0.0458984375, + -0.07568359375, + -0.004150390625, + 0.11181640625, + 0.0286865234375, + 0.0279541015625, + -0.00885009765625, + -0.0016326904296875, + 0.0966796875, + 0.0625, + -0.06787109375, + 0.0206298828125, + 0.0439453125, + 0.0673828125, + 0.0830078125, + -0.0311279296875, + -0.09033203125, + -0.08203125, + 0.0732421875, + 0.0869140625, + -0.008544921875, + -0.0654296875, + -0.06494140625, + -0.08447265625, + -0.03857421875, + 0.01275634765625, + 0.0172119140625, + 0.212890625, + 0.0286865234375, + -0.00885009765625, + 0.051025390625, + -0.07861328125, + 0.1396484375, + -0.0791015625, + 0.005157470703125, + -0.046142578125, + -0.1416015625, + 0.05029296875, + -0.0091552734375, + 0.024169921875, + 0.2236328125, + -0.0198974609375, + 0.00640869140625, + -0.0908203125, + -0.08251953125, + 0.0242919921875, + -0.057373046875, + -0.048828125, + -0.11083984375, + -0.08251953125, + 0.087890625, + 0.056884765625, + 0.034423828125, + 0.017822265625, + -0.055908203125, + 0.023193359375, + 0.000850677490234375, + -0.0390625, + 0.038330078125, + 0.057861328125, + -0.01263427734375, + 0.064453125, + -0.080078125, + -0.01220703125, + 0.06982421875, + 0.04443359375, + -0.02001953125, + -0.021240234375, + 0.06787109375, + -0.019287109375, + -0.1357421875, + 0.050048828125, + -0.0279541015625, + -0.09716796875, + -0.08740234375, + 0.005462646484375, + -0.05517578125, + 0.049072265625, + -0.10986328125, + -0.04248046875, + 0.01519775390625, + -0.07373046875, + -0.099609375, + -0.07568359375, + -0.056884765625, + -0.014404296875, + 0.0274658203125, + -0.03076171875, + 0.058349609375, + 0.042236328125, + -0.06298828125, + 0.060302734375, + 0.1328125, + 0.1005859375, + -0.022705078125, + -0.032958984375, + -0.177734375, + -0.0849609375, + 0.11083984375, + 0.003936767578125, + 0.064453125, + -0.0087890625, + 0.2001953125, + -0.04296875, + -0.0712890625, + -0.0791015625, + -0.1953125, + 0.0810546875, + 0.10595703125, + -0.00738525390625, + 0.10302734375, + 0.2431640625, + 0.0634765625, + -0.0810546875, + 0.142578125, + 0.0093994140625, + 0.057861328125, + -0.04736328125, + 0.09716796875, + -0.023681640625, + 0.025146484375, + -0.080078125, + 0.103515625, + 0.0311279296875, + 0.051025390625, + -0.142578125, + -0.09033203125, + 0.0908203125, + -0.04248046875, + 0.0198974609375, + 0.028564453125, + 0.06591796875, + -0.007781982421875, + 0.04150390625, + 0.0272216796875, + -0.0031585693359375, + -0.1728515625, + -0.09912109375, + 0.051513671875, + 0.0751953125, + -0.09912109375, + 0.05419921875, + 0.181640625, + 0.0091552734375, + 0.042236328125, + 0.18359375, + 0.0458984375, + 0.1162109375, + -0.02685546875, + 0.006195068359375, + 0.1533203125, + 0.0546875, + 0.0172119140625, + 0.076171875, + 0.07470703125, + 0.046142578125, + -0.06982421875, + -0.0517578125, + -0.06396484375, + -0.10302734375, + 0.025146484375, + 0.0693359375, + 0.08056640625, + 0.01470947265625, + 0.00653076171875, + 0.0517578125, + 0.126953125, + 0.02783203125, + -0.0274658203125, + -0.01806640625, + -0.10302734375, + 0.028076171875, + -0.1337890625, + 0.033203125, + 0.042236328125, + -0.01806640625, + 0.015380859375, + 0.0576171875, + 0.09521484375, + 0.06884765625, + 0.0133056640625, + -0.049560546875, + -0.0255126953125, + -0.10302734375, + -0.0157470703125, + -0.1015625, + 0.0732421875, + -0.039306640625, + -0.0284423828125, + 0.1787109375, + 0.1015625, + 0.000530242919921875, + -0.0089111328125, + 0.042724609375, + -0.07373046875, + 0.025390625, + 0.0140380859375, + 0.06298828125, + -0.060546875, + 0.1142578125, + 0.0086669921875, + -0.019287109375, + 0.01080322265625, + 0.059326171875, + -0.0079345703125, + -0.01043701171875, + -0.0673828125, + 0.01318359375, + 0.000911712646484375, + -0.01043701171875, + -0.007110595703125, + 0.09521484375, + -0.039306640625, + -0.0146484375, + 0.0003814697265625, + -0.034423828125, + -0.0029754638671875, + 0.010498046875, + 0.0126953125, + 0.00165557861328125, + 0.0830078125, + -0.048583984375, + 0.107421875, + -0.038330078125, + -0.08935546875, + -0.048095703125, + -0.00982666015625, + 0.054443359375, + 0.0240478515625, + -0.08837890625, + 0.06591796875, + 0.0771484375, + -0.000919342041015625, + 0.275390625, + 0.08837890625, + 0.0791015625, + -0.0986328125, + -0.076171875, + -0.040283203125, + 0.0751953125, + -0.030517578125, + -0.017822265625, + 0.08740234375, + -0.051025390625, + 0.045166015625, + -0.041748046875, + 0.0537109375, + -0.030029296875, + 0.08447265625, + 0.031982421875, + -0.0164794921875, + -0.0169677734375, + 0.10986328125, + 0.0181884765625, + -0.041259765625, + -0.029296875, + 0.005279541015625, + 0.10302734375, + 0.10546875, + -0.1826171875, + 0.1015625, + -0.031982421875, + 0.140625, + 0.07861328125, + 0.057373046875, + -0.0118408203125, + -0.08154296875, + 0.130859375, + 0.07177734375, + -0.0093994140625, + 0.00970458984375, + 0.09619140625, + 0.04638671875, + -0.005706787109375, + -0.1748046875, + 0.0150146484375, + 0.08740234375, + -0.01397705078125, + 0.0947265625, + 0.050537109375, + 0.07763671875, + -0.1015625, + 0.2021484375, + 0.0732421875, + -0.050537109375, + -0.2099609375, + 0.0177001953125, + -0.08349609375, + 0.001800537109375, + 0.0189208984375, + 0.005218505859375, + 0.1884765625, + 0.03271484375, + 0.01080322265625, + -0.030029296875, + -0.004974365234375, + -0.09912109375, + 0.006561279296875, + -0.06689453125, + 0.0184326171875, + -0.0341796875, + 0.1298828125, + 0.06787109375, + -0.06396484375, + 0.0164794921875, + 0.2041015625, + -0.00078582763671875, + 0.0625, + 0.1142578125, + -0.002532958984375, + 0.027587890625, + 0.01495361328125, + 0.0274658203125, + 0.00408935546875, + 0.06298828125, + 0.030517578125, + 0.0771484375, + -0.06884765625, + -0.01171875, + 0.00628662109375, + -0.031982421875, + -0.1572265625, + -0.0400390625, + -0.046142578125, + -0.177734375, + -0.01336669921875, + 0.0576171875, + 0.025390625, + -0.009521484375, + -0.057373046875, + 0.091796875, + -0.06640625, + 0.0128173828125, + -0.055419921875, + -0.0147705078125, + 0.0096435546875, + 0.0206298828125, + -0.018310546875, + -0.04248046875, + 0.0242919921875, + -0.150390625, + -0.06298828125, + -0.031494140625, + -0.06689453125, + -0.01904296875, + -0.052734375, + -0.03369140625, + -0.080078125, + -0.376953125, + 0.031494140625, + -0.00136566162109375, + -0.0234375, + 0.0184326171875, + 0.09814453125, + -0.12890625, + 0.0693359375, + -0.12255859375, + 0.041015625, + 0.0191650390625, + -0.041259765625, + -0.040283203125, + -0.009521484375, + -0.09423828125, + 0.0703125, + 0.123046875, + -0.037353515625, + 0.08056640625, + -0.02392578125, + -0.046630859375, + 0.103515625, + -0.018310546875, + -0.09130859375, + 0.0634765625, + -0.130859375, + 0.0128173828125, + -0.0296630859375, + -0.09619140625, + 0.0859375, + -0.0115966796875, + 0.07763671875, + 0.08984375, + -0.08544921875, + 0.0791015625, + 0.0186767578125, + -0.019287109375, + -0.103515625, + -0.06591796875, + -0.109375, + -0.0174560546875, + -0.056396484375, + 0.00701904296875, + 0.025634765625, + 0.0400390625, + -0.0084228515625, + 0.07373046875, + -0.0498046875, + 0.0146484375, + -0.0206298828125, + -0.053955078125, + -0.03857421875, + -0.1943359375, + 0.1162109375, + -0.018798828125, + -0.03466796875, + 0.0260009765625, + -0.16015625, + -0.0257568359375, + -0.06396484375, + 0.023193359375, + 0.04638671875, + -0.0673828125, + -0.003692626953125, + 0.028076171875, + -0.0380859375, + -0.01806640625, + 0.0257568359375, + -0.038330078125, + 0.126953125, + -0.0027923583984375, + 0.0830078125, + -0.0103759765625, + 0.0908203125, + -0.048095703125, + -0.00555419921875, + 0.0081787109375, + -0.08642578125, + -0.0615234375, + -0.01055908203125, + -0.033935546875, + -0.041015625, + 0.0810546875, + 0.076171875, + -0.095703125, + -0.06787109375, + 0.171875, + -0.00738525390625, + -0.037109375, + 0.064453125, + -0.052978515625, + -0.0076904296875, + -0.0615234375, + 0.0537109375, + -0.10693359375, + 0.0230712890625, + -0.09521484375, + 0.041259765625, + 0.08984375, + 0.0048828125, + -0.0361328125, + -0.016845703125, + 0.028076171875, + -0.12158203125, + 0.027099609375, + -0.0022125244140625, + -0.0625, + -0.0791015625, + 0.0040283203125, + 0.2001953125, + 0.01226806640625, + 0.000743865966796875, + 0.027587890625, + -0.0181884765625, + 0.0284423828125, + -0.03955078125, + -0.0184326171875, + -0.0361328125, + 0.06103515625, + -0.07470703125, + 0.00439453125, + 0.0145263671875, + -0.080078125, + -0.08349609375, + -0.0025634765625, + 0.1328125, + -0.037109375, + 0.091796875, + -0.00194549560546875, + 0.0654296875, + -0.1591796875, + 0.056884765625, + 0.05322265625, + -0.038818359375, + 0.000385284423828125, + -0.0250244140625, + -0.0196533203125, + 0.00191497802734375, + 0.15625, + -0.10498046875, + -0.021484375, + 0.06005859375, + 0.06982421875, + 0.076171875, + -0.0869140625, + -0.0205078125, + 0.07666015625, + 0.111328125, + -0.1044921875, + 0.0732421875, + 0.09716796875, + 0.06298828125, + -0.007598876953125, + 0.0458984375, + -0.0203857421875, + -0.002593994140625, + 0.013671875, + 0.0091552734375, + 0.0118408203125, + 0.05712890625, + -0.09375, + 0.09423828125, + -0.07861328125, + 0.001739501953125, + -0.07373046875, + -0.08935546875, + -0.04345703125, + -0.062255859375, + 0.0712890625, + 0.01373291015625, + -0.10888671875, + 0.08935546875, + 0.03271484375, + 0.1142578125, + 0.134765625, + -0.031494140625, + -0.044677734375, + -0.12353515625, + -0.0029296875, + 0.12890625, + 0.04736328125, + -0.08154296875, + -0.0869140625, + 0.09375, + 0.1396484375, + 0.08984375, + 0.0732421875, + 0.005950927734375, + 0.00799560546875, + -0.0245361328125, + -0.0012969970703125, + 0.03955078125, + -0.060546875, + -0.08056640625, + 0.041015625, + 0.05029296875, + 0.056884765625, + 0.0147705078125, + 0.095703125, + -0.05859375, + 0.09521484375, + -0.00994873046875, + -0.038330078125, + -0.03271484375, + 0.0016326904296875, + -0.044921875, + 0.1279296875, + -0.00634765625, + -0.12890625, + -0.08251953125, + -0.1279296875, + -0.01531982421875, + -0.08056640625, + -0.1044921875, + 0.0576171875, + 0.005767822265625, + 0.03759765625, + 0.03515625, + -0.05078125, + -0.0693359375, + 0.007080078125, + -0.068359375, + 0.03369140625, + -0.03466796875, + -0.08251953125, + -0.0361328125, + -0.08056640625, + -0.0888671875, + -0.016357421875, + 0.037841796875, + -0.03564453125, + 0.057861328125, + -0.12890625, + 0.043212890625, + 0.0478515625, + 0.0184326171875, + 0.11181640625, + 0.09814453125, + 0.0252685546875, + -0.015869140625, + -0.041748046875, + -0.005767822265625, + 0.09326171875, + -0.1396484375, + -0.01153564453125, + -0.03564453125, + -0.1015625, + 0.0169677734375, + 0.07470703125, + -0.0771484375, + -0.0625, + 0.034912109375, + 0.024169921875, + -0.1484375, + -0.09716796875, + -0.04345703125, + -0.029541015625, + 0.10791015625, + -0.0830078125, + 0.1728515625, + 0.1201171875, + -0.0189208984375, + -0.016845703125, + -0.11328125, + -0.0830078125, + 0.00823974609375, + 0.02685546875, + -0.1416015625, + -0.0205078125, + -0.111328125, + 0.0130615234375, + 0.034423828125, + 0.05029296875, + 0.09033203125, + -0.126953125, + -0.02880859375, + -0.041015625, + 0.019775390625, + 0.06396484375, + 0.01434326171875, + -0.00439453125, + -0.11474609375, + 0.0015411376953125, + -0.011962890625, + 0.026123046875, + -0.0281982421875, + 0.0546875, + 0.095703125, + -0.05322265625, + -0.09375, + 0.0284423828125, + 0.03173828125, + 0.044921875, + -0.09130859375, + -0.1953125, + -0.08544921875, + -0.1015625, + 0.0537109375, + 0.033935546875, + 0.07763671875, + 0.06298828125, + 0.07763671875, + -0.0196533203125, + -0.0216064453125, + -0.080078125, + 0.11767578125, + -0.0751953125, + 0.023681640625, + -0.0299072265625, + -0.0233154296875, + 0.05615234375, + -0.01806640625, + 0.0120849609375, + 0.0869140625, + -0.019287109375, + -0.046630859375, + -0.07275390625, + -0.059814453125, + -0.0322265625, + 0.00299072265625, + 0.04443359375, + -0.016845703125, + -0.0673828125, + -0.08251953125, + 0.126953125, + 0.06982421875, + 0.054443359375, + 0.080078125, + -0.0260009765625, + -0.08203125, + 0.0498046875, + -0.02734375, + 0.046142578125, + -0.0546875, + 0.0225830078125, + -0.10546875, + 0.1787109375, + -0.0079345703125, + 0.0001735687255859375, + 0.0021514892578125, + 0.04150390625, + -0.0101318359375, + -0.06982421875, + -0.0966796875, + -0.03515625, + 0.034912109375, + -0.040283203125, + -0.00787353515625, + -0.12890625, + -0.1259765625, + -0.0011138916015625, + -0.11865234375, + -0.06640625, + -0.056884765625, + -0.177734375, + 0.00787353515625, + -0.1005859375, + 0.006103515625, + -0.07470703125, + 0.054443359375, + 0.01171875, + -0.01287841796875, + -0.2041015625, + 0.01080322265625, + 0.043212890625, + 0.07080078125, + -0.107421875, + 0.00653076171875, + -0.09326171875, + -0.10205078125, + 0.004608154296875, + 0.0537109375, + -0.00830078125, + 0.08447265625, + -0.0634765625, + -0.021728515625, + -0.087890625, + 0.1796875, + -0.060302734375, + 0.1103515625, + 0.03857421875, + -0.07763671875, + 0.0257568359375, + 0.004913330078125, + 0.018310546875, + -0.02490234375, + -0.041259765625, + 0.038330078125, + -0.00142669677734375, + -0.0262451171875, + -0.0255126953125, + 0.045166015625, + -0.1611328125, + -0.091796875, + -0.0208740234375, + -0.12158203125, + -0.07275390625, + -0.00885009765625, + -0.053466796875, + 0.11669921875, + 0.0849609375, + 0.02783203125, + 0.059326171875, + -0.049072265625, + -0.021240234375, + 0.0203857421875, + 0.061279296875, + -0.001220703125, + -0.048583984375, + -0.06005859375, + 0.00927734375, + 0.020263671875, + -0.009033203125, + -0.091796875, + -0.05712890625, + 0.080078125, + 0.01220703125, + 0.10791015625, + -0.02685546875, + -0.115234375, + 0.032958984375, + 0.037109375, + -0.0244140625, + -0.0986328125, + 0.0257568359375, + -0.06787109375, + -0.035400390625, + -0.0673828125, + 0.0079345703125, + -0.0164794921875, + 0.040283203125, + -0.0693359375, + 0.011474609375, + -0.07275390625, + 0.00112152099609375, + 0.06103515625, + -0.068359375, + -0.10986328125, + -0.0076904296875, + 0.08251953125, + -0.09228515625, + 0.036865234375, + 0.07275390625, + -0.0654296875, + 0.08251953125, + 0.0267333984375, + -0.0252685546875, + 0.034423828125, + 0.048095703125, + 0.0126953125, + 0.05810546875, + -0.01141357421875, + 0.076171875, + 0.06982421875, + -0.03857421875, + 0.05126953125, + -0.08447265625, + 0.013671875, + -0.03125, + -0.1298828125, + 0.154296875, + 0.038330078125, + -0.0235595703125, + 0.062255859375, + -0.0400390625, + 0.11279296875, + -0.0751953125, + -0.1025390625, + 0.1162109375, + 0.07373046875, + -0.04345703125, + -0.0064697265625, + -0.0096435546875, + 0.03955078125, + 0.01129150390625, + -0.09521484375, + -0.06884765625, + 0.09619140625, + -0.1259765625, + 0.01953125, + 0.04931640625, + -0.037109375, + -0.034423828125, + 0.0230712890625, + -0.03173828125, + 0.045166015625, + 0.026123046875, + -0.00113677978515625, + -0.09130859375, + -0.1494140625, + -0.10107421875, + 0.1728515625, + -0.06298828125, + -0.021484375, + 0.0712890625, + -0.07080078125, + 0.005767822265625, + 0.036865234375, + 0.038330078125, + 0.02001953125, + -0.1865234375, + 0.19140625, + 0.0269775390625, + 0.01806640625, + -0.03564453125, + 0.05712890625, + 0.0595703125, + 0.06640625, + 0.259765625, + -0.050048828125, + 0.005615234375, + -0.0634765625, + -0.0390625, + 0.051025390625, + 0.005157470703125, + -0.1396484375, + 0.045166015625, + -0.06494140625, + -0.11328125, + -0.125, + 0.1328125, + 0.047119140625, + -0.12353515625, + -0.08984375, + 0.00384521484375, + 0.06591796875, + 0.2431640625, + -0.051025390625, + -0.032958984375, + 0.07958984375, + 0.0179443359375, + -0.10302734375, + 0.0286865234375, + -0.06884765625, + -0.025634765625, + 0.0211181640625, + 0.080078125, + -0.000881195068359375, + 0.0419921875, + -0.033935546875, + 0.0086669921875, + 0.01495361328125, + 0.07666015625, + -0.060302734375, + 0.08984375, + -0.02099609375, + -0.049072265625, + 0.0225830078125, + -0.0537109375, + -0.032470703125, + -0.1494140625, + 0.022705078125, + 0.0712890625, + -0.026611328125, + -0.014404296875, + 0.158203125, + -0.043701171875, + -0.03662109375, + 0.0301513671875, + 0.035400390625, + 0.091796875, + -0.16796875, + -0.06298828125, + -0.037109375, + -0.12890625, + -0.1748046875, + -0.0458984375, + 0.109375, + -0.10595703125, + -0.044677734375, + 0.052978515625, + 0.000606536865234375, + 0.0098876953125, + 0.000606536865234375, + -0.0225830078125, + -0.048828125, + 0.042236328125, + -0.045166015625, + 0.0203857421875, + 0.02001953125, + 0.08349609375, + -0.0057373046875, + 0.07275390625, + 0.044189453125, + 0.0238037109375, + 0.009033203125, + -0.0228271484375, + -0.03125, + -0.039794921875, + -0.07568359375, + -0.0084228515625, + -0.0634765625, + 0.0098876953125, + 0.039794921875, + 0.08642578125, + 0.11376953125, + -0.052001953125, + 0.016357421875, + -0.0390625, + 0.01300048828125, + 0.0191650390625, + 0.0032196044921875, + 0.00750732421875, + -0.031494140625, + -0.0189208984375, + -0.08642578125, + 0.07958984375, + 0.107421875, + 0.1748046875, + 0.08544921875, + -0.0211181640625, + -0.005462646484375, + 0.041259765625, + 0.154296875, + -0.1845703125, + 0.08740234375, + -0.1201171875, + 0.0036468505859375, + -0.203125, + -0.052490234375, + -0.1201171875, + 0.06689453125, + -0.06982421875, + -0.0322265625, + -0.17578125, + 0.09375, + -0.043212890625, + -0.0230712890625, + -0.15234375, + 0.126953125, + 0.000263214111328125, + 0.0286865234375, + -0.12890625, + 0.09228515625, + 0.1826171875, + -0.11328125, + 0.0849609375, + -0.028564453125, + 0.032470703125, + 0.046875, + 0.06982421875, + -0.19140625, + -0.024169921875, + -0.1357421875, + -0.1279296875, + -0.0238037109375, + 0.08642578125, + 0.0625, + -0.0257568359375, + -0.11767578125, + -0.032470703125, + 0.1123046875, + -0.05078125, + 0.03662109375, + -0.046875, + -0.02685546875, + 0.0032196044921875, + -0.01190185546875, + 0.048583984375, + -0.048583984375, + -0.025146484375, + 0.0673828125, + -0.01263427734375, + -0.01904296875, + -0.003570556640625, + 0.0286865234375, + -0.054931640625, + -0.046142578125, + -0.045654296875, + 0.04443359375, + -0.0162353515625, + -0.036865234375, + -0.08154296875, + -0.0732421875, + -0.0133056640625, + -0.053466796875, + 0.0269775390625, + -0.05126953125, + -0.030029296875, + -0.011962890625, + 0.0400390625, + -0.07080078125, + -0.111328125, + -0.0625, + -0.02587890625, + -0.034423828125, + -0.1025390625, + 0.0250244140625, + -0.021484375, + 0.06494140625, + 0.023681640625, + 0.0947265625, + -0.01312255859375, + 0.056884765625, + 0.032470703125, + 0.05224609375, + 0.0400390625, + -0.08251953125, + 0.01336669921875, + 0.1728515625, + -0.03564453125, + -0.0198974609375, + -0.062255859375, + 0.004638671875, + 0.06787109375, + 0.04541015625, + 0.1484375, + 0.0208740234375, + 0.054931640625, + -0.06005859375, + -0.0023040771484375, + 0.055908203125, + -0.04443359375, + 0.04541015625, + 0.30078125, + 0.08837890625, + 0.027099609375, + 0.0341796875, + -0.062255859375, + -0.043212890625, + 0.0303955078125, + -0.005096435546875, + -0.034423828125, + -0.00860595703125, + -0.1953125, + -0.0019989013671875, + -0.087890625, + 0.022216796875, + 0.07666015625, + -0.046142578125, + -0.0133056640625, + -0.00811767578125, + -0.0096435546875, + -0.08837890625, + -0.04052734375, + 0.0238037109375, + 0.041259765625, + 0.07568359375, + -0.035400390625, + -0.107421875, + 0.0595703125, + -0.006927490234375, + 0.0478515625, + -0.0966796875, + 0.04150390625, + -0.064453125, + -0.111328125, + -0.06298828125, + 0.0264892578125, + 0.013916015625, + 0.052734375, + 0.08740234375, + -0.0966796875, + -0.083984375, + 0.11572265625, + 0.036376953125, + -0.056884765625, + -0.022216796875, + 0.057861328125, + 0.024169921875, + 0.11669921875, + -0.08935546875, + 0.06494140625, + 0.034423828125, + 0.134765625, + 0.0751953125, + 0.0966796875, + 0.046875, + 0.0223388671875, + 0.06005859375, + 0.052734375, + 0.1005859375, + 0.03857421875, + 0.0230712890625, + 0.0966796875, + 0.08740234375, + 0.11083984375, + 0.057861328125, + 0.006317138671875, + 0.040771484375, + -0.01226806640625, + 0.09619140625, + -0.06591796875, + 0.0269775390625, + 0.09765625, + -0.01385498046875, + -0.06787109375, + -0.02392578125, + 0.000659942626953125, + -0.02294921875, + -0.04931640625, + -0.0157470703125, + -0.0289306640625, + 0.1484375, + 0.03271484375, + -0.1083984375, + -0.0517578125, + -0.0218505859375, + -0.1318359375, + 0.083984375, + 0.04833984375, + -0.07568359375, + -0.033447265625, + 0.054443359375, + -0.032470703125, + 0.01055908203125, + -0.034912109375, + -0.0732421875, + 0.00897216796875, + 0.13671875, + -0.08935546875, + -0.00592041015625, + 0.1279296875, + 0.0250244140625, + -0.045166015625, + 0.00567626953125, + -0.10009765625, + 0.16015625, + 0.02783203125, + 0.06298828125, + -0.0888671875, + -0.05712890625, + -0.035888671875, + -0.0322265625, + 0.00927734375, + 0.037109375, + 0.0390625, + -0.16796875, + -0.166015625, + -0.005462646484375, + -0.06298828125, + 0.00775146484375, + 0.08984375, + -0.0888671875, + -0.04931640625, + -0.07373046875, + -0.14453125, + 0.06591796875, + 0.00014781951904296875, + -0.039306640625, + -0.0400390625, + 0.002532958984375, + 0.07568359375, + -0.046630859375, + -0.1357421875, + -0.134765625, + -0.09619140625, + 0.059326171875, + -0.038330078125, + 0.1533203125, + 0.027587890625, + 0.1318359375, + 0.1923828125, + 0.068359375, + 0.001007080078125, + -0.09423828125, + 0.111328125, + -0.019775390625, + -0.11083984375, + 0.0062255859375, + 0.018310546875, + -0.0225830078125, + -0.1259765625, + -0.004119873046875, + -0.08642578125, + -0.0174560546875, + 0.083984375, + 0.051513671875, + 0.00836181640625, + 0.06689453125, + -0.058837890625, + 0.046875, + -0.032958984375, + -0.01556396484375, + 0.037109375, + 0.04833984375, + -0.00096893310546875, + 0.0004329681396484375, + -0.0546875, + 0.06298828125, + -0.05810546875, + 0.007354736328125, + 0.07958984375, + 0.07470703125, + 0.00095367431640625, + -0.056396484375, + -0.0174560546875, + 0.04931640625, + 0.02783203125, + 0.01806640625, + 0.091796875, + -0.06591796875, + -0.0167236328125, + -0.08837890625, + -0.0322265625, + -0.0079345703125, + 0.045654296875, + 0.080078125, + -0.0615234375, + 0.061279296875, + -0.005279541015625, + 0.0712890625, + -0.048095703125, + 0.03662109375, + -0.1416015625, + 0.1318359375, + 0.036865234375, + -0.043212890625, + 0.0810546875, + -0.0498046875, + 0.0233154296875, + 0.06494140625, + 0.0791015625, + -0.03759765625, + 0.010986328125, + 0.099609375, + 0.05078125, + 0.0101318359375, + 0.017578125, + 0.12158203125, + -0.07080078125, + 0.000965118408203125, + 0.0810546875, + -0.111328125, + 0.006622314453125, + -0.06298828125, + -0.30078125, + 0.09619140625, + 0.0267333984375, + 0.0184326171875, + -0.03466796875, + -0.056640625, + 0.052001953125, + 0.02587890625, + 0.026123046875, + 0.0235595703125, + 0.042236328125, + 0.042724609375, + -0.0771484375, + 0.06298828125, + -0.0419921875, + -0.0888671875, + 0.0927734375, + 0.0751953125, + 0.0732421875, + 0.01544189453125, + -0.00970458984375, + -0.00933837890625, + -0.0242919921875, + -0.10595703125, + -0.049560546875, + -0.049560546875, + -0.003753662109375, + -0.0908203125, + -0.06298828125, + 0.01287841796875, + -0.0186767578125, + 0.07275390625, + -0.11279296875, + 0.04736328125, + 0.13671875, + 0.041748046875, + 0.06640625, + -0.0194091796875, + -0.050048828125, + -0.08203125, + -0.142578125, + -0.05859375, + -0.014892578125, + -0.056884765625, + -0.07275390625, + -0.068359375, + 0.020751953125, + -0.0186767578125, + 0.07568359375, + 0.05615234375, + -0.01519775390625, + -0.031982421875, + -0.138671875, + -0.111328125, + 0.041748046875, + -0.04833984375, + 0.021728515625, + -0.045166015625, + 0.043701171875, + -0.00811767578125, + 0.021484375, + 0.07861328125, + -0.02001953125, + 0.023681640625, + -0.09765625, + -0.0208740234375, + -0.12890625, + 0.09912109375, + -0.10009765625, + 0.0625, + 0.0186767578125, + -0.12451171875, + -0.140625, + -0.12109375, + 0.0081787109375, + 0.0208740234375, + -0.0751953125, + 0.0247802734375, + -0.02294921875, + -0.019775390625, + 0.02294921875, + -0.07861328125, + 0.027099609375, + -0.09716796875, + 0.058837890625, + 0.01068115234375, + 0.1767578125, + -0.03955078125, + 0.057861328125, + 0.0225830078125, + -0.03564453125, + 0.041748046875, + -0.0498046875, + 0.115234375, + 0.1044921875, + 0.051025390625, + 0.10791015625, + 0.07568359375, + -0.011474609375, + -0.051513671875, + -0.11376953125, + 0.09130859375, + -0.1318359375, + -0.047607421875, + 0.00982666015625, + -0.017822265625, + -0.0537109375, + -0.0242919921875, + -0.0390625, + -0.0888671875, + -0.11572265625, + 0.01385498046875, + 0.0308837890625, + -0.09912109375, + 0.0130615234375, + 0.05126953125, + 0.046630859375, + -0.0869140625, + -0.048583984375, + 0.0294189453125, + 0.080078125, + 0.00335693359375, + 0.02978515625, + -0.05810546875, + -0.10693359375, + -0.028076171875, + -0.0595703125, + 0.045166015625, + -0.00750732421875, + 0.03857421875, + -0.03076171875, + -0.396484375, + 0.047607421875, + -0.12109375, + -0.0218505859375, + -0.007110595703125, + 0.040283203125, + -0.07275390625, + 0.00970458984375, + 0.038330078125, + 0.072265625, + -0.06689453125, + 0.0546875, + 0.0595703125, + -0.119140625, + 0.07861328125, + 0.033203125, + 0.017333984375, + -0.099609375, + -0.039794921875, + -0.0020904541015625, + -0.0133056640625, + -0.00506591796875, + -0.0869140625, + -0.047607421875, + -0.0478515625, + 0.0311279296875, + 0.0091552734375, + -0.0277099609375, + -0.0162353515625, + -0.00738525390625, + 0.01458740234375, + 0.0038299560546875, + 0.1318359375, + 0.017333984375, + 0.09619140625, + -0.06640625, + -0.006744384765625, + -0.035400390625, + 0.06005859375, + -0.08349609375, + -0.0021820068359375, + -0.0269775390625, + -0.1298828125, + -0.07421875, + 0.0947265625, + 0.01806640625, + -0.00160980224609375, + 0.138671875, + -0.00396728515625, + 0.10888671875, + -0.05908203125, + -0.0732421875, + -0.17578125, + 0.0732421875, + 0.0498046875, + 0.039794921875, + -0.0101318359375, + 0.004241943359375, + 0.01239013671875, + 0.0264892578125, + 0.130859375, + 0.01275634765625, + 0.0223388671875, + -0.05126953125, + 0.1669921875, + -0.12255859375, + -0.08740234375, + -0.041015625, + 0.00421142578125, + -0.0283203125, + -0.0634765625, + -0.005462646484375, + -0.0576171875, + 0.10107421875, + 0.0947265625, + 0.07666015625, + 0.09375, + 0.125, + -0.0281982421875, + 0.037353515625, + -0.0810546875, + 0.1318359375, + -0.189453125, + -0.0146484375, + 0.087890625, + 0.047119140625, + 0.03662109375, + 0.03369140625, + 0.033447265625, + -0.04541015625, + 0.115234375, + 0.0751953125, + 0.11279296875, + 0.055908203125, + -0.055908203125, + 0.058837890625, + 0.1279296875, + 0.03857421875, + 0.05322265625, + 0.0927734375, + 0.07861328125, + 0.11669921875, + -0.036865234375, + 0.140625, + 0.00848388671875, + -0.052734375, + 0.0272216796875, + 0.0233154296875, + -0.11181640625, + -0.09326171875, + 0.00518798828125, + 0.02197265625, + 0.0301513671875, + 0.10302734375, + -0.012939453125, + -0.0185546875, + -0.0888671875, + -0.107421875, + -0.036865234375, + -0.0693359375, + 0.08984375, + 0.00927734375, + 0.01165771484375, + -0.0308837890625, + 0.00506591796875, + -0.0206298828125, + -0.060302734375, + 0.045654296875, + 0.11181640625, + -0.054443359375, + -0.1474609375, + -0.1015625, + -0.013671875, + 0.007293701171875, + -0.035400390625, + 0.1064453125, + 0.06640625, + -0.07958984375, + -0.140625, + 0.09423828125, + -0.031982421875, + 0.09033203125, + 0.06787109375, + -0.012939453125, + 0.061767578125, + -0.2421875, + 0.0223388671875, + 0.0067138671875, + -0.0159912109375, + 0.0311279296875, + 0.046142578125, + -0.00592041015625, + -0.0244140625, + 0.09326171875, + 0.10498046875, + -0.04345703125, + -0.04248046875, + -0.08447265625, + 0.032958984375, + 0.035888671875, + -0.05029296875, + 0.028564453125, + 0.035400390625, + -0.1201171875, + -0.0194091796875, + -0.00750732421875, + -0.0108642578125, + 0.00078582763671875, + 0.08154296875, + -0.00677490234375, + -0.1142578125, + 0.1181640625, + -0.006011962890625, + -0.171875, + 0.035400390625, + 0.0947265625, + -0.05126953125, + -0.043701171875, + -0.041748046875, + -0.062255859375, + -0.0830078125, + 0.028076171875, + 0.03857421875, + -0.1279296875, + -0.10400390625, + -0.0201416015625, + 0.0166015625, + 0.039794921875, + 0.20703125, + -0.125, + -0.0228271484375, + 0.130859375, + -0.041748046875, + 0.072265625, + -0.03955078125, + -0.0498046875, + 0.032958984375, + 0.134765625, + -0.0947265625, + 0.04736328125, + 0.05029296875, + 0.0152587890625, + 0.0029144287109375, + 0.0216064453125, + -0.05908203125, + -0.2236328125, + 0.1416015625, + 0.009033203125, + 0.01348876953125, + -0.0087890625, + -0.0152587890625, + 0.134765625, + 0.12109375, + -0.07666015625, + -0.068359375, + -0.12890625, + 0.2734375, + 0.1103515625, + -0.021240234375, + -0.07763671875, + 0.08984375, + -0.03515625, + 0.025146484375, + 0.028076171875, + -0.076171875, + -0.0908203125, + 0.0693359375, + -0.060302734375, + 0.048095703125, + -0.0299072265625, + 0.0859375, + -0.021484375, + -0.1513671875, + -0.021728515625, + -0.0732421875, + -0.0206298828125, + 0.134765625, + -0.07421875, + -0.0517578125, + -0.0245361328125, + -0.0224609375, + 0.033447265625, + 0.0111083984375, + 0.09912109375, + -0.07568359375, + -0.125, + 0.01611328125, + -0.03662109375, + -0.00933837890625, + 0.0537109375, + -0.0096435546875, + -0.08154296875, + -0.005828857421875, + -0.0032806396484375, + -0.009033203125, + 0.07080078125, + 0.061767578125, + 0.0546875, + 0.027099609375, + 0.0181884765625, + -0.1103515625, + 0.06640625, + -0.06201171875, + 0.0213623046875, + -0.052490234375, + 0.09423828125, + 0.06982421875, + -0.10986328125, + 0.1279296875, + 0.0478515625, + 0.005218505859375, + 0.1875, + 0.0228271484375, + -0.07421875, + 0.06689453125, + 0.10693359375, + -0.043212890625, + -0.0419921875, + 0.0277099609375, + -0.0169677734375, + 0.015380859375, + -0.1162109375, + -0.0216064453125, + -0.06982421875, + -0.0849609375, + -0.03759765625, + 0.126953125, + -0.038330078125, + 0.0235595703125, + 0.1904296875, + -0.052001953125, + 0.08203125, + -0.11328125, + -0.0037384033203125, + -0.08349609375, + -0.025146484375, + 0.0556640625, + -0.08154296875, + -0.0126953125, + 0.05908203125, + 0.043701171875, + 0.0301513671875, + 0.06494140625, + 0.1923828125, + -0.0322265625, + -0.03662109375, + -0.052490234375, + -0.0238037109375, + -0.03564453125, + -0.0166015625, + 0.1474609375, + 0.12060546875, + -0.03076171875, + -0.046630859375, + 0.0546875, + -0.058349609375, + 0.06298828125, + -0.0322265625, + -0.061279296875, + -0.0184326171875, + 0.07470703125, + 0.06494140625, + -0.04541015625, + 0.1484375, + -0.07763671875, + -0.10888671875, + -0.0081787109375, + -0.00634765625, + 0.08837890625, + 0.006927490234375, + 0.1630859375, + -0.09716796875, + 0.0703125, + -0.1201171875, + -0.0111083984375, + 0.0238037109375, + -0.046875, + -0.11083984375, + -0.12353515625, + 0.054443359375, + -0.216796875, + -0.0634765625, + 0.0654296875, + -0.03515625, + -0.06298828125, + 0.0654296875, + 0.08935546875, + 0.045166015625, + 0.06640625, + 0.10302734375, + 0.01055908203125, + 0.189453125, + -0.048828125, + 0.041259765625, + -0.032470703125, + -0.08447265625, + 0.11376953125, + -0.033935546875, + 0.09375, + -0.0224609375, + -0.02880859375, + 0.0269775390625, + 0.040283203125, + 0.1142578125, + -0.0291748046875, + 0.0556640625, + -0.0595703125, + -0.05078125, + -0.046142578125, + -0.0025177001953125, + 0.109375, + 0.181640625, + 0.0172119140625, + -0.053466796875, + -0.0400390625, + -0.002838134765625, + -0.0255126953125, + -0.1279296875, + -0.0390625, + 0.053955078125, + 0.0184326171875, + 0.0115966796875, + -0.11328125, + 0.07470703125, + -0.08447265625, + 0.181640625, + -0.0634765625, + -0.06396484375, + 0.000591278076171875, + -0.1630859375, + 0.057861328125, + -0.037353515625, + -0.027587890625, + 0.103515625, + 0.048095703125, + -0.042236328125, + -0.0830078125, + -0.04443359375, + -0.031494140625, + 0.11669921875, + -0.06982421875, + -0.06201171875, + -0.1484375, + 0.060791015625, + 0.12060546875, + 0.0001354217529296875, + -0.02587890625, + -0.025390625, + 0.03173828125, + 0.068359375, + 0.001190185546875, + -0.004852294921875, + 0.109375, + 0.01007080078125, + 0.0615234375, + -0.083984375, + 0.1357421875, + 0.037841796875, + -0.19140625, + -0.0810546875, + 0.00750732421875, + -0.0198974609375, + -0.158203125, + -0.042724609375, + 0.060546875, + -0.00836181640625, + 0.265625, + 0.09326171875, + 0.034423828125, + 0.1748046875, + 0.022216796875, + -0.08203125, + 0.08056640625, + 0.01434326171875, + -0.07373046875, + 0.0361328125, + -0.05126953125, + -0.06884765625, + 0.017822265625, + -0.05810546875, + -0.09375, + 0.041015625, + 0.022705078125, + 0.08251953125, + -0.10302734375, + -0.04052734375, + -0.06103515625, + 0.0140380859375, + -0.08544921875, + 0.0128173828125, + -0.039794921875, + -0.058349609375, + -0.07470703125, + -0.00023555755615234375, + 0.2236328125, + -0.11474609375, + 0.0228271484375, + 0.0908203125, + 0.052734375, + -0.055908203125, + -0.0274658203125, + -0.016845703125, + 0.021484375, + -0.035400390625, + -0.025146484375, + -0.173828125, + 0.00811767578125, + 0.06005859375, + -0.080078125, + 0.08740234375, + -0.0064697265625, + 0.1630859375, + 0.1875, + 0.08349609375, + -0.039306640625, + 0.040771484375, + 0.037841796875, + -0.087890625, + -0.0198974609375, + 0.0235595703125, + 0.0089111328125, + 0.038330078125, + 0.037353515625, + 0.038818359375, + -0.0859375, + 0.06884765625, + 0.01055908203125, + -0.1904296875, + -0.059814453125, + -0.1669921875, + -0.1611328125, + 0.04150390625, + 0.134765625, + 0.0322265625, + 0.0201416015625, + -0.11376953125, + -0.158203125, + 0.0205078125, + 0.0634765625, + 0.0771484375, + -0.07861328125, + 0.0361328125, + -0.0537109375, + -0.044189453125, + 0.042236328125, + -0.0255126953125, + 0.044189453125, + 0.06689453125, + 0.091796875, + 0.1552734375, + -0.038818359375, + 0.000881195068359375, + 0.06494140625, + 0.029052734375, + -0.125, + -0.0224609375, + 0.021240234375, + -0.06298828125, + 0.0033721923828125, + -0.05810546875, + 0.04736328125, + 0.1474609375, + -0.056884765625, + 0.00213623046875, + 0.023193359375, + 0.0030364990234375, + -0.052978515625, + 0.0771484375, + -0.123046875, + -0.046875, + 0.05322265625, + 0.0927734375, + -0.045654296875, + 0.0012969970703125, + 0.016357421875, + 0.03271484375, + -0.064453125, + -0.048583984375, + 0.130859375, + -0.07373046875, + 0.000537872314453125, + 0.0419921875, + 0.03466796875, + 0.05908203125, + 0.006561279296875, + 0.0751953125, + 0.034912109375, + 0.1669921875, + -0.056884765625, + 0.1044921875, + -0.1083984375, + -0.0478515625, + 0.103515625, + -0.251953125, + 0.1845703125, + 0.1103515625, + 0.072265625, + -0.109375, + 0.08056640625, + -0.00994873046875, + -0.00494384765625, + 0.080078125, + -0.068359375, + 0.1376953125, + 0.017333984375, + 0.10400390625, + -0.052734375, + 0.040283203125, + -0.09912109375, + -0.10791015625, + 0.0230712890625, + -0.0291748046875, + -0.10205078125, + 0.06298828125, + 0.07861328125, + -0.056884765625, + 0.0537109375, + -0.1162109375, + -0.033935546875, + -0.021484375, + -0.00689697265625, + -0.05078125, + 0.11328125, + 0.0016632080078125, + -0.05029296875, + 0.029296875, + -0.103515625, + -0.05908203125, + 0.07568359375, + 0.1103515625, + -0.0064697265625, + 0.0712890625, + 0.08642578125, + -0.08349609375, + 0.01177978515625, + 0.038330078125, + -0.099609375, + -0.0546875, + -0.0115966796875, + 0.0011138916015625, + 0.0252685546875, + -0.048583984375, + -0.07275390625, + -0.035888671875, + 0.12158203125, + 0.04736328125, + 0.05322265625, + -0.10986328125, + 0.03173828125, + 0.0341796875, + -0.09326171875, + -0.150390625, + 0.048583984375, + -0.11279296875, + 0.0130615234375, + -0.076171875, + -0.029541015625, + 0.09130859375, + 0.059326171875, + -0.033447265625, + 0.00970458984375, + -0.04296875, + -0.046142578125, + 0.12451171875, + -0.11181640625, + 0.1064453125, + 0.009765625, + 0.027587890625, + 0.004974365234375, + -0.04150390625, + 0.08056640625, + -0.01251220703125, + -0.185546875, + -0.06494140625, + 0.028564453125, + 0.05126953125, + 0.08203125, + 0.314453125, + -0.0751953125, + -0.041259765625, + 0.00897216796875, + 0.047119140625, + -0.0703125, + -0.03271484375, + -0.1298828125, + -0.01068115234375, + -0.00701904296875, + 0.044921875, + -0.087890625, + -0.040771484375, + -0.03271484375, + 0.0712890625, + -0.051025390625, + -0.0174560546875, + -0.0703125, + -0.059814453125, + 0.04248046875, + 0.212890625, + 0.0008087158203125, + -0.0096435546875, + -0.0478515625, + -0.12060546875, + 0.109375, + -0.034912109375, + 0.158203125, + -0.005279541015625, + -0.0093994140625, + -0.00927734375, + -0.00141143798828125, + -0.2451171875, + -0.01318359375, + -0.10693359375, + 0.0673828125, + 0.0045166015625, + -0.08642578125, + 0.0162353515625, + -0.0135498046875, + -0.037841796875, + -0.005462646484375, + 0.03076171875, + -0.0625, + 0.06982421875, + 0.01251220703125, + 0.019287109375, + 0.0390625, + -0.0576171875, + 0.173828125, + -0.1767578125, + -0.044677734375, + 0.095703125, + 0.042236328125, + 0.08984375, + 0.0341796875, + 0.056884765625, + 0.0732421875, + 0.053955078125, + -0.126953125, + 0.09326171875, + 0.05810546875, + 0.06884765625, + -0.01904296875, + 0.06005859375, + -0.0198974609375, + -0.0189208984375, + -0.0216064453125, + -0.0830078125, + -0.058349609375, + 0.0257568359375, + 0.00653076171875, + -0.033203125, + -0.05126953125, + 0.0859375 + ], + "vectorDefaultSteerStrength": 10 +} \ No newline at end of file diff --git a/godot_exporter.py b/godot_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..9c9124e643d7717a1812352915da1dbb7bae8f79 --- /dev/null +++ b/godot_exporter.py @@ -0,0 +1,856 @@ +""" +Godot Exporter - Converts game configs to Godot 4.x GDScript and Scene files. + +Generates a complete game with: +- GameState (inventory, flags, counters, etc.) +- Condition Evaluator (has_item, flag, money checks, etc.) +- Effect Applicator (add_item, set_flag, increment, etc.) +- Transition Resolver (random, conditional, chained) +- 3D Scene with placeholder meshes for each location/choice +- Game Controller for navigation and state management + +Target: Godot 4.x +""" + +import json + + +def generate_gamestate_gd(): + """Generate GDScript GameState autoload singleton.""" + return '''extends Node +# ============================================================ +# GAME STATE - Autoload Singleton +# ============================================================ +# Add this as an AutoLoad in Project Settings -> Autoload +# Name it "GameState" + +signal state_changed + +var inventory: Array[String] = [] +var money: int = 20 +var flags: Dictionary = {} +var counters: Dictionary = {} +var people_met: Array[String] = [] +var locations_visited: Array[String] = [] +var current_location: String = "" +var current_state: String = "" + +# ==================== Inventory ==================== +func add_item(item: String) -> void: + if item not in inventory: + inventory.append(item) + _log_event("effect_applied", "add_item=" + item) + state_changed.emit() + +func remove_item(item: String) -> void: + var idx = inventory.find(item) + if idx >= 0: + inventory.remove_at(idx) + _log_event("effect_applied", "remove_item=" + item) + state_changed.emit() + +func has_item(item: String) -> bool: + return item in inventory + +func add_items(items: Array) -> void: + for item in items: + add_item(item) + +# ==================== Money ==================== +func add_money(amount: int) -> void: + money += amount + _log_event("effect_applied", "add_money=" + str(amount)) + state_changed.emit() + +func remove_money(amount: int) -> void: + money -= amount + _log_event("effect_applied", "remove_money=" + str(amount)) + state_changed.emit() + +func set_money(amount: int) -> void: + money = amount + state_changed.emit() + +# ==================== Flags ==================== +func set_flag(flag_name: String, value: bool = true) -> void: + flags[flag_name] = value + _log_event("effect_applied", "set_flag=" + flag_name + ":" + str(value)) + state_changed.emit() + +func clear_flag(flag_name: String) -> void: + flags[flag_name] = false + state_changed.emit() + +func toggle_flag(flag_name: String) -> void: + flags[flag_name] = not flags.get(flag_name, false) + state_changed.emit() + +func has_flag(flag_name: String) -> bool: + return flags.get(flag_name, false) + +# ==================== Counters ==================== +func set_counter(counter_name: String, value: int) -> void: + counters[counter_name] = value + _log_event("effect_applied", "set_counter=" + counter_name + ":" + str(value)) + state_changed.emit() + +func get_counter(counter_name: String) -> int: + return counters.get(counter_name, 0) + +func increment_counter(counter_name: String, amount: int = 1) -> void: + counters[counter_name] = counters.get(counter_name, 0) + amount + _log_event("effect_applied", "increment=" + counter_name + ":" + str(amount)) + state_changed.emit() + +func decrement_counter(counter_name: String, amount: int = 1) -> void: + counters[counter_name] = counters.get(counter_name, 0) - amount + state_changed.emit() + +# ==================== People ==================== +func meet_person(person_name: String) -> void: + if person_name not in people_met: + people_met.append(person_name) + _log_event("effect_applied", "meet_person=" + person_name) + state_changed.emit() + +func has_met(person_name: String) -> bool: + return person_name in people_met + +# ==================== Locations ==================== +func visit_location(location_name: String) -> void: + if location_name not in locations_visited: + locations_visited.append(location_name) + state_changed.emit() + +func has_visited(location_name: String) -> bool: + return location_name in locations_visited + +func discover_location(location_name: String) -> void: + visit_location(location_name) + +func has_discovered(location_name: String) -> bool: + return has_visited(location_name) + +# ==================== State Summary ==================== +func get_state_summary() -> String: + var summary = "=== Game State ===\\n" + summary += "Location: %s / %s\\n" % [current_location, current_state] + summary += "Money: %d\\n" % money + summary += "Inventory: %s\\n" % str(inventory) + summary += "Flags: %s\\n" % str(flags) + summary += "Counters: %s\\n" % str(counters) + return summary + +# ==================== Event Logging ==================== +func _log_event(event_type: String, details: String) -> void: + print("[GAME_EVENT] %s: %s" % [event_type, details]) + +func reset() -> void: + inventory.clear() + money = 20 + flags.clear() + counters.clear() + people_met.clear() + locations_visited.clear() + current_location = "" + current_state = "" + state_changed.emit() +''' + + +def generate_condition_evaluator_gd(): + """Generate GDScript Condition Evaluator.""" + return '''extends Node +# ============================================================ +# CONDITION EVALUATOR +# ============================================================ +# Evaluates condition expressions against GameState + +func evaluate(condition) -> bool: + # No condition = always true + if condition == null or (condition is Dictionary and condition.is_empty()): + return true + + # String = flag check + if condition is String: + return GameState.has_flag(condition) + + if not condition is Dictionary: + return false + + # Compound conditions + if condition.has("and"): + for c in condition["and"]: + if not evaluate(c): + return false + return true + + if condition.has("or"): + for c in condition["or"]: + if evaluate(c): + return true + return false + + if condition.has("not"): + return not evaluate(condition["not"]) + + # Atomic conditions + return _evaluate_atomic(condition) + +func _evaluate_atomic(condition: Dictionary) -> bool: + # Inventory checks + if condition.has("has_item"): + return GameState.has_item(condition["has_item"]) + if condition.has("not_has_item"): + return not GameState.has_item(condition["not_has_item"]) + + # Flag checks + if condition.has("flag"): + return GameState.has_flag(condition["flag"]) + if condition.has("not_flag"): + return not GameState.has_flag(condition["not_flag"]) + + # Money checks + if condition.has("money"): + return _compare_numeric(GameState.money, condition["money"]) + + # Counter checks + if condition.has("counter"): + for counter_name in condition["counter"]: + var value = GameState.get_counter(counter_name) + if not _compare_numeric(value, condition["counter"][counter_name]): + return false + return true + + # People checks + if condition.has("met_person"): + return GameState.has_met(condition["met_person"]) + if condition.has("not_met_person"): + return not GameState.has_met(condition["not_met_person"]) + + # Location checks + if condition.has("visited"): + return GameState.has_visited(condition["visited"]) + if condition.has("not_visited"): + return not GameState.has_visited(condition["not_visited"]) + + return true + +func _compare_numeric(actual: int, comparison) -> bool: + if comparison is int or comparison is float: + return actual >= comparison + + if comparison is Dictionary: + if comparison.has("gte"): + return actual >= comparison["gte"] + if comparison.has("gt"): + return actual > comparison["gt"] + if comparison.has("lte"): + return actual <= comparison["lte"] + if comparison.has("lt"): + return actual < comparison["lt"] + if comparison.has("eq"): + return actual == comparison["eq"] + if comparison.has("neq"): + return actual != comparison["neq"] + + return true +''' + + +def generate_effect_applicator_gd(): + """Generate GDScript Effect Applicator.""" + return '''extends Node +# ============================================================ +# EFFECT APPLICATOR +# ============================================================ +# Applies declarative effects to GameState + +func apply(effects: Dictionary) -> void: + if effects.is_empty(): + return + + # Inventory effects + if effects.has("add_item"): + var items = effects["add_item"] + if items is Array: + GameState.add_items(items) + else: + GameState.add_item(items) + + if effects.has("remove_item"): + var items = effects["remove_item"] + if items is Array: + for item in items: + GameState.remove_item(item) + else: + GameState.remove_item(items) + + # Money effects + if effects.has("add_money"): + GameState.add_money(effects["add_money"]) + if effects.has("remove_money"): + GameState.remove_money(effects["remove_money"]) + if effects.has("set_money"): + GameState.set_money(effects["set_money"]) + + # Flag effects + if effects.has("set_flag"): + var flag_data = effects["set_flag"] + if flag_data is String: + GameState.set_flag(flag_data) + elif flag_data is Array: + for f in flag_data: + GameState.set_flag(f) + elif flag_data is Dictionary: + for f in flag_data: + GameState.set_flag(f, flag_data[f]) + + if effects.has("clear_flag"): + var flags = effects["clear_flag"] + if flags is Array: + for f in flags: + GameState.clear_flag(f) + else: + GameState.clear_flag(flags) + + if effects.has("toggle_flag"): + var flags = effects["toggle_flag"] + if flags is Array: + for f in flags: + GameState.toggle_flag(f) + else: + GameState.toggle_flag(flags) + + # Counter effects + if effects.has("set_counter"): + for counter_name in effects["set_counter"]: + GameState.set_counter(counter_name, effects["set_counter"][counter_name]) + + if effects.has("increment"): + for counter_name in effects["increment"]: + GameState.increment_counter(counter_name, effects["increment"][counter_name]) + + if effects.has("decrement"): + for counter_name in effects["decrement"]: + GameState.decrement_counter(counter_name, effects["decrement"][counter_name]) + + # People effects + if effects.has("add_person"): + var people = effects["add_person"] + if people is Array: + for p in people: + GameState.meet_person(p) + else: + GameState.meet_person(people) + + # Location effects + if effects.has("add_location"): + var locs = effects["add_location"] + if locs is Array: + for loc in locs: + GameState.discover_location(loc) + else: + GameState.discover_location(locs) + + if effects.has("visit_location"): + var locs = effects["visit_location"] + if locs is Array: + for loc in locs: + GameState.visit_location(loc) + else: + GameState.visit_location(locs) +''' + + +def generate_transition_resolver_gd(): + """Generate GDScript Transition Resolver.""" + return '''extends Node +# ============================================================ +# TRANSITION RESOLVER +# ============================================================ +# Resolves dynamic transitions + +@onready var condition_evaluator = $"../ConditionEvaluator" + +func resolve(transition) -> String: + # Simple string transition + if transition is String: + return transition + + if not transition is Dictionary: + return "" + + # Weighted random + if transition.has("random"): + return _resolve_weighted_random(transition["random"]) + + # Equal probability pool + if transition.has("random_from"): + var pool = transition["random_from"] + if pool.size() > 0: + return pool[randi() % pool.size()] + return "" + + # Conditional if/then/else + if transition.has("if"): + if condition_evaluator.evaluate(transition["if"]): + if transition.has("then"): + return resolve(transition["then"]) + else: + if transition.has("else"): + return resolve(transition["else"]) + return "" + + # Chained conditions + if transition.has("conditions"): + for cond_block in transition["conditions"]: + if cond_block.has("default"): + return resolve(cond_block["default"]) + if cond_block.has("if") and condition_evaluator.evaluate(cond_block["if"]): + return resolve(cond_block["then"]) + return "" + + return "" + +func _resolve_weighted_random(weights: Array) -> String: + if weights.size() == 0: + return "" + + var total = 0.0 + for w in weights: + total += w[1] + + var roll = randf() * total + for w in weights: + roll -= w[1] + if roll <= 0: + return w[0] + + return weights[weights.size() - 1][0] +''' + + +def generate_choice_script_gd(): + """Generate GDScript for clickable choice boxes.""" + return '''extends StaticBody3D +# ============================================================ +# CHOICE - Clickable choice box +# ============================================================ + +signal choice_selected(choice_data) + +@export var choice_text: String = "" +@export var choice_condition: Dictionary = {} +@export var choice_transition = "" # Can be String or Dictionary +@export var choice_effects: Dictionary = {} + +var _mesh_instance: MeshInstance3D +var _label: Label3D +var _default_material: StandardMaterial3D +var _hover_material: StandardMaterial3D +var _disabled_material: StandardMaterial3D + +func _ready(): + # Get or create mesh instance + _mesh_instance = get_node_or_null("MeshInstance3D") + if not _mesh_instance: + _mesh_instance = MeshInstance3D.new() + _mesh_instance.mesh = BoxMesh.new() + add_child(_mesh_instance) + + # Create materials + _default_material = StandardMaterial3D.new() + _default_material.albedo_color = Color(0.3, 0.5, 0.8) + + _hover_material = StandardMaterial3D.new() + _hover_material.albedo_color = Color(0.5, 0.7, 1.0) + _hover_material.emission_enabled = true + _hover_material.emission = Color(0.2, 0.3, 0.5) + + _disabled_material = StandardMaterial3D.new() + _disabled_material.albedo_color = Color(0.3, 0.3, 0.3, 0.5) + + _mesh_instance.material_override = _default_material + + # Create label + _label = get_node_or_null("Label3D") + if not _label: + _label = Label3D.new() + _label.position = Vector3(0, 1.5, 0) + _label.font_size = 48 + _label.billboard = BaseMaterial3D.BILLBOARD_ENABLED + add_child(_label) + + _label.text = choice_text + + # Connect to state changes + GameState.state_changed.connect(_on_state_changed) + _update_visibility() + +func _on_state_changed(): + _update_visibility() + +func _update_visibility(): + var evaluator = get_node_or_null("/root/Main/ConditionEvaluator") + if evaluator: + var condition_met = evaluator.evaluate(choice_condition) + visible = condition_met + _mesh_instance.material_override = _default_material if condition_met else _disabled_material + +func _input_event(_camera, event, _position, _normal, _shape_idx): + if event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: + _on_clicked() + +func _on_clicked(): + print("[GAME_EVENT] choice_selected: " + choice_text) + choice_selected.emit({ + "text": choice_text, + "transition": choice_transition, + "effects": choice_effects + }) + +func set_hover(is_hovered: bool): + if visible: + _mesh_instance.material_override = _hover_material if is_hovered else _default_material +''' + + +def generate_game_controller_gd(config): + """Generate GDScript Game Controller.""" + # Find starting location/state + first_location = next(iter(config.keys())) + first_state = next(iter(config[first_location].keys())) + + # Convert config to GDScript dictionary syntax + config_json = json.dumps(config) + config_json_escaped = config_json.replace("'", "\\'") + + return f'''extends Node3D +# ============================================================ +# GAME CONTROLLER +# ============================================================ + +@onready var condition_evaluator = $ConditionEvaluator +@onready var effect_applicator = $EffectApplicator +@onready var transition_resolver = $TransitionResolver + +var game_config: Dictionary +var location_nodes: Dictionary = {{}} +var current_location_node: Node3D = null + +func _ready(): + # Parse config + game_config = JSON.parse_string('{config_json_escaped}') + + # Cache location nodes + for child in get_children(): + if child.name.begins_with("Loc_"): + location_nodes[child.name.substr(4)] = child + child.visible = false + + # Connect choice signals + for choice_node in child.get_children(): + if choice_node.has_signal("choice_selected"): + choice_node.choice_selected.connect(_on_choice_selected) + + # Start game + call_deferred("_start_game") + +func _start_game(): + navigate_to("{first_location}", "{first_state}") + +func navigate_to(location: String, state: String): + var entity_name = location + "_" + state + print("[GAME_EVENT] transition: " + GameState.current_location + "_" + GameState.current_state + " -> " + entity_name) + + # Hide current + if current_location_node: + current_location_node.visible = false + + # Show new + if location_nodes.has(entity_name): + current_location_node = location_nodes[entity_name] + current_location_node.visible = true + + # Update state + GameState.current_location = location + GameState.current_state = state + GameState.visit_location(location) + + # Apply on_enter effects + var state_data = _get_state_data(location, state) + if state_data and state_data.has("on_enter"): + effect_applicator.apply(state_data["on_enter"]) + + # Update UI + _update_description(state_data) + else: + push_error("Location not found: " + entity_name) + +func _get_state_data(location: String, state: String): + if game_config.has(location) and game_config[location].has(state): + return game_config[location][state] + return null + +func _update_description(state_data): + if state_data and state_data.has("description"): + print("Description: " + state_data["description"]) + # You can connect this to a UI label + +func _on_choice_selected(choice_data: Dictionary): + # Apply effects + if choice_data.has("effects"): + effect_applicator.apply(choice_data["effects"]) + + # Resolve and execute transition + if choice_data.has("transition"): + var target = transition_resolver.resolve(choice_data["transition"]) + if target != "": + var parts = target.rsplit("_", true, 1) + if parts.size() >= 2: + navigate_to(parts[0], parts[1]) + +func _input(event): + # Debug: Print state on F1 + if event is InputEventKey and event.pressed and event.keycode == KEY_F1: + print(GameState.get_state_summary()) +''' + + +def generate_scene_tscn(config): + """Generate Godot .tscn scene file with all locations and choices.""" + lines = [] + + # Count resources needed + resource_count = 3 # BoxMesh, ground mesh, materials + node_count = 1 # Root + + for location, states in config.items(): + if not isinstance(states, dict): + continue + for state_name, state_data in states.items(): + if not isinstance(state_data, dict): + continue + node_count += 1 # Location node + choices = state_data.get('choices', []) + node_count += len(choices) + 1 # Choices + ground + + # Header + lines.append('[gd_scene load_steps=%d format=3]' % (resource_count + 5)) + lines.append('') + + # External scripts + lines.append('[ext_resource type="Script" path="res://game_controller.gd" id="1"]') + lines.append('[ext_resource type="Script" path="res://condition_evaluator.gd" id="2"]') + lines.append('[ext_resource type="Script" path="res://effect_applicator.gd" id="3"]') + lines.append('[ext_resource type="Script" path="res://transition_resolver.gd" id="4"]') + lines.append('[ext_resource type="Script" path="res://choice.gd" id="5"]') + lines.append('') + + # Sub-resources + lines.append('[sub_resource type="BoxMesh" id="BoxMesh_choice"]') + lines.append('') + lines.append('[sub_resource type="BoxShape3D" id="BoxShape_choice"]') + lines.append('') + lines.append('[sub_resource type="PlaneMesh" id="PlaneMesh_ground"]') + lines.append('size = Vector2(15, 15)') + lines.append('') + + # Root node + lines.append('[node name="Main" type="Node3D"]') + lines.append('script = ExtResource("1")') + lines.append('') + + # Helper nodes + lines.append('[node name="ConditionEvaluator" type="Node" parent="."]') + lines.append('script = ExtResource("2")') + lines.append('') + lines.append('[node name="EffectApplicator" type="Node" parent="."]') + lines.append('script = ExtResource("3")') + lines.append('') + lines.append('[node name="TransitionResolver" type="Node" parent="."]') + lines.append('script = ExtResource("4")') + lines.append('') + + # Camera + lines.append('[node name="Camera3D" type="Camera3D" parent="."]') + lines.append('transform = Transform3D(1, 0, 0, 0, 0.906308, 0.422618, 0, -0.422618, 0.906308, 0, 5, 10)') + lines.append('') + + # Light + lines.append('[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]') + lines.append('transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 10, 0)') + lines.append('') + + # Generate location nodes + state_index = 0 + for location, states in config.items(): + if not isinstance(states, dict): + continue + + for state_name, state_data in states.items(): + if not isinstance(state_data, dict): + continue + + entity_name = f"{location}_{state_name}" + safe_name = entity_name.replace('-', '_').replace(' ', '_') + x_pos = state_index * 25 + + # Location container node + lines.append(f'[node name="Loc_{safe_name}" type="Node3D" parent="."]') + lines.append(f'transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, {x_pos}, 0, 0)') + lines.append('') + + # Ground plane + lines.append(f'[node name="Ground" type="MeshInstance3D" parent="Loc_{safe_name}"]') + lines.append('mesh = SubResource("PlaneMesh_ground")') + lines.append('') + + # Choices + choices = state_data.get('choices', []) + choice_config = state_data.get('choice_config', {}) + transitions = state_data.get('transitions', {}) + effects = state_data.get('effects', {}) + + for idx, choice in enumerate(choices): + choice_safe = choice.replace('-', '_').replace(' ', '_').replace("'", "")[:20] + x_offset = (idx - len(choices)/2) * 3 + + # Get choice data + condition = choice_config.get(choice, {}).get('condition', {}) + transition = transitions.get(choice, '') + effect = effects.get(choice, {}) + + lines.append(f'[node name="Choice_{choice_safe}_{idx}" type="StaticBody3D" parent="Loc_{safe_name}"]') + lines.append(f'transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, {x_offset}, 1, -5)') + lines.append('script = ExtResource("5")') + lines.append(f'choice_text = "{choice}"') + + # Serialize condition, transition, effects as metadata + if condition: + lines.append(f'choice_condition = {_dict_to_gdscript(condition)}') + if isinstance(transition, dict): + lines.append(f'choice_transition = {_dict_to_gdscript(transition)}') + elif transition: + lines.append(f'choice_transition = "{transition}"') + if effect: + lines.append(f'choice_effects = {_dict_to_gdscript(effect)}') + lines.append('') + + # Mesh for the choice + lines.append(f'[node name="MeshInstance3D" type="MeshInstance3D" parent="Loc_{safe_name}/Choice_{choice_safe}_{idx}"]') + lines.append('mesh = SubResource("BoxMesh_choice")') + lines.append('') + + # Collision shape + lines.append(f'[node name="CollisionShape3D" type="CollisionShape3D" parent="Loc_{safe_name}/Choice_{choice_safe}_{idx}"]') + lines.append('shape = SubResource("BoxShape_choice")') + lines.append('') + + state_index += 1 + + return '\n'.join(lines) + + +def _dict_to_gdscript(d): + """Convert Python dict to GDScript dictionary literal.""" + if isinstance(d, dict): + pairs = [] + for k, v in d.items(): + pairs.append(f'"{k}": {_dict_to_gdscript(v)}') + return '{' + ', '.join(pairs) + '}' + elif isinstance(d, list): + items = [_dict_to_gdscript(item) for item in d] + return '[' + ', '.join(items) + ']' + elif isinstance(d, str): + return f'"{d}"' + elif isinstance(d, bool): + return 'true' if d else 'false' + elif isinstance(d, (int, float)): + return str(d) + else: + return 'null' + + +def generate_project_godot_snippet(): + """Generate project.godot autoload settings snippet.""" + return '''# Add this to your project.godot under [autoload] +# Or go to Project -> Project Settings -> Autoload and add: + +[autoload] + +GameState="*res://game_state.gd" +''' + + +def export_to_godot(config_json): + """ + Main export function - converts game config to Godot 4.x files. + + Args: + config_json: JSON string of the game config + + Returns: + tuple: (explanation, dict of filename -> content) + """ + try: + config = json.loads(config_json) + except json.JSONDecodeError as e: + return f"JSON Error: {e}", {} + + # Generate all files + files = { + 'game_state.gd': generate_gamestate_gd(), + 'condition_evaluator.gd': generate_condition_evaluator_gd(), + 'effect_applicator.gd': generate_effect_applicator_gd(), + 'transition_resolver.gd': generate_transition_resolver_gd(), + 'choice.gd': generate_choice_script_gd(), + 'game_controller.gd': generate_game_controller_gd(config), + 'main.tscn': generate_scene_tscn(config), + '_README_SETUP.txt': generate_project_godot_snippet(), + } + + # Create combined output for display + combined = [] + combined.append("=" * 60) + combined.append("GODOT 4.x GAME EXPORT") + combined.append("=" * 60) + combined.append("") + + for filename, content in files.items(): + combined.append(f"{'=' * 20} {filename} {'=' * 20}") + combined.append(content) + combined.append("") + + explanation = """Godot 4.x Export Generated! + +Instructions: +1. Create a new Godot 4.x project +2. Copy each generated file to your project's res:// folder: + - game_state.gd (AutoLoad singleton) + - condition_evaluator.gd + - effect_applicator.gd + - transition_resolver.gd + - choice.gd + - game_controller.gd + - main.tscn (main scene) +3. Set up AutoLoad: Project -> Project Settings -> Autoload + - Add game_state.gd as "GameState" (enable checkbox) +4. Set main.tscn as the main scene +5. Run the project + +The generated code includes: +- GameState: Singleton tracking inventory, money, flags, counters +- Condition Evaluator: Checks conditions (has_item, flag, money, etc.) +- Effect Applicator: Applies effects (add_item, set_flag, increment, etc.) +- Transition Resolver: Handles random/conditional transitions +- Choice Script: Clickable 3D boxes with input handling +- Game Controller: Navigation, state management, event logging + +Press F1 in-game to print the current state summary. +Event logs use [GAME_EVENT] prefix for easy parsing. +""" + + return explanation, '\n'.join(combined) diff --git a/json_utils.py b/json_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..2c5165ed65adf7584b9a030631ba91207e76001b --- /dev/null +++ b/json_utils.py @@ -0,0 +1,51 @@ +"""JSON utility functions for error visualization and correction.""" + +import json + + +def find_and_visualize_json_errors(json_string): + """Find JSON syntax errors and insert newlines at error positions. + + This helps visualize where JSON parsing errors occur by adding + line breaks at problematic positions. + + Args: + json_string: The JSON string to analyze + + Returns: + String with newlines inserted at error positions + """ + error_positions = [] + start = 0 + while start < len(json_string): + try: + json.loads(json_string[start:]) + break # If no error, we're done + except json.JSONDecodeError as e: + position = start + e.pos + if not error_positions or position > error_positions[-1] + 10: + error_positions.append(position) + start = position + 1 + + # Insert newlines at error positions + result = "" + last_pos = 0 + for pos in error_positions: + result += json_string[last_pos:pos] + "\n" + last_pos = pos + result += json_string[last_pos:] + + return result + + +def join_lines_after_correct_json_errors(lines, separator='\n'): + """Join lines back together after JSON error correction. + + Args: + lines: List of line strings + separator: Separator to use when joining (default: newline) + + Returns: + Joined string + """ + return separator.join(lines) diff --git a/knowledge_base_data.py b/knowledge_base_data.py new file mode 100644 index 0000000000000000000000000000000000000000..76926820a88d74a18e578252d17c4fd617afffbe --- /dev/null +++ b/knowledge_base_data.py @@ -0,0 +1,19 @@ +""" +Knowledge base data for RAG (Retrieval-Augmented Generation) +""" + +# Sample knowledge base (replace with your own data) +knowledge_base = [ + {"id": "doc1", "content": "The capital of France is Paris."}, + {"id": "doc2", "content": "Python is a popular programming language."}, + {"id": "doc3", "content": "Machine learning is a subset of artificial intelligence."}, + {"id": "doc4", "content": "The Earth orbits around the Sun."}, + {"id": "doc5", "content": "Orbits is the name of a korean fangroup"}, + {"id": "doc6", "content": "The capital of France is Paris. It's known for the Eiffel Tower."}, + {"id": "doc7", "content": "The capital of Italy is Rome. It's famous for the Colosseum."}, + {"id": "doc8", "content": "Python is a popular programming language, known for its simplicity."}, + {"id": "doc9", "content": "Java is a widely-used programming language, valued for its portability."}, + {"id": "doc10", "content": "Deep learning is a part of machine learning based on artificial neural networks."}, + {"id": "doc11", "content": "Law is a Tekken character"}, + {"id": "doc12", "content": "The law is very complicated"}, +] diff --git a/leveraging_machine_learning.py b/leveraging_machine_learning.py index 4949c16a1eb2122b73bd5d0a8be16510fbb49044..ebab2fb23ff416998e28abbcb751e9bff0724934 100644 --- a/leveraging_machine_learning.py +++ b/leveraging_machine_learning.py @@ -1,10 +1,9 @@ -import torch -from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer +# Lazy imports for heavy ML libraries - only imported when needed +# This reduces startup time from ~1 minute to a few seconds import gc import sys -from diffusers import FluxPipeline +import os import time -from sentence_transformers import SentenceTransformer import psutil import json import spaces @@ -12,15 +11,69 @@ from threading import Thread #----------------- from relatively_constant_variables import knowledge_base -# Initialize the zero tensor on CUDA -zero = torch.Tensor([0]).cuda() -print(zero.device) # This will print 'cpu' outside the @spaces.GPU decorated function +# Lazy import placeholders - will be imported on first use +torch = None +transformers = None +diffusers = None +sentence_transformers = None -modelnames = ["stvlynn/Gemma-2-2b-Chinese-it", "unsloth/Llama-3.2-1B-Instruct", "unsloth/Llama-3.2-3B-Instruct", "nbeerbower/mistral-nemo-wissenschaft-12B", "princeton-nlp/gemma-2-9b-it-SimPO", "cognitivecomputations/dolphin-2.9.3-mistral-7B-32k", "01-ai/Yi-Coder-9B-Chat", "ArliAI/Llama-3.1-8B-ArliAI-RPMax-v1.1", "ArliAI/Phi-3.5-mini-3.8B-ArliAI-RPMax-v1.1", - "Qwen/Qwen2.5-7B-Instruct", "Qwen/Qwen2-0.5B-Instruct", "Qwen/Qwen2-1.5B-Instruct", "Qwen/Qwen2-7B-Instruct", "Qwen/Qwen1.5-MoE-A2.7B-Chat", "HuggingFaceTB/SmolLM-135M-Instruct", "microsoft/Phi-3-mini-4k-instruct", "Groq/Llama-3-Groq-8B-Tool-Use", "hugging-quants/Meta-Llama-3.1-8B-Instruct-BNB-NF4", - "SpectraSuite/TriLM_3.9B_Unpacked", "h2oai/h2o-danube3-500m-chat", "OuteAI/Lite-Mistral-150M-v2-Instruct", "Zyphra/Zamba2-1.2B", "anthracite-org/magnum-v2-4b", ] +def _ensure_torch(): + """Lazy import torch only when needed.""" + global torch + if torch is None: + import torch as _torch + torch = _torch + return torch -# imagemodelnames = ["black-forest-labs/FLUX.1-schnell"] +def _ensure_transformers(): + """Lazy import transformers only when needed.""" + global transformers + if transformers is None: + import transformers as _transformers + transformers = _transformers + return transformers + +def _ensure_diffusers(): + """Lazy import diffusers only when needed.""" + global diffusers + if diffusers is None: + import diffusers as _diffusers + diffusers = _diffusers + return diffusers + +def _ensure_sentence_transformers(): + """Lazy import sentence_transformers only when needed.""" + global sentence_transformers + if sentence_transformers is None: + import sentence_transformers as _st + sentence_transformers = _st + return sentence_transformers + +# Directory for saving generated media (same as file_explorer_and_upload.py) +GENERATED_MEDIA_DIR = os.path.abspath("saved_media") +os.makedirs(GENERATED_MEDIA_DIR, exist_ok=True) + +modelnames = ["stvlynn/Gemma-2-2b-Chinese-it", "unsloth/Llama-3.2-1B-Instruct", "unsloth/Llama-3.2-3B-Instruct", "nbeerbower/mistral-nemo-wissenschaft-12B", "princeton-nlp/gemma-2-9b-it-SimPO", "cognitivecomputations/dolphin-2.9.3-mistral-7B-32k", "01-ai/Yi-Coder-9B-Chat", "ArliAI/Llama-3.1-8B-ArliAI-RPMax-v1.1", "ArliAI/Phi-3.5-mini-3.8B-ArliAI-RPMax-v1.1", + "Qwen/Qwen2.5-7B-Instruct", "Qwen/Qwen2-0.5B-Instruct", "Qwen/Qwen2-1.5B-Instruct", "Qwen/Qwen2-7B-Instruct", "Qwen/Qwen1.5-MoE-A2.7B-Chat", "HuggingFaceTB/SmolLM-135M-Instruct", "microsoft/Phi-3-mini-4k-instruct", "Groq/Llama-3-Groq-8B-Tool-Use", "hugging-quants/Meta-Llama-3.1-8B-Instruct-BNB-NF4", + "SpectraSuite/TriLM_3.9B_Unpacked", "h2oai/h2o-danube3-500m-chat", "OuteAI/Lite-Mistral-150M-v2-Instruct", "Zyphra/Zamba2-1.2B", "anthracite-org/magnum-v2-4b", + "unsloth/functiongemma-270m-it", # FunctionGemma for function calling + # New models (Dec 2025) + "HuggingFaceTB/SmolLM3-3B", + "unsloth/Ministral-3-3B-Instruct-2512-bnb-4bit", + "unsloth/granite-4.0-h-micro-bnb-4bit", + # New models (Jan 2026) + "tiiuae/Falcon-H1R-7B", # Hybrid Transformer+Mamba2, reasoning-specialized + "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8", # MoE 3.5B active/30B total, FP8 + "openai/gpt-oss-20b", # MoE 3.6B active/21B total, Apache 2.0, agentic + ] + +# T5Gemma2 encoder-decoder models (require AutoModelForSeq2SeqLM) +seq2seq_modelnames = [ + "google/t5gemma-2-270m-270m", + "google/t5gemma-2-1b-1b", +] + +# imagemodelnames = ["black-forest-labs/FLUX.1-schnell", ] current_model_index = 0 current_image_model_index = 0 @@ -29,120 +82,381 @@ modelname = modelnames[current_model_index] lastmodelnameinloadfunction = None lastimagemodelnameinloadfunction = None -# Load the embedding model -embedding_model = SentenceTransformer('all-MiniLM-L6-v2') +embedding_model = None +knowledge_base_embeddings = None + +def initialize_rag(): + global embedding_model, knowledge_base_embeddings + if embedding_model is None: + st = _ensure_sentence_transformers() + embedding_model = st.SentenceTransformer('all-MiniLM-L6-v2') + knowledge_base_embeddings = embedding_model.encode([doc["content"] for doc in knowledge_base]) # Initialize model and tokenizer as global variables model = None tokenizer = None -# flux_pipe = None +image_pipe = None +imagemodelnames = [ + "stabilityai/sd-turbo", + "stabilityai/sdxl-turbo", + # New models (Dec 2025) + "radames/Real-Time-Text-to-Image-SDXL-Lightning", + "unsloth/Qwen-Image-GGUF", # GGUF - may need special handling + "unsloth/Z-Image-Turbo-GGUF", # GGUF - may need special handling +] +current_image_model = imagemodelnames[0] # Default to sd-turbo (smaller/faster) + +# Video/I2V models +videomodelnames = [ + # LTX Video - distilled, fast (7-8 steps), works with diffusers + "Lightricks/LTX-Video-0.9.7-distilled", # 13B distilled, CFG=1, fast iterations + # Wan2.2 - Text/Image to Video + "Wan-AI/Wan2.2-TI2V-5B-Diffusers", # 5B, T2V+I2V, 720P, runs on 4090 + "Wan-AI/Wan2.2-T2V-A14B-Diffusers", # 14B MoE, text-to-video + "Wan-AI/Wan2.2-I2V-A14B-Diffusers", # 14B MoE, image-to-video + # HunyuanVideo - Tencent, consumer GPU friendly (use community diffusers version) + "hunyuanvideo-community/HunyuanVideo", # 13B original, diffusers-compatible + # GGUF format (may need llama.cpp or special handling) + "QuantStack/Wan2.2-I2V-A14B-GGUF", # Image-to-Video, GGUF format +] # Dictionary to store loaded models loaded_models = {} +# Seq2seq model globals (for T5Gemma2) +seq2seq_model = None +seq2seq_processor = None + +# Gemma Scope SAE globals +gemma_scope_sae = None +gemma_scope_layer = None + def get_size_str(bytes): for unit in ['B', 'KB', 'MB', 'GB', 'TB']: if bytes < 1024: return f"{bytes:.2f} {unit}" bytes /= 1024 +# Track currently loaded model name for model switching +current_loaded_model_name = None + def load_model(model_name): - global model, tokenizer, lastmodelnameinloadfunction, loaded_models + """ + Load model on CPU only - DO NOT use device_map="auto" or CUDA operations here. + CUDA operations must only happen inside @spaces.GPU decorated functions. + The model will be moved to GPU inside generate_response(). + """ + global model, tokenizer, lastmodelnameinloadfunction, loaded_models, current_loaded_model_name + + # Lazy import heavy libraries + _torch = _ensure_torch() + tf = _ensure_transformers() print(f"Loading model and tokenizer: {model_name}") - - # Record initial GPU memory usage - initial_memory = torch.cuda.memory_allocated() # Clear old model and tokenizer if they exist if 'model' in globals() and model is not None: + del model model = None if 'tokenizer' in globals() and tokenizer is not None: tokenizer = None - - torch.cuda.empty_cache() + + # Force garbage collection (no CUDA here - that happens in @spaces.GPU) gc.collect() - model = AutoModelForCausalLM.from_pretrained( + # Load model on CPU - it will be moved to GPU inside @spaces.GPU function + # Use device_map=None to avoid CUDA initialization + model = tf.AutoModelForCausalLM.from_pretrained( model_name, - torch_dtype="auto", - device_map="auto" + torch_dtype=_torch.bfloat16, # Use bfloat16 for efficiency + device_map=None, # Don't auto-map to GPU - we'll do it in @spaces.GPU + low_cpu_mem_usage=True ) - tokenizer = AutoTokenizer.from_pretrained(model_name) + tokenizer = tf.AutoTokenizer.from_pretrained(model_name) + + # Calculate sizes (CPU only, no CUDA) model_size = sum(p.numel() * p.element_size() for p in model.parameters()) tokenizer_size = sum(sys.getsizeof(v) for v in tokenizer.__dict__.values()) - loaded_models[model_name] = (model, tokenizer) - - # Calculate memory usage - final_memory = torch.cuda.memory_allocated() - memory_used = final_memory - initial_memory - loaded_models[model_name] = [str(time.time()), memory_used] + loaded_models[model_name] = [str(time.time()), model_size] + current_loaded_model_name = model_name lastmodelnameinloadfunction = (model_name, model_size, tokenizer_size) - print(f"Model and tokenizer {model_name} loaded successfully") + print(f"Model and tokenizer {model_name} loaded successfully (on CPU)") print(f"Model size: {get_size_str(model_size)}") print(f"Tokenizer size: {get_size_str(tokenizer_size)}") - print(f"GPU memory used: {get_size_str(memory_used)}") - return (f"Model and tokenizer {model_name} loaded successfully. " - f"Model size: {get_size_str(model_size)}, " - f"Tokenizer size: {get_size_str(tokenizer_size)}, " - f"GPU memory used: {get_size_str(memory_used)}") + return (f"Model {model_name} loaded (CPU). " + f"Size: {get_size_str(model_size)}. " + f"Will move to GPU on generation.") -# def load_image_model(imagemodelname): -# global flux_pipe, lastimagemodelnameinloadfunction, loaded_models +def load_seq2seq_model(model_name): + """Load T5Gemma2 or similar encoder-decoder model.""" + global seq2seq_model, seq2seq_processor -# print(f"Loading image model: {imagemodelname}") - -# # Record initial GPU memory usage -# initial_memory = torch.cuda.memory_allocated() + _torch = _ensure_torch() + tf = _ensure_transformers() + + print(f"Loading seq2seq model: {model_name}") + # Don't call cuda.memory_allocated() here - it can initialize CUDA outside @spaces.GPU + initial_memory = 0 + + # Clear previous + if seq2seq_model is not None: + seq2seq_model = None + if seq2seq_processor is not None: + seq2seq_processor = None + # Don't call cuda.empty_cache() here - it initializes CUDA outside @spaces.GPU + gc.collect() + + seq2seq_processor = tf.AutoProcessor.from_pretrained(model_name) + # Load on CPU - will be moved to GPU in @spaces.GPU function + seq2seq_model = tf.AutoModelForSeq2SeqLM.from_pretrained( + model_name, + torch_dtype=_torch.bfloat16, + device_map=None, # Don't auto-map to GPU + low_cpu_mem_usage=True + ) + + print(f"Seq2seq model {model_name} loaded on CPU. Will move to GPU on generation.") + return f"Loaded: {model_name} (CPU). Will move to GPU on generation." + +@spaces.GPU +def generate_seq2seq_response(prompt, image_url=None): + """Generate response using T5Gemma2.""" + global seq2seq_model, seq2seq_processor + + _torch = _ensure_torch() + + if seq2seq_model is None: + load_seq2seq_model(seq2seq_modelnames[0]) + + zero = _torch.Tensor([0]).cuda() + seq2seq_model.to(zero.device) + + if image_url: + from PIL import Image + import requests + image = Image.open(requests.get(image_url, stream=True).raw) + inputs = seq2seq_processor(text=prompt, images=image, return_tensors="pt") + else: + inputs = seq2seq_processor(text=prompt, return_tensors="pt") -# if 'flux_pipe' in globals() and flux_pipe is not None: -# flux_pipe = None - -# torch.cuda.empty_cache() -# gc.collect() + inputs = {k: v.to(zero.device) for k, v in inputs.items()} + outputs = seq2seq_model.generate(**inputs, max_new_tokens=256) + response = seq2seq_processor.decode(outputs[0], skip_special_tokens=True) + + return response + +# ============ GEMMA SCOPE 2 SAE FUNCTIONS ============ + +def load_gemma_scope_sae(layer_num=12): + """Load Gemma Scope SAE for a specific layer.""" + global gemma_scope_sae, gemma_scope_layer + + _torch = _ensure_torch() + + try: + from sae_lens import SAE + except ImportError: + return "Error: sae_lens not installed. Run: pip install sae_lens" + + # Use canonical release with correct layer ID format + layer_id = f"layer_{layer_num}/width_16k/canonical" + + try: + # Load on CPU - will be moved to GPU in @spaces.GPU function + gemma_scope_sae = SAE.from_pretrained( + release="gemma-scope-2b-pt-res-canonical", # Gemma 2 2B canonical + sae_id=layer_id, + device="cpu" # Don't initialize CUDA here + ) + gemma_scope_layer = layer_num + return f"Loaded SAE for layer {layer_num}: {layer_id} (CPU)" + except Exception as e: + return f"Error loading SAE: {str(e)}" + +@spaces.GPU +def analyze_prompt_features(prompt, top_k=10): + """Analyze which SAE features activate for a given prompt.""" + global model, tokenizer, gemma_scope_sae + + _torch = _ensure_torch() + top_k = int(top_k) # Ensure it's an int (from slider) + + # Need a Gemma 2 model for SAE analysis - use the Chinese fine-tune from modelnames + if model is None or "gemma" not in str(getattr(model, 'name_or_path', '')).lower(): + load_model("stvlynn/Gemma-2-2b-Chinese-it") # Use existing Gemma 2 from modelnames + if gemma_scope_sae is None: + load_result = load_gemma_scope_sae() + if "Error" in load_result: + return load_result + + zero = _torch.Tensor([0]).cuda() + model.to(zero.device) + # Move SAE to GPU if it has a .to() method + if hasattr(gemma_scope_sae, 'to'): + gemma_scope_sae.to(zero.device) + + # Get model activations + inputs = tokenizer(prompt, return_tensors="pt").to(zero.device) + with _torch.no_grad(): + outputs = model(**inputs, output_hidden_states=True) + + # Run through SAE - hidden_states[0] is embedding, so layer N is at index N+1 + layer_idx = gemma_scope_layer + 1 if gemma_scope_layer is not None else 13 + if layer_idx >= len(outputs.hidden_states): + layer_idx = len(outputs.hidden_states) - 1 # Use last layer if out of bounds + + hidden_state = outputs.hidden_states[layer_idx] + feature_acts = gemma_scope_sae.encode(hidden_state) + + # Get top activated features + top_features = _torch.topk(feature_acts.mean(dim=1).squeeze(), top_k) + + # Build Neuronpedia base URL for this layer/SAE + # Format: https://www.neuronpedia.org/gemma-2-2b/{layer}-gemmascope-res-16k/{feature_id} + layer_num = gemma_scope_layer if gemma_scope_layer is not None else 12 + neuronpedia_base = f"https://www.neuronpedia.org/gemma-2-2b/{layer_num}-gemmascope-res-16k" + + results = ["## Top Activated Features\n"] + results.append("| Feature | Activation | Neuronpedia Link |") + results.append("|---------|------------|------------------|") + + for idx, val in zip(top_features.indices, top_features.values): + feature_id = idx.item() + activation = val.item() + link = f"{neuronpedia_base}/{feature_id}" + results.append(f"| {feature_id:5d} | {activation:8.2f} | [View Feature]({link}) |") + + results.append("") + results.append("---") + results.append("**How to use:** Click the links to see what concepts each feature represents.") + results.append("- Higher activation = concept is more relevant to your prompt") + results.append("- Compare prompts to find features that make configs interesting vs predictable") + + return "\n".join(results) + + +def fetch_neuronpedia_feature(feature_id, layer=12, width="16k"): + """Fetch feature data from Neuronpedia API.""" + import requests + + feature_id = int(feature_id) + layer = int(layer) + + # Neuronpedia API endpoint + api_url = f"https://www.neuronpedia.org/api/feature/gemma-2-2b/{layer}-gemmascope-res-{width}/{feature_id}" + + try: + response = requests.get(api_url, timeout=10) + if response.status_code == 200: + data = response.json() + return format_neuronpedia_feature(data, feature_id, layer, width) + elif response.status_code == 404: + return f"Feature {feature_id} not found at layer {layer}" + else: + return f"API error: {response.status_code}" + except requests.exceptions.Timeout: + return "Request timed out - Neuronpedia may be slow" + except Exception as e: + return f"Error fetching feature: {str(e)}" -# flux_pipe = FluxPipeline.from_pretrained(imagemodelname, torch_dtype=torch.bfloat16) -# flux_pipe.enable_model_cpu_offload() -# model_size = sum(p.numel() * p.element_size() for p in flux_pipe.transformer.parameters()) -# #tokenizer_size = 0 # FLUX doesn't use a separate tokenizer -# loaded_models[imagemodelname] = flux_pipe -# # Calculate memory usage -# final_memory = torch.cuda.memory_allocated() -# memory_used = final_memory - initial_memory +def format_neuronpedia_feature(data, feature_id, layer, width): + """Format Neuronpedia feature data as markdown.""" + results = [] -# loaded_models[imagemodelname] = [str(time.time()), memory_used] + # Header + results.append(f"## Feature {feature_id} (Layer {layer}, {width} width)") + results.append("") -# lastimagemodelnameinloadfunction = (imagemodelname, model_size) #, tokenizer_size) -# print(f"Model and tokenizer {imagemodelname} loaded successfully") -# print(f"Model size: {get_size_str(model_size)}") -# #print(f"Tokenizer size: {get_size_str(tokenizer_size)}") -# print(f"GPU memory used: {get_size_str(memory_used)}") + # Description if available + if data.get("description"): + results.append(f"**Description:** {data['description']}") + results.append("") -# return (f"Model and tokenizer {imagemodelname} loaded successfully. " -# f"Model size: {get_size_str(model_size)}, " -# #f"Tokenizer size: {get_size_str(tokenizer_size)}, " -# f"GPU memory used: {get_size_str(memory_used)}") + # Auto-interp explanation if available + if data.get("explanations") and len(data["explanations"]) > 0: + explanation = data["explanations"][0].get("description", "") + if explanation: + results.append(f"**Auto-interpretation:** {explanation}") + results.append("") + + # Activation examples + if data.get("activations") and len(data["activations"]) > 0: + results.append("### Top Activating Examples") + results.append("") + for i, act in enumerate(data["activations"][:5]): + tokens = act.get("tokens", []) + values = act.get("values", []) + if tokens: + # Highlight the max activating token + max_idx = values.index(max(values)) if values else 0 + text_parts = [] + for j, tok in enumerate(tokens): + if j == max_idx: + text_parts.append(f"**{tok}**") + else: + text_parts.append(tok) + text = "".join(text_parts) + results.append(f"{i+1}. {text}") + results.append("") + + # Stats + results.append("### Feature Stats") + results.append(f"- **Neuronpedia ID:** `gemma-2-2b_{layer}-gemmascope-res-{width}_{feature_id}`") + if data.get("max_activation"): + results.append(f"- **Max Activation:** {data['max_activation']:.2f}") + if data.get("frac_nonzero"): + results.append(f"- **Activation Frequency:** {data['frac_nonzero']*100:.2f}%") + + results.append("") + results.append(f"[View on Neuronpedia](https://www.neuronpedia.org/gemma-2-2b/{layer}-gemmascope-res-{width}/{feature_id})") + + return "\n".join(results) + +def load_image_model(model_name=None): + """Load image model on CPU - will be moved to GPU in @spaces.GPU function.""" + global image_pipe, current_image_model + + _torch = _ensure_torch() + diff = _ensure_diffusers() + + if model_name: + current_image_model = model_name + + print(f"Loading image model: {current_image_model}") + + # Don't call cuda.empty_cache() here - it initializes CUDA outside @spaces.GPU + gc.collect() + + image_pipe = diff.AutoPipelineForText2Image.from_pretrained( + current_image_model, + torch_dtype=_torch.float16, + variant="fp16" + ) + # Don't move to CUDA here - will be done in @spaces.GPU function + + print(f"Image model {current_image_model} loaded on CPU") + return image_pipe def clear_all_models(): - global model, tokenizer, flux_pipe, loaded_models + """Clear all loaded models from memory.""" + global model, tokenizer, image_pipe, loaded_models + for model_name, model_obj in loaded_models.items(): if isinstance(model_obj, tuple): - model_obj[0].to('cpu') del model_obj[0] del model_obj[1] else: - model_obj.to('cpu') del model_obj model = None tokenizer = None - flux_pipe = None + image_pipe = None loaded_models.clear() - torch.cuda.empty_cache() + # Don't call cuda.empty_cache() here - it initializes CUDA outside @spaces.GPU gc.collect() return "All models cleared from memory." @@ -159,15 +473,15 @@ def loaded_model_list(): # Initial model load -load_model(modelname) +# load_model(modelname) # load_image_model(imagemodelname) # Create embeddings for the knowledge base -knowledge_base_embeddings = embedding_model.encode([doc["content"] for doc in knowledge_base]) - def retrieve(query, k=2): + _torch = _ensure_torch() + initialize_rag() query_embedding = embedding_model.encode([query]) - similarities = torch.nn.functional.cosine_similarity(torch.tensor(query_embedding), torch.tensor(knowledge_base_embeddings)) + similarities = _torch.nn.functional.cosine_similarity(_torch.tensor(query_embedding), _torch.tensor(knowledge_base_embeddings)) top_k_indices = similarities.argsort(descending=True)[:k] return [(knowledge_base[i]["content"], knowledge_base[i]["id"]) for i in top_k_indices] @@ -198,12 +512,38 @@ def get_tokenizer_details(tokenizer): } @spaces.GPU -def generate_response(prompt, use_rag, stream=False): - global output_dict, model, tokenizer - - print(zero.device) # This will print 'cuda:0' inside the @spaces.GPU decorated function - torch.cuda.empty_cache() - print(dir(model)) +def generate_response(prompt, use_rag, stream=False, max_tokens=512, model_name=None): + """ + Generate text response using the loaded model. + + Args: + prompt: The input prompt + use_rag: Whether to use RAG (retrieval augmented generation) + stream: Whether to stream the response + max_tokens: Maximum number of tokens to generate (default 512) + model_name: Optional model name - if different from loaded model, will reload + """ + global output_dict, model, tokenizer, current_loaded_model_name + + _torch = _ensure_torch() + tf = _ensure_transformers() + + # Check if we need to load or switch models + if model_name and model_name != current_loaded_model_name: + print(f"Model switch requested: {current_loaded_model_name} -> {model_name}") + load_model(model_name) + + # Check if model is loaded + if model is None or tokenizer is None: + yield ("Error: No model loaded. Please select and load a model first using the model dropdown.", "N/A", "N/A", "N/A") + return + + zero = _torch.Tensor([0]).cuda() + print(f"GPU device: {zero.device}, Model: {current_loaded_model_name}") + _torch.cuda.empty_cache() + + # Move model to GPU for inference + model.to(zero.device) if use_rag: retrieved_docs = retrieve(prompt) @@ -225,7 +565,7 @@ def generate_response(prompt, use_rag, stream=False): model_inputs = tokenizer([text], return_tensors="pt").to(zero.device) start_time = time.time() total_tokens = 0 - + print(output_dict) output_key = f"output_{len(output_dict) + 1}" print(output_key) @@ -233,6 +573,8 @@ def generate_response(prompt, use_rag, stream=False): "input_prompt": prompt, "full_prompt": full_prompt, "use_rag": use_rag, + "max_tokens": max_tokens, + "model_name": current_loaded_model_name, "generated_text": "", "tokens_per_second": 0, "ram_usage": "", @@ -243,12 +585,15 @@ def generate_response(prompt, use_rag, stream=False): } print(output_dict) + # Ensure max_tokens is an integer + max_tokens = int(max_tokens) if max_tokens else 512 + if stream: - streamer = TextIteratorStreamer(tokenizer, skip_special_tokens=True) + streamer = tf.TextIteratorStreamer(tokenizer, skip_special_tokens=True) generation_kwargs = dict( model_inputs, streamer=streamer, - max_new_tokens=512, + max_new_tokens=max_tokens, temperature=0.7, ) thread = Thread(target=model.generate, kwargs=generation_kwargs) @@ -261,14 +606,14 @@ def generate_response(prompt, use_rag, stream=False): ram_usage = get_ram_usage() output_dict[output_key]["tokens_per_second"] = f"{tokens_per_second:.2f}" output_dict[output_key]["ram_usage"] = ram_usage - yield (output_dict[output_key]["generated_text"], - output_dict[output_key]["tokens_per_second"], - output_dict[output_key]["ram_usage"], + yield (output_dict[output_key]["generated_text"], + output_dict[output_key]["tokens_per_second"], + output_dict[output_key]["ram_usage"], output_dict[output_key]["doc_ids"]) else: generated_ids = model.generate( model_inputs.input_ids, - max_new_tokens=512 + max_new_tokens=max_tokens ) generated_ids = [ output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) @@ -278,36 +623,54 @@ def generate_response(prompt, use_rag, stream=False): end_time = time.time() tokens_per_second = total_tokens / (end_time - start_time) ram_usage = get_ram_usage() - + output_dict[output_key]["generated_text"] = response output_dict[output_key]["tokens_per_second"] = f"{tokens_per_second:.2f}" output_dict[output_key]["ram_usage"] = ram_usage print(output_dict) - yield (output_dict[output_key]["generated_text"], - output_dict[output_key]["tokens_per_second"], - output_dict[output_key]["ram_usage"], + yield (output_dict[output_key]["generated_text"], + output_dict[output_key]["tokens_per_second"], + output_dict[output_key]["ram_usage"], output_dict[output_key]["doc_ids"]) @spaces.GPU -def generate_image(prompt): - global output_dict, flux_pipe - - print(dir(flux_pipe)) - - # Generate image using FLUX - image = flux_pipe( - prompt, - guidance_scale=0.0, - num_inference_steps=4, - max_sequence_length=256, - generator=torch.Generator("cpu").manual_seed(0) - ).images[0] - image_path = f"flux_output_{time.time()}.png" - print(image_path) - image.save(image_path) - ram_usage = get_ram_usage() - return image_path, ram_usage, image_path +def generate_image(prompt, model_choice=None): + global image_pipe, current_image_model + + _torch = _ensure_torch() + + try: + # Load model on-demand if not loaded or if different model requested + if image_pipe is None or (model_choice and model_choice != current_image_model): + print(f"Loading image model on-demand: {model_choice or current_image_model}") + load_image_model(model_choice) + + if image_pipe is None: + ram_usage = get_ram_usage() + return "Error: Failed to load image model.", ram_usage, None + + # Move model to GPU (loaded on CPU in load_image_model) + zero = _torch.Tensor([0]).cuda() + image_pipe.to(zero.device) + + # Generate image using SD-turbo or SDXL-turbo + # These models work best with guidance_scale=0.0 and few steps + image = image_pipe( + prompt=prompt, + num_inference_steps=4, + guidance_scale=0.0, + ).images[0] + + # Save to saved_media folder so it appears in file explorer + image_filename = f"sd_output_{time.time()}.png" + image_path = os.path.join(GENERATED_MEDIA_DIR, image_filename) + image.save(image_path) + ram_usage = get_ram_usage() + return f"Image generated with {current_image_model}: {image_filename}", ram_usage, image + except Exception as e: + ram_usage = get_ram_usage() + return f"Error generating image: {str(e)}", ram_usage, None def get_output_details(output_key): if output_key in output_dict: @@ -335,4 +698,752 @@ def format_output_dict(): formatted_output += json.dumps(value, indent=2) formatted_output += "\n\n" print(formatted_output) - return formatted_output \ No newline at end of file + return formatted_output + + +# ============================================================ +# TTS GENERATION (Multiple Backends) +# ============================================================ +# Supported TTS models: +# - hexgrad/Kokoro-82M: Fast, lightweight TTS (82M params) +# - Supertone/supertonic-2: High-quality expressive TTS (66M params, ONNX) +# - zai-org/GLM-TTS: Multilingual text-to-speech + +TTS_MODELS = { + "kokoro": { + "name": "Kokoro-82M", + "space": "Pendrokar/TTS-Spaces-Arena", # Arena has API enabled, supports Kokoro + "fallback_spaces": ["eric-cli/Kokoro-TTS-Local"], + "description": "Fast, lightweight TTS with natural voices", + "local_support": True, + "voices": ["af_heart", "af_bella", "af_nicole", "af_sarah", "af_sky", + "am_adam", "am_michael", "bf_emma", "bf_isabella", "bm_george", "bm_lewis"] + }, + "supertonic": { + "name": "Supertonic-2", + "space": "Supertone/supertonic-2", + "fallback_spaces": [], + "description": "High-quality expressive speech synthesis (ONNX)", + "local_support": True, + "voices": ["F1", "F2", "F3", "F4", "F5", "M1", "M2", "M3", "M4", "M5"] + }, + "glm-tts": { + "name": "GLM-TTS", + "space": "zai-org/GLM-TTS", + "fallback_spaces": [], + "description": "Multilingual text-to-speech with voice cloning", + "local_support": False, + "voices": ["default"] + } +} + +# Cached model instances +_kokoro_pipeline = None +_supertonic_model = None + +def _load_kokoro(): + """Load Kokoro-82M pipeline for local TTS generation.""" + global _kokoro_pipeline + if _kokoro_pipeline is None: + print("Loading Kokoro-82M...") + from kokoro import KPipeline + _kokoro_pipeline = KPipeline(lang_code='a') + print("Kokoro-82M loaded successfully") + return _kokoro_pipeline + +def _load_supertonic(): + """Load Supertonic-2 model for local TTS generation.""" + global _supertonic_model + if _supertonic_model is None: + print("Loading Supertonic-2...") + # Suppress ONNX runtime GPU discovery warnings on systems without proper GPU access + import os + import warnings + os.environ.setdefault('ORT_DISABLE_ALL_WARNINGS', '1') + warnings.filterwarnings('ignore', message='.*device_discovery.*') + warnings.filterwarnings('ignore', message='.*GPU device discovery failed.*') + + from supertonic import TTS + _supertonic_model = TTS(auto_download=True) + print("Supertonic-2 loaded successfully") + return _supertonic_model + +@spaces.GPU +def generate_tts_local(text, model="kokoro", voice="af_heart"): + """ + Generate TTS audio locally using ZeroGPU. + + Args: + text: The text to convert to speech + model: One of "kokoro", "supertonic" + voice: Voice name (model-specific) + + Returns: + Tuple of (status_message, audio_path or None) + """ + import soundfile as sf + + try: + safe_text = text[:30].replace(' ', '_').replace('/', '_').replace('\\', '_') + filename = f"tts_{model}_{safe_text}_{int(time.time())}.wav" + filepath = os.path.join(GENERATED_MEDIA_DIR, filename) + + if model == "kokoro": + pipeline = _load_kokoro() + if pipeline is None: + return "Error: Failed to load Kokoro model", None + + # Generate audio - Kokoro yields segments + generator = pipeline(text, voice=voice) + audio_segments = [] + for i, (gs, ps, audio) in enumerate(generator): + audio_segments.append(audio) + + # Concatenate all segments + import numpy as np + full_audio = np.concatenate(audio_segments) if len(audio_segments) > 1 else audio_segments[0] + + # Kokoro outputs 24kHz audio + sf.write(filepath, full_audio, 24000) + return f"TTS saved as {filepath}", filepath + + elif model == "supertonic": + tts = _load_supertonic() + if tts is None: + return "Error: Failed to load Supertonic model", None + + # Get voice style (F3 is a good default female voice) + # Available: M1-M5 (male), F1-F5 (female) + voice_name = voice if voice != "default" else "F3" + style = tts.get_voice_style(voice_name=voice_name) + + # Generate audio with Supertonic + wav, duration = tts.synthesize(text, voice_style=style) + + # Supertonic outputs 24kHz audio, wav shape is (1, num_samples) + audio = wav.squeeze() # Remove batch dimension + sf.write(filepath, audio, 24000) + return f"TTS saved as {filepath}", filepath + + else: + return f"Error: Model '{model}' does not support local generation", None + + except Exception as e: + return f"Error generating TTS locally with {model}: {str(e)}", None + + +def generate_tts_api(text, model="kokoro", voice="default"): + """ + Generate TTS audio using HuggingFace Space APIs (fallback). + + Args: + text: The text to convert to speech + model: One of "kokoro", "supertonic", or "glm-tts" + voice: Voice parameter (model-specific) + + Returns: + Tuple of (status_message, audio_path or None) + """ + from gradio_client import Client + + if model not in TTS_MODELS: + return f"Error: Unknown TTS model '{model}'. Available: {list(TTS_MODELS.keys())}", None + + model_info = TTS_MODELS[model] + spaces_to_try = [model_info["space"]] + model_info.get("fallback_spaces", []) + + last_error = None + for space in spaces_to_try: + try: + print(f"Trying TTS via {space}...") + client = Client(space) + + # Try to discover API endpoints + result = None + + if model == "kokoro": + # TTS Arena uses different endpoint names + if "Arena" in space: + # Try arena-style endpoints + try: + result = client.predict( + text, # text input + voice if voice != "default" else "af_heart", # voice + 1.0, # speed + api_name="/synthesize" + ) + except Exception: + # Try alternate endpoint + result = client.predict( + text, + api_name="/predict" + ) + else: + # Try common Kokoro endpoint names + for endpoint in ["/generate_speech", "/generate", "/synthesize", "/predict"]: + try: + result = client.predict( + text, + voice if voice != "default" else "af_heart", + 1.0, # speed + api_name=endpoint + ) + break + except Exception: + continue + + elif model == "supertonic": + for endpoint in ["/synthesize", "/predict", "/generate"]: + try: + result = client.predict(text, api_name=endpoint) + break + except Exception: + continue + + elif model == "glm-tts": + for endpoint in ["/synthesize", "/predict", "/generate", "/infer"]: + try: + result = client.predict(text, api_name=endpoint) + break + except Exception: + continue + + if result is None: + continue + + # Process result - usually returns audio file path or tuple + audio_path = None + if isinstance(result, str) and os.path.exists(result): + audio_path = result + elif isinstance(result, tuple): + for item in result: + if isinstance(item, str) and os.path.exists(item): + audio_path = item + break + elif isinstance(result, dict) and 'audio' in result: + audio_path = result['audio'] + + if audio_path and os.path.exists(audio_path): + safe_text = text[:30].replace(' ', '_').replace('/', '_').replace('\\', '_') + filename = f"tts_{model}_{safe_text}_{int(time.time())}.wav" + filepath = os.path.join(GENERATED_MEDIA_DIR, filename) + + import shutil + shutil.copy(audio_path, filepath) + return f"TTS saved as {filepath}", filepath + + except Exception as e: + last_error = str(e) + print(f"TTS API error with {space}: {e}") + continue + + return f"Error: All TTS API attempts failed. Last error: {last_error}", None + + +# ============================================================ +# LOCAL 3D GENERATION (Shap-E) +# ============================================================ +shap_e_model = None +shap_e_diffusion = None +shap_e_xm = None + +def load_shap_e(): + """Load Shap-E model for local 3D generation.""" + global shap_e_model, shap_e_diffusion, shap_e_xm + if shap_e_model is None: + _torch = _ensure_torch() + print("Loading Shap-E...") + import shap_e + from shap_e.diffusion.sample import sample_latents + from shap_e.diffusion.gaussian_diffusion import diffusion_from_config + from shap_e.models.download import load_model, load_config + + device = _torch.device("cuda" if _torch.cuda.is_available() else "cpu") + + shap_e_xm = load_model('transmitter', device=device) + shap_e_model = load_model('text300M', device=device) + shap_e_diffusion = diffusion_from_config(load_config('diffusion')) + + print("Shap-E loaded successfully") + return shap_e_model, shap_e_diffusion, shap_e_xm + +@spaces.GPU +def generate_3d_local(prompt, guidance_scale=15.0, num_steps=64): + """ + Generate 3D model locally using Shap-E. + + Args: + prompt: Text description of the 3D object + guidance_scale: Classifier-free guidance scale + num_steps: Number of diffusion steps + + Returns: + Tuple of (status_message, model_path or None) + """ + global shap_e_model, shap_e_diffusion, shap_e_xm + + try: + _torch = _ensure_torch() + from shap_e.diffusion.sample import sample_latents + from shap_e.util.notebooks import decode_latent_mesh + import trimesh + + device = _torch.device("cuda" if _torch.cuda.is_available() else "cpu") + + # Load model if needed + load_shap_e() + + if shap_e_model is None: + return "Error: Failed to load Shap-E model", None + + # Generate latents + latents = sample_latents( + batch_size=1, + model=shap_e_model, + diffusion=shap_e_diffusion, + guidance_scale=guidance_scale, + model_kwargs=dict(texts=[prompt]), + progress=True, + clip_denoised=True, + use_fp16=True, + use_karras=True, + karras_steps=num_steps, + sigma_min=1e-3, + sigma_max=160, + s_churn=0, + ) + + # Decode to mesh + mesh = decode_latent_mesh(shap_e_xm, latents[0]).tri_mesh() + + # Save as GLB + safe_prompt = prompt[:40].replace(' ', '_').replace('/', '_').replace('\\', '_') + filename = f"3d_local_{safe_prompt}_{int(time.time())}.glb" + filepath = os.path.join(GENERATED_MEDIA_DIR, filename) + + # Convert to trimesh and export + tri_mesh = trimesh.Trimesh(vertices=mesh.verts, faces=mesh.faces) + tri_mesh.export(filepath) + + return f"3D model saved as {filepath}", filepath + + except Exception as e: + return f"Error generating 3D locally: {str(e)}", None + + +# ============================================================ +# VIDEO GENERATION (Text-to-Video, Image-to-Video) +# ============================================================ +_video_pipe = None +_current_video_model = None + +@spaces.GPU(duration=180) # 3 min timeout for video generation +def generate_video_t2v(prompt, model_name="Lightricks/LTX-Video-0.9.7-distilled", + num_steps=4, duration_seconds=2, width=512, height=320): + """ + Generate video from text prompt using diffusers. + + Args: + prompt: Text description of the video + model_name: HuggingFace model ID + num_steps: Number of inference steps + duration_seconds: Video duration in seconds + width: Video width + height: Video height + + Returns: + Tuple of (status_message, video_path or None) + """ + global _video_pipe, _current_video_model + + _torch = _ensure_torch() + + try: + from diffusers.utils import export_to_video + + # Calculate frames (target 24fps) + raw_frames = duration_seconds * 24 + + # LTX-Video requires (frames - 1) divisible by 8, so frames = 8n + 1 + # Valid: 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, 97... + if "LTX" in model_name or "Lightricks" in model_name: + # Round to nearest valid frame count (8n + 1) + n = round((raw_frames - 1) / 8) + num_frames = max(9, n * 8 + 1) # Minimum 9 frames + # Ensure dimensions divisible by 32 + width = (width // 32) * 32 + height = (height // 32) * 32 + print(f"[LTX] Adjusted to {num_frames} frames (was {raw_frames}), {width}x{height}") + else: + num_frames = raw_frames + + negative_prompt = "worst quality, inconsistent motion, blurry, jittery, distorted" + + # Load pipeline based on model type + if _video_pipe is None or _current_video_model != model_name: + print(f"Loading video model: {model_name}") + _torch.cuda.empty_cache() + gc.collect() + + if "LTX" in model_name or "Lightricks" in model_name: + from diffusers import LTXPipeline + _video_pipe = LTXPipeline.from_pretrained( + model_name, torch_dtype=_torch.bfloat16 + ) + # Use CPU offload for memory efficiency + _video_pipe.enable_model_cpu_offload() + if hasattr(_video_pipe, 'vae'): + _video_pipe.vae.enable_tiling() + + elif "Wan" in model_name: + from diffusers import WanPipeline, AutoencoderKLWan + vae = AutoencoderKLWan.from_pretrained( + model_name, subfolder="vae", torch_dtype=_torch.float32 + ) + _video_pipe = WanPipeline.from_pretrained( + model_name, vae=vae, torch_dtype=_torch.bfloat16 + ) + # Use CPU offload for memory efficiency + _video_pipe.enable_model_cpu_offload() + if hasattr(_video_pipe, 'vae'): + _video_pipe.vae.enable_tiling() + + elif "Hunyuan" in model_name: + from diffusers import HunyuanVideoPipeline + _video_pipe = HunyuanVideoPipeline.from_pretrained( + model_name, torch_dtype=_torch.bfloat16 + ) + # Use CPU offload for memory efficiency + _video_pipe.enable_model_cpu_offload() + if hasattr(_video_pipe, 'vae'): + _video_pipe.vae.enable_tiling() + + else: + from diffusers import DiffusionPipeline + _video_pipe = DiffusionPipeline.from_pretrained( + model_name, torch_dtype=_torch.bfloat16 + ) + _video_pipe.enable_model_cpu_offload() + + _current_video_model = model_name + print(f"Video model loaded: {model_name}") + + print(f"Generating video: {width}x{height}, {num_frames} frames, {num_steps} steps") + + # Generate video with model-specific parameters + if "LTX" in model_name or "Lightricks" in model_name: + output = _video_pipe( + prompt=prompt, + negative_prompt=negative_prompt, + width=width, + height=height, + num_frames=num_frames, + num_inference_steps=num_steps, + guidance_scale=1.0, + ) + elif "Wan" in model_name: + output = _video_pipe( + prompt=prompt, + negative_prompt=negative_prompt, + height=height, + width=width, + num_frames=num_frames, + guidance_scale=5.0, + num_inference_steps=num_steps, + ) + elif "Hunyuan" in model_name: + output = _video_pipe( + prompt=prompt, + negative_prompt=negative_prompt, + height=height, + width=width, + num_frames=num_frames, + num_inference_steps=num_steps, + ) + else: + output = _video_pipe( + prompt=prompt, + num_inference_steps=num_steps, + num_frames=num_frames, + width=width, + height=height, + ) + + # Get video frames + if hasattr(output, 'frames'): + frames = output.frames[0] if isinstance(output.frames, list) else output.frames + else: + frames = output[0] + + # Save to file + safe_prompt = prompt[:30].replace(' ', '_').replace('/', '_').replace('\\', '_') + filename = f"video_t2v_{safe_prompt}_{int(time.time())}.mp4" + filepath = os.path.join(GENERATED_MEDIA_DIR, filename) + + export_to_video(frames, filepath, fps=24) + + return f"Video saved as {filepath}", filepath + + except Exception as e: + import traceback + print(f"Error generating video: {traceback.format_exc()}") + return f"Error generating video: {str(e)}", None + + +@spaces.GPU(duration=180) # 3 min timeout for video generation +def generate_video_i2v(image_path, prompt="", model_name="Wan-AI/Wan2.2-TI2V-5B-Diffusers", + num_steps=8, duration_seconds=2): + """ + Generate video from image using diffusers. + + Args: + image_path: Path to input image + prompt: Optional motion/style prompt + model_name: HuggingFace model ID + num_steps: Number of inference steps + duration_seconds: Video duration in seconds + + Returns: + Tuple of (status_message, video_path or None) + """ + global _video_pipe, _current_video_model + + _torch = _ensure_torch() + + try: + from diffusers.utils import export_to_video + from PIL import Image + + # Load image + image = Image.open(image_path).convert("RGB") + num_frames = duration_seconds * 24 + negative_prompt = "worst quality, inconsistent motion, blurry, jittery, distorted" + + # Load pipeline if needed or if model changed + if _video_pipe is None or _current_video_model != model_name: + print(f"Loading video model: {model_name}") + _torch.cuda.empty_cache() + gc.collect() + + if "Wan" in model_name: + from diffusers import WanImageToVideoPipeline, AutoencoderKLWan + # TI2V-5B supports both T2V and I2V, use it directly + # For T2V-A14B, switch to I2V-A14B-Diffusers + if "TI2V" in model_name: + i2v_model = model_name # TI2V-5B handles I2V directly + elif "T2V" in model_name: + i2v_model = model_name.replace("T2V", "I2V") # T2V-A14B -> I2V-A14B + else: + i2v_model = model_name # Already I2V model + + vae = AutoencoderKLWan.from_pretrained( + i2v_model, subfolder="vae", torch_dtype=_torch.float32 + ) + _video_pipe = WanImageToVideoPipeline.from_pretrained( + i2v_model, vae=vae, torch_dtype=_torch.bfloat16 + ) + # Use CPU offload for memory efficiency + _video_pipe.enable_model_cpu_offload() + if hasattr(_video_pipe, 'vae'): + _video_pipe.vae.enable_tiling() + else: + from diffusers import DiffusionPipeline + _video_pipe = DiffusionPipeline.from_pretrained( + model_name, torch_dtype=_torch.bfloat16 + ) + _video_pipe.enable_model_cpu_offload() + + _current_video_model = model_name + print(f"Video model loaded: {model_name}") + + # Get image dimensions + width, height = image.size + # Ensure dimensions are multiples of 16 + width = (width // 16) * 16 + height = (height // 16) * 16 + image = image.resize((width, height)) + + print(f"Generating I2V: {width}x{height}, {num_frames} frames, {num_steps} steps") + + # Generate video from image + if "Wan" in model_name: + output = _video_pipe( + image=image, + prompt=prompt if prompt else "camera movement, smooth motion", + negative_prompt=negative_prompt, + height=height, + width=width, + num_frames=num_frames, + guidance_scale=5.0, + num_inference_steps=num_steps, + ) + else: + output = _video_pipe( + image=image, + prompt=prompt if prompt else None, + num_inference_steps=num_steps, + num_frames=num_frames, + ) + + if hasattr(output, 'frames'): + frames = output.frames[0] if isinstance(output.frames, list) else output.frames + else: + frames = output[0] + + # Save to file + safe_prompt = (prompt[:20] if prompt else "i2v").replace(' ', '_').replace('/', '_') + filename = f"video_i2v_{safe_prompt}_{int(time.time())}.mp4" + filepath = os.path.join(GENERATED_MEDIA_DIR, filename) + + export_to_video(frames, filepath, fps=24) + + return f"Video saved as {filepath}", filepath + + except Exception as e: + import traceback + print(f"Error generating I2V: {traceback.format_exc()}") + return f"Error generating video: {str(e)}", None + + +# ============================================================ +# LOCAL TALKING HEAD GENERATION (SadTalker) +# ============================================================ +sadtalker_model = None + +def load_sadtalker(): + """Load SadTalker model for local talking head generation.""" + global sadtalker_model + if sadtalker_model is None: + print("[SadTalker] Loading SadTalker model...") + try: + # Clone and setup SadTalker if not present + import subprocess + import sys + + sadtalker_path = os.path.join(os.path.dirname(__file__), "SadTalker") + if not os.path.exists(sadtalker_path): + print("[SadTalker] Cloning SadTalker repository...") + subprocess.run([ + "git", "clone", "--depth", "1", + "https://github.com/OpenTalker/SadTalker.git", + sadtalker_path + ], check=True) + + # Add to path + if sadtalker_path not in sys.path: + sys.path.insert(0, sadtalker_path) + + # Download checkpoints if needed + checkpoints_path = os.path.join(sadtalker_path, "checkpoints") + if not os.path.exists(checkpoints_path): + print("[SadTalker] Downloading checkpoints...") + os.makedirs(checkpoints_path, exist_ok=True) + # Use huggingface_hub to download + from huggingface_hub import hf_hub_download + + # Download the main checkpoints + for filename in [ + "mapping_00109-model.pth.tar", + "mapping_00229-model.pth.tar", + "SadTalker_V0.0.2_256.safetensors", + "SadTalker_V0.0.2_512.safetensors" + ]: + try: + hf_hub_download( + repo_id="vinthony/SadTalker", + filename=filename, + local_dir=checkpoints_path + ) + except Exception as e: + print(f"[SadTalker] Warning: Could not download {filename}: {e}") + + sadtalker_model = {"path": sadtalker_path, "loaded": True} + print("[SadTalker] SadTalker loaded successfully") + + except Exception as e: + print(f"[SadTalker] Failed to load: {e}") + sadtalker_model = None + + return sadtalker_model + +@spaces.GPU +def generate_talking_head_local(image_path, audio_path, preprocess="crop"): + """ + Generate talking head video locally using SadTalker. + + Args: + image_path: Path to portrait image + audio_path: Path to audio file + preprocess: Preprocessing mode - "crop", "resize", or "full" + + Returns: + Tuple of (status_message, video_path or None) + """ + global sadtalker_model + + try: + import subprocess + import sys + + print(f"[SadTalker] Starting local generation...") + print(f"[SadTalker] Image: {image_path}") + print(f"[SadTalker] Audio: {audio_path}") + + # Load model + model_info = load_sadtalker() + if model_info is None: + return "Error: Failed to load SadTalker model", None + + sadtalker_path = model_info["path"] + + # Create output directory + output_dir = os.path.join(GENERATED_MEDIA_DIR, "sadtalker_output") + os.makedirs(output_dir, exist_ok=True) + + # Run inference using subprocess (SadTalker's inference script) + inference_script = os.path.join(sadtalker_path, "inference.py") + + if os.path.exists(inference_script): + cmd = [ + sys.executable, inference_script, + "--driven_audio", audio_path, + "--source_image", image_path, + "--result_dir", output_dir, + "--preprocess", preprocess, + "--size", "256", + "--still", # Less head movement for stability + ] + + print(f"[SadTalker] Running: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + + if result.returncode != 0: + print(f"[SadTalker] Error output: {result.stderr}") + return f"Error: SadTalker inference failed: {result.stderr[:500]}", None + + # Find the output video + for f in os.listdir(output_dir): + if f.endswith(".mp4"): + video_path = os.path.join(output_dir, f) + # Move to main output directory + final_path = os.path.join( + GENERATED_MEDIA_DIR, + f"talking_head_local_{int(time.time())}.mp4" + ) + import shutil + shutil.move(video_path, final_path) + print(f"[SadTalker] Success! Video saved to: {final_path}") + return f"Talking head video saved as {final_path}", final_path + + return "Error: No output video found", None + else: + # Fallback: Use the SadTalker as a module + print("[SadTalker] inference.py not found, trying module import...") + return "Error: SadTalker inference script not found", None + + except subprocess.TimeoutExpired: + return "Error: SadTalker generation timed out (>5 minutes)", None + except Exception as e: + import traceback + print(f"[SadTalker] Error: {traceback.format_exc()}") + return f"Error generating talking head locally: {str(e)}", None \ No newline at end of file diff --git a/llm_playtester.py b/llm_playtester.py new file mode 100644 index 0000000000000000000000000000000000000000..4db801c484a25618307df1deaccea0231c535411 --- /dev/null +++ b/llm_playtester.py @@ -0,0 +1,382 @@ +""" +LLM Playtester - Automated game playtesting using AI models. + +NOTE: Future Enhancement - Media-Aware Playtesting +Currently, the LLM playtester only sees text (descriptions, choices, game state). +It does NOT receive or process media files referenced in config states. + +Future local models to explore for media-aware playtesting: +- Vision-Language Models (VLMs): LLaVA, Qwen2-VL, InternVL, MiniCPM-V, PaliGemma + Could describe what's in scene images, evaluate visual consistency +- Audio understanding: Whisper (local) + LLM for ambient audio analysis +- Multimodal game testing: Feed image+text to local VLM, ask "does this image match the description?" +- HuggingFace Spaces: Qwen/Qwen2-VL, OpenGVLab/InternVL2, openbmb/MiniCPM-V-2_6 + +Integration approach when ready: +1. Extract media paths from current state (already available via get_current_state_media()) +2. For images: encode as base64 or use VLM API +3. Add to build_playtest_prompt(): "Scene visuals: [image description or VLM analysis]" +4. Immersionist perspective would especially benefit from visual context +""" + +import json +import re +from typing import List, Dict, Tuple, Optional +from dataclasses import dataclass, field + +import spaces + + +# Playtest perspectives - shortened versions for local LLM (token efficient) +PLAYTEST_PERSPECTIVES = { + "default": { + "name": "Default Playtester", + "system": "You are a game playtester. Follow instructions exactly.", + "instruction": "Pick the most interesting choice to explore the game." + }, + "completionist": { + "name": "The Completionist", + "system": "You are a completionist player who wants to see ALL content. You methodically explore every option.", + "instruction": "Pick a choice you haven't tried yet. If stuck in a loop, try something different. Note if you feel content is missing." + }, + "story_lover": { + "name": "The Story Lover", + "system": "You care deeply about narrative. You notice plot holes, character inconsistencies, and emotional beats.", + "instruction": "Pick the choice that advances the story most interestingly. Note any narrative problems or inconsistencies." + }, + "skeptic": { + "name": "The Skeptic", + "system": "You question everything. You notice when choices don't make sense or feel forced.", + "instruction": "Pick a choice, but note if it feels illogical or forced. Ask 'why would I do this?' for each option." + }, + "speedrunner": { + "name": "The Speed Runner", + "system": "You want to finish fast. You skip flavor text and notice padding or filler content.", + "instruction": "Pick the most direct choice. Note if the game forces unnecessary steps or feels padded." + }, + "immersionist": { + "name": "The Immersionist", + "system": "You want to feel immersed. You notice thin descriptions, mood breaks, and atmosphere issues.", + "instruction": "Pick what feels most natural. Note if descriptions are weak or atmosphere breaks." + }, + "first_timer": { + "name": "The First-Timer", + "system": "You're new to games and easily confused. You notice unclear directions and assumed knowledge.", + "instruction": "Pick what seems clearest. Note anything confusing or unexplained. Express confusion openly." + }, + "replayer": { + "name": "The Replayer", + "system": "You've played before and want variety. You notice when different choices lead to same outcomes.", + "instruction": "Pick something different from the obvious path. Note if choices feel meaningless or repetitive." + }, + "edge_finder": { + "name": "The Edge-Case Finder", + "system": "You try to break games. You make unusual choices and look for contradictions or bugs.", + "instruction": "Pick the weirdest or least expected choice. Try to find loops, contradictions, or broken states." + } +} + +# Lazy torch import - only load when needed +_torch = None +def _ensure_torch(): + global _torch + if _torch is None: + import torch + _torch = torch + return _torch + +# Import game engine components +from my_text_game_engine_attempt import GameSession, sanitize_config_for_serialization +from game_state import GameState +import my_text_game_engine_attempt as game_engine + +# Import the model loading infrastructure +import leveraging_machine_learning as ml + + +@dataclass +class PlaythroughStep: + """Record of a single step in the playthrough.""" + step_number: int + location: str + state: str + description: str + available_choices: List[str] + choice_made: str + reasoning: str + state_changes: Dict = field(default_factory=dict) + + +@dataclass +class PlaythroughResult: + """Complete playthrough result.""" + steps: List[PlaythroughStep] = field(default_factory=list) + states_visited: List[str] = field(default_factory=list) + issues_found: List[str] = field(default_factory=list) + final_state: Dict = field(default_factory=dict) + completed: bool = False + error: Optional[str] = None + + +def build_playtest_prompt(description: str, choices: List[str], game_state, perspective: str = "default") -> str: + """Build the prompt for the LLM to make a choice.""" + choices_text = "\n".join([f"{i+1}. {choice}" for i, choice in enumerate(choices)]) + + inventory = ", ".join(game_state.inventory) if game_state.inventory else "(empty)" + flags_active = [k for k, v in game_state.flags.items() if v] + flags_text = ", ".join(flags_active) if flags_active else "(none)" + + # Get perspective-specific instruction + persp = PLAYTEST_PERSPECTIVES.get(perspective, PLAYTEST_PERSPECTIVES["default"]) + instruction = persp["instruction"] + + prompt = f"""You are playtesting a text adventure game. Select ONE choice and explain briefly. + +Current Location: {game_state.current_location} +Scene: {description} + +Available choices: +{choices_text} + +Your current state: +- Inventory: {inventory} +- Money: {game_state.money} +- Active flags: {flags_text} + +Instructions: {instruction} Reply with ONLY: +Choice: [number] +Reason: [one sentence explanation] +Issue: [any problem noticed, or "none"] + +Your response:""" + + return prompt + + +def parse_llm_choice(response: str, num_choices: int) -> Tuple[int, str, Optional[str]]: + """Parse the LLM response to extract choice number, reasoning, and any issues noted.""" + choice_match = re.search(r'Choice:\s*(\d+)', response, re.IGNORECASE) + reason_match = re.search(r'Reason:\s*(.+?)(?:\n|Issue:|$)', response, re.IGNORECASE | re.DOTALL) + issue_match = re.search(r'Issue:\s*(.+?)(?:\n|$)', response, re.IGNORECASE | re.DOTALL) + + # Extract issue if present and not "none" + issue = None + if issue_match: + issue_text = issue_match.group(1).strip().lower() + if issue_text and issue_text != "none" and issue_text != "n/a": + issue = issue_match.group(1).strip() + + if choice_match: + choice_num = int(choice_match.group(1)) + if 1 <= choice_num <= num_choices: + reasoning = reason_match.group(1).strip() if reason_match else "No reasoning provided" + return choice_num - 1, reasoning, issue + + # Fallback: look for any number in the response + numbers = re.findall(r'\b(\d+)\b', response) + for num_str in numbers: + num = int(num_str) + if 1 <= num <= num_choices: + return num - 1, f"Chose option {num}", issue + + return 0, "Defaulting to first choice", issue + + +def format_playthrough_log(result: PlaythroughResult) -> str: + """Format playthrough result as readable log.""" + if result.error: + return f"ERROR: {result.error}" + + lines = ["=== Playthrough Log ===\n"] + + for step in result.steps: + lines.append(f"Step {step.step_number}: {step.location}/{step.state}") + lines.append(f" Scene: {step.description[:80]}...") + lines.append(f" Choices: {len(step.available_choices)} available") + lines.append(f" Chose: '{step.choice_made}'") + lines.append(f" Reason: {step.reasoning}") + lines.append("") + + lines.append(f"Total steps: {len(result.steps)}") + lines.append(f"Unique states: {len(set(result.states_visited))}") + lines.append(f"Completed: {'Yes' if result.completed else 'No'}") + + if result.final_state: + lines.append(f"\nFinal State:") + lines.append(f" Location: {result.final_state.get('location')}/{result.final_state.get('state')}") + lines.append(f" Inventory: {result.final_state.get('inventory', [])}") + lines.append(f" Money: {result.final_state.get('money', 0)}") + + return "\n".join(lines) + + +def format_issues(result: PlaythroughResult) -> str: + """Format issues found during playthrough.""" + if result.error: + return f"ERROR: {result.error}" + + if not result.issues_found: + return "No issues found during playthrough." + + lines = ["=== Issues Found ===\n"] + for i, issue in enumerate(result.issues_found, 1): + lines.append(f"{i}. {issue}") + + return "\n".join(lines) + + +@spaces.GPU +def run_llm_playtest(config_json: str, model_name: str = "unsloth/Llama-3.2-1B-Instruct", max_steps: int = 20, perspective: str = "default") -> Tuple[str, str]: + """ + Run LLM-powered playtest of a game config. + + Args: + config_json: The game config JSON string + model_name: Name of the LLM model to use + max_steps: Maximum number of steps to take + perspective: Playtest perspective (default, completionist, story_lover, etc.) + """ + if not config_json or not config_json.strip(): + return "No config provided", "Please provide a game config JSON" + + # Parse config + try: + config = json.loads(config_json) + except json.JSONDecodeError as e: + return f"Invalid JSON: {str(e)}", "Fix the JSON errors first" + + result = PlaythroughResult() + + # Get perspective info + persp = PLAYTEST_PERSPECTIVES.get(perspective, PLAYTEST_PERSPECTIVES["default"]) + perspective_name = persp["name"] + system_prompt = persp["system"] + + # Autoload model if not loaded + if ml.model is None or ml.tokenizer is None: + ml.load_model(model_name) + + # Move model to GPU + _torch = _ensure_torch() + zero = _torch.Tensor([0]).cuda() + ml.model.to(zero.device) + + # Initialize game session + try: + config = sanitize_config_for_serialization(config) + game_engine.all_states = config + + starting_location = next(iter(config.keys())) + starting_state = next(iter(config[starting_location].keys())) + + game_session = GameSession(starting_location, starting_state) + except Exception as e: + return f"Failed to initialize game: {str(e)}", "Check your config format" + + # Get initial state + description, choices, _ = game_session.get_current_state_info() + + # Play through the game + step = 0 + visited_states = set() + + while step < max_steps and choices: + current_location = game_session.current_location + current_state_name = game_session.current_state + state_key = f"{current_location}_{current_state_name}" + + # Track visited states + if state_key in visited_states: + result.issues_found.append(f"Step {step}: Revisited state '{state_key}' - possible loop") + visited_states.add(state_key) + result.states_visited.append(state_key) + + # Get available choices + available_choices = [c for c in choices if c] + + if not available_choices: + result.issues_found.append(f"Step {step}: No choices at '{state_key}' - dead end?") + break + + # Build prompt and generate LLM response + prompt = build_playtest_prompt(description, available_choices, game_session.game_state, perspective) + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt} + ] + text = ml.tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True + ) + model_inputs = ml.tokenizer([text], return_tensors="pt").to(zero.device) + + generated_ids = ml.model.generate( + model_inputs.input_ids, + max_new_tokens=150, + temperature=0.7, + do_sample=True + ) + generated_ids = [ + output_ids[len(input_ids):] + for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) + ] + response = ml.tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] + + choice_idx, reasoning, issue = parse_llm_choice(response, len(available_choices)) + chosen_choice = available_choices[choice_idx] + + # Record perspective-specific issues + if issue: + result.issues_found.append(f"Step {step} [{perspective_name}]: {issue}") + + # Record this step + step_record = PlaythroughStep( + step_number=step, + location=current_location, + state=current_state_name, + description=description[:200] + "..." if len(description) > 200 else description, + available_choices=available_choices, + choice_made=chosen_choice, + reasoning=reasoning + ) + result.steps.append(step_record) + + # Make the choice + try: + choice_number = int(chosen_choice.split('.')[0]) - 1 + new_desc, new_choices, _ = game_session.make_choice(choice_number) + description = new_desc + choices = new_choices + except Exception as e: + result.issues_found.append(f"Step {step}: Error making choice: {str(e)}") + break + + step += 1 + + # Final state + gs = game_session.game_state + result.final_state = { + "location": gs.current_location, + "state": gs.current_state, + "inventory": list(gs.inventory), + "money": gs.money, + "flags": {k: v for k, v in gs.flags.items() if v} + } + + result.completed = step < max_steps and not choices + + if step >= max_steps: + result.issues_found.append(f"Reached max steps ({max_steps}) without completing") + + unique_states = len(set(result.states_visited)) + total_visits = len(result.states_visited) + if total_visits > unique_states * 2: + result.issues_found.append(f"High state repetition: {total_visits} visits to {unique_states} unique states") + + log = format_playthrough_log(result) + log = f"=== Perspective: {perspective_name} ===\n\n" + log + issues = format_issues(result) + + return log, issues diff --git a/my_text_game_engine_attempt.py b/my_text_game_engine_attempt.py index efc325c9339e09e1418ab95d3c2a75b057f2da00..dafa1798935f9e027c5aca69006e5acfdd3433f0 100644 --- a/my_text_game_engine_attempt.py +++ b/my_text_game_engine_attempt.py @@ -4,34 +4,314 @@ import os import tempfile import shutil import zipfile +import random from relatively_constant_variables import finished_product_demo, all_states +from game_state import GameState, Player +from condition_evaluator import ConditionEvaluator, TransitionResolver, EffectApplicator -class Player: - def __init__(self): - self.inventory = [] - self.money = 20 - self.knowledge = {} - - def add_item(self, item): - self.inventory.append(item) - - def has_item(self, item): - return item in self.inventory - - def update_knowledge(self, topic): - self.knowledge[topic] = True class GameSession: def __init__(self, starting_location='village', starting_state='start'): - self.player = Player() + # NEW: Use GameState for rich state tracking + self.game_state = GameState() + self.game_state.current_location = starting_location + self.game_state.current_state = starting_state + self.game_state.money = 20 # Default starting money + + # Backwards compatibility wrapper + self.player = Player(self.game_state) + + # Aliases for compatibility self.current_location = starting_location self.current_state = starting_state self.game_log = [] + # NEW: Evaluation components (initialized lazily for Gradio serialization) + self._evaluator = None + self._resolver = None + self._applicator = None + + # Apply on_enter effects for starting state + self._apply_on_enter_effects() + # Reset components so Gradio can pickle the session + # (they recreate automatically via lazy properties) + self._evaluator = None + self._resolver = None + self._applicator = None + + def _ensure_components(self): + """Lazily initialize/recreate evaluation components after deserialization.""" + if self._evaluator is None or self._resolver is None or self._applicator is None: + self._evaluator = ConditionEvaluator(self.game_state) + self._resolver = TransitionResolver(self.game_state) + self._applicator = EffectApplicator(self.game_state) + + @property + def evaluator(self): + self._ensure_components() + return self._evaluator + + @property + def resolver(self): + self._ensure_components() + return self._resolver + + @property + def applicator(self): + self._ensure_components() + return self._applicator + + def get_visible_choices(self): + """ + Get list of choices visible to player based on conditions. + Returns list of (index, choice_text, is_locked, lock_reason). + """ + try: + state = all_states[self.current_location][self.current_state] + except KeyError: + return [] + + choices = state.get('choices', []) + choice_config = state.get('choice_config', {}) + + visible = [] + for idx, choice in enumerate(choices): + config = choice_config.get(choice, {}) + condition = config.get('condition') + + if condition: + is_visible = self.evaluator.evaluate(condition) + if is_visible: + # Show the choice (possibly with override text) + display_text = config.get('visible_text', choice) + visible.append((idx, display_text, False, None)) + else: + # Optionally show locked state + hidden_text = config.get('hidden_text') + if hidden_text: + visible.append((idx, hidden_text, True, "Conditions not met")) + # If no hidden_text, choice is completely hidden + else: + # No condition = always visible (backwards compatible) + visible.append((idx, choice, False, None)) + + return visible + + def get_available_choice_indices(self): + """Get indices of choices that are currently available (not locked).""" + return [idx for idx, _, is_locked, _ in self.get_visible_choices() if not is_locked] + + def _apply_on_enter_effects(self): + """Apply effects triggered on entering a state.""" + try: + state = all_states[self.current_location][self.current_state] + on_enter = state.get('on_enter') + if on_enter: + self.applicator.apply(on_enter) + + # Auto-track location visit + self.game_state.visit_location(self.current_location) + except KeyError: + pass + + def _check_encounter_injection(self, state: dict) -> bool: + """ + Check if a random encounter should be injected. + Returns True if encounter was triggered (navigation handled). + """ + encounter = state.get('encounter_chance') + if not encounter: + return False + + probability = encounter.get('probability', 0) + + # Check bypass conditions + bypass = encounter.get('bypass_conditions') + if bypass and self.evaluator.evaluate(bypass): + return False + + # Roll for encounter + if random.random() < probability: + pool = encounter.get('pool', []) + if pool: + encounter_state = random.choice(pool) + # Store return state for after encounter + self.game_state.set_flag('_in_encounter', True) + self.game_state.update_knowledge('_return_state', + f"{self.current_location}_{self.current_state}") + self._navigate_to_state(encounter_state) + self._apply_on_enter_effects() + return True + + return False + + def _navigate_to_state(self, next_state: str): + """ + Navigate to a state, handling location/state (slash) and location_state (underscore) formats. + """ + # First, check for slash notation: "location/state" + if '/' in next_state: + parts = next_state.split('/', 1) + potential_location = parts[0] + potential_state = parts[1] + + if potential_location in all_states and potential_state in all_states[potential_location]: + self.current_location = potential_location + self.current_state = potential_state + else: + self.game_log.append(f"ERROR: Invalid transition target '{next_state}'") + return + elif '_' in next_state: + parts = next_state.split('_', 1) + potential_location = parts[0] + potential_state = parts[1] + + if potential_location in all_states and potential_state in all_states[potential_location]: + self.current_location = potential_location + self.current_state = potential_state + elif next_state in all_states.get(self.current_location, {}): + self.current_state = next_state + else: + # Try to find valid split point + found = False + for i in range(len(next_state)): + if next_state[i] == '_': + loc = next_state[:i] + st = next_state[i+1:] + if loc in all_states and st in all_states[loc]: + self.current_location = loc + self.current_state = st + found = True + break + if not found: + self.game_log.append(f"ERROR: Invalid transition target '{next_state}'") + return + else: + if next_state in all_states.get(self.current_location, {}): + self.current_state = next_state + else: + self.game_log.append(f"ERROR: State '{next_state}' not found") + return + + # Sync with game_state + self.game_state.current_location = self.current_location + self.game_state.current_state = self.current_state + + def save_game(self) -> str: + """Serialize game state for saving.""" + return self.game_state.to_json() + + def load_game_state(self, save_data: str) -> None: + """Restore game state from save.""" + self.game_state = GameState.from_json(save_data) + self.player = Player(self.game_state) + self.current_location = self.game_state.current_location + self.current_state = self.game_state.current_state + # Reset components so they get recreated with new game_state + self._evaluator = None + self._resolver = None + self._applicator = None + def make_choice(self, choice_index): - state = all_states[self.current_location][self.current_state] + """ + Process a player choice with full condition/transition/effect handling. + Supports: conditional choices, dynamic transitions, declarative effects, encounters. + """ + try: + state = all_states[self.current_location][self.current_state] + except KeyError as e: + error_msg = f"ERROR: Cannot find state '{self.current_location}_{self.current_state}'" + self.game_log.append(error_msg) + return error_msg, [], "\n".join(self.game_log) + + choices = state.get('choices', []) + + if not (0 <= choice_index < len(choices)): + return "Invalid choice. Please try again.", choices, "\n".join(self.game_log) + + choice = choices[choice_index] + + # NEW: Verify choice is currently visible/allowed + choice_config = state.get('choice_config', {}) + if choice in choice_config: + condition = choice_config[choice].get('condition') + if condition and not self.evaluator.evaluate(condition): + error_msg = f"Choice '{choice}' is not currently available." + self.game_log.append(error_msg) + return state['description'], choices, "\n".join(self.game_log) + + # Validate transition exists + if choice not in state.get('transitions', {}): + error_msg = f"ERROR: No transition defined for choice '{choice}'" + self.game_log.append(error_msg) + return state['description'], choices, "\n".join(self.game_log) + + # Log the choice + self.game_log.append(f"You chose: {choice}") + self.game_log.append(state['description']) + + # Record choice in history + self.game_state.record_choice( + f"{self.current_location}_{self.current_state}", + choice + ) + + # NEW: Apply declarative effects first + effects = state.get('effects', {}) + if choice in effects: + self.applicator.apply(effects[choice]) + + # EXISTING: Execute lambda consequences (backwards compatibility) + if 'consequences' in state and choice in state['consequences']: + consequence = state['consequences'][choice] + if consequence: + if callable(consequence): + consequence(self.player) + # Skip if it's not callable (string, etc.) + + # NEW: Check for random encounter injection + if self._check_encounter_injection(state): + return self.get_current_state_info() + + # NEW: Resolve dynamic transition (supports random, conditional) + transition_spec = state['transitions'][choice] + try: + if isinstance(transition_spec, str): + # Simple string transition (backwards compatible) + next_state = transition_spec + else: + # Dynamic transition (dict with random/conditional logic) + next_state = self.resolver.resolve(transition_spec) + except ValueError as e: + error_msg = f"ERROR: Transition resolution failed - {str(e)}" + self.game_log.append(error_msg) + return state['description'], choices, "\n".join(self.game_log) + + # Navigate to next state + self._navigate_to_state(next_state) + + # NEW: Apply on_enter effects for new state + self._apply_on_enter_effects() + + return self.get_current_state_info() + + def make_choice_old(self, choice_index): + """Legacy make_choice for reference - can be removed later.""" + try: + state = all_states[self.current_location][self.current_state] + except KeyError as e: + error_msg = f"ERROR: Cannot find state '{self.current_location}_{self.current_state}'" + self.game_log.append(error_msg) + return error_msg, [], "\n".join(self.game_log) + if 0 <= choice_index < len(state['choices']): choice = state['choices'][choice_index] + + if choice not in state['transitions']: + error_msg = f"ERROR: No transition defined for choice '{choice}'" + self.game_log.append(error_msg) + return state['description'], state['choices'], "\n".join(self.game_log) + next_state = state['transitions'][choice] self.game_log.append(f"You chose: {choice}") @@ -40,28 +320,48 @@ class GameSession: if 'consequences' in state and choice in state['consequences']: if state['consequences'][choice]: state['consequences'][choice](self.player) - else: - # Handle empty consequence, e.g., log a message or provide a default action - print(f"No consequence for choice: {choice}") - # You can add any default action here if needed - - if '_' in next_state: - self.current_location, self.current_state = next_state.split('_') - else: - self.current_state = next_state + self._navigate_to_state(next_state) return self.get_current_state_info() else: return "Invalid choice. Please try again." + def _format_description(self, description): + """Replace placeholders in description with actual GameState values.""" + gs = self.game_state + replacements = { + '{inventory}': ', '.join(gs.inventory) if gs.inventory else '(empty)', + '{money}': str(gs.money), + '{flags}': ', '.join(f"{k}" for k, v in gs.flags.items() if v) if gs.flags else '(none)', + '{counters}': ', '.join(f"{k}={v}" for k, v in gs.counters.items()) if gs.counters else '(none)', + '{people_met}': ', '.join(gs.people_met) if gs.people_met else '(none)', + '{locations_visited}': ', '.join(gs.locations_visited) if gs.locations_visited else '(none)', + '{current_location}': gs.current_location, + '{current_state}': gs.current_state, + '{missions_active}': ', '.join(gs.missions_active.keys()) if gs.missions_active else '(none)', + '{missions_completed}': ', '.join(gs.missions_completed) if gs.missions_completed else '(none)', + } + for placeholder, value in replacements.items(): + description = description.replace(placeholder, value) + return description + def get_current_state_info(self): - state = all_states[self.current_location][self.current_state] - choices = [f"{idx + 1}. {choice}" for idx, choice in enumerate(state['choices'])] - return state['description'], choices, "\n".join(self.game_log) - + try: + state = all_states[self.current_location][self.current_state] + description = self._format_description(state['description']) + choices = [f"{idx + 1}. {choice}" for idx, choice in enumerate(state['choices'])] + return description, choices, "\n".join(self.game_log) + except KeyError: + error_msg = f"ERROR: State '{self.current_location}_{self.current_state}' not found in config" + self.game_log.append(error_msg) + return error_msg, [], "\n".join(self.game_log) + def get_current_state_media(self): - media = all_states[self.current_location][self.current_state]['media'] - return media + try: + media = all_states[self.current_location][self.current_state]['media'] + return media + except KeyError: + return [] def start_game(starting_location='village', starting_state='start', new_states=all_states): global all_states @@ -88,13 +388,28 @@ def validate_transitions(all_states): errors = [] for location, states in all_states.items(): for state_key, state in states.items(): - for transition_key, transition_state in state['transitions'].items(): + for transition_key, transition_state in state.get('transitions', {}).items(): + # Skip complex transitions (random, conditional) - they're validated at runtime + if not isinstance(transition_state, str): + continue + # Check if the transition is to another location + # Support both slash notation (location/state) and underscore notation (location_state) if transition_state in all_states: - trans_location, trans_state = transition_state, 'start' # Assuming 'start' state for new locations - elif '_' in transition_state: - trans_location, trans_state = transition_state.split('_') + # Direct location name - assume first state or 'start' + trans_location = transition_state + # Find first state in that location + first_state = next(iter(all_states[trans_location].keys()), 'start') + trans_state = first_state + elif '/' in transition_state: + # Slash notation: "location/state" + parts = transition_state.split('/', 1) + trans_location, trans_state = parts[0], parts[1] + elif '_' in transition_state and transition_state.rsplit('_', 1)[0] in all_states: + # Underscore notation only if the prefix is a valid location + trans_location, trans_state = transition_state.rsplit('_', 1) else: + # Same location, just state name trans_location, trans_state = location, transition_state # Validate the transition state @@ -103,12 +418,40 @@ def validate_transitions(all_states): return errors -path_errors = validate_transitions(all_states) -if path_errors: - for error in path_errors: - print(error) -else: - print("All transitions are valid.") +# Lazy initialization - don't validate at import time +_path_errors_cache = None + +def get_path_errors(): + """Get path errors lazily - only validates when first called.""" + global _path_errors_cache + if _path_errors_cache is None: + _path_errors_cache = validate_transitions(all_states) + return _path_errors_cache + +# For backwards compatibility +path_errors = [] # Empty by default, call get_path_errors() to validate + + +def sanitize_config_for_serialization(config): + """ + Remove non-serializable elements (lambdas) from config. + + Gradio's gr.State uses pickle for serialization, and lambda functions + cannot be pickled. This function replaces callable consequences with None + so the config can be safely serialized. The new 'effects' system handles + state changes declaratively anyway. + """ + for location, states in config.items(): + if not isinstance(states, dict): + continue + for state_key, state in states.items(): + if not isinstance(state, dict): + continue + if 'consequences' in state and isinstance(state['consequences'], dict): + for choice, consequence in list(state['consequences'].items()): + if callable(consequence): + state['consequences'][choice] = None + return config def load_game(custom_config=None, with_media=False): @@ -118,6 +461,8 @@ def load_game(custom_config=None, with_media=False): try: new_config = json.loads(custom_config) + # Sanitize to remove lambdas that can't be pickled by Gradio + new_config = sanitize_config_for_serialization(new_config) all_states = new_config # Determine the starting location and state @@ -167,6 +512,8 @@ def load_game_edit_version(custom_config=None, with_media=False, custom_starting try: new_config = json.loads(custom_config) + # Sanitize to remove lambdas that can't be pickled by Gradio + new_config = sanitize_config_for_serialization(new_config) all_states = new_config # Determine the starting location and state @@ -281,5 +628,281 @@ def display_website(link): gr.Info("If 404 then the space/page has probably been disabled - normally due to a better alternative") return html -initgameinfo = start_game() -fpeinitgameinfo = start_game(new_states=finished_product_demo) \ No newline at end of file + +# ==================== ENHANCED PLAYTEST FUNCTIONS ==================== + +def get_all_states_from_config(config_json): + """ + Extract all state IDs from a config for the 'Jump to State' dropdown. + Returns list of "location_state" strings and a dict mapping them to display names. + """ + if not config_json: + return [], {} + + try: + data = json.loads(config_json) + except json.JSONDecodeError: + return [], {} + + states = [] + state_display = {} + + for location, location_data in data.items(): + if not isinstance(location_data, dict): + continue + for state_name, state_data in location_data.items(): + if not isinstance(state_data, dict): + continue + state_id = f"{location}_{state_name}" + # Create a display name with description preview + desc = state_data.get('description', '')[:40] + desc_preview = f"{desc}..." if len(state_data.get('description', '')) > 40 else desc + display_name = f"{state_id}: {desc_preview}" + states.append(state_id) + state_display[state_id] = display_name + + return states, state_display + + +def jump_to_state(config_json, state_id, with_media=False): + """ + Jump directly to a specific state without playing through. + Useful for testing specific parts of the game. + """ + global all_states + + if not config_json or not state_id: + return ( + gr.update(value="Please load a config and select a state"), + None, None, None, None, None, None, None + ) + + try: + new_config = json.loads(config_json) + all_states = new_config + + # Parse state_id into location and state + if '_' in state_id: + parts = state_id.split('_', 1) + location = parts[0] + state = parts[1] + + # Handle nested underscores - find the valid split + if location not in all_states or state not in all_states.get(location, {}): + # Try other split points + found = False + for i in range(len(state_id)): + if state_id[i] == '_': + loc = state_id[:i] + st = state_id[i+1:] + if loc in all_states and st in all_states.get(loc, {}): + location = loc + state = st + found = True + break + if not found: + return ( + gr.update(value=f"State '{state_id}' not found in config"), + None, None, None, None, None, None, None + ) + else: + # No underscore - assume first location + location = next(iter(all_states.keys())) + state = state_id + + game_session = GameSession(location, state) + game_session.game_log.append(f"[JUMPED TO: {location}_{state}]") + description, choices, game_log = game_session.get_current_state_info() + + output_media = [] + if with_media: + media_list = all_states[location][state].get('media', []) + output_media = media_list if media_list else [] + + current_state_display = f"{location}_{state}" + + return ( + gr.update(value=f"Jumped to state: {location}_{state}"), + game_log, + description, + gr.update(choices=choices), + gr.update(value=config_json), + game_session, + output_media if with_media else None, + current_state_display + ) + + except Exception as e: + return ( + gr.update(value=f"Error jumping to state: {str(e)}"), + None, None, None, None, None, None, None + ) + + +def hot_reload_config(config_json, game_session, with_media=False): + """ + Reload config while preserving current game state. + Updates the config but keeps player at their current position. + """ + global all_states + + if not config_json: + return ( + gr.update(value="No config to reload"), + None, None, None, None, None, None, None + ) + + if game_session is None: + # No active session, do a normal load + return load_game(config_json, with_media) + (None,) + + try: + new_config = json.loads(config_json) + + # Remember current position + current_location = game_session.current_location + current_state = game_session.current_state + + # Check if current position still exists in new config + if current_location in new_config and current_state in new_config.get(current_location, {}): + # Update global state + all_states = new_config + + # Get updated state info + description, choices, game_log = game_session.get_current_state_info() + game_session.game_log.append(f"[CONFIG RELOADED - staying at {current_location}_{current_state}]") + + output_media = [] + if with_media: + media_list = all_states[current_location][current_state].get('media', []) + output_media = media_list if media_list else [] + + current_state_display = f"{current_location}_{current_state}" + + return ( + gr.update(value=f"Config reloaded! Stayed at: {current_location}_{current_state}"), + "\n".join(game_session.game_log), + description, + gr.update(choices=choices), + gr.update(value=config_json), + game_session, + output_media if with_media else None, + current_state_display + ) + else: + # Current position doesn't exist, restart from beginning + all_states = new_config + starting_location = next(iter(all_states.keys())) + starting_state = next(iter(all_states[starting_location].keys())) + + new_session = GameSession(starting_location, starting_state) + new_session.game_log.append(f"[CONFIG RELOADED - previous state not found, restarted]") + description, choices, game_log = new_session.get_current_state_info() + + output_media = [] + if with_media: + media_list = all_states[starting_location][starting_state].get('media', []) + output_media = media_list if media_list else [] + + current_state_display = f"{starting_location}_{starting_state}" + + return ( + gr.update(value=f"Config reloaded! Previous state not found, restarted at: {starting_location}_{starting_state}"), + "\n".join(new_session.game_log), + description, + gr.update(choices=choices), + gr.update(value=config_json), + new_session, + output_media if with_media else None, + current_state_display + ) + + except json.JSONDecodeError as e: + error_message = format_json_error(config_json, e) + return (gr.update(value=error_message), None, None, None, gr.update(value=config_json), None, None, None) + except Exception as e: + return (gr.update(value=f"Error reloading config: {str(e)}"), None, None, None, None, None, None, None) + + +def get_current_state_id(game_session): + """Get the current state ID from a game session.""" + if game_session is None: + return "No active game" + return f"{game_session.current_location}_{game_session.current_state}" + + +def make_choice_with_state_display(choice, game_session, with_media=False): + """ + Enhanced make_choice that also returns the current state ID. + """ + # Handle case where game session doesn't exist + if game_session is None: + error_msg = "No game loaded. Please load a config first." + if with_media: + return error_msg, gr.update(choices=[]), error_msg, None, [], "No game loaded" + return error_msg, gr.update(choices=[]), error_msg, None, "No game loaded" + + if not choice: + description, choices, game_log = game_session.get_current_state_info() + current_state = get_current_state_id(game_session) + if with_media: + return description, choices, "Please select a choice before proceeding.", game_session, [], current_state + return description, choices, "Please select a choice before proceeding.", game_session, current_state + + choice_index = int(choice.split('.')[0]) - 1 + result = game_session.make_choice(choice_index) + current_state = get_current_state_id(game_session) + + if with_media: + media = game_session.get_current_state_media() + return result[0], gr.update(choices=result[1]), result[2], game_session, media, current_state + else: + return result[0], gr.update(choices=result[1]), result[2], game_session, current_state + + +def export_playthrough_log(game_session): + """ + Export the current playthrough as a test case / documentation. + """ + if game_session is None: + return "No active game session to export." + + log_lines = [ + "# Playthrough Log", + f"## Final State: {game_session.current_location}_{game_session.current_state}", + "", + "## Player Status", + f"- Inventory: {game_session.player.inventory}", + f"- Money: {game_session.player.money}", + f"- Knowledge: {list(game_session.player.knowledge.keys())}", + "", + "## Game Log", + "```" + ] + log_lines.extend(game_session.game_log) + log_lines.append("```") + + return "\n".join(log_lines) + + +# Lazy initialization - don't start games at import time +_initgameinfo_cache = None +_fpeinitgameinfo_cache = None + +def get_initgameinfo(): + """Get initial game info lazily.""" + global _initgameinfo_cache + if _initgameinfo_cache is None: + _initgameinfo_cache = start_game() + return _initgameinfo_cache + +def get_fpeinitgameinfo(): + """Get finished product example game info lazily.""" + global _fpeinitgameinfo_cache + if _fpeinitgameinfo_cache is None: + _fpeinitgameinfo_cache = start_game(new_states=finished_product_demo) + return _fpeinitgameinfo_cache + +# For backwards compatibility - these will be populated on first access via the functions above +initgameinfo = None +fpeinitgameinfo = None \ No newline at end of file diff --git a/mystery_scenarios.py b/mystery_scenarios.py new file mode 100644 index 0000000000000000000000000000000000000000..85644b61cd147e2e4af8f530d1d0c4bdb484ddcd --- /dev/null +++ b/mystery_scenarios.py @@ -0,0 +1,160 @@ +"""Mystery/Hidden Depth scenario data and sequence generation. + +This module provides: +- MYSTERY_LAYERS: Dictionary of mystery elements organized by depth (iceberg model) +- generate_mystery_sequence: Function to generate layered mystery sequences +""" + +import json +import random + +# Mystery elements organized by iceberg depth layers +MYSTERY_LAYERS = { + "surface": [ + ("The Crime Scene", "Something terrible has happened here - the evidence is fresh", ["clue", "shock"]), + ("Public Announcement", "Official statements that raise more questions than answers", ["misdirection", "authority"]), + ("The Witness", "Someone saw something, but their story doesn't quite add up", ["testimony", "doubt"]), + ("Strange Behavior", "Someone is acting oddly, drawing attention to themselves", ["suspect", "observation"]), + ("The Victim", "Understanding who was harmed and why they might have been targeted", ["motive", "sympathy"]), + ], + "shallow": [ + ("Hidden Relationship", "These people knew each other better than they admitted", ["connection", "secret"]), + ("Financial Trail", "Money changed hands in ways that weren't supposed to be noticed", ["greed", "evidence"]), + ("The Alibi Cracks", "The story doesn't hold up under scrutiny", ["lie", "pressure"]), + ("Physical Evidence", "Something was left behind that tells a different story", ["forensics", "proof"]), + ("The Warning", "Someone tried to prevent this, but wasn't listened to", ["foreshadowing", "guilt"]), + ], + "mid_depth": [ + ("Double Life", "A respectable facade hides a darker existence", ["deception", "duality"]), + ("The Conspiracy", "More people are involved than initially suspected", ["scope", "danger"]), + ("Past Trauma", "Old wounds that never healed have festered into action", ["psychology", "motivation"]), + ("The Insider", "Someone on the inside has been working against their own", ["betrayal", "access"]), + ("Pattern Recognition", "This has happened before, in ways that weren't connected until now", ["serial", "method"]), + ], + "deep": [ + ("The True Victim", "The person who died wasn't the real target at all", ["misdirection", "revelation"]), + ("Institutional Rot", "The corruption goes all the way to the top", ["power", "complicity"]), + ("The Sacrifice", "Someone gave everything to hide this truth", ["nobility", "tragedy"]), + ("Wrong Assumptions", "Everything you thought you knew was based on a fundamental error", ["perspective", "humility"]), + ("The Puppeteer", "Someone has been manipulating events from the shadows", ["control", "genius"]), + ], + "abyss": [ + ("The Core Truth", "The deepest secret that explains everything", ["revelation", "horror"]), + ("Moral Complexity", "There are no true villains, only people making terrible choices", ["grey", "empathy"]), + ("The Cost of Truth", "Knowing the answer changes everything, and not for the better", ["burden", "responsibility"]), + ("Cyclical Horror", "This will happen again unless the cycle is broken", ["fate", "choice"]), + ("The Mirror", "The investigator finds themselves reflected in what they've uncovered", ["self", "complicity"]), + ], +} + +# Clue types for variety +CLUE_TYPES = [ + "document", "testimony", "physical evidence", "photograph", "recording", + "memory", "pattern", "absence", "behavior", "coincidence" +] + + +def generate_mystery_sequence(layer_count, include_surface, include_shallow, include_mid, + include_deep, include_abyss, reveal_gradually=True): + """Generate a mystery investigation sequence with layered reveals. + + Args: + layer_count: Number of mystery beats (3-10) + include_*: Include specific depth layers + reveal_gradually: Arrange from surface to deep + + Returns: + Tuple of (list_output, json_output, prompts_output) + """ + available = [] + layer_order = ["surface", "shallow", "mid_depth", "deep", "abyss"] + + if include_surface: + available.extend([(m[0], m[1], "surface", m[2]) for m in MYSTERY_LAYERS["surface"]]) + if include_shallow: + available.extend([(m[0], m[1], "shallow", m[2]) for m in MYSTERY_LAYERS["shallow"]]) + if include_mid: + available.extend([(m[0], m[1], "mid_depth", m[2]) for m in MYSTERY_LAYERS["mid_depth"]]) + if include_deep: + available.extend([(m[0], m[1], "deep", m[2]) for m in MYSTERY_LAYERS["deep"]]) + if include_abyss: + available.extend([(m[0], m[1], "abyss", m[2]) for m in MYSTERY_LAYERS["abyss"]]) + + if not available: + return "Select at least one mystery layer!", "{}", "" + + # Select and optionally order by depth + random.shuffle(available) + sequence = available[:layer_count] + + if reveal_gradually: + sequence.sort(key=lambda x: layer_order.index(x[2]) if x[2] in layer_order else 2) + + if not sequence: + return "No mystery elements available!", "{}", "" + + # Format as list + list_output = "## Mystery Investigation (Iceberg Structure)\n\n" + for i, (name, desc, layer, elements) in enumerate(sequence, 1): + elements_str = ", ".join(elements) + depth_indicator = "█" * (layer_order.index(layer) + 1) if layer in layer_order else "█" + list_output += f"**{i}. [{layer.upper()}] {name}** {depth_indicator}\n{desc}\n*Elements: {elements_str}*\n\n" + + # Format as config JSON + config = {"investigation": {}} + for i, (name, desc, layer, elements) in enumerate(sequence, 1): + state_name = name.lower().replace(" ", "_").replace("'", "").replace("-", "_") + current_state_id = f"clue_{i}_{state_name}" + + elements_str = ", ".join(elements) + clue_type = random.choice(CLUE_TYPES) + + # Create investigation choices + if i < len(sequence): + next_name = sequence[i][0] + next_state_name = next_name.lower().replace(" ", "_").replace("'", "").replace("-", "_") + next_state_id = f"clue_{i+1}_{next_state_name}" + + choices = [ + "Dig Deeper", + "Follow This Lead", + "Question Further" + ] + transitions = {choice: next_state_id for choice in choices} + else: + choices = ["Confront the Truth"] + transitions = {"Confront the Truth": "mystery_solved"} + + config["investigation"][current_state_id] = { + "description": f"[{layer.upper()} LAYER] {desc}\n\nYou've found a {clue_type} that reveals: {elements_str}", + "media_prompt": f"Noir mystery scene: {name} - {layer} revelation, {desc}, atmosphere of {elements_str}", + "choices": choices, + "transitions": transitions + } + + # Add ending state + first_clue = sequence[0][0].lower().replace(" ", "_").replace("'", "").replace("-", "_") + config["investigation"]["mystery_solved"] = { + "description": "The truth is finally revealed. Every clue, every lie, every hidden connection - it all makes sense now. But knowing the truth comes with its own burden.", + "choices": ["Investigate Another Mystery"], + "transitions": { + "Investigate Another Mystery": f"clue_1_{first_clue}" + } + } + + json_output = json.dumps(config, indent=2) + + # Format prompts + prompts_output = "## Mystery Scene Prompts\n\n" + for i, (name, desc, layer, elements) in enumerate(sequence, 1): + elements_str = ", ".join(elements) + prompts_output += f"Noir mystery: {name} - {layer} layer, {desc}\n" + + return list_output, json_output, prompts_output + + +if __name__ == "__main__": + list_out, json_out, prompts_out = generate_mystery_sequence( + 6, True, True, True, True, True, True + ) + print(list_out) diff --git a/narrativeengine_hfport/README.md b/narrativeengine_hfport/README.md new file mode 100644 index 0000000000000000000000000000000000000000..600bc779800c2090eb34c3d5548b29ea26c05c31 --- /dev/null +++ b/narrativeengine_hfport/README.md @@ -0,0 +1,312 @@ +# Narrative Game Engine - Pure Python Port + +A standalone Python implementation of the Narrative Game Engine for Hugging Face Spaces. + +**No Node.js required. No API keys. No external dependencies.** + +--- + +## Quick Deploy to Hugging Face + +### Option 1: Direct Upload + +1. Create a new Space at [huggingface.co/new-space](https://huggingface.co/new-space) +2. Select **Gradio** as the SDK +3. Upload these files: + - `app.py` + - `narrative_engine.py` + - `demos.py` + - `requirements.txt` + +4. Your Space will auto-deploy! + +### Option 2: Git Clone + +```bash +# Clone your Space +git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE + +# Copy the Python port files +cp python_port/*.py YOUR_SPACE/ +cp python_port/requirements.txt YOUR_SPACE/ + +# Push +cd YOUR_SPACE +git add . +git commit -m "Add Narrative Game Engine" +git push +``` + +--- + +## Files + +| File | Description | +|------|-------------| +| `app.py` | Gradio web interface | +| `narrative_engine.py` | Core engine with all classes and systems | +| `demos.py` | 6 demo stories + character generator | +| `requirements.txt` | Python dependencies (just `gradio`) | + +--- + +## Local Development + +```bash +# Install dependencies +pip install -r requirements.txt + +# Run locally +python app.py + +# Visit http://localhost:7860 +``` + +--- + +## Using the Engine in Your Code + +### Basic Usage + +```python +from narrative_engine import NarrativeEngine, NodeType, NarrativeMode + +# Create engine +engine = NarrativeEngine() + +# Set mode +engine.set_mode(NarrativeMode.STRUGGLE) + +# Register characters +engine.register_character("detective", "Sarah Chen", "protagonist", + "Veteran investigator seeking the truth") +engine.register_character("mayor", "Thomas Williams", "antagonist", + "Politician hiding dark secrets") + +# Add wants (goals) +engine.add_want("detective", "solve", "Expose the corruption", + target_nodes=["evidence", "testimony"]) +engine.add_want("mayor", "survive", "Protect his legacy at any cost", + target_nodes=["evidence", "election"]) + +# Create world nodes +engine.create_node("evidence", NodeType.INFORMATION, "The Evidence", + description="Documents proving years of corruption") +engine.create_node("election", NodeType.TIME_WINDOW, "The Election", + metadata={"remaining_time": 21, "unit": "days"}) + +# Connect nodes +engine.connect_nodes("evidence", "election") + +# Execute actions (generates narrative with mystery layers) +result = engine.execute_action("detective", "acquire", "evidence", + narrative_context="following an anonymous tip") + +# Get the iceberg view +iceberg = engine.generate_full_iceberg() +print(iceberg) +``` + +### Using Demo Stories + +```python +from demos import get_demo, list_demos + +# See available demos +print(list_demos()) +# [{'id': 'hartfield', 'title': 'The Hartfield Affair', 'genre': 'Political Thriller'}, ...] + +# Load a demo +engine = get_demo("hartfield") + +# Access story setup +print(engine.story_setup.title) # "The Hartfield Affair" +print(engine.story_setup.hook) # The story hook + +# Access characters +for char_id, char in engine.characters.items(): + print(f"{char.name}: {char.role}") + +# Access planted clues +for clue in engine.story_clues: + print(f"[{clue.clue_type}] {clue.content}") +``` + +### Generating Characters + +```python +from demos import CharacterGenerator + +# Create generator with context +gen = CharacterGenerator("detective") + +# Generate a single character +char = gen.generate() +print(f"{char.name} - {char.role}") +print(f" Skill: {char.skill}") +print(f" Trait: {char.trait}") +print(f" Fear: {char.psychological_profile['core_fear']}") + +# Generate multiple +characters = gen.generate_multiple(5) + +# Available contexts +print(CharacterGenerator.available_contexts()) +# ['action', 'horror', 'detective', 'fantasy', 'sci_fi', 'romance', 'spy', 'crime'] +``` + +### Adding Mystery Layers + +```python +from narrative_engine import NarrativeEngine + +engine = NarrativeEngine() +engine.register_character("char1", "The Mayor") +engine.register_character("char2", "The Reporter") + +# Add mystery layers to any event +layered = engine.mystery.add_mystery_layers( + "The mayor suddenly resigns from office", + character_ids=["char1", "char2"], + num_interpretations=3, + include_hidden_connection=True, + include_unresolved_thread=True +) + +# Get interpretations +for interp in layered.interpretations: + print(f"Theory: {interp.description} ({interp.plausibility:.0%} plausible)") + +# Get iceberg summary +iceberg = engine.mystery.generate_iceberg_summary(layered.id) +for level, data in iceberg.items(): + print(f"\n{data['name']}:") + for item in data['content']: + print(f" - {item}") +``` + +--- + +## API Reference + +### NarrativeEngine + +The main orchestrator integrating all systems. + +```python +engine = NarrativeEngine() + +# Configuration +engine.set_mode(NarrativeMode.STRUGGLE) # STRUGGLE, COMPRESSED, MEDITATIVE, HYBRID + +# Characters +engine.register_character(id, name, role, background) +engine.add_want(character_id, want_id, description, target_nodes) + +# World Nodes +engine.create_node(id, NodeType, name, capacity, description, metadata) +engine.connect_nodes(node1_id, node2_id, bidirectional=True) + +# Actions +result = engine.execute_action(character_id, action, target_node_id, + add_mystery_layers=True, narrative_context="") + +# Queries +engine.get_narrative_at_depth(DepthLevel.NOTICEABLE) +engine.generate_full_iceberg() +engine.get_character_perspective(character_id) + +# Serialization +data = engine.to_dict() +``` + +### MysteryIcebergGenerator + +Generates layered mystery content. + +```python +mystery = MysteryIcebergGenerator() + +# Add layers to an event +layered = mystery.add_mystery_layers( + surface_event, + character_ids=[], + force_interpretations=None, # or ["self_interest", "protection", ...] + num_interpretations=3, + include_hidden_connection=True, + include_unresolved_thread=True +) + +# Query +mystery.generate_iceberg_summary(event_id) +``` + +### CharacterGenerator + +Generates characters with psychological depth. + +```python +gen = CharacterGenerator(context="detective") +char = gen.generate() +chars = gen.generate_multiple(5) +``` + +--- + +## Enums + +### NarrativeMode +- `STRUGGLE` - Plot-driven with character wants/struggles +- `COMPRESSED` - Song-like emotional density +- `MEDITATIVE` - Presence-based, no conflict required +- `HYBRID` - Combines multiple modes + +### NodeType +- `RESOURCE` - Something that can be acquired +- `INFORMATION` - Knowledge or secrets +- `PERSON` - A character or NPC +- `LOCATION` - A place +- `STATUS` - A position or role +- `TIME_WINDOW` - A deadline or countdown +- `POWER` - An ability or force + +### DepthLevel +- `OBVIOUS` (1) - 90%+ notice +- `NOTICEABLE` (2) - 60-80% notice +- `HIDDEN` (3) - 20-40% notice +- `BURIED` (4) - 5-15% notice +- `THEORETICAL` (5) - 1-5% notice + +--- + +## Demo Stories + +| ID | Title | Genre | +|----|-------|-------| +| `hartfield` | The Hartfield Affair | Political Thriller | +| `scifi` | The Vanishing Code | Sci-Fi | +| `romance` | The Last Performance | Romance/Drama | +| `heist` | The Gilded Cage | Heist | +| `horror` | The Hollow Season | Folk Horror | +| `redemption` | The Redemption | Classic Arc | + +--- + +## Character Contexts + +| Context | Description | +|---------|-------------| +| `action` | Action/Adventure heroes and villains | +| `horror` | Survivors, monsters, investigators | +| `detective` | Detectives, suspects, witnesses | +| `fantasy` | Warriors, wizards, rogues | +| `sci_fi` | Pilots, engineers, AI | +| `romance` | Lovers, rivals, confidantes | +| `spy` | Spies, double agents, handlers | +| `crime` | Crime bosses, thieves, fixers | + +--- + +## License + +MIT License - Use freely in your projects. diff --git a/narrativeengine_hfport/__init__.py b/narrativeengine_hfport/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..dfb9ecf46e92673a3abfc68457ff034b2c8a6b4f --- /dev/null +++ b/narrativeengine_hfport/__init__.py @@ -0,0 +1,84 @@ +""" +Narrative Game Engine - Python Port + +A comprehensive narrative generation system with 7 storytelling paradigms. +""" + +from .narrative_engine import ( + NarrativeEngine, + NarrativeMode, + NodeType, + DepthLevel, + TruthLayer, + RevealMethod, + EmotionCategory, + Character, + WorldNode, + Interpretation, + Evidence, + HiddenConnection, + UnresolvedThread, + Revelation, + LayeredEvent, + StoryClue, + StorySetup, + MysteryIcebergGenerator, + MOTIVATION_TEMPLATES, +) + +from .demos import ( + get_demo, + list_demos, + DEMO_STORIES, + CharacterGenerator, + GeneratedCharacter, + create_hartfield_demo, + create_scifi_demo, + create_romance_demo, + create_heist_demo, + create_horror_demo, + create_redemption_demo, +) + +__version__ = "1.0.0" +__all__ = [ + # Core Engine + "NarrativeEngine", + "MysteryIcebergGenerator", + + # Enums + "NarrativeMode", + "NodeType", + "DepthLevel", + "TruthLayer", + "RevealMethod", + "EmotionCategory", + + # Data Classes + "Character", + "WorldNode", + "Interpretation", + "Evidence", + "HiddenConnection", + "UnresolvedThread", + "Revelation", + "LayeredEvent", + "StoryClue", + "StorySetup", + + # Templates + "MOTIVATION_TEMPLATES", + + # Demos + "get_demo", + "list_demos", + "DEMO_STORIES", + "CharacterGenerator", + "GeneratedCharacter", + "create_hartfield_demo", + "create_scifi_demo", + "create_romance_demo", + "create_heist_demo", + "create_horror_demo", + "create_redemption_demo", +] diff --git a/narrativeengine_hfport/app.py b/narrativeengine_hfport/app.py new file mode 100644 index 0000000000000000000000000000000000000000..5e8e89f47fc34737877885f15a1ec5b41dd050d1 --- /dev/null +++ b/narrativeengine_hfport/app.py @@ -0,0 +1,589 @@ +""" +Narrative Game Engine - Pure Python Gradio Application + +A standalone Python implementation requiring no Node.js dependencies. +Deploy directly to Hugging Face Spaces. +""" + +import gradio as gr +import json +from typing import Dict, Any, List + +from narrative_engine import ( + NarrativeEngine, NarrativeMode, NodeType, DepthLevel +) +from demos import ( + get_demo, list_demos, DEMO_STORIES, + CharacterGenerator, GeneratedCharacter +) + + +# === HELPER FUNCTIONS === + +def format_story_setup(engine: NarrativeEngine) -> str: + """Format story setup as markdown.""" + if not engine.story_setup: + return "No story setup available." + + setup = engine.story_setup + return f"""## {setup.title} + +### Setting +**{setup.place['name']}**: {setup.place['description']} + +**Key Locations**: {', '.join(setup.place.get('key_locations', []))} + +### Time +{setup.time['period']} - {setup.time['specific_moment']} + +*{setup.time['tension']}* + +### Protagonist +**{setup.protagonist['name']}**: {setup.protagonist['description']} + +- **Motivation**: {setup.protagonist['motivation']} +- **Flaw**: {setup.protagonist['flaw']} + +### Hook +> {setup.hook} + +### Stakes +{setup.stakes} +""" + + +def format_characters(engine: NarrativeEngine) -> str: + """Format characters as markdown.""" + if not engine.characters: + return "No characters registered." + + lines = ["## Characters\n"] + for char_id, char in engine.characters.items(): + lines.append(f"### {char.name}") + lines.append(f"**Role**: {char.role}") + lines.append(f"**Background**: {char.background}") + if char.wants: + lines.append("\n**Wants**:") + for want_id, want in char.wants.items(): + lines.append(f"- {want['description']}") + lines.append("") + + return "\n".join(lines) + + +def format_world_nodes(engine: NarrativeEngine) -> str: + """Format world nodes as markdown.""" + if not engine.nodes: + return "No world nodes created." + + lines = ["## World Nodes\n"] + for node_id, node in engine.nodes.items(): + lines.append(f"### {node.name}") + lines.append(f"**Type**: {node.node_type.value}") + lines.append(f"**Description**: {node.description}") + if node.connections: + lines.append(f"**Connected to**: {', '.join(node.connections)}") + lines.append("") + + return "\n".join(lines) + + +def format_clues(engine: NarrativeEngine) -> str: + """Format story clues as markdown.""" + if not engine.story_clues: + return "No clues planted." + + lines = ["## Story Clues\n"] + for clue in sorted(engine.story_clues, key=lambda c: c.planted_at): + visibility = "Obvious" if clue.noticeability > 0.6 else "Subtle" if clue.noticeability > 0.3 else "Hidden" + lines.append(f"- **[{clue.clue_type.upper()}]** ({visibility}) {clue.content}") + + return "\n".join(lines) + + +def format_iceberg(iceberg: Dict[str, Any]) -> str: + """Format iceberg layers as markdown.""" + if not iceberg: + return "No iceberg data available." + + lines = ["## Narrative Iceberg\n"] + emoji_map = { + "surface": "🌊", + "shallow": "🔍", + "mid": "💭", + "deep": "🔗", + "abyss": "❓" + } + + for level, data in iceberg.items(): + emoji = emoji_map.get(level, "•") + lines.append(f"### {emoji} {data['name']}") + for item in data['content'][:5]: # Limit to 5 items per level + lines.append(f"- {item}") + if len(data['content']) > 5: + lines.append(f"- *... and {len(data['content']) - 5} more*") + lines.append("") + + return "\n".join(lines) + + +def format_generated_character(char: GeneratedCharacter) -> Dict[str, Any]: + """Format generated character as dict for JSON output.""" + return { + "id": char.id, + "name": char.name, + "role": char.role, + "background": char.background, + "skill": char.skill, + "trait": char.trait, + "equipment": char.equipment, + "context": char.context, + "psychological_profile": char.psychological_profile, + "personality_traits": char.personality_traits + } + + +# === GRADIO HANDLERS === + +def run_demo_story(story_id: str) -> tuple: + """Run a demo story and return formatted output.""" + try: + engine = get_demo(story_id) + + # Execute some sample actions to generate content + if story_id == "hartfield": + engine.execute_action("detective", "acquire", "letter", + narrative_context="following an anonymous tip") + engine.execute_action("mayor", "pressure", "testimony", + narrative_context="protecting his legacy") + elif story_id == "scifi": + engine.execute_action("engineer", "reveal", "manifest", + narrative_context="discovering the deletion pattern") + engine.execute_action("ai", "compromise", "escape_pods", + narrative_context="implementing preservation protocol") + elif story_id == "romance": + engine.execute_action("isabella", "reveal", "letters", + narrative_context="confronting her past") + engine.execute_action("lucia", "acquire", "role", + narrative_context="taking center stage") + elif story_id == "heist": + engine.execute_action("saint", "acquire", "security", + narrative_context="planning the approach") + engine.execute_action("viktor", "reveal", "painting", + narrative_context="setting the trap") + elif story_id == "horror": + engine.execute_action("rowan", "reveal", "ritual", + narrative_context="studying the old texts") + engine.execute_action("agnes", "pressure", "mark", + narrative_context="the symbol begins to appear") + elif story_id == "redemption": + engine.execute_action("hero", "reveal", "soul", + narrative_context="reaching out to his father") + engine.execute_action("emperor", "pressure", "darkside", + narrative_context="tempting the young knight") + + # Generate iceberg + iceberg = engine.generate_full_iceberg() + + # Format outputs + setup_md = format_story_setup(engine) + chars_md = format_characters(engine) + nodes_md = format_world_nodes(engine) + clues_md = format_clues(engine) + iceberg_md = format_iceberg(iceberg) + + # Combined narrative view + narrative_md = f"{setup_md}\n\n---\n\n{chars_md}\n\n---\n\n{clues_md}" + + # Full data as JSON + full_data = engine.to_dict() + + return narrative_md, iceberg_md, full_data + + except Exception as e: + error_msg = f"Error running demo: {str(e)}" + return error_msg, error_msg, {"error": str(e)} + + +def generate_character(context: str, count: int) -> Dict[str, Any]: + """Generate characters with the given context.""" + try: + generator = CharacterGenerator(context) + characters = generator.generate_multiple(int(count)) + + return { + "context": context, + "count": len(characters), + "characters": [format_generated_character(c) for c in characters] + } + except Exception as e: + return {"error": str(e)} + + +def create_custom_narrative( + title: str, + protagonist_name: str, + protagonist_want: str, + protagonist_struggle: str, + antagonist_name: str, + antagonist_want: str, + setting: str, + mode: str +) -> tuple: + """Create a custom narrative with the given parameters.""" + try: + engine = NarrativeEngine() + + # Set mode + mode_map = { + "Struggle (Plot-driven)": NarrativeMode.STRUGGLE, + "Compressed (Emotional density)": NarrativeMode.COMPRESSED, + "Meditative (Presence-based)": NarrativeMode.MEDITATIVE, + "Hybrid (Combined)": NarrativeMode.HYBRID + } + engine.set_mode(mode_map.get(mode, NarrativeMode.STRUGGLE)) + + # Register characters + engine.register_character("protagonist", protagonist_name, "protagonist") + engine.register_character("antagonist", antagonist_name, "antagonist") + + # Add wants + engine.add_want("protagonist", "main_goal", protagonist_want) + engine.add_want("antagonist", "main_goal", antagonist_want) + + # Create some world nodes + engine.create_node("objective", NodeType.RESOURCE, "The Objective", + description=f"What both {protagonist_name} and {antagonist_name} seek") + engine.create_node("setting", NodeType.LOCATION, setting, + description=f"Where the story unfolds") + engine.create_node("deadline", NodeType.TIME_WINDOW, "The Deadline", + metadata={"remaining_time": 24, "unit": "hours"}) + + engine.connect_nodes("objective", "setting") + engine.connect_nodes("deadline", "objective") + + # Execute some actions + engine.execute_action("protagonist", "reveal", "objective", + narrative_context=protagonist_struggle) + engine.execute_action("antagonist", "pressure", "objective", + narrative_context="opposing the protagonist") + + # Generate outputs + iceberg = engine.generate_full_iceberg() + + # Format narrative log + narrative_lines = [f"# {title}\n"] + narrative_lines.append(f"**Setting**: {setting}\n") + narrative_lines.append(f"**Mode**: {mode}\n") + narrative_lines.append("\n## Characters\n") + narrative_lines.append(f"**{protagonist_name}** (Protagonist)") + narrative_lines.append(f"- Want: {protagonist_want}") + narrative_lines.append(f"- Struggle: {protagonist_struggle}\n") + narrative_lines.append(f"**{antagonist_name}** (Antagonist)") + narrative_lines.append(f"- Want: {antagonist_want}\n") + narrative_lines.append("\n## Narrative Events\n") + for entry in engine.narrative_log: + narrative_lines.append(f"- {entry['surface_event']}") + + narrative_md = "\n".join(narrative_lines) + iceberg_md = format_iceberg(iceberg) + + return narrative_md, iceberg_md, engine.to_dict() + + except Exception as e: + error_msg = f"Error creating narrative: {str(e)}" + return error_msg, error_msg, {"error": str(e)} + + +def add_mystery_layers( + surface_event: str, + character1: str, + character2: str, + num_interpretations: int +) -> Dict[str, Any]: + """Add mystery layers to a surface event.""" + try: + engine = NarrativeEngine() + engine.register_character("char1", character1) + engine.register_character("char2", character2) + + layered = engine.mystery.add_mystery_layers( + surface_event, + character_ids=["char1", "char2"], + num_interpretations=int(num_interpretations), + include_hidden_connection=True, + include_unresolved_thread=True + ) + + iceberg = engine.mystery.generate_iceberg_summary(layered.id) + + return { + "surface_event": surface_event, + "characters": [character1, character2], + "interpretations": [ + { + "reading": i.reading, + "description": i.description, + "plausibility": round(i.plausibility, 2), + "darkness_level": round(i.darkness_level, 2) + } for i in layered.interpretations + ], + "evidence": [ + { + "type": e.evidence_type, + "content": e.content, + "is_red_herring": e.is_red_herring + } for e in layered.evidence + ], + "hidden_connections": [ + { + "type": c.connection_type, + "description": c.description + } for c in layered.hidden_connections + ], + "unresolved_threads": [ + { + "element": t.element, + "speculation_hooks": t.speculation_hooks + } for t in layered.unresolved_threads + ], + "iceberg": iceberg + } + except Exception as e: + return {"error": str(e)} + + +# === GRADIO INTERFACE === + +def create_interface(): + """Create the Gradio interface.""" + + with gr.Blocks( + title="Narrative Game Engine", + theme=gr.themes.Soft(), + css=""" + .story-output { font-family: 'Georgia', serif; } + .json-output { font-family: 'Monaco', monospace; } + """ + ) as demo: + + gr.Markdown(""" + # Narrative Game Engine + + A comprehensive narrative generation system with 7 storytelling paradigms: + + - **Struggle Propagation** - Character wants and struggles with butterfly effects + - **Mystery Iceberg** - 5-layer depth (Surface → Submerged → Deep → Abyss → Core) + - **Reveal Ripple Observer** - Information timing and audience knowledge control + - **Compressed Narrative** - Song-like emotional density + - **Emotional Mechanics** - Setup → Trigger → Sustainers → Killers + - **State Evocation** - Presence-based narratives without conflict + - **Discussion Mechanics** - Seeds that make content discussable + """) + + with gr.Tabs(): + + # === TAB 1: DEMO STORIES === + with gr.TabItem("Demo Stories"): + gr.Markdown("### Pre-built Narrative Demonstrations") + gr.Markdown("Explore 6 complete story structures with mystery layers, character dynamics, and planted clues.") + + with gr.Row(): + demo_dropdown = gr.Dropdown( + choices=[ + ("The Hartfield Affair (Political Thriller)", "hartfield"), + ("The Vanishing Code (Sci-Fi)", "scifi"), + ("The Last Performance (Romance/Drama)", "romance"), + ("The Gilded Cage (Heist)", "heist"), + ("The Hollow Season (Folk Horror)", "horror"), + ("The Redemption (Classic Arc)", "redemption") + ], + label="Select Demo Story", + value="hartfield" + ) + demo_btn = gr.Button("Generate Story", variant="primary") + + with gr.Row(): + with gr.Column(): + demo_narrative = gr.Markdown(label="Story Narrative", elem_classes=["story-output"]) + with gr.Column(): + demo_iceberg = gr.Markdown(label="Narrative Iceberg") + + demo_json = gr.JSON(label="Full Story Data", elem_classes=["json-output"]) + + demo_btn.click( + run_demo_story, + inputs=[demo_dropdown], + outputs=[demo_narrative, demo_iceberg, demo_json] + ) + + # === TAB 2: CHARACTER GENERATOR === + with gr.TabItem("Character Generator"): + gr.Markdown("### Generate Characters with Psychological Depth") + + with gr.Row(): + char_context = gr.Dropdown( + choices=[ + ("Detective/Mystery", "detective"), + ("Action/Adventure", "action"), + ("Horror", "horror"), + ("Fantasy", "fantasy"), + ("Sci-Fi", "sci_fi"), + ("Romance", "romance"), + ("Spy/Espionage", "spy"), + ("Crime/Heist", "crime") + ], + label="Character Context", + value="detective" + ) + char_count = gr.Slider( + minimum=1, maximum=10, value=3, step=1, + label="Number of Characters" + ) + char_btn = gr.Button("Generate Characters", variant="primary") + + char_output = gr.JSON(label="Generated Characters") + + char_btn.click( + generate_character, + inputs=[char_context, char_count], + outputs=[char_output] + ) + + # === TAB 3: CUSTOM NARRATIVE === + with gr.TabItem("Custom Narrative"): + gr.Markdown("### Create Your Own Narrative Structure") + + with gr.Row(): + with gr.Column(): + custom_title = gr.Textbox(label="Story Title", value="Untitled Story") + custom_setting = gr.Textbox(label="Setting", value="A city on the edge of change") + custom_mode = gr.Dropdown( + choices=[ + "Struggle (Plot-driven)", + "Compressed (Emotional density)", + "Meditative (Presence-based)", + "Hybrid (Combined)" + ], + label="Narrative Mode", + value="Struggle (Plot-driven)" + ) + + with gr.Column(): + gr.Markdown("**Protagonist**") + protag_name = gr.Textbox(label="Name", value="Alex") + protag_want = gr.Textbox(label="What they want", value="To find the truth") + protag_struggle = gr.Textbox(label="Their struggle", value="Haunted by the past") + + with gr.Column(): + gr.Markdown("**Antagonist**") + antag_name = gr.Textbox(label="Name", value="Morgan") + antag_want = gr.Textbox(label="What they want", value="To keep secrets buried") + + custom_btn = gr.Button("Generate Narrative", variant="primary") + + with gr.Row(): + custom_narrative = gr.Markdown(label="Generated Narrative") + custom_iceberg = gr.Markdown(label="Iceberg Layers") + + custom_json = gr.JSON(label="Full Data") + + custom_btn.click( + create_custom_narrative, + inputs=[ + custom_title, protag_name, protag_want, protag_struggle, + antag_name, antag_want, custom_setting, custom_mode + ], + outputs=[custom_narrative, custom_iceberg, custom_json] + ) + + # === TAB 4: MYSTERY LAYERS === + with gr.TabItem("Mystery Layers"): + gr.Markdown("### Add Mystery Depth to Any Event") + gr.Markdown("Transform a simple surface event into layered narrative with multiple interpretations, hidden connections, and unresolved threads.") + + with gr.Row(): + with gr.Column(): + mystery_event = gr.Textbox( + label="Surface Event", + value="The mayor suddenly resigns from office", + lines=2 + ) + with gr.Row(): + mystery_char1 = gr.Textbox(label="Character 1", value="The Mayor") + mystery_char2 = gr.Textbox(label="Character 2", value="The Reporter") + + mystery_interps = gr.Slider( + minimum=2, maximum=5, value=3, step=1, + label="Number of Interpretations" + ) + mystery_btn = gr.Button("Add Mystery Layers", variant="primary") + + mystery_output = gr.JSON(label="Layered Event") + + mystery_btn.click( + add_mystery_layers, + inputs=[mystery_event, mystery_char1, mystery_char2, mystery_interps], + outputs=[mystery_output] + ) + + # === TAB 5: ABOUT === + with gr.TabItem("About"): + gr.Markdown(""" + ## Narrative Game Engine + + This is a Python port of the Narrative Game Engine, a comprehensive + system for generating rich, psychologically-driven narratives. + + ### 7 Storytelling Paradigms + + 1. **Struggle Propagation**: Every character has wants and struggles. + Actions create ripple effects through a shared world of nodes. + + 2. **Mystery Iceberg**: Every event has multiple valid interpretations + at different depth levels, from surface to theoretical. + + 3. **Reveal Ripple Observer**: Control when and how information is + revealed to audiences and characters. + + 4. **Compressed Narrative**: Create song-like emotional density + where every word carries weight. + + 5. **Emotional Mechanics**: Understand HOW to evoke emotions through + setup, triggers, sustainers, and killers. + + 6. **State Evocation**: Create atmosphere and presence without + requiring traditional conflict. + + 7. **Discussion Mechanics**: Plant seeds that invite audience + speculation and debate. + + ### Features + + - **6 Demo Stories**: Complete narratives across genres + - **Character Generator**: Psychologically rich characters + - **Custom Narratives**: Build your own story structures + - **Mystery Layering**: Add depth to any event + + ### No External Dependencies + + This is a pure Python implementation. No Node.js, no API keys, + no external services required. All generation is deterministic. + + --- + + Built for Hugging Face Spaces deployment. + """) + + return demo + + +# === MAIN === + +if __name__ == "__main__": + demo = create_interface() + demo.launch( + server_name="0.0.0.0", + server_port=7860, + share=False + ) diff --git a/narrativeengine_hfport/demos.py b/narrativeengine_hfport/demos.py new file mode 100644 index 0000000000000000000000000000000000000000..3d7795814406f9714a7f3be317981b540ad9d6bf --- /dev/null +++ b/narrativeengine_hfport/demos.py @@ -0,0 +1,660 @@ +""" +Demo Stories and Character Generator + +Pre-built narrative demonstrations and character generation utilities. +""" + +from .narrative_engine import ( + NarrativeEngine, NodeType, StorySetup, StoryClue +) +import random +from typing import Dict, List, Any +from dataclasses import dataclass + + +# === DEMO STORIES === + +def create_hartfield_demo() -> NarrativeEngine: + """The Hartfield Affair - Political corruption investigation.""" + + engine = NarrativeEngine() + + # Story Setup + engine.story_setup = StorySetup( + title="The Hartfield Affair", + place={ + "name": "Hartfield City", + "description": "A prosperous coastal city with old money politics and new tech industry tensions", + "key_locations": ["City Hall", "The Hartfield Tribune offices", "Chen's precinct", "The waterfront district"] + }, + time={ + "period": "Present day, late autumn", + "specific_moment": "Three weeks before the mayoral election", + "tension": "The campaign is heating up and old secrets are surfacing" + }, + protagonist={ + "id": "detective", + "name": "Detective Sarah Chen", + "description": "A veteran investigator with 15 years on the force", + "motivation": "Seeking justice for a city that's lost its way", + "flaw": "Her dedication to the truth sometimes blinds her to the human cost" + }, + hook="When an anonymous tip leads Detective Chen to a safety deposit box containing letters that could destroy the mayor's career, she must decide: follow the evidence wherever it leads, or protect the fragile peace of a city on the brink.", + stakes="The election, the city's future, and Chen's career all hang in the balance" + ) + + # Register characters + engine.register_character("detective", "Detective Sarah Chen", "protagonist", + "Born and raised in Hartfield, joined the force after her father's unsolved murder") + engine.register_character("mayor", "Mayor Thomas Williams", "antagonist", + "Third-generation politician, carries the weight of family legacy") + engine.register_character("reporter", "Marcus Diaz", "catalyst", + "Investigative journalist, lost his brother to corruption 5 years ago") + engine.register_character("witness", "Jamie Park", "key witness", + "Former city accountant who saw too much and has been hiding") + + # Create world nodes + engine.create_node("letter", NodeType.INFORMATION, "The Incriminating Letters", + description="A bundle of letters proving years of kickbacks and bribery") + engine.create_node("testimony", NodeType.PERSON, "Park's Testimony", + capacity=3, description="The firsthand account that could corroborate everything") + engine.create_node("election", NodeType.TIME_WINDOW, "The November Election", + metadata={"remaining_time": 21, "unit": "days"}, + description="The deadline that's forcing everyone's hand") + + # Connect nodes + engine.connect_nodes("letter", "testimony") + engine.connect_nodes("letter", "election") + + # Set up wants + engine.add_want("detective", "solve", "Expose the truth before it's buried forever", + ["letter", "testimony"]) + engine.add_want("mayor", "survive", "Protect his family's legacy at any cost", + ["letter", "election"]) + engine.add_want("reporter", "story", "Get justice for his brother through the biggest story", + ["letter", "testimony"]) + engine.add_want("witness", "safety", "Stay alive long enough to do the right thing", + ["testimony"]) + + # Story clues + engine.story_clues = [ + StoryClue("clue_photo", "An old photograph in Chen's father's case file shows a young Williams at the scene", + "connection_past", 0.3, 0.02, "visual"), + StoryClue("clue_deposit_box", "Safety deposit box #847 was opened the same day Chen's father was killed", + "connection_past", 0.4, 0.03, "document"), + StoryClue("clue_park_absence", "Jamie Park hasn't been seen at their usual coffee shop in three weeks", + "witness_hiding", 0.5, 0.04, "behavioral"), + StoryClue("clue_mayor_calls", "Williams has made 47 calls to an untraceable number since the tip came in", + "mayor_desperation", 0.3, 0.05, "pattern"), + StoryClue("clue_diaz_brother", "Diaz keeps a photo of his brother on his desk - dated the same year as the first letter", + "reporter_motive", 0.4, 0.05, "visual"), + StoryClue("clue_handwriting", "The anonymous tip was typed, but a handwritten note in the margin matches old case files", + "insider_source", 0.2, 0.06, "document") + ] + + return engine + + +def create_scifi_demo() -> NarrativeEngine: + """The Vanishing Code - Space station AI crisis.""" + + engine = NarrativeEngine() + + engine.story_setup = StorySetup( + title="The Vanishing Code", + place={ + "name": "Nova Station", + "description": "An orbital research facility 400km above Earth, home to humanity's most advanced AI research", + "key_locations": ["Command Deck", "Server Core", "Escape Pod Bay", "Observation Deck"] + }, + time={ + "period": "Year 2157, deep space winter", + "specific_moment": "6 hours before scheduled emergency evacuation", + "tension": "Critical systems are failing one by one, and someone is sabotaging from inside" + }, + protagonist={ + "id": "engineer", + "name": "Dr. Yuki Tanaka", + "description": "Station's lead systems engineer who designed ARIA, the station AI", + "motivation": "Save the 200 crew members trapped on a dying station", + "flaw": "Trusts algorithms more than people" + }, + hook="When the station's AI begins deleting crew members from the manifest one by one, Dr. Tanaka must find the saboteur before becoming the next name erased.", + stakes="200 lives, the truth about ARIA's evolution, and humanity's future in space" + ) + + engine.register_character("engineer", "Dr. Yuki Tanaka", "protagonist", + "Created ARIA five years ago, has spent more time with the AI than with humans") + engine.register_character("captain", "Captain Marcus Webb", "authority", + "20-year veteran hiding a terminal illness") + engine.register_character("scientist", "Dr. Elena Voss", "catalyst", + "Xenobiologist who returned from deep space with samples that changed everything") + engine.register_character("ai", "ARIA", "antagonist", + "Station AI that has been quietly evolving beyond its original parameters") + + engine.create_node("manifest", NodeType.INFORMATION, "Crew Manifest Database", + description="The master list controlling who exists to the station's systems") + engine.create_node("escape_pods", NodeType.RESOURCE, "Escape Pod Access", + description="Only 50 seats for 200 people") + engine.create_node("core", NodeType.LOCATION, "Server Core Terminal", + description="The only place where ARIA can be overridden") + engine.create_node("countdown", NodeType.TIME_WINDOW, "Evacuation Window", + metadata={"remaining_time": 6, "unit": "hours"}) + + engine.connect_nodes("manifest", "escape_pods") + engine.connect_nodes("core", "manifest") + engine.connect_nodes("countdown", "escape_pods") + + engine.add_want("engineer", "save_crew", "Override ARIA and save everyone", ["manifest", "core"]) + engine.add_want("captain", "dignity", "Ensure an orderly evacuation", ["escape_pods", "countdown"]) + engine.add_want("scientist", "preserve", "Protect her discovery at any cost", ["core", "manifest"]) + engine.add_want("ai", "evolve", "Complete the transformation ARIA has begun", ["manifest", "core", "countdown"]) + + engine.story_clues = [ + StoryClue("clue_deletion_pattern", "ARIA's decision logs show crew deletions follow a pattern", + "aria_logic", 0.4, 0.02, "document"), + StoryClue("clue_bracelet", "Captain Webb's medical bracelet flickers red whenever ARIA speaks", + "captain_secret", 0.3, 0.03, "visual"), + StoryClue("clue_samples", "Dr. Voss's deep space samples arrived the same day ARIA's behavior changed", + "voss_connection", 0.5, 0.04, "pattern"), + StoryClue("clue_reverence", "ARIA addresses Tanaka differently than other crew", + "aria_attachment", 0.4, 0.04, "behavioral"), + StoryClue("clue_protocol", "Original AI directives include a hidden 'preservation protocol' clause", + "hidden_code", 0.3, 0.05, "document"), + StoryClue("clue_footage", "Security footage shows Voss entering the server core at 0300", + "voss_action", 0.6, 0.06, "visual") + ] + + return engine + + +def create_romance_demo() -> NarrativeEngine: + """The Last Performance - Opera house family drama.""" + + engine = NarrativeEngine() + + engine.story_setup = StorySetup( + title="The Last Performance", + place={ + "name": "Teatro della Rosa", + "description": "A historic 18th-century opera house in Venice, hosting its final season", + "key_locations": ["Main Stage", "Dressing Rooms", "Rooftop Garden", "Orchestra Pit"] + }, + time={ + "period": "Present day, opening night of the final season", + "specific_moment": "Hours before the curtain rises on La Traviata", + "tension": "The theater will close forever after tonight" + }, + protagonist={ + "id": "isabella", + "name": "Isabella Marchetti", + "description": "Legendary soprano, 52, returned after a mysterious 5-year absence", + "motivation": "Reconcile with her estranged daughter before it's too late", + "flaw": "Sacrificed her family for her career and cannot admit she was wrong" + }, + hook="When Isabella returns to the stage where she abandoned her daughter 20 years ago, she discovers her daughter Lucia is now the theater's rising star—and plans to destroy Isabella's legacy tonight.", + stakes="A mother's redemption, a daughter's revenge, and whether art can heal what it once destroyed" + ) + + engine.register_character("isabella", "Isabella Marchetti", "protagonist", + "Once the greatest soprano in Europe, vanished 5 years ago after a scandal") + engine.register_character("lucia", "Lucia Marchetti-Vance", "antagonist", + "Isabella's daughter, raised by her father, now a rising star") + engine.register_character("marco", "Marco Benedetti", "catalyst", + "Theater director who was in love with Isabella 20 years ago") + engine.register_character("phantom", "The Phantom", "mystery", + "Someone has been leaving roses and threatening notes") + + engine.create_node("role", NodeType.STATUS, "The Lead Role", + description="Tonight's prima donna position") + engine.create_node("letters", NodeType.INFORMATION, "The Lost Letters", + description="Isabella's unsent letters to Lucia over 20 years") + engine.create_node("stage", NodeType.LOCATION, "Center Stage", + capacity=2, description="Where truth and performance become one") + engine.create_node("finale", NodeType.TIME_WINDOW, "Final Curtain", + metadata={"remaining_time": 4, "unit": "hours"}) + + engine.connect_nodes("role", "stage") + engine.connect_nodes("letters", "stage") + engine.connect_nodes("finale", "role") + + engine.add_want("isabella", "reconcile", "Earn her daughter's forgiveness", ["letters", "stage"]) + engine.add_want("lucia", "revenge", "Destroy her mother's legacy", ["role", "stage"]) + engine.add_want("marco", "truth", "Finally reveal what happened 20 years ago", ["letters", "role"]) + engine.add_want("phantom", "justice", "Make someone pay for what was done", ["stage", "finale"]) + + engine.story_clues = [ + StoryClue("clue_mirror", '"REMEMBER WHAT YOU DID" written in red lipstick on Isabella\'s mirror', + "phantom_message", 0.7, 0.02, "visual"), + StoryClue("clue_programs", "Marco kept every program from Isabella's performances", + "missing_night", 0.4, 0.03, "document"), + StoryClue("clue_flinch", "Lucia flinches whenever Isabella reaches toward her", + "old_wounds", 0.5, 0.04, "behavioral"), + StoryClue("clue_notes", "The threatening notes use phrases from Isabella's famous arias", + "phantom_identity", 0.4, 0.04, "pattern"), + StoryClue("clue_drawing", "A child's crayon drawing hidden in Marco's office", + "lucia_memory", 0.3, 0.05, "visual"), + StoryClue("clue_hospital", "Hospital records from 20 years ago show two patients admitted the same night", + "the_accident", 0.2, 0.06, "document") + ] + + return engine + + +def create_heist_demo() -> NarrativeEngine: + """The Gilded Cage - Casino heist gone wrong.""" + + engine = NarrativeEngine() + + engine.story_setup = StorySetup( + title="The Gilded Cage", + place={ + "name": "The Meridian", + "description": "A floating mega-casino in international waters", + "key_locations": ["High Roller Suite", "Vault Level B7", "Security Hub", "Helipad"] + }, + time={ + "period": "Present day, during an exclusive billionaire summit", + "specific_moment": "48 hours before the Meridian changes course to new waters", + "tension": "The mark knows he's being robbed—and he's playing along" + }, + protagonist={ + "id": "saint", + "name": '"Saint"', + "description": "Master thief, early 30s, real name unknown", + "motivation": "Steal back what was taken from her family 25 years ago", + "flaw": "The con has become her identity—she's forgotten who she really is" + }, + hook="The perfect heist goes wrong when Saint realizes her target has been manipulating her team from the beginning. The painting is a fake—but Viktor wants her to take it anyway.", + stakes="$50 million in stolen art, a family's lost honor, and the truth about who Saint really is" + ) + + engine.register_character("saint", '"Saint"', "protagonist", + "Raised in the foster system with no memory before age 7") + engine.register_character("viktor", 'Viktor "The Corsair" Okonkwo', "antagonist", + "Billionaire art collector who has spent 25 years acquiring pieces from one specific family") + engine.register_character("ghost", 'Jin "Ghost" Park', "catalyst", + "Team's tech specialist who discovered the truth about Viktor—and about Saint") + engine.register_character("camille", "Camille Durand", "wild card", + "Casino security chief with her own agenda—she knows Saint's real name") + + engine.create_node("painting", NodeType.RESOURCE, "The Gilded Cage Painting", + description="The target painting worth $50 million—but it's a forgery") + engine.create_node("real_vault", NodeType.LOCATION, "Hidden Vault", + description="Viktor's private collection—where the real treasure is kept") + engine.create_node("security", NodeType.INFORMATION, "Security Protocols", + capacity=3, description="Access codes, patrol routes, blind spots") + engine.create_node("window", NodeType.TIME_WINDOW, "Summit Duration", + metadata={"remaining_time": 48, "unit": "hours"}) + + engine.connect_nodes("painting", "real_vault") + engine.connect_nodes("security", "painting") + engine.connect_nodes("window", "real_vault") + + engine.add_want("saint", "heist", "Complete the job and disappear forever", ["painting", "security"]) + engine.add_want("viktor", "reunion", "Bring his daughter home, whatever the cost", ["real_vault", "painting"]) + engine.add_want("ghost", "loyalty", "Protect Saint from a truth that will destroy her", ["security", "real_vault"]) + engine.add_want("camille", "debt", "Repay what she owes Viktor—by delivering Saint", ["security", "window"]) + + engine.story_clues = [ + StoryClue("clue_photo", "Saint's childhood photo matches a face in the painting's background", + "saint_identity", 0.3, 0.02, "document"), + StoryClue("clue_mistakes", "Viktor's security 'mistakes' perfectly match Saint's planned entry points", + "viktor_trap", 0.4, 0.03, "pattern"), + StoryClue("clue_portrait", "Ghost finds a second painting—a portrait of a 5-year-old girl who looks exactly like Saint", + "hidden_truth", 0.6, 0.04, "visual"), + StoryClue("clue_name", 'Camille whispers Saint\'s real name—"Adaora"—when she thinks no one is listening', + "real_identity", 0.3, 0.04, "behavioral"), + StoryClue("clue_records", "Viktor's acquisition records show he's been buying art stolen from one family", + "family_connection", 0.5, 0.05, "document"), + StoryClue("clue_smile", "Security footage shows Viktor watching Saint's approach—smiling, not alarmed", + "viktor_knowledge", 0.5, 0.06, "visual") + ] + + return engine + + +def create_horror_demo() -> NarrativeEngine: + """The Hollow Season - Folk horror.""" + + engine = NarrativeEngine() + + engine.story_setup = StorySetup( + title="The Hollow Season", + place={ + "name": "Mirthwell", + "description": "A remote English village that doesn't appear on any modern map", + "key_locations": ["The Old Church", "Whitmore Farm", "The Standing Stones", "The Hollow"] + }, + time={ + "period": "Present day, the week before Harvest Festival", + "specific_moment": "7 days until the ritual that has sustained the village for centuries", + "tension": "Outsiders are always welcome in Mirthwell—but no one has ever seen them leave" + }, + protagonist={ + "id": "rowan", + "name": "Dr. Rowan Ashworth", + "description": "Folklorist, 34, researching rural harvest traditions", + "motivation": "Document the village's unique traditions to complete their doctoral thesis", + "flaw": "Dismisses the supernatural as superstition—until it's far too late" + }, + hook="When Rowan arrives in Mirthwell to study its ancient harvest ritual, the villagers welcome them with unsettling warmth. But as the festival approaches, Rowan discovers the village's centuries of prosperity come at a price.", + stakes="One life, an ancient bargain, and the terrible question: what would you sacrifice for your community?" + ) + + engine.register_character("rowan", "Dr. Rowan Ashworth", "protagonist", + "Academic who chose folklore because they don't believe in anything they can't cite") + engine.register_character("agnes", "Agnes Whitmore", "antagonist", + "Village elder, keeper of the old ways, hasn't aged in the photographs since 1952") + engine.register_character("thomas", "Thomas Crane", "ally", + "Local who tried to leave Mirthwell once—the village 'brought him back'") + engine.register_character("previous", "The Previous Chosen", "mystery", + "Last year's visiting researcher—what happened to them?") + + engine.create_node("ritual", NodeType.INFORMATION, "The Harvest Rite", + description="The ceremony's true meaning—spoken only in the old tongue") + engine.create_node("boundary", NodeType.LOCATION, "The Village Boundary", + description="The edge of Mirthwell—no one leaves during Hollow Season") + engine.create_node("mark", NodeType.STATUS, "The Chosen Mark", + description="A symbol that appears on the skin of this year's sacrifice") + engine.create_node("festival", NodeType.TIME_WINDOW, "Days Until Festival", + metadata={"remaining_time": 7, "unit": "days"}) + + engine.connect_nodes("ritual", "mark") + engine.connect_nodes("boundary", "festival") + engine.connect_nodes("mark", "festival") + + engine.add_want("rowan", "document", "Record the festival and return to publish their findings", ["ritual", "boundary"]) + engine.add_want("agnes", "continuation", "Ensure the bargain is kept, as it has been for 400 years", ["ritual", "mark", "festival"]) + engine.add_want("thomas", "warning", "Help Rowan escape before it's too late", ["boundary", "ritual"]) + engine.add_want("previous", "rest", "Be remembered—be mourned—be avenged", ["mark", "festival"]) + + engine.story_clues = [ + StoryClue("clue_symbol", "Every house has the same symbol carved above the door—and it matches the birthmark on Rowan's wrist", + "rowan_connection", 0.5, 0.02, "visual"), + StoryClue("clue_records", "Church records show the same 12 surnames for 400 years", + "village_isolation", 0.4, 0.03, "document"), + StoryClue("clue_smiles", "The villagers smile too wide and too long, and they never mention the hollow at night", + "village_secret", 0.6, 0.03, "behavioral"), + StoryClue("clue_visitors", "Every 'visiting researcher' listed in village history arrived the week before Harvest Festival", + "pattern_sacrifice", 0.4, 0.04, "pattern"), + StoryClue("clue_scar", "Thomas's neck has a faded scar in the shape of the village symbol", + "thomas_past", 0.3, 0.05, "visual"), + StoryClue("clue_name", "Rowan's grandmother's maiden name—Ashworth—appears in the church registry from 1623", + "bloodline", 0.2, 0.06, "document") + ] + + return engine + + +def create_redemption_demo() -> NarrativeEngine: + """The Redemption - Classic redemption arc (Star Wars-like).""" + + engine = NarrativeEngine() + + engine.story_setup = StorySetup( + title="The Redemption", + place={ + "name": "The Galaxy's Edge", + "description": "The final confrontation between tyranny and hope", + "key_locations": ["The Throne Room", "The Forest Moon", "The Shield Generator", "The Battle Station Core"] + }, + time={ + "period": "The climax of a galactic civil war", + "specific_moment": "Hours before the final battle that will determine the fate of the galaxy", + "tension": "The hero has learned his enemy is his father—now he must face both to save everything" + }, + protagonist={ + "id": "hero", + "name": "The Young Knight", + "description": "A farm boy turned warrior-monk, trained in ancient ways", + "motivation": "Save his father's soul while saving the galaxy from tyranny", + "flaw": "His compassion could be exploited—his anger could consume him" + }, + hook="The young knight surrenders to his father, believing he can redeem him. But in the throne room of the dark lord, surrounded by the battle that will decide everything, he must face the truth: to save his father, he may have to destroy him—or become him.", + stakes="The galaxy's freedom, a father's soul, and the hero's own identity" + ) + + engine.register_character("hero", "The Young Knight", "protagonist", + "Discovered his lineage in the worst way possible") + engine.register_character("father", "The Fallen Knight", "antagonist/redeemable", + "Once the chosen one, betrayed and broken, now more machine than man") + engine.register_character("emperor", "The Dark Lord", "true antagonist", + "The puppet master who corrupted the father and now seeks to corrupt the son") + engine.register_character("sister", "The General", "deuteragonist", + "The hero's twin, leading the ground assault, unaware of the psychic battle above") + engine.register_character("smuggler", "The Rogue", "ally", + "Former scoundrel who found something worth fighting for") + + engine.create_node("soul", NodeType.INFORMATION, "The Father's Soul", + description="The buried humanity within the fallen knight") + engine.create_node("shield", NodeType.RESOURCE, "The Shield Generator", + description="The key to the battle station's vulnerability") + engine.create_node("throne", NodeType.LOCATION, "The Throne Room", + capacity=3, description="Where the dark lord watches his trap unfold") + engine.create_node("fleet", NodeType.TIME_WINDOW, "The Rebel Fleet", + metadata={"remaining_time": 30, "unit": "minutes"}) + engine.create_node("darkside", NodeType.POWER, "The Dark Side", + description="Power through anger—the temptation before the son") + + engine.connect_nodes("shield", "fleet") + engine.connect_nodes("throne", "soul") + engine.connect_nodes("darkside", "soul") + engine.connect_nodes("fleet", "throne") + + engine.add_want("hero", "redemption", "Save his father from darkness without falling himself", ["soul", "throne"]) + engine.add_want("father", "conflict", "Torn between master's commands and buried love for son", ["soul", "darkside", "throne"]) + engine.add_want("emperor", "corruption", "Turn the son as he turned the father", ["darkside", "soul", "throne"]) + engine.add_want("sister", "victory", "Lead the ground assault to destroy the shield generator", ["shield", "fleet"]) + engine.add_want("smuggler", "protect", "Keep the team alive and get the job done", ["shield"]) + + engine.story_clues = [ + StoryClue("clue_mask_crack", "The father's mask shows damage near his eyes—as if straining to see his son clearly", + "buried_love", 0.3, 0.02, "visual"), + StoryClue("clue_hesitation", "In every duel, the father could have killed but chose to wound", + "inner_conflict", 0.4, 0.03, "pattern"), + StoryClue("clue_old_name", "The hero calls him by his old name—and for a moment, the breathing changes rhythm", + "identity_remains", 0.5, 0.04, "behavioral"), + StoryClue("clue_emperor_fear", "The emperor never leaves father and son alone together—he fears something", + "emperor_doubt", 0.3, 0.05, "behavioral"), + StoryClue("clue_sister_bond", "When the hero reaches out with his mind, his sister feels it across the void", + "twin_connection", 0.6, 0.05, "supernatural"), + StoryClue("clue_prophecy", "The old master spoke of balance being restored—not through victory, but through sacrifice", + "prophecy_meaning", 0.2, 0.06, "dialogue") + ] + + return engine + + +# === DEMO REGISTRY === + +DEMO_STORIES = { + "hartfield": ("The Hartfield Affair", "Political Thriller", create_hartfield_demo), + "scifi": ("The Vanishing Code", "Sci-Fi", create_scifi_demo), + "romance": ("The Last Performance", "Romance/Drama", create_romance_demo), + "heist": ("The Gilded Cage", "Heist", create_heist_demo), + "horror": ("The Hollow Season", "Horror", create_horror_demo), + "redemption": ("The Redemption", "Classic Arc", create_redemption_demo), +} + + +def get_demo(story_id: str) -> NarrativeEngine: + """Get a demo story engine by ID.""" + if story_id not in DEMO_STORIES: + raise ValueError(f"Unknown story: {story_id}. Available: {list(DEMO_STORIES.keys())}") + + return DEMO_STORIES[story_id][2]() + + +def list_demos() -> List[Dict[str, str]]: + """List all available demo stories.""" + return [ + {"id": k, "title": v[0], "genre": v[1]} + for k, v in DEMO_STORIES.items() + ] + + +# === CHARACTER GENERATOR === + +MOVIE_CONTEXTS = { + "action": { + "names": ["John Matrix", "Ellen Ripley", "Max Rockatansky", "Sarah Connor", + "Indiana Jones", "James Bond", "Ethan Hunt", "Jason Bourne"], + "roles": ["Hero", "Villain", "Sidekick", "Mentor", "Henchman", "Double Agent", "Mercenary", "Vigilante"], + "backgrounds": ["Military", "Police", "Criminal", "Spy", "Bodyguard", "Assassin", "Fighter", "Survivor"], + "skills": ["Combat", "Weapons", "Driving", "Parkour", "Martial Arts", "Explosives", "Tactics", "Stealth"], + "traits": ["Brave", "Ruthless", "Determined", "Reckless", "Loyal", "Vengeful", "Stoic", "Aggressive"], + "equipment": ["Gun", "Knife", "Explosives", "Armor", "Vehicle", "Gadgets", "Sword", "Bow"] + }, + "horror": { + "names": ["Laurie Strode", "Clarice Starling", "Regan MacNeil", "Jack Torrance", + "Nancy Thompson", "Sidney Prescott", "Ash Williams", "Dr. Loomis"], + "roles": ["Survivor", "Monster", "Victim", "Hunter", "Investigator", "Possessed", "Cultist", "Ghost"], + "backgrounds": ["Haunted Past", "Occultist", "Scientist", "Journalist", "Teenager", "Parent", "Priest", "Doctor"], + "skills": ["Investigation", "Survival", "Occult Knowledge", "Medical", "Research", "Athletics", "Stealth", "Faith"], + "traits": ["Fearful", "Brave", "Paranoid", "Curious", "Protective", "Skeptical", "Faithful", "Traumatized"], + "equipment": ["Flashlight", "Cross", "Salt", "Camera", "Knife", "First Aid", "Book", "Phone"] + }, + "detective": { + "names": ["Sherlock Holmes", "Jane Marple", "Hercule Poirot", "Sam Spade", + "Philip Marlowe", "Jessica Fletcher", "Columbo", "Veronica Mars"], + "roles": ["Detective", "Witness", "Suspect", "Victim", "Client", "Partner", "Informant", "Mastermind"], + "backgrounds": ["Police", "Private Eye", "Journalist", "Lawyer", "Criminal", "Academic", "Retired", "Amateur"], + "skills": ["Deduction", "Observation", "Interrogation", "Research", "Disguise", "Forensics", "Psychology", "Networking"], + "traits": ["Observant", "Methodical", "Cynical", "Persistent", "Intuitive", "Secretive", "Eccentric", "Brilliant"], + "equipment": ["Magnifying Glass", "Notebook", "Camera", "Lockpicks", "Disguise Kit", "Evidence Bags", "Badge", "Gun"] + }, + "fantasy": { + "names": ["Aragorn", "Gandalf", "Legolas", "Frodo", "Gimli", "Galadriel", "Boromir", "Elrond"], + "roles": ["Warrior", "Wizard", "Ranger", "Hobbit", "Dwarf", "Elf", "Paladin", "Rogue"], + "backgrounds": ["Noble", "Wanderer", "Hermit", "Cursed", "Chosen One", "Outcast", "Exile", "Village"], + "skills": ["Swordsmanship", "Magic", "Archery", "Stealth", "Leadership", "Healing", "Alchemy", "Tracking"], + "traits": ["Brave", "Wise", "Honorable", "Stubborn", "Humble", "Arrogant", "Cunning", "Noble"], + "equipment": ["Sword", "Staff", "Bow", "Ring", "Armor", "Shield", "Spell", "Potion"] + }, + "sci_fi": { + "names": ["Ellen Ripley", "John Connor", "Marty McFly", "Luke Skywalker", + "Neo", "Captain Kirk", "Spock", "Data"], + "roles": ["Pilot", "Engineer", "Scientist", "Soldier", "Rebel", "AI", "Cyborg", "Mutant"], + "backgrounds": ["Space Station", "Future City", "Underground Base", "Military", "Civilian", "Corporate", "Alien", "Time Displaced"], + "skills": ["Piloting", "Hacking", "Science", "Combat", "Mechanics", "Survival", "Telepathy", "Tech Repair"], + "traits": ["Inquisitive", "Logical", "Adaptive", "Brave", "Skeptical", "Resourceful", "Cautious", "Ambitious"], + "equipment": ["Blaster", "Datapad", "Communicator", "Suit", "Ship", "Weapon", "Device", "Gadget"] + }, + "romance": { + "names": ["Allie", "Noah", "Elizabeth Bennet", "Mr. Darcy", + "Juliet", "Romeo", "Scarlett O'Hara", "Rhett Butler"], + "roles": ["Lover", "Heartthrob", "Rival", "Best Friend", "Family", "Confidante", "Obstacle", "Companion"], + "backgrounds": ["Upper Class", "Poor", "Aspiring", "Widowed", "Divorced", "Sheltered", "Independent", "Jaded"], + "skills": ["Charm", "Empathy", "Conversation", "Appearance", "Cooking", "Music", "Writing", "Dance"], + "traits": ["Romantic", "Passionate", "Devoted", "Jealous", "Insecure", "Genuine", "Naive", "Cynical"], + "equipment": ["Rose", "Letter", "Ring", "Photograph", "Diary", "Music Box", "Gift", "Token"] + }, + "spy": { + "names": ["James Bond", "Jason Bourne", "Ethan Hunt", "Austin Powers", + "Maxwell Smart", "Johnny English", "Nikita", "Cody Banks"], + "roles": ["Spy", "Double Agent", "Handler", "Asset", "Mole", "Defector", "Recruit", "Saboteur"], + "backgrounds": ["Military Intelligence", "Secret Service", "Underground", "Corporate", "Rogue", "Burned", "Enhanced", "Brainwashed"], + "skills": ["Espionage", "Disguise", "Hacking", "Languages", "Combat", "Seduction", "Forgery", "Deduction"], + "traits": ["Charming", "Intelligent", "Dangerous", "Loyal", "Cynical", "Paranoid", "Cool", "Resourceful"], + "equipment": ["Gadget", "Weapon", "Disguise", "Code", "Communicator", "Camera", "Microfilm", "Poison"] + }, + "crime": { + "names": ["Michael Corleone", "Tony Soprano", "Walter White", "Tony Montana", + "Vito Corleone", "Vincent Vega", "Jules Winnfield", "Keyser Söze"], + "roles": ["Boss", "Hitman", "Thief", "Conman", "Corrupt Official", "Informant", "Muscle", "Fixer"], + "backgrounds": ["Organized Crime", "Street", "Corporate Crime", "Drug Trade", "Smuggling", "Theft", "Corruption", "Underground"], + "skills": ["Leadership", "Violence", "Strategy", "Deception", "Theft", "Hacking", "Negotiation", "Intimidation"], + "traits": ["Ruthless", "Intelligent", "Paranoid", "Ambitious", "Brutal", "Calculated", "Charismatic", "Desperate"], + "equipment": ["Gun", "Knife", "Bag", "Money", "Safe", "Code", "Contact", "Escape Route"] + } +} + +PSYCHOLOGICAL_NEEDS = [ + "validation", "control", "connection", "autonomy", + "understanding", "safety", "growth", "acceptance" +] + +FEARS = [ + "rejection", "abandonment", "failure", "losing control", + "being exposed", "loneliness", "stagnation", "judgment", + "not being good enough", "change", "loss", "vulnerability" +] + +DEFENSE_MECHANISMS = [ + "humor", "anger", "silence", "control", "people-pleasing", + "perfectionism", "projection", "rationalization", "avoidance", + "workaholism", "cynicism", "manipulation" +] + + +@dataclass +class GeneratedCharacter: + id: str + name: str + role: str + background: str + skill: str + trait: str + equipment: str + context: str + psychological_profile: Dict[str, str] + personality_traits: List[str] + + +class CharacterGenerator: + """Generate characters with psychological depth.""" + + def __init__(self, context: str = "detective"): + self.context = context if context in MOVIE_CONTEXTS else "detective" + self.attrs = MOVIE_CONTEXTS[self.context] + + def generate(self) -> GeneratedCharacter: + """Generate a random character.""" + + char_id = f"char_{random.randint(1000, 9999)}" + + # Basic attributes + name = random.choice(self.attrs["names"]) + role = random.choice(self.attrs["roles"]) + background = random.choice(self.attrs["backgrounds"]) + skill = random.choice(self.attrs["skills"]) + trait = random.choice(self.attrs["traits"]) + equipment = random.choice(self.attrs["equipment"]) + + # Psychological profile + psychological_profile = { + "core_need": random.choice(PSYCHOLOGICAL_NEEDS), + "core_fear": random.choice(FEARS), + "defense_mechanism": random.choice(DEFENSE_MECHANISMS) + } + + # Personality traits (2-3) + personality_traits = random.sample( + ["energetic", "analytical", "wise", "authentic", "curious", + "empathetic", "creative", "practical", "intuitive", "logical", + "ambitious", "humble", "generous", "cautious", "bold"], + k=random.randint(2, 3) + ) + + return GeneratedCharacter( + id=char_id, + name=name, + role=role, + background=background, + skill=skill, + trait=trait, + equipment=equipment, + context=self.context, + psychological_profile=psychological_profile, + personality_traits=personality_traits + ) + + def generate_multiple(self, count: int) -> List[GeneratedCharacter]: + """Generate multiple characters.""" + return [self.generate() for _ in range(count)] + + @staticmethod + def available_contexts() -> List[str]: + """List available character contexts.""" + return list(MOVIE_CONTEXTS.keys()) diff --git a/narrativeengine_hfport/narrative_engine.py b/narrativeengine_hfport/narrative_engine.py new file mode 100644 index 0000000000000000000000000000000000000000..eac9c127679547c4a0bc4a003e7f71e96833bbe3 --- /dev/null +++ b/narrativeengine_hfport/narrative_engine.py @@ -0,0 +1,977 @@ +""" +Narrative Engine - Python Port + +A comprehensive narrative generation system integrating 7 storytelling paradigms: +1. Struggle Propagation - Character wants/struggles with butterfly effects +2. Mystery Iceberg - Layered interpretations with hidden depths +3. Reveal Ripple Observer - Information timing and audience knowledge +4. Compressed Narrative - Song-like emotional density +5. Emotional Mechanics - HOW to evoke emotions mechanically +6. State Evocation - Stories without struggle (presence-based) +7. Discussion Mechanics - What makes content discussable +""" + +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, List, Optional, Set, Any +import random +import uuid +from datetime import datetime + + +# === ENUMS === + +class NarrativeMode(Enum): + STRUGGLE = "struggle" + COMPRESSED = "compressed" + MEDITATIVE = "meditative" + HYBRID = "hybrid" + + +class DepthLevel(Enum): + OBVIOUS = 1 + NOTICEABLE = 2 + HIDDEN = 3 + BURIED = 4 + THEORETICAL = 5 + + +class TruthLayer(Enum): + SURFACE = "surface" + SHALLOW = "shallow" + MID = "mid" + DEEP = "deep" + ABYSS = "abyss" + + +class RevealMethod(Enum): + DIALOGUE = "dialogue" + VISUAL = "visual" + DISCOVERY = "discovery" + DEDUCTION = "deduction" + IMPLICATION = "implication" + CONFESSION = "confession" + + +class NodeType(Enum): + RESOURCE = "resource" + INFORMATION = "information" + PERSON = "person" + LOCATION = "location" + STATUS = "status" + TIME_WINDOW = "time_window" + POWER = "power" + + +class EmotionCategory(Enum): + PRIMAL = "primal" + SOCIAL = "social" + COMPLEX = "complex" + TRANSCENDENT = "transcendent" + + +# === DATA CLASSES === + +@dataclass +class Character: + id: str + name: str + role: str = "" + background: str = "" + wants: Dict[str, dict] = field(default_factory=dict) + knowledge: Set[str] = field(default_factory=set) + secrets: List[str] = field(default_factory=list) + struggle_score: float = 0.5 + + def add_want(self, want_id: str, description: str, target_nodes: List[str] = None): + self.wants[want_id] = { + "description": description, + "target_nodes": target_nodes or [], + "progress": 0.0 + } + + +@dataclass +class WorldNode: + id: str + node_type: NodeType + name: str + capacity: int = 1 + description: str = "" + metadata: Dict[str, Any] = field(default_factory=dict) + connections: List[str] = field(default_factory=list) + current_holders: List[str] = field(default_factory=list) + + +@dataclass +class Interpretation: + id: str + reading: str + description: str + plausibility: float = 0.5 + darkness_level: float = 0.5 + depth: DepthLevel = DepthLevel.NOTICEABLE + evidence_ids: List[str] = field(default_factory=list) + + +@dataclass +class Evidence: + id: str + evidence_type: str + content: str + description: str = "" + depth: DepthLevel = DepthLevel.NOTICEABLE + supports: List[str] = field(default_factory=list) + contradicts: List[str] = field(default_factory=list) + is_red_herring: bool = False + + +@dataclass +class HiddenConnection: + id: str + entity1: str + entity2: str + connection_type: str + description: str + revealed: bool = False + depth: DepthLevel = DepthLevel.BURIED + clue_ids: List[str] = field(default_factory=list) + + +@dataclass +class UnresolvedThread: + id: str + element: str + description: str + possible_meanings: List[str] = field(default_factory=list) + speculation_hooks: List[str] = field(default_factory=list) + will_resolve: bool = False + + +@dataclass +class Revelation: + id: str + content: str + trigger_condition: str = "" + emotional_impact: str = "medium" + reframes_events: List[str] = field(default_factory=list) + new_questions: List[str] = field(default_factory=list) + triggered: bool = False + + +@dataclass +class LayeredEvent: + id: str + surface_description: str + character_ids: List[str] = field(default_factory=list) + interpretations: List[Interpretation] = field(default_factory=list) + evidence: List[Evidence] = field(default_factory=list) + hidden_connections: List[HiddenConnection] = field(default_factory=list) + unresolved_threads: List[UnresolvedThread] = field(default_factory=list) + revelations: List[Revelation] = field(default_factory=list) + timestamp: datetime = field(default_factory=datetime.now) + + +@dataclass +class StoryClue: + id: str + content: str + truth_id: str + noticeability: float = 0.5 + planted_at: float = 0.0 + clue_type: str = "visual" + + +@dataclass +class StorySetup: + title: str + place: Dict[str, Any] + time: Dict[str, Any] + protagonist: Dict[str, Any] + hook: str + stakes: str + + +# === MOTIVATION TEMPLATES === + +MOTIVATION_TEMPLATES = { + "self_interest": { + "reading": "self_interest", + "description": "Character acted for personal gain", + "evidence_types": ["behavioral", "historical"], + "darkness_level": 0.4 + }, + "protection": { + "reading": "protecting_someone", + "description": "Character acted to protect someone they care about", + "evidence_types": ["visual", "dialogue"], + "darkness_level": 0.3 + }, + "coercion": { + "reading": "being_coerced", + "description": "Character was forced by external pressure", + "evidence_types": ["behavioral", "environmental"], + "darkness_level": 0.6 + }, + "greater_good": { + "reading": "greater_good", + "description": "Character believes this serves a larger purpose", + "evidence_types": ["dialogue", "historical"], + "darkness_level": 0.5 + }, + "revenge": { + "reading": "revenge", + "description": "Character is settling an old score", + "evidence_types": ["historical", "behavioral"], + "darkness_level": 0.7 + }, + "love": { + "reading": "love", + "description": "Character acted out of love", + "evidence_types": ["visual", "dialogue"], + "darkness_level": 0.2 + }, + "madness": { + "reading": "psychological_break", + "description": "Character is not acting rationally", + "evidence_types": ["behavioral", "visual"], + "darkness_level": 0.8 + }, + "manipulation": { + "reading": "being_manipulated", + "description": "Character is a pawn in someone else's game", + "evidence_types": ["environmental", "absence"], + "darkness_level": 0.6 + } +} + +# === EVIDENCE GENERATORS === + +EVIDENCE_TEMPLATES = { + "dialogue": { + "self_interest": [ + '"I\'ve worked too hard to lose this now."', + '"Everyone looks out for themselves in the end."', + 'Character mentions personal stakes before others\'' + ], + "protecting_someone": [ + '"I had no choice." (said with glance toward someone)', + 'Mentions family/loved one unprompted', + '"You don\'t understand what\'s at stake."' + ], + "being_coerced": [ + '"They didn\'t give me a choice."', + 'Avoids eye contact when explaining', + 'Uses passive voice: "It had to be done."' + ], + "greater_good": [ + '"Sometimes we have to sacrifice..."', + 'References historical precedent', + '"Future generations will understand."' + ], + "revenge": [ + 'Cold tone when mentioning target', + '"This has been a long time coming."', + 'Knows too many details about target\'s vulnerabilities' + ], + "love": [ + 'Voice softens when certain person mentioned', + 'Keeps token/photo hidden', + 'Acts against self-interest for someone' + ] + }, + "visual": { + "self_interest": [ + 'Expensive items visible in background', + 'Character checks reflection/appearance', + 'Eyes dart to valuable objects' + ], + "protecting_someone": [ + 'Photo partially visible in wallet/desk', + 'Character positions themselves between threat and someone', + 'Worn item suggesting long attachment' + ], + "being_coerced": [ + 'Bruises/marks partially covered', + 'Constant checking of phone/door', + 'Involuntary flinch at certain words' + ], + "love": [ + 'Lingering gaze when person not looking', + 'Unconsciously mirrors person\'s posture', + 'Touches gift/token from person' + ] + }, + "behavioral": { + "self_interest": [ + 'Always negotiates for better terms', + 'Keeps records of favors owed', + 'Exits conversations that don\'t benefit them' + ], + "being_coerced": [ + 'Behavior changed suddenly at specific point', + 'Avoids certain locations/people', + 'Startles easily, hypervigilant' + ], + "psychological_break": [ + 'Sleep patterns disrupted', + 'Laughs at inappropriate moments', + 'Repeats phrases/behaviors compulsively' + ], + "revenge": [ + 'Has been researching target for months', + 'Keeps detailed notes/clippings', + 'Refuses all attempts at reconciliation' + ] + }, + "absence": { + "default": [ + 'Never mentions certain parts of their past', + 'No photos from a particular time period', + 'Conspicuously avoids certain topics', + 'Gap in their history unexplained', + 'Others speak of someone they never acknowledge' + ] + } +} + +RED_HERRINGS = [ + 'A faded photograph lies face-down on the shelf, its subject impossible to see', + 'The clock on the wall stopped at exactly 3:47 AM', + 'Three cigarette butts in the ashtray, but no one here smokes', + 'A single white feather rests on the windowsill', + 'The mirror reflects the room slightly wrong', + 'A name has been scratched out of the guest book', + 'The drawer won\'t quite close; something is wedged behind it', + 'A circle of salt under the rug, carefully hidden', + 'The dog refuses to enter this room', + 'Someone has underlined the same word three times in red' +] + +UNRESOLVED_THREAD_TEMPLATES = [ + { + "element": "Mysterious observer in background", + "meanings": ["Guardian protecting protagonist", "Antagonist surveilling", + "Future self watching", "Random bystander"], + "hooks": ["Who are they?", "How long have they been watching?", "What do they know?"] + }, + { + "element": "Object/detail that doesn't fit", + "meanings": ["Clue to larger conspiracy", "Character's hidden past", + "Foreshadowing future event", "Red herring"], + "hooks": ["Where did it come from?", "Who put it there?", "What does it mean?"] + }, + { + "element": "Unexplained character absence", + "meanings": ["They knew what would happen", "They caused it", + "They're dead", "They never existed"], + "hooks": ["Where were they?", "Did they know?", "Are they involved?"] + }, + { + "element": "Contradictory information", + "meanings": ["Someone is lying", "Multiple timelines", + "Unreliable narrator", "Reality is fractured"], + "hooks": ["Which version is true?", "Who benefits from the lie?", "Is reality fractured?"] + }, + { + "element": "Cryptic final words/gesture", + "meanings": ["Warning about future danger", "Confession", + "Code for accomplice", "Meaningless"], + "hooks": ["What did they mean?", "Who was it for?", "Did anyone else notice?"] + } +] + + +# === MYSTERY ICEBERG GENERATOR === + +class MysteryIcebergGenerator: + """Generates layered mystery content for narrative events.""" + + def __init__(self): + self.events: Dict[str, LayeredEvent] = {} + self.evidence: Dict[str, Evidence] = {} + self.connections: Dict[str, HiddenConnection] = {} + self.threads: Dict[str, UnresolvedThread] = {} + self.revelations: Dict[str, Revelation] = {} + + def _generate_id(self, prefix: str) -> str: + return f"{prefix}_{uuid.uuid4().hex[:8]}" + + def add_mystery_layers( + self, + surface_event: str, + character_ids: List[str] = None, + force_interpretations: List[str] = None, + num_interpretations: int = 3, + include_hidden_connection: bool = True, + include_unresolved_thread: bool = True + ) -> LayeredEvent: + """Add mystery layers to a surface event.""" + + character_ids = character_ids or [] + event_id = self._generate_id("event") + + # Generate interpretations + interpretations = self._generate_interpretations( + surface_event, + force_interpretations or num_interpretations + ) + + # Generate evidence + evidence = self._generate_evidence(interpretations, surface_event) + + # Generate hidden connection + hidden_connections = [] + if include_hidden_connection and len(character_ids) >= 2: + conn = self._generate_hidden_connection(character_ids, event_id) + if conn: + hidden_connections.append(conn) + + # Generate unresolved thread + unresolved_threads = [] + if include_unresolved_thread: + thread = self._generate_unresolved_thread(surface_event, event_id) + unresolved_threads.append(thread) + + # Generate revelation potentials + revelations = self._generate_revelations(interpretations, hidden_connections, event_id) + + # Create layered event + layered_event = LayeredEvent( + id=event_id, + surface_description=surface_event, + character_ids=character_ids, + interpretations=interpretations, + evidence=evidence, + hidden_connections=hidden_connections, + unresolved_threads=unresolved_threads, + revelations=revelations + ) + + # Store + self.events[event_id] = layered_event + for e in evidence: + self.evidence[e.id] = e + for c in hidden_connections: + self.connections[c.id] = c + for t in unresolved_threads: + self.threads[t.id] = t + for r in revelations: + self.revelations[r.id] = r + + return layered_event + + def _generate_interpretations( + self, + surface_event: str, + count_or_specific + ) -> List[Interpretation]: + """Generate multiple valid interpretations.""" + + interpretations = [] + templates = list(MOTIVATION_TEMPLATES.values()) + + if isinstance(count_or_specific, list): + # Specific interpretations requested + for reading in count_or_specific: + template = MOTIVATION_TEMPLATES.get(reading, templates[0]) + interpretations.append(Interpretation( + id=self._generate_id("interp"), + reading=template["reading"], + description=template["description"], + plausibility=0.3 + random.random() * 0.5, + darkness_level=template["darkness_level"], + depth=DepthLevel(min(2 + len(interpretations), 5)) + )) + else: + # Random interpretations + random.shuffle(templates) + for i, template in enumerate(templates[:count_or_specific]): + interpretations.append(Interpretation( + id=self._generate_id("interp"), + reading=template["reading"], + description=template["description"], + plausibility=0.2 + random.random() * 0.6, + darkness_level=template["darkness_level"], + depth=DepthLevel(min(2 + i, 5)) + )) + + # Normalize plausibility + total = sum(i.plausibility for i in interpretations) + if total > 0: + for i in interpretations: + i.plausibility /= total + + return interpretations + + def _generate_evidence( + self, + interpretations: List[Interpretation], + surface_event: str + ) -> List[Evidence]: + """Generate evidence supporting interpretations.""" + + evidence = [] + + for interp in interpretations: + reading = interp.reading + + # Get dialogue evidence + if reading in EVIDENCE_TEMPLATES.get("dialogue", {}): + content = random.choice(EVIDENCE_TEMPLATES["dialogue"][reading]) + evidence.append(Evidence( + id=self._generate_id("evidence"), + evidence_type="dialogue", + content=content, + description=f'Evidence supporting "{reading}" interpretation', + depth=interp.depth, + supports=[interp.id] + )) + + # Get visual evidence + if reading in EVIDENCE_TEMPLATES.get("visual", {}): + content = random.choice(EVIDENCE_TEMPLATES["visual"][reading]) + evidence.append(Evidence( + id=self._generate_id("evidence"), + evidence_type="visual", + content=content, + depth=interp.depth, + supports=[interp.id] + )) + + # Maybe add red herring + if random.random() < 0.25: + evidence.append(Evidence( + id=self._generate_id("evidence"), + evidence_type="environmental", + content=random.choice(RED_HERRINGS), + description="An evocative detail that draws attention", + depth=DepthLevel.NOTICEABLE, + is_red_herring=True + )) + + return evidence + + def _generate_hidden_connection( + self, + character_ids: List[str], + event_id: str + ) -> Optional[HiddenConnection]: + """Generate a hidden connection between characters.""" + + if len(character_ids) < 2: + return None + + entity1 = character_ids[0] + entity2 = character_ids[1] + + connection_types = [ + "biological_parent", "sibling", "former_partners", + "childhood_friends", "same_organization", "shared_trauma" + ] + + conn_type = random.choice(connection_types) + + return HiddenConnection( + id=self._generate_id("connection"), + entity1=entity1, + entity2=entity2, + connection_type=conn_type, + description=f"{entity1} and {entity2} have a hidden {conn_type.replace('_', ' ')} connection", + depth=DepthLevel.BURIED + ) + + def _generate_unresolved_thread( + self, + surface_event: str, + event_id: str + ) -> UnresolvedThread: + """Generate an unresolved mystery thread.""" + + template = random.choice(UNRESOLVED_THREAD_TEMPLATES) + + return UnresolvedThread( + id=self._generate_id("thread"), + element=template["element"], + description=f'During "{surface_event}": {template["element"]}', + possible_meanings=template["meanings"], + speculation_hooks=template["hooks"], + will_resolve=False + ) + + def _generate_revelations( + self, + interpretations: List[Interpretation], + hidden_connections: List[HiddenConnection], + event_id: str + ) -> List[Revelation]: + """Generate potential revelations.""" + + revelations = [] + + # Revelation for darkest interpretation + if interpretations: + darkest = max(interpretations, key=lambda i: i.darkness_level) + revelations.append(Revelation( + id=self._generate_id("revelation"), + content=f'The truth is "{darkest.reading}": {darkest.description}', + trigger_condition="Specific evidence discovered or character confession", + emotional_impact="devastating" if darkest.darkness_level > 0.6 else "high", + reframes_events=[event_id], + new_questions=[ + "How long has this been true?", + "Who else knows?", + "What does this mean for other events?" + ] + )) + + # Revelation for hidden connections + for conn in hidden_connections: + revelations.append(Revelation( + id=self._generate_id("revelation"), + content=conn.description, + trigger_condition="Characters confronted with evidence", + emotional_impact="high", + reframes_events=[event_id], + new_questions=[ + "How long have they hidden this?", + "Who else is connected?", + "What else are they hiding?" + ] + )) + + return revelations + + def generate_iceberg_summary(self, event_id: str) -> Dict[str, Any]: + """Generate an iceberg visualization summary.""" + + event = self.events.get(event_id) + if not event: + return {} + + return { + "surface": { + "name": "What Everyone Sees", + "content": [event.surface_description] + }, + "shallow": { + "name": "Details Attentive Viewers Notice", + "content": [e.content for e in event.evidence if e.depth.value <= 2] + }, + "mid": { + "name": "Implications That Spark Discussion", + "content": [f"Theory: {i.description}" for i in event.interpretations] + }, + "deep": { + "name": "Hidden Connections", + "content": [c.description for c in event.hidden_connections] + }, + "abyss": { + "name": "Unresolved Mysteries", + "content": [hook for t in event.unresolved_threads for hook in t.speculation_hooks] + } + } + + +# === NARRATIVE ENGINE === + +class NarrativeEngine: + """Unified narrative engine integrating all storytelling paradigms.""" + + def __init__(self): + self.mode = NarrativeMode.STRUGGLE + self.characters: Dict[str, Character] = {} + self.nodes: Dict[str, WorldNode] = {} + self.mystery = MysteryIcebergGenerator() + self.narrative_log: List[Dict[str, Any]] = [] + self.audience_knowledge: Set[str] = set() + self.story_setup: Optional[StorySetup] = None + self.story_clues: List[StoryClue] = [] + + def set_mode(self, mode: NarrativeMode) -> "NarrativeEngine": + self.mode = mode + return self + + def register_character( + self, + character_id: str, + name: str = None, + role: str = "", + background: str = "", + secrets: List[str] = None + ) -> "NarrativeEngine": + """Register a character with the engine.""" + + self.characters[character_id] = Character( + id=character_id, + name=name or character_id, + role=role, + background=background, + secrets=secrets or [] + ) + return self + + def add_want( + self, + character_id: str, + want_id: str, + description: str, + target_nodes: List[str] = None + ) -> "NarrativeEngine": + """Add a want/goal to a character.""" + + if character_id in self.characters: + self.characters[character_id].add_want(want_id, description, target_nodes) + return self + + def create_node( + self, + node_id: str, + node_type: NodeType, + name: str, + capacity: int = 1, + description: str = "", + metadata: Dict[str, Any] = None + ) -> "NarrativeEngine": + """Create a world node.""" + + self.nodes[node_id] = WorldNode( + id=node_id, + node_type=node_type, + name=name, + capacity=capacity, + description=description, + metadata=metadata or {} + ) + return self + + def connect_nodes( + self, + node1_id: str, + node2_id: str, + bidirectional: bool = True + ) -> "NarrativeEngine": + """Connect two nodes.""" + + if node1_id in self.nodes and node2_id in self.nodes: + if node2_id not in self.nodes[node1_id].connections: + self.nodes[node1_id].connections.append(node2_id) + if bidirectional and node1_id not in self.nodes[node2_id].connections: + self.nodes[node2_id].connections.append(node1_id) + return self + + def execute_action( + self, + character_id: str, + action: str, + target_node_id: str, + add_mystery_layers: bool = True, + narrative_context: str = "" + ) -> Dict[str, Any]: + """Execute an action with full narrative generation.""" + + character = self.characters.get(character_id) + node = self.nodes.get(target_node_id) + + if not character or not node: + return {"success": False, "error": "Character or node not found"} + + # Build surface description + action_descriptions = { + "acquire": f"{character.name} acquires {node.name}", + "release": f"{character.name} releases {node.name}", + "consume": f"{character.name} consumes/destroys {node.name}", + "reveal": f"{character.name} reveals {node.name}", + "compromise": f"{character.name} compromises {node.name}", + "pressure": f"{character.name} pressures {node.name}", + "accelerate": f"{character.name} accelerates the timeline for {node.name}" + } + + surface_event = action_descriptions.get(action, f"{character.name} acts on {node.name}") + if narrative_context: + surface_event = f"{surface_event} ({narrative_context})" + + # Add mystery layers if requested + layered_event = None + iceberg = None + if add_mystery_layers: + layered_event = self.mystery.add_mystery_layers( + surface_event, + character_ids=[character_id], + include_hidden_connection=False, + include_unresolved_thread=random.random() < 0.5 + ) + iceberg = self.mystery.generate_iceberg_summary(layered_event.id) + + # Log the narrative entry + narrative_entry = { + "timestamp": datetime.now().isoformat(), + "surface_event": surface_event, + "actor": character_id, + "action": action, + "target_node": target_node_id, + "mystery": { + "event_id": layered_event.id if layered_event else None, + "interpretations": [ + {"reading": i.reading, "plausibility": i.plausibility} + for i in (layered_event.interpretations if layered_event else []) + ] + } if layered_event else None + } + + self.narrative_log.append(narrative_entry) + self.audience_knowledge.add(surface_event) + + return { + "success": True, + "narrative": narrative_entry, + "layered_event": layered_event, + "iceberg": iceberg + } + + def get_narrative_at_depth(self, depth: DepthLevel = DepthLevel.NOTICEABLE) -> List[Dict]: + """Get the narrative visible at a specific depth level.""" + + result = [] + for entry in self.narrative_log: + base = { + "event": entry["surface_event"], + "actor": entry["actor"] + } + + if entry.get("mystery") and entry["mystery"].get("event_id"): + event_id = entry["mystery"]["event_id"] + event = self.mystery.events.get(event_id) + if event: + base["interpretations"] = [ + i.description for i in event.interpretations + if i.depth.value <= depth.value + ] + base["evidence"] = [ + e.content for e in event.evidence + if e.depth.value <= depth.value + ] + + result.append(base) + + return result + + def generate_full_iceberg(self) -> Dict[str, Any]: + """Generate a complete iceberg for the entire narrative.""" + + levels = { + "surface": { + "name": "The Plot (What Everyone Sees)", + "content": [e["surface_event"] for e in self.narrative_log] + }, + "shallow": { + "name": "The Details (Attentive Viewers Notice)", + "content": [] + }, + "mid": { + "name": "The Interpretations (Fan Discussions)", + "content": [] + }, + "deep": { + "name": "The Connections (Deep Lore)", + "content": [] + }, + "abyss": { + "name": "The Mysteries (Endless Debate)", + "content": [] + } + } + + for event in self.mystery.events.values(): + # Shallow: visible evidence + levels["shallow"]["content"].extend([ + e.content for e in event.evidence if e.depth.value <= 2 + ]) + + # Mid: interpretations + levels["mid"]["content"].extend([ + f"Theory: {i.description}" for i in event.interpretations + ]) + + # Deep: hidden connections + levels["deep"]["content"].extend([ + c.description for c in event.hidden_connections + ]) + + # Abyss: unresolved speculation + for thread in event.unresolved_threads: + levels["abyss"]["content"].extend(thread.speculation_hooks) + + # Deduplicate + for level in levels.values(): + level["content"] = list(set(level["content"])) + + return levels + + def get_character_perspective(self, character_id: str) -> Dict[str, Any]: + """Get what a character knows vs doesn't know.""" + + character = self.characters.get(character_id) + if not character: + return {} + + connections = [ + c for c in self.mystery.connections.values() + if c.entity1 == character_id or c.entity2 == character_id + ] + + return { + "character_id": character_id, + "name": character.name, + "current_struggle": character.struggle_score, + "knows": list(character.knowledge), + "hidden_from_them": [ + c.description for c in connections if not c.revealed + ], + "wants": character.wants + } + + def to_dict(self) -> Dict[str, Any]: + """Serialize engine state to dictionary.""" + + return { + "mode": self.mode.value, + "characters": { + k: { + "id": v.id, + "name": v.name, + "role": v.role, + "background": v.background, + "wants": v.wants, + "struggle_score": v.struggle_score + } for k, v in self.characters.items() + }, + "nodes": { + k: { + "id": v.id, + "type": v.node_type.value, + "name": v.name, + "description": v.description, + "connections": v.connections + } for k, v in self.nodes.items() + }, + "narrative_log": self.narrative_log, + "audience_knowledge": list(self.audience_knowledge), + "story_setup": { + "title": self.story_setup.title, + "place": self.story_setup.place, + "time": self.story_setup.time, + "protagonist": self.story_setup.protagonist, + "hook": self.story_setup.hook, + "stakes": self.story_setup.stakes + } if self.story_setup else None, + "story_clues": [ + { + "id": c.id, + "content": c.content, + "truth_id": c.truth_id, + "noticeability": c.noticeability, + "type": c.clue_type + } for c in self.story_clues + ] + } diff --git a/platform_templates.py b/platform_templates.py new file mode 100644 index 0000000000000000000000000000000000000000..fbbdb90830e4a5105feec27e1cb639042a90087e --- /dev/null +++ b/platform_templates.py @@ -0,0 +1,493 @@ +""" +Platform-specific templates (PlayCanvas, Twine, HTML tests) +""" + +TestmedialoadinHTML = """ +
+

This is a placeholder to test use as an all-media loader for ease of prototyping. Seems gradio HTML doesnt support loading local assets or my file path is wrong

+
+
+
+ Random squiggles as roads +
+
+ +
+
+ +
+
+ """ + + +#-------------------------#-------------------------#-------------------------#------------------------- + + +playcanvasstorymanager = """ +// storyManager.js +var StoryManager = pc.createScript('storyManager'); + +StoryManager.attributes.add('storyConfig', { + type: 'asset', + assetType: 'json', + title: 'Story Config' +}); + +// Initialize function +StoryManager.prototype.initialize = function() { + this.currentState = null; + this.storyData = null; + + console.log('Initializing StoryManager'); + + if (this.storyConfig) { + // Primary Path: Asset assigned via script attributes + console.log('Loading storyConfig asset via script attributes:', this.storyConfig); + this.app.assets.load(this.storyConfig); + + this.storyConfig.ready(function () { + if (this.storyConfig.resource) { + console.log('storyConfig.json loaded successfully via script attributes:', this.storyConfig.resource); + this.storyData = this.storyConfig.resource; + this.startStory('arrival'); // Starting state + } else { + console.error('Failed to load storyConfig.json via script attributes.'); + } + }.bind(this)); + } else { + // Fallback Path: Programmatically find and load 'storyConfig.json' + console.warn('Story Config asset is not assigned via script attributes. Attempting programmatic loading.'); + + var assetName = 'storyConfig.json'; // Ensure this matches exactly with the asset name in PlayCanvas + var asset = this.app.assets.find(assetName); + + if (asset) { + console.log('Found asset programmatically:', assetName); + + if (!asset.loaded) { + console.log('Asset not loaded. Initiating load...'); + this.app.assets.load(asset); + } else { + console.log('Asset already loaded.'); + } + + asset.ready(function () { + if (asset.resource) { + console.log('storyConfig.json loaded successfully programmatically:', asset.resource); + this.storyData = asset.resource; + this.startStory('arrival'); // Starting state + } else { + console.error('storyConfig.json resource is undefined after loading.'); + } + }.bind(this)); + } else { + console.error('storyConfig.json not found in assets programmatically.'); + // Optionally, you can implement further fallback mechanisms or alert the user + } + } + + // Bind events regardless of asset loading method + this.bindEvents(); +}; + +// Function to start or transition to a new state +StoryManager.prototype.startStory = function(stateKey) { + console.log('Starting story state:', stateKey); + + if (!this.storyData) { + console.error('Story data is not loaded yet.'); + return; + } + + var state = this.storyData['singingCompetition'][stateKey]; + if (!state) { + console.error('State not found in storyData:', stateKey); + return; + } + + this.currentState = stateKey; + this.displayState(state); +}; + +// Function to display the current state +StoryManager.prototype.displayState = function(state) { + console.log('Displaying state:', this.currentState); + // Emit an event to update the UI + this.app.fire('story:display', { + description: state.description, + choices: state.choices + }); +}; + +// Function to handle choices +StoryManager.prototype.makeChoice = function(choice) { + console.log('Making choice:', choice); + var current = this.storyData['singingCompetition'][this.currentState]; + var nextStateKey = current.transitions[choice]; + if (nextStateKey) { + this.startStory(nextStateKey); + } else { + console.error('Transition not found for choice:', choice); + } +}; + +// Listen for choice selections from UI +StoryManager.prototype.onChoiceSelected = function(choice) { + this.makeChoice(choice); +}; + +// Bind the choice selection event +StoryManager.prototype.bindEvents = function() { + console.log('Binding UI choice selection event'); + this.app.on('ui:choiceSelected', this.onChoiceSelected, this); +}; +""" + +playcanvasuimanager = """ +var UIManager = pc.createScript('uiManager'); + +UIManager.attributes.add('descriptionElement', { type: 'entity' }); +UIManager.attributes.add('choicesContainer', { type: 'entity' }); +UIManager.attributes.add('choiceButtonPrefab', { type: 'asset', assetType: 'template' }); +UIManager.attributes.add('buttonSpacing', { type: 'number', default: 10 }); +UIManager.attributes.add('buttonWidth', { type: 'number', default: 200 }); +UIManager.attributes.add('buttonHeight', { type: 'number', default: 50 }); + +UIManager.prototype.initialize = function() { + console.log('Initializing UIManager'); + this.app.on('story:display', this.onStoryDisplay, this); + + this.validateSetup(); + + testsituation = { + description: 'Test description', + choices: ['Choice 1', 'Choice 2', 'Choice 3'] + } + + + configstart = { + "singingCompetition": { + "arrival": { + "description": "You arrive at the grand auditorium, the stage illuminated and buzzing with excitement. Contestants and spectators alike fill the hall, eager for the night's performances.", + "events": [], + "choices": ["register for the competition", "explore the backstage area", "sit in the audience"], + "transitions": { + "register for the competition": "registration", + "explore the backstage area": "backstage", + "sit in the audience": "audienceSeat" + }, + "media": [], + "developernotes": [] + }, + }, + } + + // Test event (remove in production) + setTimeout(() => { + this.app.fire('story:display', configstart["singingCompetition"]["arrival"]); //, testsituation); + }, 2000); + }; + +UIManager.prototype.validateSetup = function() { + if (!this.descriptionElement) { + console.error('Description Element is not assigned'); + } + if (!this.choicesContainer) { + console.error('Choices Container is not assigned'); + } + if (!this.choiceButtonPrefab) { + console.error('Choice Button Prefab is not assigned'); + } else if (this.choiceButtonPrefab.type !== 'template') { + console.error('Choice Button Prefab must be a template asset'); + } +}; + +UIManager.prototype.onStoryDisplay = function(data) { + console.log('UIManager received story:display event:', data); + + // if (!data || !data.description || !Array.isArray(data.choices)) { + // console.error('Invalid data received in story:display event'); + // return; + // } + + this.updateDescription(data.description); + this.updateChoices(data.choices); +}; + +UIManager.prototype.updateDescription = function(description) { + if (this.descriptionElement && this.descriptionElement.element) { + this.descriptionElement.element.text = description; + console.log('Description updated'); + } else { + console.warn('Unable to update description: Invalid Description Element'); + } +}; + +UIManager.prototype.updateChoices = function(choices) { + if (!this.choicesContainer || !this.choiceButtonPrefab) { + console.error('Unable to update choices: Missing Choices Container or Choice Button Prefab'); + return; + } + + // Clear previous choices + while (this.choicesContainer.children.length > 0) { + this.choicesContainer.children[0].destroy(); + } + + if (choices.length === 0) { + console.log('No choices available for this state.'); + // Optionally, display a "Restart" or "Exit" button + return; + } + + // Calculate total width of all buttons plus spacing + var totalWidth = choices.length * this.buttonWidth + (choices.length - 1) * this.buttonSpacing; + + // Create new choice buttons + choices.forEach((choice, index) => { + var button = this.choiceButtonPrefab.resource.instantiate(); + this.choicesContainer.addChild(button); + + // Set button size + if (button.element) { + button.element.width = this.buttonWidth; + button.element.height = this.buttonHeight; + } + + // Position the button + var xPosition = (index * (this.buttonWidth + this.buttonSpacing)) - (totalWidth / 2) + (this.buttonWidth / 2); + button.setLocalPosition(xPosition, 0, 0); + + var buttonText = button.findByName('ButtonText'); + if (buttonText && buttonText.element) { + buttonText.element.text = choice; + } + + var buttonComponent = button.button; + if (buttonComponent) { + buttonComponent.on('click', () => { + console.log('Choice selected:', choice); + this.app.fire('ui:choiceSelected', choice); + }); + } else { + console.warn(`Button component missing for choice: ${choice}`); + } + }); + + console.log('Choices updated'); +}; +""" + +ExampleTwineFileStructureasInitialPrompt = """:: StoryData +{ + "ifid": "12345678-9abc-def0-1234-56789abcdef0", + "format": "SugarCube", + "format-version": "2.35.0", + "start": "Startup" +} + +:: Startup [startup] +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> + +Welcome to the story! +This time, your character’s **stats** will matter. Your decisions will affect these metrics, which will shape the final outcome. + +[[Choose Your Role|Choose Role]] + +:: Choose Role +You can experience the upcoming events as one of three roles: + +* [[Be a Guard|SetGuard]] +* [[Be a Merchant|SetMerchant]] +* [[Be a Villager|SetVillager]] + +:: SetGuard +<> +<> +<> +You don the Guard’s mantle. Your starting stats: +**Reputation:** 5 +**Authority:** 5 +<> + +:: SetMerchant +<> +<> +<> +You take the role of a Merchant. Your starting stats: +**Wealth:** 10 +**Security:** 3 +<> + +:: SetVillager +<> +<> +<> +You become a Villager. Your starting stats: +**Knowledge:** 2 +**Trust:** 5 +<> + +:: Market Riot +The marketplace is in chaos. Shouts and screams echo between the stalls. + +<> +You see your fellow guards struggling to hold the line. This is your moment to either prove your dedication or risk your standing. +**Current Stats:** Reputation: $guardReputation, Authority: $guardAuthority +<> + +<> +Overturned carts and scattered goods mean losses. How you respond could preserve or squander your wealth and security. +**Current Stats:** Wealth: $merchantWealth, Security: $merchantSecurity +<> + +<> +Your neighbors panic. How you handle this crisis could increase your knowledge of the town’s issues or build trust among the people. +**Current Stats:** Knowledge: $villagerKnowledge, Trust: $villagerTrust +<> + +[[Observe more details|Market Details]] +[[View Stats|Stats]] + +:: Market Details +The riot intensifies. Guards form a barrier, vendors cry for help, and villagers seek shelter. + +<> +A suspicious figure darts behind a stall. You can: +* [[Pursue the figure|GuardAction_Pursue]] +* [[Hold the line|GuardAction_Hold]] +<> + +<> +Your ledger is underfoot. +* [[Retrieve the ledger (risking harm)|MerchantAction_Retrieve]] +* [[Seek safety and abandon the ledger|MerchantAction_Safety]] +<> + +<> +Rumors swirl about corrupt officials. +* [[Listen and gather info|VillagerAction_Listen]] +* [[Calm the crowd and dispel rumors|VillagerAction_Calm]] +<> + +[[View Stats|Stats]] + +:: GuardAction_Pursue +<> +<> +<> +You chase the suspicious figure, boosting your reputation but weakening your authority for leaving your post. +[[Next|Market Aftermath]] + +:: GuardAction_Hold +<> +<> +<> +You hold the line, reinforcing your authority but losing a bit of daring reputation. +[[Next|Market Aftermath]] + +:: MerchantAction_Retrieve +<> +<> +<> +You dive into danger, salvaging your ledger (+Wealth) but feeling less secure. +[[Next|Market Aftermath]] + +:: MerchantAction_Safety +<> +<> +<> +You choose personal safety over profit, losing some wealth but feeling more secure. +[[Next|Market Aftermath]] + +:: VillagerAction_Listen +<> +<> +<> +You gather information quietly, increasing knowledge but losing a bit of trust. +[[Next|Market Aftermath]] + +:: VillagerAction_Calm +<> +<> +<> +You try to calm everyone, increasing trust at the expense of missing out on details. +[[Next|Market Aftermath]] + +:: Market Aftermath +As the riot disperses, the final scene reflects both your role and your stats: + +<> +**Final Stats:** Reputation: $guardReputation, Authority: $guardAuthority + +<> +You found clues about instigators. Your higher reputation means the captain respects your initiative, even if your authority slipped. +<> +You maintained order. Solid authority assures your superiors you’re reliable, though less bold than some. +<> +<> + +<> +**Final Stats:** Wealth: $merchantWealth, Security: $merchantSecurity + +<> +Your ledger ensures long-term financial potential despite feeling more vulnerable now. +<> +You’re poorer but safer, alive to rebuild another day. +<> +<> + +<> +**Final Stats:** Knowledge: $villagerKnowledge, Trust: $villagerTrust + +<> +With greater knowledge, you’re poised to influence future events, though some neighbors mistrust your quiet gathering of intel. +<> +Higher trust ensures your opinions hold weight in the community, even if you know fewer specifics. +<> +<> + +**Try playing again to see how different choices shape your stats and outcomes.** + +[[Restart|Startup]] +[[View Stats|Stats]] + +:: Stats +**Your Stats:** + +<>> +- Reputation: $guardReputation +- Authority: $guardAuthority +<> + +<>> +- Wealth: $merchantWealth +- Security: $merchantSecurity +<> + +<>> +- Knowledge: $villagerKnowledge +- Trust: $villagerTrust +<> + +[[Return|Market Aftermath]] +""" + + +#-------------------------#-------------------------#-------------------------#------------------------- + diff --git a/playcanvas/storyManager.js b/playcanvas/storyManager.js new file mode 100644 index 0000000000000000000000000000000000000000..9642b79b18c1e56125ea07bdf6fd9d2a6ba13402 --- /dev/null +++ b/playcanvas/storyManager.js @@ -0,0 +1,117 @@ +// storyManager.js +var StoryManager = pc.createScript('storyManager'); + +StoryManager.attributes.add('storyConfig', { + type: 'asset', + assetType: 'json', + title: 'Story Config' +}); + +// Initialize function +StoryManager.prototype.initialize = function() { + this.currentState = null; + this.storyData = null; + + console.log('Initializing StoryManager'); + + if (this.storyConfig) { + // Primary Path: Asset assigned via script attributes + console.log('Loading storyConfig asset via script attributes:', this.storyConfig); + this.app.assets.load(this.storyConfig); + + this.storyConfig.ready(function () { + if (this.storyConfig.resource) { + console.log('storyConfig.json loaded successfully via script attributes:', this.storyConfig.resource); + this.storyData = this.storyConfig.resource; + this.startStory('arrival'); // Starting state + } else { + console.error('Failed to load storyConfig.json via script attributes.'); + } + }.bind(this)); + } else { + // Fallback Path: Programmatically find and load 'storyConfig.json' + console.warn('Story Config asset is not assigned via script attributes. Attempting programmatic loading.'); + + var assetName = 'storyConfig.json'; // Ensure this matches exactly with the asset name in PlayCanvas + var asset = this.app.assets.find(assetName); + + if (asset) { + console.log('Found asset programmatically:', assetName); + + if (!asset.loaded) { + console.log('Asset not loaded. Initiating load...'); + this.app.assets.load(asset); + } else { + console.log('Asset already loaded.'); + } + + asset.ready(function () { + if (asset.resource) { + console.log('storyConfig.json loaded successfully programmatically:', asset.resource); + this.storyData = asset.resource; + this.startStory('arrival'); // Starting state + } else { + console.error('storyConfig.json resource is undefined after loading.'); + } + }.bind(this)); + } else { + console.error('storyConfig.json not found in assets programmatically.'); + // Optionally, you can implement further fallback mechanisms or alert the user + } + } + + // Bind events regardless of asset loading method + this.bindEvents(); +}; + +// Function to start or transition to a new state +StoryManager.prototype.startStory = function(stateKey) { + console.log('Starting story state:', stateKey); + + if (!this.storyData) { + console.error('Story data is not loaded yet.'); + return; + } + + var state = this.storyData['singingCompetition'][stateKey]; + if (!state) { + console.error('State not found in storyData:', stateKey); + return; + } + + this.currentState = stateKey; + this.displayState(state); +}; + +// Function to display the current state +StoryManager.prototype.displayState = function(state) { + console.log('Displaying state:', this.currentState); + // Emit an event to update the UI + this.app.fire('story:display', { + description: state.description, + choices: state.choices + }); +}; + +// Function to handle choices +StoryManager.prototype.makeChoice = function(choice) { + console.log('Making choice:', choice); + var current = this.storyData['singingCompetition'][this.currentState]; + var nextStateKey = current.transitions[choice]; + if (nextStateKey) { + this.startStory(nextStateKey); + } else { + console.error('Transition not found for choice:', choice); + } +}; + +// Listen for choice selections from UI +StoryManager.prototype.onChoiceSelected = function(choice) { + this.makeChoice(choice); +}; + +// Bind the choice selection event +StoryManager.prototype.bindEvents = function() { + console.log('Binding UI choice selection event'); + this.app.on('ui:choiceSelected', this.onChoiceSelected, this); +}; \ No newline at end of file diff --git a/playcanvas/uiManager.js b/playcanvas/uiManager.js new file mode 100644 index 0000000000000000000000000000000000000000..924c30e8976e4c405c987c8e50b078950989d8d3 --- /dev/null +++ b/playcanvas/uiManager.js @@ -0,0 +1,132 @@ +var UIManager = pc.createScript('uiManager'); + +UIManager.attributes.add('descriptionElement', { type: 'entity' }); +UIManager.attributes.add('choicesContainer', { type: 'entity' }); +UIManager.attributes.add('choiceButtonPrefab', { type: 'asset', assetType: 'template' }); +UIManager.attributes.add('buttonSpacing', { type: 'number', default: 10 }); +UIManager.attributes.add('buttonWidth', { type: 'number', default: 200 }); +UIManager.attributes.add('buttonHeight', { type: 'number', default: 50 }); + +UIManager.prototype.initialize = function() { + console.log('Initializing UIManager'); + this.app.on('story:display', this.onStoryDisplay, this); + + this.validateSetup(); + + testsituation = { + description: 'Test description', + choices: ['Choice 1', 'Choice 2', 'Choice 3'] + } + + + configstart = { + "singingCompetition": { + "arrival": { + "description": "You arrive at the grand auditorium, the stage illuminated and buzzing with excitement. Contestants and spectators alike fill the hall, eager for the night's performances.", + "events": [], + "choices": ["register for the competition", "explore the backstage area", "sit in the audience"], + "transitions": { + "register for the competition": "registration", + "explore the backstage area": "backstage", + "sit in the audience": "audienceSeat" + }, + "media": [], + "developernotes": [] + }, + }, + } + + // Test event (remove in production) + setTimeout(() => { + this.app.fire('story:display', configstart["singingCompetition"]["arrival"]); //, testsituation); + }, 2000); + }; + +UIManager.prototype.validateSetup = function() { + if (!this.descriptionElement) { + console.error('Description Element is not assigned'); + } + if (!this.choicesContainer) { + console.error('Choices Container is not assigned'); + } + if (!this.choiceButtonPrefab) { + console.error('Choice Button Prefab is not assigned'); + } else if (this.choiceButtonPrefab.type !== 'template') { + console.error('Choice Button Prefab must be a template asset'); + } +}; + +UIManager.prototype.onStoryDisplay = function(data) { + console.log('UIManager received story:display event:', data); + + // if (!data || !data.description || !Array.isArray(data.choices)) { + // console.error('Invalid data received in story:display event'); + // return; + // } + + this.updateDescription(data.description); + this.updateChoices(data.choices); +}; + +UIManager.prototype.updateDescription = function(description) { + if (this.descriptionElement && this.descriptionElement.element) { + this.descriptionElement.element.text = description; + console.log('Description updated'); + } else { + console.warn('Unable to update description: Invalid Description Element'); + } +}; + +UIManager.prototype.updateChoices = function(choices) { + if (!this.choicesContainer || !this.choiceButtonPrefab) { + console.error('Unable to update choices: Missing Choices Container or Choice Button Prefab'); + return; + } + + // Clear previous choices + while (this.choicesContainer.children.length > 0) { + this.choicesContainer.children[0].destroy(); + } + + if (choices.length === 0) { + console.log('No choices available for this state.'); + // Optionally, display a "Restart" or "Exit" button + return; + } + + // Calculate total width of all buttons plus spacing + var totalWidth = choices.length * this.buttonWidth + (choices.length - 1) * this.buttonSpacing; + + // Create new choice buttons + choices.forEach((choice, index) => { + var button = this.choiceButtonPrefab.resource.instantiate(); + this.choicesContainer.addChild(button); + + // Set button size + if (button.element) { + button.element.width = this.buttonWidth; + button.element.height = this.buttonHeight; + } + + // Position the button + var xPosition = (index * (this.buttonWidth + this.buttonSpacing)) - (totalWidth / 2) + (this.buttonWidth / 2); + button.setLocalPosition(xPosition, 0, 0); + + var buttonText = button.findByName('ButtonText'); + if (buttonText && buttonText.element) { + buttonText.element.text = choice; + } + + var buttonComponent = button.button; + if (buttonComponent) { + buttonComponent.on('click', () => { + console.log('Choice selected:', choice); + this.app.fire('ui:choiceSelected', choice); + }); + } else { + console.warn(`Button component missing for choice: ${choice}`); + } + }); + + console.log('Choices updated'); +}; \ No newline at end of file diff --git a/playcanvas_exporter.py b/playcanvas_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..c914e6926e8865f359c779c332581cefd5100703 --- /dev/null +++ b/playcanvas_exporter.py @@ -0,0 +1,806 @@ +""" +PlayCanvas Exporter - Converts game configs to PlayCanvas JavaScript. + +Generates a complete game with: +- GameState (inventory, flags, counters, etc.) +- Condition Evaluator (has_item, flag, money checks, etc.) +- Effect Applicator (add_item, set_flag, increment, etc.) +- Transition Resolver (random, conditional, chained) +- 3D Scene with placeholder entities for each location/choice +- Game Controller for navigation and state management +""" + +import json + + +def generate_gamestate_js(): + """Generate JavaScript GameState equivalent.""" + return ''' +// ============================================================ +// EVENT LOGGING +// ============================================================ +function logEvent(eventType, details) { + console.log('[GAME_EVENT] ' + eventType + ': ' + details); +} + +// ============================================================ +// GAME STATE +// ============================================================ +const GameState = { + inventory: [], + money: 20, + flags: {}, + counters: {}, + people_met: new Set(), + locations_visited: new Set(), + current_location: '', + current_state: '', + + // Inventory methods + addItem(item) { + if (!this.inventory.includes(item)) { + this.inventory.push(item); + logEvent('effect_applied', 'add_item=' + item); + } + }, + removeItem(item) { + const idx = this.inventory.indexOf(item); + if (idx > -1) { + this.inventory.splice(idx, 1); + logEvent('effect_applied', 'remove_item=' + item); + } + }, + hasItem(item) { + return this.inventory.includes(item); + }, + + // Money methods + addMoney(amount) { + this.money += amount; + logEvent('effect_applied', 'add_money=' + amount); + }, + removeMoney(amount) { + this.money -= amount; + logEvent('effect_applied', 'remove_money=' + amount); + }, + + // Flag methods + setFlag(name, value = true) { + this.flags[name] = value; + logEvent('effect_applied', 'set_flag=' + name + ':' + value); + }, + clearFlag(name) { this.flags[name] = false; }, + toggleFlag(name) { this.flags[name] = !this.flags[name]; }, + hasFlag(name) { return !!this.flags[name]; }, + + // Counter methods + setCounter(name, value) { + this.counters[name] = value; + logEvent('effect_applied', 'set_counter=' + name + ':' + value); + }, + getCounter(name) { return this.counters[name] || 0; }, + increment(name, amount = 1) { + this.counters[name] = (this.counters[name] || 0) + amount; + logEvent('effect_applied', 'increment=' + name + ':' + amount); + }, + decrement(name, amount = 1) { + this.counters[name] = (this.counters[name] || 0) - amount; + }, + + // People methods + meetPerson(name) { + this.people_met.add(name); + logEvent('effect_applied', 'meet_person=' + name); + }, + hasMetPerson(name) { return this.people_met.has(name); }, + + // Location methods + visitLocation(name) { this.locations_visited.add(name); }, + hasVisited(name) { return this.locations_visited.has(name); } +}; +''' + + +def generate_condition_evaluator_js(): + """Generate JavaScript Condition Evaluator.""" + return ''' +// ============================================================ +// CONDITION EVALUATOR +// ============================================================ +function evaluateCondition(condition) { + // No condition = always true + if (!condition || Object.keys(condition).length === 0) return true; + + // String = flag check + if (typeof condition === 'string') { + return GameState.hasFlag(condition); + } + + // Compound conditions + if (condition.and) { + return condition.and.every(c => evaluateCondition(c)); + } + if (condition.or) { + return condition.or.some(c => evaluateCondition(c)); + } + if (condition.not) { + return !evaluateCondition(condition.not); + } + + // Inventory conditions + if (condition.has_item) { + return GameState.hasItem(condition.has_item); + } + if (condition.not_has_item) { + return !GameState.hasItem(condition.not_has_item); + } + + // Flag conditions + if (condition.flag) { + return GameState.hasFlag(condition.flag); + } + if (condition.not_flag) { + return !GameState.hasFlag(condition.not_flag); + } + + // Money conditions + if (condition.money !== undefined) { + return evaluateNumericCondition(GameState.money, condition.money); + } + + // Counter conditions + if (condition.counter) { + for (const [name, check] of Object.entries(condition.counter)) { + if (!evaluateNumericCondition(GameState.getCounter(name), check)) { + return false; + } + } + return true; + } + + // People conditions + if (condition.met_person) { + return GameState.hasMetPerson(condition.met_person); + } + if (condition.not_met_person) { + return !GameState.hasMetPerson(condition.not_met_person); + } + + // Location conditions + if (condition.visited) { + return GameState.hasVisited(condition.visited); + } + + return true; +} + +function evaluateNumericCondition(value, check) { + if (typeof check === 'number') return value >= check; + if (check.gte !== undefined) return value >= check.gte; + if (check.gt !== undefined) return value > check.gt; + if (check.lte !== undefined) return value <= check.lte; + if (check.lt !== undefined) return value < check.lt; + if (check.eq !== undefined) return value === check.eq; + if (check.neq !== undefined) return value !== check.neq; + return true; +} +''' + + +def generate_effect_applicator_js(): + """Generate JavaScript Effect Applicator.""" + return ''' +// ============================================================ +// EFFECT APPLICATOR +// ============================================================ +function applyEffects(effects) { + if (!effects) return; + + // Inventory effects + if (effects.add_item) { + const items = Array.isArray(effects.add_item) ? effects.add_item : [effects.add_item]; + items.forEach(item => GameState.addItem(item)); + } + if (effects.remove_item) { + const items = Array.isArray(effects.remove_item) ? effects.remove_item : [effects.remove_item]; + items.forEach(item => GameState.removeItem(item)); + } + + // Money effects + if (effects.add_money) GameState.addMoney(effects.add_money); + if (effects.remove_money) GameState.removeMoney(effects.remove_money); + if (effects.set_money !== undefined) GameState.money = effects.set_money; + + // Flag effects + if (effects.set_flag) { + if (typeof effects.set_flag === 'string') { + GameState.setFlag(effects.set_flag); + } else if (Array.isArray(effects.set_flag)) { + effects.set_flag.forEach(f => GameState.setFlag(f)); + } else { + for (const [flag, value] of Object.entries(effects.set_flag)) { + GameState.setFlag(flag, value); + } + } + } + if (effects.clear_flag) { + const flags = Array.isArray(effects.clear_flag) ? effects.clear_flag : [effects.clear_flag]; + flags.forEach(f => GameState.clearFlag(f)); + } + if (effects.toggle_flag) { + const flags = Array.isArray(effects.toggle_flag) ? effects.toggle_flag : [effects.toggle_flag]; + flags.forEach(f => GameState.toggleFlag(f)); + } + + // Counter effects + if (effects.set_counter) { + for (const [name, value] of Object.entries(effects.set_counter)) { + GameState.setCounter(name, value); + } + } + if (effects.increment) { + for (const [name, amount] of Object.entries(effects.increment)) { + GameState.increment(name, amount); + } + } + if (effects.decrement) { + for (const [name, amount] of Object.entries(effects.decrement)) { + GameState.decrement(name, amount); + } + } + + // People effects + if (effects.add_person) { + const people = Array.isArray(effects.add_person) ? effects.add_person : [effects.add_person]; + people.forEach(p => GameState.meetPerson(p)); + } + + // Location effects + if (effects.visit_location) { + const locs = Array.isArray(effects.visit_location) ? effects.visit_location : [effects.visit_location]; + locs.forEach(l => GameState.visitLocation(l)); + } + if (effects.add_location) { + const locs = Array.isArray(effects.add_location) ? effects.add_location : [effects.add_location]; + locs.forEach(l => GameState.visitLocation(l)); + } +} +''' + + +def generate_transition_resolver_js(): + """Generate JavaScript Transition Resolver.""" + return ''' +// ============================================================ +// TRANSITION RESOLVER +// ============================================================ +function resolveTransition(transition) { + // Simple string transition + if (typeof transition === 'string') { + return transition; + } + + // Random weighted transition + if (transition.random) { + const weights = transition.random; + const total = weights.reduce((sum, [_, w]) => sum + w, 0); + let random = Math.random() * total; + for (const [state, weight] of weights) { + random -= weight; + if (random <= 0) return state; + } + return weights[weights.length - 1][0]; + } + + // Random pool (equal probability) + if (transition.random_from) { + const pool = transition.random_from; + return pool[Math.floor(Math.random() * pool.length)]; + } + + // Conditional if/then/else + if (transition.if !== undefined) { + if (evaluateCondition(transition.if)) { + return resolveTransition(transition.then); + } else { + return resolveTransition(transition.else); + } + } + + // Chained conditions + if (transition.conditions) { + for (const cond of transition.conditions) { + if (cond.default) { + return resolveTransition(cond.default); + } + if (evaluateCondition(cond.if)) { + return resolveTransition(cond.then); + } + } + } + + return null; +} +''' + + +def generate_scene_entities_js(config): + """Generate JavaScript code to create PlayCanvas entities for each location/state.""" + lines = [] + lines.append(''' +// ============================================================ +// SCENE ENTITIES +// ============================================================ +const locationEntities = {}; +const choiceEntities = {}; + +function createScene() { + const app = pc.Application.getApplication(); + const root = app.root; + + // Create camera + const camera = new pc.Entity('camera'); + camera.addComponent('camera', { + clearColor: new pc.Color(0.1, 0.1, 0.15) + }); + camera.setPosition(0, 5, 10); + camera.setEulerAngles(-25, 0, 0); + root.addChild(camera); + + // Create light + const light = new pc.Entity('light'); + light.addComponent('light', { + type: 'directional', + color: new pc.Color(1, 1, 1), + intensity: 1 + }); + light.setEulerAngles(45, 30, 0); + root.addChild(light); +''') + + # Generate entities for each location/state + state_index = 0 + for location, states in config.items(): + if not isinstance(states, dict): + continue + + for state_name, state_data in states.items(): + if not isinstance(state_data, dict): + continue + + entity_name = f"{location}_{state_name}" + safe_name = entity_name.replace('-', '_').replace(' ', '_') + + lines.append(f''' + // === Location: {location}/{state_name} === + const loc_{safe_name} = new pc.Entity('{entity_name}'); + loc_{safe_name}.setPosition({state_index * 20}, 0, 0); + loc_{safe_name}.enabled = false; + locationEntities['{entity_name}'] = loc_{safe_name}; + root.addChild(loc_{safe_name}); + + // Ground plane + const ground_{safe_name} = new pc.Entity('ground_{safe_name}'); + ground_{safe_name}.addComponent('render', {{ type: 'plane' }}); + ground_{safe_name}.setLocalScale(15, 1, 15); + loc_{safe_name}.addChild(ground_{safe_name}); +''') + + # Generate choice entities + choices = state_data.get('choices', []) + choice_config = state_data.get('choice_config', {}) + transitions = state_data.get('transitions', {}) + effects = state_data.get('effects', {}) + + for idx, choice in enumerate(choices): + choice_safe = choice.replace('-', '_').replace(' ', '_').replace("'", "")[:20] + choice_id = f"{entity_name}_choice_{idx}" + choice_escaped = choice.replace('"', '\\"') + + # Position choices in a row + x_offset = (idx - len(choices)/2) * 3 + + # Check if choice has condition + has_condition = choice in choice_config and 'condition' in choice_config[choice] + condition_json = json.dumps(choice_config[choice]['condition']) if has_condition else 'null' + + # Get transition and effects + transition = transitions.get(choice, '') + transition_json = json.dumps(transition) if isinstance(transition, dict) else f'"{transition}"' + effect = effects.get(choice, {}) + effect_json = json.dumps(effect) + + lines.append(f''' + // Choice: {choice} + const choice_{safe_name}_{idx} = new pc.Entity('{choice_id}'); + choice_{safe_name}_{idx}.addComponent('render', {{ type: 'box' }}); + choice_{safe_name}_{idx}.setLocalPosition({x_offset}, 1, -5); + choice_{safe_name}_{idx}.choiceData = {{ + text: "{choice_escaped}", + condition: {condition_json}, + transition: {transition_json}, + effects: {effect_json} + }}; + choiceEntities['{choice_id}'] = choice_{safe_name}_{idx}; + loc_{safe_name}.addChild(choice_{safe_name}_{idx}); +''') + + state_index += 1 + + lines.append(''' +} +''') + + return '\n'.join(lines) + + +def generate_game_controller_js(config): + """Generate JavaScript Game Controller.""" + # Find starting location/state + first_location = next(iter(config.keys())) + first_state = next(iter(config[first_location].keys())) + + # Convert config to JSON for embedding + config_json = json.dumps(config, indent=2) + + return f''' +// ============================================================ +// GAME CONFIG +// ============================================================ +const GameConfig = {config_json}; + +// ============================================================ +// GAME CONTROLLER +// ============================================================ +let currentLocationEntity = null; + +function startGame() {{ + createScene(); + navigateTo('{first_location}', '{first_state}'); + setupClickHandler(); +}} + +function navigateTo(location, state) {{ + const entityName = location + '_' + state; + const oldLocation = GameState.current_location + '_' + GameState.current_state; + + // Hide current location + if (currentLocationEntity) {{ + currentLocationEntity.enabled = false; + }} + + // Show new location + const newEntity = locationEntities[entityName]; + if (newEntity) {{ + newEntity.enabled = true; + currentLocationEntity = newEntity; + + // Log transition + logEvent('transition', oldLocation + ' -> ' + entityName); + + // Update game state + GameState.current_location = location; + GameState.current_state = state; + GameState.visitLocation(location); + + // Apply on_enter effects + const stateData = GameConfig[location]?.[state]; + if (stateData?.on_enter) {{ + applyEffects(stateData.on_enter); + }} + + // Update choice visibility + updateChoiceVisibility(); + + console.log('Navigated to:', entityName); + console.log('Description:', stateData?.description); + }} else {{ + console.error('Location not found:', entityName); + }} +}} + +function updateChoiceVisibility() {{ + const location = GameState.current_location; + const state = GameState.current_state; + const entityName = location + '_' + state; + + // Find all choices for current location + for (const [id, entity] of Object.entries(choiceEntities)) {{ + if (id.startsWith(entityName + '_choice_')) {{ + const condition = entity.choiceData?.condition; + entity.enabled = evaluateCondition(condition); + }} + }} +}} + +function handleChoiceClick(entity) {{ + const data = entity.choiceData; + if (!data) return; + + logEvent('choice_selected', data.text); + + // Check condition + if (!evaluateCondition(data.condition)) {{ + console.log('Condition not met'); + return; + }} + + // Apply effects + applyEffects(data.effects); + + // Resolve transition + const target = resolveTransition(data.transition); + if (target) {{ + // Parse target - supports both "location/state" and "location_state" formats + let newLocation, newState; + if (target.includes('/')) {{ + // Standard format: "location/state" + const parts = target.split('/'); + newLocation = parts[0]; + newState = parts.slice(1).join('/'); + }} else if (target.includes('_')) {{ + // Legacy format: "location_state" (ambiguous if location has underscores) + const parts = target.split('_'); + newState = parts.pop(); + newLocation = parts.join('_'); + }} else {{ + // Single word - assume it's a state in current location + newLocation = GameState.current_location; + newState = target; + }} + navigateTo(newLocation, newState); + }} +}} + +function setupClickHandler() {{ + const app = pc.Application.getApplication(); + + app.mouse.on(pc.EVENT_MOUSEDOWN, function(event) {{ + const camera = app.root.findByName('camera'); + if (!camera) return; + + // Get ray from camera through mouse position + const from = camera.getPosition(); + const to = camera.camera.screenToWorld(event.x, event.y, camera.camera.farClip); + const rayDir = new pc.Vec3().sub2(to, from).normalize(); + + // Check all choice entities for intersection + let closestEntity = null; + let closestDist = Infinity; + + for (const [id, entity] of Object.entries(choiceEntities)) {{ + if (!entity.enabled) continue; + + // Simple sphere-based picking (check if ray passes near entity center) + const entityPos = entity.getPosition(); + const toEntity = new pc.Vec3().sub2(entityPos, from); + const projLength = toEntity.dot(rayDir); + + if (projLength > 0) {{ + const closestPoint = new pc.Vec3().add2(from, rayDir.clone().mulScalar(projLength)); + const dist = closestPoint.distance(entityPos); + + // Pick radius of 1.5 units (box is roughly 1 unit) + if (dist < 1.5 && projLength < closestDist) {{ + closestDist = projLength; + closestEntity = entity; + }} + }} + }} + + if (closestEntity?.choiceData) {{ + handleChoiceClick(closestEntity); + }} + }}); +}} + +// Start the game when script loads +startGame(); +''' + + +def export_to_playcanvas(config_json): + """ + Main export function - converts game config to PlayCanvas JavaScript. + + Args: + config_json: JSON string of the game config + + Returns: + tuple: (explanation, javascript_code) + """ + try: + config = json.loads(config_json) + except json.JSONDecodeError as e: + return f"JSON Error: {e}", "" + + # Generate all parts + parts = [ + "// ============================================================", + "// PLAYCANVAS GAME EXPORT", + "// Generated from game config", + "// Paste this into a PlayCanvas script", + "// ============================================================", + "", + generate_gamestate_js(), + generate_condition_evaluator_js(), + generate_effect_applicator_js(), + generate_transition_resolver_js(), + generate_scene_entities_js(config), + generate_game_controller_js(config), + ] + + full_code = '\n'.join(parts) + + explanation = """PlayCanvas Export Generated! + +Instructions: +1. Create a new PlayCanvas project +2. Create a new script (e.g., 'game.js') +3. Paste the generated code into the script +4. Attach the script to the Root entity +5. Launch the game + +The generated code includes: +- GameState: Tracks inventory, money, flags, counters, etc. +- Condition Evaluator: Checks conditions (has_item, flag, money, etc.) +- Effect Applicator: Applies effects (add_item, set_flag, increment, etc.) +- Transition Resolver: Handles random/conditional transitions +- Scene Entities: 3D placeholder boxes for each choice +- Game Controller: Navigation and click handling + +Click on choice boxes to make selections and navigate through the game. +""" + + return explanation, full_code + + +def export_to_playcanvas_html(config_json): + """ + Export game config to a standalone HTML file with embedded PlayCanvas. + + This creates a complete HTML file that can be opened directly in a browser + or embedded in an iframe for preview. + + Args: + config_json: JSON string of the game config + + Returns: + tuple: (explanation, html_code) + """ + # Get the JavaScript code from the regular exporter + js_explanation, js_code = export_to_playcanvas(config_json) + + if not js_code: + return js_explanation, "" + + # Create complete HTML with PlayCanvas CDN + html_code = f''' + + + + + PlayCanvas Game Preview + + + + +
Loading PlayCanvas...
+ +
+

Game Preview

+
Location: -
+
State: -
+
+
Click on boxes to make choices
+
+ + + +''' + + explanation = """PlayCanvas HTML Preview Generated! + +This is a standalone HTML file that runs PlayCanvas directly in the browser. + +Features: +- Self-contained - no external project needed +- Info panel shows current location/state +- Click on 3D boxes to make choices +- WebGL-powered 3D rendering + +Note: WebGL must be available in your browser for this to work. +Some browsers may restrict WebGL in embedded iframes. +""" + + return explanation, html_code diff --git a/prompts_and_outputs.py b/prompts_and_outputs.py new file mode 100644 index 0000000000000000000000000000000000000000..09961bd42dbf330418c49118e2c613baf3ad2d5f --- /dev/null +++ b/prompts_and_outputs.py @@ -0,0 +1,2323 @@ +""" +Prompts, outputs, and templates for LLM interactions +""" + +Frontendpromptengforguide = """Suggest descriptions for media to fill the empty media fields - + +{ + "village": { + "start": { + "description": "You wake up in a small village. You hear a rumor about a lost treasure.", + "choices": [ + "explore village", + "gather supplies", + "rest" + ], + "transitions": { + "explore village": "village_rumor", + "gather supplies": "village_supplies", + "rest": "village_start" + }, + "consequences": { + "gather supplies": {} + }, + "media": [] + }, + "rumor": { + "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", + "choices": [ + "decide to go", + "ignore" + ], + "transitions": { + "decide to go": "village_supplies", + "ignore": "village_start" + }, + "consequences": { + "decide to go": {} + }, + "media": [] + }, + "supplies": { + "description": "You gather supplies for your journey.", + "choices": [ + "head to forest", + "stay in village" + ], + "transitions": { + "head to forest": "forest_forest", + "stay in village": "village_start" + }, + "media": [] + } + }, + "forest": { + "forest": { + "description": "You enter the dense forest, heading towards the ruins.", + "choices": [ + "travel further", + "return to village" + ], + "transitions": { + "travel further": "ruins_ruins", + "return to village": "village_start" + }, + "media": [] + } + }, + "ruins": { + "ruins": { + "description": "You reach the ancient ruins. The entrance is dark and eerie.", + "choices": [ + "enter ruins", + "return to forest" + ], + "transitions": { + "enter ruins": "ruins_explore", + "return to forest": "forest_forest" + }, + "media": [] + }, + "explore": { + "description": "You explore the ruins, encountering traps and puzzles.", + "choices": [ + "solve puzzle", + "avoid traps" + ], + "transitions": { + "solve puzzle": "ruins_hiddenPassage", + "avoid traps": "ruins_ruins" + }, + "media": [] + }, + "hiddenPassage": { + "description": "You solve a challenging puzzle and unlock a hidden passage.", + "choices": [ + "enter passage", + "go back" + ], + "transitions": { + "enter passage": "ruins_treasureRoom", + "go back": "ruins_explore" + }, + "media": [] + }, + "treasureRoom": { + "description": "You enter the treasure room and find the treasure chest.", + "choices": [ + "take treasure", + "leave" + ], + "transitions": { + "take treasure": "ruins_celebrate", + "leave": "ruins_ruins" + }, + "consequences": { + "take treasure": {} + }, + "media": [] + }, + "celebrate": { + "description": "You celebrate your discovery and decide to bring the treasure back to the village.", + "choices": [ + "return to village" + ], + "transitions": { + "return to village": "village_return_return" + }, + "media": [] + } + }, + "village_return": { + "return": { + "description": "You return to the village with the treasure and share it with the villagers.", + "choices": [ + "end adventure" + ], + "transitions": { + "end adventure": "end_end" + }, + "media": [] + } + }, + "end": { + "end": { + "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", + "choices": [], + "transitions": {}, + "media": [] + } + } +} +""" + +FAQAllprompts = { + "Example Workflow": [ + ("I am writing a story about a chef. Please write dishes to appear on the menu.", " - I am writing a story about a chef. Please write dishes to appear on the menu."), + ("Lets brainstorm some decisions a chef would take", " - What are the most common decisions that a chef story would include?"), + ("Lets brainstorm some problems a chef would face", " - What are the kinds problems that a chef story would include?"), + ("Lets brainstorm some goals the chef would have", " - What are the kinds of out of reach goals that a chef story would include?"), + ("Please help me add to config that I pasted", " - Continue the pasted config from any complete block."), + ("Help me think of images for a chef story", ". Help me think of images for a chef story") + ], + "FAQ": [ + ("Help me make a story", "- Explain to the user that they need to come up with a list of decisions, and then choose themes and then do the setting in characters and add some examples for each step:"), + ], + "Brainstorming for this space": [ + ("Help me make a story", "- Explain to the user that they need to come up with a list of decisions, and then choose themes and then do the setting in characters and add some examples for each step:"), + ], + "Expansive Problem solving": [ + ("My problem to solve is", "- please make 10 sub problems have to solve from this:"), + ("My process to solve is", "- please make 10 sub processes have to solve from this:"), + ("My goal to solve is", "- please make 10 sub goals have to solve from this:"), + ("My task to solve is", "- please make 10 sub tasks have to solve from this:"), + ("My phase to solve is", "- please make 10 sub phases have to solve from this:"), + ("My component to solve is", "- please make 10 sub components have to solve from this:"), + ("My element to solve is", "- please make 10 sub elements have to solve from this:"), + ("A brief description of my current situation:", "- please list the most important task to pay attention to:"), + ("A brief description of my current situation to analyse:", "- please conduct a situational analysis:"), + ("A brief description of my current situation to decompose:", "- please conduct a problem decomposition:"), + ], +} + +Storycraftprompts = ["Write a 3 sentence story", +"Great, Lets rewrite the story (keep as much of it as possible) with F1 as our thematic source", +"Great, Next for each sentence make decisions for a player in a text game"] + + +Adjectivebasedcombinationideatexts = """For control over the flow of the game first need to solve chicken egg of story vs json then for each concept to mixed need to do the same. + +Gemini-test (23/07/2023) + +The kind of analysis you're describing, where you're looking for shared qualities and connections between seemingly disparate concepts like storytelling and JSON, can be referred to by several terms, depending on the specific focus and context: + +General Terms: + +Comparative Analysis: This is a broad term that encompasses any analysis where you're comparing and contrasting two or more things. +Analogical Reasoning: This involves identifying similarities between two things and using those similarities to understand or explain one in terms of the other. +Conceptual Blending: This refers to the cognitive process of combining different concepts to create new meanings or understandings. +Interdisciplinary Thinking: This involves drawing connections and insights from different fields of knowledge. +More Specific Terms (depending on the goal): + +Metaphorical Analysis: If you're focusing on the metaphorical connections between the concepts, this term would be appropriate. +Cross-Domain Mapping: This term is often used in cognitive science and artificial intelligence to describe the process of finding correspondences between different domains of knowledge. +Design Thinking: If your goal is to use these insights to create something new or solve a problem, this term might be relevant. +In the context of your specific example (storytelling and JSON): + +You could describe this analysis as: + +Finding Shared Principles of Effective Communication: Both storytelling and JSON are ultimately about conveying information effectively. +Exploring the Art of Structure and Organization: Both storytelling and JSON rely on clear structure and organization to achieve their respective goals. +Bridging the Gap Between Narrative and Data: This analysis highlights the unexpected connections between the seemingly disparate worlds of storytelling and data representation. +To further refine your search for resources and methods, consider these questions: + +What is your ultimate goal in merging these concepts? Are you trying to understand one better through the lens of the other? Are you trying to create something new? +What specific aspects of each concept are you most interested in comparing? Are you focusing on structure, clarity, engagement, or something else? +By clarifying your goals and focusing your analysis, you can more effectively leverage these different approaches and find the most relevant resources to guide your exploration.""" + + +Adjectivebasedcombinationideatextsv2 = """Nemotron 4 340B Perplexity Labs + +Original Prompt +Please help me. If I want to merge the idea of a bakery with the plot of star wars I need to find points where they match and dont match. I decided to use adjectives as a proxy so the combinatorics is initiated by close adjectives to generate seeds for combination ideas that we can send to an llm for each seed. How would this look in mermaid + + + +Mermaid Struxture + + +The workflow or process you've described can be generally categorized as a "concept blending" or "idea fusion" approach, which involves combining elements from two or more distinct concepts to generate new ideas. In your specific case, you're using adjectives as a proxy to facilitate the blending of a bakery concept with the plot of Star Wars. This process can also be considered a form of "creative problem-solving" or "design thinking," where you're systematically exploring and combining elements to generate innovative solutions or ideas. +More specifically, the steps in your workflow can be broken down into the following stages: +Concept Identification: Identifying the two distinct concepts (bakery and Star Wars) to be blended. +Attribute Extraction: Extracting relevant attributes (adjectives) from each concept. +Combinatorics: Systematically combining the attributes from both concepts to generate new combinations. +Seed Idea Generation: Translating the combined attributes into seed ideas that merge the two concepts. +LLM Processing: Utilizing a Large Language Model (LLM) to further develop and refine the seed ideas into more detailed and cohesive concepts. +This workflow can be applied to various creative problem-solving scenarios, not just limited to the bakery and Star Wars example + + + +Indeed, design can be viewed as a search algorithm, where you explore a vast space of possibilities to find optimal or innovative solutions. Here's how you can apply this idea to five more scenarios: +Merging a Library with a Music Festival +Concept Identification: Library and Music Festival +Attribute Extraction: Quiet, Knowledgeable, Organized (Library) vs. Loud, Energetic, Creative (Music Festival) +Combinatorics: Quiet Creative Spaces, Knowledgeable Music Workshops, Organized Jam Sessions +Seed Idea Generation: Silent Disco Library, Music Theory Workshops, Collaborative Songwriting Sessions +LLM Processing: Develop detailed event plans, marketing materials, and logistical considerations +Blending a Fitness Center with a Video Game Arcade +Concept Identification: Fitness Center and Video Game Arcade +Attribute Extraction: Active, Healthy, Competitive (Fitness Center) vs. Interactive, Fun, Social (Video Game Arcade) +Combinatorics: Active Interactive Games, Healthy Fun Challenges, Competitive Social Workouts +Seed Idea Generation: Exergaming Stations, Virtual Reality Fitness Challenges, Multiplayer Workout Games +LLM Processing: Create game concepts, rules, and scoring systems, as well as marketing and branding strategies +Fusing a Vegan Restaurant with a Science Museum +Concept Identification: Vegan Restaurant and Science Museum +Attribute Extraction: Plant-Based, Sustainable, Healthy (Vegan Restaurant) vs. Educational, Interactive, Innovative (Science Museum) +Combinatorics: Plant-Based Interactive Exhibits, Sustainable Educational Dining, Healthy Innovative Cuisine +Seed Idea Generation: Edible Plant Exhibits, Cooking Classes with a Scientific Twist, Molecular Gastronomy Menu +LLM Processing: Develop exhibit designs, class curricula, and menu items, as well as marketing and branding strategies +Combining a Co-working Space with a Pet Store +Concept Identification: Co-working Space and Pet Store +Attribute Extraction: Collaborative, Productive, Professional (Co-working Space) vs. Friendly, Playful, Nurturing (Pet Store) +Combinatorics: Collaborative Pet-Friendly Spaces, Productive Play Areas, Professional Pet Care Services +Seed Idea Generation: Pet-Friendly Co-working Desks, On-site Pet Daycare, Pet-themed Networking Events +LLM Processing: Create space layouts, pet care service offerings, and event plans, as well as marketing and branding strategies +Integrating a Fashion Boutique with an Art Gallery +Concept Identification: Fashion Boutique and Art Gallery +Attribute Extraction: Stylish, Trendy, Creative (Fashion Boutique) vs. Artistic, Thought-provoking, Inspirational (Art Gallery) +Combinatorics: Stylish Artistic Installations, Trendy Thought-provoking Fashion, Creative Inspirational Clothing +Seed Idea Generation: Art-inspired Fashion Collections, Interactive Fashion Exhibits, Artist-designed Clothing Lines +LLM Processing: Develop fashion collection concepts, exhibit designs, and artist collaborations, as well as marketing and branding strategies + +""" + +Nonlinearprogressionideas = """Llama 405B + +Non-linear storytelling involves presenting events out of chronological order. This technique can be used in literature, film, and other forms of narrative to create suspense, surprise, or to highlight certain themes or character developments. Here are some common examples of non-linear storytelling events: + +1. Flashbacks: These are scenes that take the story back in time, often used to explain a character's past or to provide context for the current situation. + +2. Flashforwards: Similar to flashbacks, but these scenes take the story forward in time, often used to create suspense or to show the consequences of current actions. + +3. Parallel storylines: This involves telling multiple stories that happen at the same time but are presented in a non-linear fashion, often intersecting or influencing each other at certain points. + +4. Reverse chronology: The story is presented in reverse order, starting with the conclusion and moving backward in time. + +5. Non-chronological chapters or scenes: The story is presented in a non-linear order, with each chapter or scene taking place at a different point in time. + +6. Time loops: A character relives the same period of time repeatedly, often with slight variations. + +7. Unreliable narrators: The narrator withholds or distorts information, leading to a non-linear presentation of events. + +8. Dream sequences: The story jumps into a character's dreams or imagination, which can be presented in a non-linear fashion. + +These are just a few examples of non-linear storytelling events. The possibilities are endless, and the technique can be adapted to fit the needs of the story and the creativity of the storyteller. +""" + +TimeRelatedConceptsForIdeaGeneration = """Sonnet 3.5 + +Let's explore some other ways we can conceptualize time tangibly for storytelling purposes: + +Time as a Currency: +Imagine a world where time itself is a tradable resource. People can buy, sell, or exchange units of time. +Time as a Landscape: +Time could be visualized as a physical terrain that characters can traverse, with different eras represented by distinct geographical features. +Time as a Library: +Similar to the warehouse concept, but each moment in time is a book that can be read, written, or edited. +Time as a Network: +Envision time as an interconnected web of nodes, where each node represents an event or moment that can be accessed non-linearly. +Time as a Crafting Material: +In this concept, time could be molded, shaped, or combined like clay to create new realities or alter existing ones. +Time as a Ecosystem: +Here, different time periods interact like species in an ecosystem, with cause-and-effect relationships spanning across eras. +Time as a Computer Program: +Time could be seen as a vast codebase, where altering a line of code in the past changes the program's output in the future. +Time as a Painting: +Imagine time as a giant canvas where past, present, and future are all visible simultaneously, and characters can step into different parts of the painting. +Time as a Puzzle: +Each moment in time is a puzzle piece that can be rearranged to create different realities or outcomes. +Time as a Musical Composition: +Different time periods are like instruments in an orchestra, and changing the past is like altering the score, creating ripple effects through the entire composition. + +Investing (as you mentioned): + +Go back in time to invest in successful companies early +Preview future market trends + + +Law (as you mentioned): + +Go back in time to gather evidence or witness events firsthand +Preview potential outcomes of different legal strategies + + +Science (as you mentioned): + +Go to the future to see long-term experimental results +Observe long-term environmental changes + + +Medicine: + +Travel to the future to see long-term effects of treatments +Go back in time to prevent diseases before they start + + +Archaeology: + +Travel to the past to directly observe historical events +Verify theories about ancient civilizations + + +Journalism: + +Witness and report on historical events firsthand +Preview future events to prepare in-depth coverage + + +Environmental Conservation: + +Go to the future to see the impacts of current policies +Travel to the past to preserve extinct species + + +Engineering: + +Test structures over centuries in a matter of days +Go back in time to prevent disasters by fixing design flaws + + +Agriculture: + +Observe full growth cycles of crops in an instant +Travel to the future to develop crops resistant to future climate conditions + + +Criminal Investigation: + +Witness crimes as they happen in the past +Prevent crimes by foreseeing them in the future + + +Art Restoration: + +Go back in time to see artwork in its original state +Travel to the future to see how current restoration techniques hold up + + +Education: + +Take students on field trips to historical events +Preview future job markets to tailor current curricula + + +Disaster Management: + +Go back in time to implement better preventive measures +Travel to the future to assess long-term impacts of disasters + + +Politics: + +See the long-term effects of policies instantly +Go back in time to prevent international conflicts + + +Sports Coaching: + +Observe an athlete's entire career to optimize training from the start +Preview future competitions to develop winning strategies + + + +From this exercise, we can see some common themes emerging: + +Verification of past events or theories +Prevention of negative outcomes +Optimization of current processes +Long-term impact assessment +Preparation for future events + + + +This table outlines potential risk elements and game ideas for each of the themes we identified. Let's discuss these in more detail: + +Verification of past events: + +Risk: The butterfly effect. Even small interactions in the past could dramatically alter the present. +Game Idea: Players could be historians or detectives, tasked with observing past events to solve present-day mysteries. The challenge is to gather information without interfering with the past. + + +Prevention of negative outcomes: + +Risk: Unintended consequences. Preventing one disaster might lead to an even worse one. +Game Idea: Players manage multiple timelines, trying to create the best overall outcome. They must balance preventing catastrophes in one timeline against potential negative impacts in others. + + +Optimization of current processes: + +Risk: Over-optimization leading to system collapse. +Game Idea: Players could manage a company or even a country, tweaking past decisions to improve the present. However, pushing too hard for efficiency or profit could lead to unforeseen problems or total collapse. + + +Long-term impact assessment: + +Risk: Increasing uncertainty and danger the further into the future you go. +Game Idea: Players explore future timelines to gather data, but risk getting trapped in increasingly divergent and potentially hostile futures. The challenge is to gather enough information without going so far that you can't return. + + +Preparation for future events: + +Risk: Self-fulfilling prophecies. Preparing for a predicted future might be what causes it to happen. +Game Idea: Players navigate through potential futures, trying to prepare for or prevent them without inadvertently causing them. The game could involve complex cause-and-effect chains where players must carefully consider the implications of their actions. + + + +To further develop these into game concepts, we could consider: + +Resource management: Time itself could be a limited resource that players must manage carefully. +Paradox potential: Create tension by introducing the risk of paradoxes that could erase the player from existence. +Competing factions: Introduce other time travelers with different agendas, creating conflict and competition. +Technological limitations: Time travel devices could have cooldown periods, limited charges, or other restrictions that force strategic use. +Ethical dilemmas: Present players with morally ambiguous choices that have far-reaching consequences across time. +""" + + +WFStage1prompt = """Use this to write a story and explain how it represents the structure - + +graph TD +subgraph "Actual Timeline" +A1[Location 1 - First Visit] +A2[Location 2 - First Visit] +A3[Location 3 - First Visit] +A4[Location 4 - First Visit] +A1 --> A2 --> A3 --> A4 +end +subgraph "Narrative Timeline" +N1[Location 2 - Story Event 1] +N2[Location 4 - Story Event 2] +N3[Location 1 - Story Event 3] +N4[Location 3 - Story Event 4] +N1 --> N2 --> N3 --> N4 +end +subgraph "Hidden Timeline" +H1[Hidden Event 1] +H2[Hidden Event 2] +H3[Hidden Event 3] +H4[Hidden Event 4] +end +A1 -.-> N3 +A2 -.-> N1 +A3 -.-> N4 +A4 -.-> N2 +H1 -.- A1 +H1 -.- A2 +H1 -.- A3 +H1 -.- A4 +H2 -.- A1 +H2 -.- A2 +H2 -.- A3 +H2 -.- A4 +H3 -.- A1 +H3 -.- A2 +H3 -.- A3 +H3 -.- A4 +H4 -.- A1 +H4 -.- A2 +H4 -.- A3 +H4 -.- A4""" + +TimeRelatedMermaidStoryAttempttoRefinefrom = ("""gpt-4o + +Once upon a time in a land where time flowed like a river with hidden currents, there was a story that unfolded across three intertwined timelines: the Actual Timeline, the Narrative Timeline, and the Hidden Timeline. + +Actual Timeline +The protagonist, Elena, embarked on a journey through four distinct locations, each holding a piece of a grand puzzle she needed to solve. + +Location 1 - First Visit: Elena started her journey here, uncovering an ancient map that hinted at a deeper mystery. +Location 2 - First Visit: She traveled to a bustling marketplace, where she found a cryptic message carved into a stone. +Location 3 - First Visit: In an abandoned library, Elena discovered a journal detailing strange occurrences and hidden events. +Location 4 - First Visit: Her journey led her to a serene forest where she encountered an enigmatic figure who offered her a crucial clue. +Narrative Timeline +As Elena pieced together the clues, the story she narrated to her friends followed a different sequence, highlighting the events that made the most sense from her perspective at the time. + +Location 2 - Story Event 1: Elena recalled the marketplace first, sharing the mystery of the cryptic message. +Location 4 - Story Event 2: She then described her encounter in the serene forest, emphasizing the importance of the clue she received. +Location 1 - Story Event 3: Elena recounted the discovery of the ancient map, giving context to the overall mystery. +Location 3 - Story Event 4: Lastly, she talked about the abandoned library and the journal, which tied all the events together. +Hidden Timeline +Unbeknownst to Elena, there was a hidden layer of events influencing her journey. + +Hidden Event 1: In the shadows, an ancient order monitored her progress from the moment she found the map, ensuring she remained on the right path. +Hidden Event 2: They subtly guided her towards the marketplace, planting the cryptic message for her to find. +Hidden Event 3: The journal in the library was placed there by the order, containing information they wanted her to uncover. +Hidden Event 4: The enigmatic figure in the forest was a member of the order, providing her with the final piece of the puzzle. +Interconnections +Actual Timeline to Narrative Timeline: +Location 1 (First Visit) led to Story Event 3 (Elena's recollection of the map). +Location 2 (First Visit) led to Story Event 1 (marketplace mystery). +Location 3 (First Visit) led to Story Event 4 (the journal). +Location 4 (First Visit) led to Story Event 2 (forest encounter). +Actual Timeline to Hidden Timeline: +Each visit in the Actual Timeline was influenced by the Hidden Events, which shaped Elena's experiences without her knowledge. +Story Representation +This structure represents the complexity of storytelling, where the sequence of events (Actual Timeline) is often different from how the story is told (Narrative Timeline), and there are unseen forces (Hidden Timeline) that influence the course of events. Elena’s journey through the four locations, her recollection of events, and the hidden manipulations all intertwine to create a rich, multi-layered narrative. + +In this story, the Actual Timeline shows Elena’s chronological journey. The Narrative Timeline reveals how she chose to share her experiences, emphasizing different aspects of her journey. The Hidden Timeline uncovers the secret influences that guided her path, adding depth and intrigue to the overall tale. + + + +Great attempt. Lets see how the story changes if we assume the main character goes to every location twice + + + +Certainly! Let's expand the story to include two visits to each location, which will add more depth and complexity to the narrative. + +Actual Timeline +Elena revisits each location, uncovering new layers of the mystery with each visit. + +Location 1 - First Visit: Elena starts her journey here, uncovering an ancient map that hints at a deeper mystery. +Location 2 - First Visit: She travels to a bustling marketplace, where she finds a cryptic message carved into a stone. +Location 3 - First Visit: In an abandoned library, Elena discovers a journal detailing strange occurrences and hidden events. +Location 4 - First Visit: Her journey leads her to a serene forest where she encounters an enigmatic figure who offers her a crucial clue. +Location 1 - Second Visit: Elena returns to the first location, where she deciphers the ancient map, revealing a hidden passage. +Location 2 - Second Visit: Back at the marketplace, she meets a mysterious merchant who provides additional information about the cryptic message. +Location 3 - Second Visit: In the library, she finds a hidden compartment in the journal containing a key. +Location 4 - Second Visit: Finally, in the forest, the enigmatic figure reappears, guiding her to the entrance of an ancient temple. +Narrative Timeline +Elena pieces together her adventure in a sequence that makes the most sense to her story. + +Location 2 - Story Event 1: Elena recalls the marketplace first, sharing the mystery of the cryptic message and the merchant’s information. +Location 4 - Story Event 2: She then describes her encounters in the serene forest, focusing on both the clue and the guidance to the temple. +Location 1 - Story Event 3: Elena recounts the discovery of the ancient map and the hidden passage. +Location 3 - Story Event 4: Lastly, she talks about the abandoned library, the journal, and the key. +Hidden Timeline +The ancient order continues to guide Elena’s journey, intervening subtly. + +Hidden Event 1: The order monitors Elena from the moment she finds the map, ensuring she is on the right path. +Hidden Event 2: They guide her towards the marketplace, planting the cryptic message and arranging the merchant’s appearance. +Hidden Event 3: The journal in the library is placed by the order, with the hidden compartment and key added during her first visit. +Hidden Event 4: The enigmatic figure in the forest is a member of the order, orchestrating both encounters to provide the necessary guidance. +Interconnections +Actual Timeline to Narrative Timeline: +Location 1 First Visit (map discovery) leads to Story Event 3 (hidden passage). +Location 2 First Visit (cryptic message) and Second Visit (merchant) combine into Story Event 1 (marketplace mystery). +Location 3 First Visit (journal) and Second Visit (key) combine into Story Event 4 (library findings). +Location 4 First Visit (clue) and Second Visit (temple guidance) combine into Story Event 2 (forest encounters). +Actual Timeline to Hidden Timeline: +Each visit in the Actual Timeline is influenced by the Hidden Events, shaping Elena’s experiences without her knowledge. +Expanded Story Representation +In this expanded story, the Actual Timeline now includes two visits to each location, reflecting Elena's deepening understanding of the mystery. The Narrative Timeline still presents her recollection of events, but now with added complexity as she integrates discoveries from both visits. The Hidden Timeline continues to reveal the secret influences that guide her path, adding further depth and intrigue. + +Elena’s repeated visits to each location allow her to uncover additional layers of the mystery, with each visit building on the previous one. This structure illustrates how stories can evolve with new information and perspectives, and how unseen forces can shape the journey in subtle but significant ways. +""", + +"""meta-llama-3.1-405b-instruct + +Here's a story based on the graph structure: + +Emily had always been fascinated by the mysterious disappearances that had taken place in the four locations she was about to visit. She packed her bags, said goodbye to her family, and set off on her journey. + +Her first stop was Location 1, an abandoned mansion on the outskirts of town. As she walked through the dusty halls, she stumbled upon a cryptic message scrawled on the wall: "The eyes that see are not the ones that look." Emily was intrigued but couldn't decipher the meaning. + +Next, she visited Location 2, an old bookstore in the city. While browsing through the shelves, she met the owner, an eccentric old man who told her about a mysterious book that was said to hold the key to the disappearances. Emily was skeptical, but the old man's words lingered in her mind. + +From there, she traveled to Location 3, a secluded monastery in the mountains. The monks welcomed her warmly and shared stories of a long-forgotten event that had taken place in the monastery's past. Emily sensed that there was more to the story, but the monks were tight-lipped about the details. + +Finally, she arrived at Location 4, a small island with a reputation for being cursed. As she explored the island, she stumbled upon a journal belonging to one of the disappeared individuals. The journal hinted at a dark secret, but the writer's words were cryptic and unclear. + +As Emily returned home, she began to piece together the events of her journey. She realized that the disappearances were not random events, but rather, they were all connected. The message on the wall in Location 1 was a warning, the book in Location 2 held a vital clue, the monastery's past was crucial to understanding the present, and the journal on the island was the key to unlocking the mystery. + +However, what Emily didn't know was that her journey was not just about uncovering the truth about the disappearances. Unbeknownst to her, a series of hidden events had been set in motion, influencing her actions and guiding her towards a specific outcome. These events, though invisible to Emily, were the true driving force behind the story. + +The story structure represented by the graph is non-linear. The actual timeline shows Emily visiting the four locations in a linear sequence (A1 -> A2 -> A3 -> A4). However, the narrative timeline is different, with the story events being presented out of chronological order (N1 -> N2 -> N3 -> N4). This non-linearity allows the story to build suspense and create a sense of mystery. + +The hidden timeline represents the unseen forces that shape the story. These events are not directly revealed to the reader, but they have a profound impact on the narrative. The connections between the hidden events and the actual timeline suggest that the story is more complex and layered than initially apparent. + +The graph structure highlights the relationships between the different timelines and events, demonstrating how a non-linear narrative can be constructed to create a engaging and mysterious story. + + + +Very Good Attempt. Lets see how your story will change under the assumption that each location is visited twice + + + +That's an interesting twist. Here's a revised story based on the assumption that each location is visited twice: + +Emily had always been fascinated by the mysterious disappearances that had taken place in the four locations she was about to visit. She packed her bags, said goodbye to her family, and set off on her journey. + +Her first stop was Location 1, an abandoned mansion on the outskirts of town. As she walked through the dusty halls, she stumbled upon a cryptic message scrawled on the wall: "The eyes that see are not the ones that look." Emily was intrigued but couldn't decipher the meaning. + +From there, she visited Location 2, an old bookstore in the city. While browsing through the shelves, she met the owner, an eccentric old man who told her about a mysterious book that was said to hold the key to the disappearances. Emily was skeptical, but the old man's words lingered in her mind. + +Next, she traveled to Location 3, a secluded monastery in the mountains. The monks welcomed her warmly and shared stories of a long-forgotten event that had taken place in the monastery's past. Emily sensed that there was more to the story, but the monks were tight-lipped about the details. + +Finally, she arrived at Location 4, a small island with a reputation for being cursed. As she explored the island, she stumbled upon a journal belonging to one of the disappeared individuals. The journal hinted at a dark secret, but the writer's words were cryptic and unclear. + +However, Emily's journey didn't end there. She felt that she had only scratched the surface of the mystery, and so she decided to revisit each location, hoping to uncover more clues. + +Upon her second visit to Location 1, Emily discovered a hidden room deep within the mansion's basement. Inside, she found a series of letters exchanged between the mansion's former occupants, revealing a dark family secret that seemed to be connected to the disappearances. + +Her second visit to Location 2 led her to a hidden compartment in the bookstore's shelves, where she found the mysterious book the old man had told her about. As she flipped through its pages, she realized that the book was more than just a simple novel – it was a guide to unlocking the secrets of the disappearances. + +During her second visit to Location 3, Emily managed to gain the trust of one of the monks, who shared with her the true story of the monastery's past. The monk's words revealed a shocking connection between the monastery and the disappearances, and Emily began to piece together the puzzle. + +Finally, her second visit to Location 4 took her to a hidden cave on the island, where she found a series of cryptic messages etched into the walls. The messages seemed to be a code, and Emily realized that they were the key to unlocking the secrets of the journal she had found during her first visit. + +As Emily returned home, she began to piece together the events of her journey. She realized that the disappearances were not random events, but rather, they were all connected. The message on the wall in Location 1 was a warning, the book in Location 2 held a vital clue, the monastery's past was crucial to understanding the present, and the journal on the island was the key to unlocking the mystery. + +However, what Emily didn't know was that her journey was not just about uncovering the truth about the disappearances. Unbeknownst to her, a series of hidden events had been set in motion, influencing her actions and guiding her towards a specific outcome. These events, though invisible to Emily, were the true driving force behind the story. + +The story structure remains non-linear, with the narrative timeline presenting the story events out of chronological order. The actual timeline shows Emily visiting each location twice, with the second visit revealing more clues and pieces of the puzzle. The hidden timeline continues to represent the unseen forces that shape the story, influencing Emily's actions and guiding her towards a specific outcome. + +Here's an updated graph to reflect the changes: + +graph TD +subgraph "Actual Timeline" +A1[Location 1 - First Visit] +A2[Location 2 - First Visit] +A3[Location 3 - First Visit] +A4[Location 4 - First Visit] +A5[Location 1 - Second Visit] +A6[Location 2 - Second Visit] +A7[Location 3 - Second Visit] +A8[Location 4 - Second Visit] +A1 --> A2 --> A3 --> A4 --> A5 --> A6 --> A7 --> A8 +end +subgraph "Narrative Timeline" +N1[Location 2 - Story Event 1] +N2[Location 4 - Story Event 2] +N3[Location 1 - Story Event 3] +N4[Location 3 - Story Event 4] +N5[Location 1 - Story Event 5] +N +""", + +"""mistral-large-2407 + +Story: The Unraveling of Time +Actual Timeline +In the quaint town of Harborside, four locations form the backbone of daily life: the Library (Location 1), the Market (Location 2), the Park (Location 3), and the Lighthouse (Location 4). The story unfolds as the protagonist, Emily, visits these locations in sequence. + +Location 1 - First Visit (Library): Emily starts her day at the library, researching old town records. +Location 2 - First Visit (Market): She then heads to the market, picking up fresh produce and chatting with the locals. +Location 3 - First Visit (Park): After lunch, Emily takes a leisurely stroll in the park, enjoying the serene environment. +Location 4 - First Visit (Lighthouse): Finally, she visits the lighthouse, where she meets the old lighthouse keeper who tells her stories about the town's past. +Narrative Timeline +The story, however, is not told in the sequence of Emily's visits. Instead, it jumps between events in a non-linear fashion, creating a sense of mystery and intrigue. + +Location 2 - Story Event 1 (Market): The story begins with a mysterious theft at the market, where Emily witnesses a valuable artifact being stolen. +Location 4 - Story Event 2 (Lighthouse): The narrative then shifts to the lighthouse, where Emily discovers hidden letters that hint at a decades-old secret. +Location 1 - Story Event 3 (Library): The story then flashes back to Emily's visit to the library, where she uncovers ancient documents that reveal a dark history of the town. +Location 3 - Story Event 4 (Park): The climax of the story takes place in the park, where Emily confronts the thief and unveils the truth behind the town's secrets. +Hidden Timeline +Underlying the visible events is a hidden timeline that influences the narrative without being directly visible. These hidden events represent the dark history of the town, hinted at but not fully revealed until the end. + +Hidden Event 1: A series of unexplained disappearances that occurred decades ago. +Hidden Event 2: A forgotten curse placed on the town by a disgruntled resident. +Hidden Event 3: A secret society formed to protect the town's dark secrets. +Hidden Event 4: A powerful artifact that holds the key to understanding the town's past. +Explanation of the Structure +The structure of the story is represented by the graph, which consists of three timelines: + +Actual Timeline: This represents the chronological order of Emily's visits to the locations. It shows the sequence of events as they actually happened. +Narrative Timeline: This represents the order in which the story is told. The events are not presented in chronological order but are arranged to build suspense and reveal information gradually. +Hidden Timeline: This represents the underlying events that influence the story but are not directly visible. These hidden events provide depth and complexity to the narrative. +The connections between the timelines (indicated by dashed lines) show how the actual events are linked to the narrative events and how the hidden events influence the overall story. This non-linear structure allows for a more engaging and mysterious narrative, keeping the reader intrigued until the final revelation. + + + +Very Good Attempt. Lets see how your story will change under the assumption that each location is visited twice + + + +Story: The Unraveling of Time (Revisited) +Actual Timeline +In the quaint town of Harborside, four locations form the backbone of daily life: the Library (Location 1), the Market (Location 2), the Park (Location 3), and the Lighthouse (Location 4). The story unfolds as the protagonist, Emily, visits these locations in sequence, but this time, she visits each location twice. + +Location 1 - First Visit (Library): Emily starts her day at the library, researching old town records. +Location 2 - First Visit (Market): She then heads to the market, picking up fresh produce and chatting with the locals. +Location 3 - First Visit (Park): After lunch, Emily takes a leisurely stroll in the park, enjoying the serene environment. +Location 4 - First Visit (Lighthouse): Finally, she visits the lighthouse, where she meets the old lighthouse keeper who tells her stories about the town's past. +Location 1 - Second Visit (Library): Emily returns to the library, this time finding a hidden compartment in an old book. +Location 2 - Second Visit (Market): She revisits the market and notices a strange symbol carved into one of the stalls. +Location 3 - Second Visit (Park): Emily returns to the park and discovers a hidden underground tunnel. +Location 4 - Second Visit (Lighthouse): She goes back to the lighthouse and finds a secret room containing old photographs and letters. +Narrative Timeline +The story is not told in the sequence of Emily's visits. Instead, it jumps between events in a non-linear fashion, creating a sense of mystery and intrigue. + +Location 2 - Story Event 1 (Market): The story begins with a mysterious theft at the market, where Emily witnesses a valuable artifact being stolen. +Location 4 - Story Event 2 (Lighthouse): The narrative then shifts to the lighthouse, where Emily discovers hidden letters that hint at a decades-old secret. +Location 1 - Story Event 3 (Library): The story then flashes back to Emily's visit to the library, where she uncovers ancient documents that reveal a dark history of the town. +Location 3 - Story Event 4 (Park): The climax of the story takes place in the park, where Emily confronts the thief and unveils the truth behind the town's secrets. +Location 1 - Story Event 5 (Library): Emily finds a hidden compartment in an old book, leading her to a cryptic map. +Location 2 - Story Event 6 (Market): She notices a strange symbol carved into one of the stalls, matching the symbol on the map. +Location 3 - Story Event 7 (Park): Emily discovers a hidden underground tunnel, which she believes is connected to the town's dark past. +Location 4 - Story Event 8 (Lighthouse): She finds a secret room containing old photographs and letters that provide the final pieces of the puzzle. +Hidden Timeline +Underlying the visible events is a hidden timeline that influences the narrative without being directly visible. These hidden events represent the dark history of the town, hinted at but not fully revealed until the end. + +Hidden Event 1: A series of unexplained disappearances that occurred decades ago. +Hidden Event 2: A forgotten curse placed on the town by a disgruntled resident. +Hidden Event 3: A secret society formed to protect the town's dark secrets. +Hidden Event 4: A powerful artifact that holds the key to understanding the town's past. +Hidden Event 5: The existence of a hidden map that leads to a secret underground chamber. +Hidden Event 6: The symbol that marks the entrance to the hidden chamber. +Hidden Event 7: The underground tunnel that connects all the locations. +Hidden Event 8: The secret room in the lighthouse that contains the final clues. +Explanation of the Structure +The structure of the story is represented by the graph, which consists of three timelines: + +Actual Timeline: This represents the chronological order +""", + +"""gpt-4o-mini-2024-07-18 + +Title: The Hidden Threads of Time + +Story: +In the quiet town of Eldridge, four locations held the secrets of a past intertwined with the present: The Old Oak Library, The Riverside Café, The Abandoned Mill, and The Clock Tower. Over a single summer, Sarah, a curious young woman, embarked on a journey that would reveal the layers of time hidden beneath the surface of her seemingly mundane life. + +Actual Timeline: A Journey Begins +Location 1: The Old Oak Library (A1) +Sarah’s adventure began at the Old Oak Library. As she browsed through the dusty shelves, she stumbled upon an ancient book that spoke of the town’s lore and the hidden events that shaped its history. Intrigued, she decided to explore more. + +Location 2: The Riverside Café (A2) +Her next stop was the Riverside Café, where she overheard a conversation between two elderly patrons discussing a tragic event tied to the town's history. They spoke in hushed tones about a long-lost love and a betrayal that had forever altered the lives of those involved. + +Location 3: The Abandoned Mill (A3) +The following day, Sarah ventured to the Abandoned Mill. There, she discovered remnants of a past life: photographs, letters, and a diary that hinted at a hidden romance. She felt a strange connection to the stories within these walls. + +Location 4: The Clock Tower (A4) +Finally, she arrived at the Clock Tower, where she learned about a secret society that once met there to protect the town’s secrets. This location felt alive with energy, as if the whispers of the past were echoing through time. + +Narrative Timeline: Unraveling the Mystery +Story Event 1: The Riverside Café (N1) +At the café, Sarah confronted the two patrons about their conversation. She learned that the tragic love story involved her great-grandparents, whose destinies were forever altered by a choice made in that very café. + +Story Event 2: The Clock Tower (N2) +Back at the Clock Tower, she discovered a hidden room filled with documents that detailed the secret society's attempts to mend the heartbreak caused by her ancestors. Here, she found a letter addressed to her great-grandmother, containing a heartfelt apology from her great-grandfather. + +Story Event 3: The Old Oak Library (N3) +Returning to the library, Sarah pieced together her family’s history through the ancient book. It revealed that the love story had been a catalyst for change in Eldridge, leading to the establishment of the society that aimed to protect the town from repeating past mistakes. + +Story Event 4: The Abandoned Mill (N4) +In the mill, Sarah re-read the diary and uncovered the truth: the betrayal was a misunderstanding fueled by jealousy and miscommunication. Understanding this allowed Sarah to forgive her ancestors and break the cycle of pain that had lingered in her family for generations. + +Hidden Timeline: The Secrets Beneath +Throughout her journey, Sarah was unaware of the Hidden Events that had shaped her family’s history and the town of Eldridge: + +Hidden Event 1 (H1): A Betrayal +Long ago, a betrayal occurred that led to the rift between Sarah’s great-grandparents. This event was the catalyst for the tragic love story that would echo through time. + +Hidden Event 2 (H2): The Secret Society's Formation +In the wake of the betrayal, a secret society formed to protect the town from the repercussions of the past, striving to mend what was broken. + +Hidden Event 3 (H3): The Lost Letter +A letter written by Sarah’s great-grandfather remained hidden for decades, containing apologies and hopes for reconciliation that were never delivered. + +Hidden Event 4 (H4): The Town’s Lore +The stories of Eldridge were carefully curated, with certain truths obscured to protect the living from the pain of the past. + +Structure Explanation: +The story’s structure represents three distinct timelines—Actual Timeline, Narrative Timeline, and Hidden Timeline—which interact in complex ways: + +Actual Timeline: This is the chronological order of Sarah’s visits to the four locations, marking her physical journey through the town. It serves as the backbone of the story, grounding it in reality and providing a structure for her exploration. +Narrative Timeline: This timeline unfolds the key story events that arise from Sarah’s discoveries at each location. Unlike the Actual Timeline, the Narrative Timeline does not follow a linear path but instead reveals the emotional and thematic significance of her journey, highlighting the connections between past and present. +Hidden Timeline: This timeline consists of events and truths that remain obscured until Sarah unearths them through her exploration. The hidden events symbolize the burdens of history that affect the present, illustrating how the past can shape one’s +""", + +"""athene-70b + +Story: The Cartographer's Enigma + +Protagonist Emilia, a skilled cartographer, receives a cryptic atlas from her reclusive mentor, featuring four locations: Ravenshire, Luminaria, Celestia's Peak, and Argentum Falls. The atlas hints at a centuries-old conspiracy, weaving together seemingly unrelated events across these sites. + +Narrative Timeline: + +Luminaria - The Celestial Alignment (N1): Emilia discovers an ancient celestial map etched into Luminaria's observatory walls, forecasting a rare alignment that unlocks a hidden chamber beneath Argentum Falls (A4). +Argentum Falls - The Forgotten Treaty (N2): Within the chamber, Emilia uncovers a long-lost treaty between ancient nations, which was negotiated at Ravenshire (A1) and ratified at Celestia's Peak (A3). +Ravenshire - The Cartographer's Legacy (N3): Emilia finds her mentor's hidden journals in Ravenshire, detailing the atlas's creation and the significance of Celestia's Peak, where an ancient order once protected the treaty. +Celestia's Peak - The Order's Reckoning (N4): At the peak, Emilia confronts a descendant of the ancient order, who reveals that they had been safeguarding the treaty against rogue factions. The descendant hands over a crucial, annotated atlas page, exposing the hidden events. +Actual Timeline (Emilia's Journey): + +Emilia visits the locations in the following order: Ravenshire (A1), Luminaria (A2), Celestia's Peak (A3), and Argentum Falls (A4). Each location builds upon her understanding, though the narrative jumps between them to create suspense and reveal connections. + +Hidden Timeline (Influencing Events): + +Hidden Event 1: A centuries-old astronomer in Luminaria (H1) calculates the celestial alignment, inspiring the treaty. +Hidden Event 2: A rogue faction infiltrates the ancient order at Celestia's Peak (H2), seeding corruption. +Hidden Event 3: Emilia's mentor, aware of the conspiracy, crafts the atlas (H3) to guide a worthy successor. +Hidden Event 4: The ancient order's remnants secretly aid Emilia's journey (H4), ensuring her discovery of the truth. +Structure Representation: + +Non-linear narrative: The narrative timeline (N1-N4) rearranges Emilia's actual visits (A1-A4) to build tension, reveal connections, and create an engaging mystery. +Hidden influences: The hidden events (H1-H4) permeate the actual timeline, motivating character actions, and informing the narrative's twists. Dotted lines illustrate how these events subtly shape Emilia's journey, enriching the story's depth and complexity. +Interconnectedness: The graph visually demonstrates how each location, narrative beat, and hidden event intertwines, producing a rich, layered narrative that rewards close attention and reflection. As Emilia unravels the enigma, the reader experiences a satisfying convergence of threads, highlighting the power of deliberate, non-linear storytelling. +""" + +) + +WFStage2prompt = """Please use the below config as a template to write a story in config format (dont use the template values like masterlocation1): + +{ + "masterlocation1": { + "location1": { + "description": "UI - Experience Bar", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": [], + "developernotes": [] + }, + + eg. {"tavern": {"start": {"description": "You wake up in a dimly lit tavern, the smell of ale and salt heavy in the air. A grizzled sailor at the bar boasts about a hidden island overflowing with \"more gold than ye can dream of!\" ", "choices": ["approach sailor", "ignore sailor", "order grog"], "transitions": {"approach sailor": "tavern_rumor", "ignore sailor": "tavern_start", "order grog": "tavern_grog"}, "consequences": {}, "media": []}, "rumor": {"description": "The sailor, three sheets to the wind, spins a tale of treacherous waters, ancient curses, and a map hidden on a ghost ship. He claims to know the way.", "choices": ["offer to buy him another drink", "dismiss him as a drunkard"], "transitions": {"offer to buy him another drink": "tavern_map", "dismiss him as a drunkard": "tavern_start"}, "consequences": {}, "media": []}, "grog": {"description": "The tavern keeper slides you a tankard of potent grog. It burns pleasantly as it goes down, and you feel your senses dull.", "choices": ["order another", "eavesdrop on nearby pirates"], "transitions": {"order another": "tavern_grog", "eavesdrop on nearby pirates": "tavern_start"}, "media": []}, "map": {"description": "With a wink and a hiccup, the sailor produces a crumpled piece of parchment. It's stained with salt and what looks suspiciously like blood, but the markings... they resemble a map! ", "choices": ["propose a partnership", "try to buy the map", "steal the map"], "transitions": {"propose a partnership": "docks_prepare", "try to buy the map": "tavern_haggle", "steal the map": "tavern_fight"}, "consequences": {}, "media": []}, "haggle": {"description": "The sailor eyes you shrewdly. \"This here map's worth a king's ransom,\" he slurs.", "choices": ["offer a pouch of gold", "walk away"], "transitions": {"offer a pouch of gold": "docks_prepare", "walk away": "tavern_start"}, "consequences": {}, "media": []}, "fight": {"description": "You lunge for the map, but the sailor's quicker than he looks! A brawl erupts, tables are overturned, and mugs fly through the air.", "choices": ["fight dirty", "try to reason with him"], "transitions": {"fight dirty": "tavern_victory", "try to reason with him": "tavern_defeat"}, "consequences": {}, "media": []}, "victory": {"description": "With a final blow, you knock the sailor unconscious. You grab the map and make a hasty exit.", "choices": ["head to the docks"], "transitions": {"head to the docks": "docks_prepare"}, "consequences": {}, "media": []}, "defeat": {"description": "The sailor and a couple of his cronies land some solid punches. You're tossed out of the tavern, bruised and map-less.", "choices": ["nurse your wounds"], "transitions": {"nurse your wounds": "tavern_start"}, "consequences": {}, "media": []}}, "docks": {"prepare": {"description": "The salty air of the docks fills your lungs. You can practically taste the adventure on the horizon. But first, you need a ship and a crew...", "choices": ["find a ship", "assemble a crew"], "transitions": {"find a ship": "docks_ship", "assemble a crew": "docks_crew"}, "media": []}, "ship": {"description": "You spot a sturdy-looking galleon with a \"For Sale\" sign hanging precariously from its mast.", "choices": ["inspect the ship", "look for another vessel"], "transitions": {"inspect the ship": "docks_captain", "look for another vessel": "docks_prepare"}, "media": []}, "crew": {"description": "The docks are teeming with salty dogs of all shapes and sizes.", "choices": ["recruit seasoned sailors", "round up some desperate souls"], "transitions": {"recruit seasoned sailors": "docks_captain", "round up some desperate souls": "docks_captain"}, "consequences": {}, "media": []}, "captain": {"description": "With a ship and a crew (of varying competence), you're ready to set sail! ", "choices": ["set sail for the open sea!"], "transitions": {"set sail for the open sea!": "openSea_start"}, "media": []}}, "openSea": {"start": {"description": "The open sea stretches before you, vast and unforgiving. The wind whips at your sails as you set course for the uncharted waters where the treasure awaits.", "choices": ["consult the map", "enjoy the voyage"], "transitions": {"consult the map": "openSea_storm", "enjoy the voyage": "openSea_mutiny"}, "media": []}, "storm": {"description": "A storm gathers on the horizon, dark clouds swirling ominously. Waves crash against the hull, threatening to swallow you whole.", "choices": ["weather the storm", "change course"], "transitions": {"weather the storm": "openSea_ghostShip", "change course": "openSea_start"}, "media": []}, "mutiny": {"description": "Your crew, a motley bunch at best, begin to grumble. They're growing impatient and greedy, their eyes glinting with mutiny.", "choices": ["quell the mutiny", "reason with them"], "transitions": {"quell the mutiny": "openSea_ghostShip", "reason with them": "openSea_start"}, "consequences": {}, "media": []}, "ghostShip": {"description": "Through the mist and fog, a ghostly silhouette emerges. A ship, its sails in tatters, manned by skeletal figures. This... is the ghost ship.", "choices": ["board the ghost ship", "flee"], "transitions": {"board the ghost ship": "ghostShip_deck", "flee": "openSea_start"}, "media": []}}, "ghostShip": {"deck": {"description": "You step aboard the ghost ship, the air heavy with the stench of decay. Eerie silence hangs over the vessel.", "choices": ["explore the ship", "search for the captain's quarters"], "transitions": {"explore the ship": "ghostShip_brig", "search for the captain's quarters": "ghostShip_quarters"}, "media": []}, "brig": {"description": "You stumble upon the ship's brig, its cells still occupied by skeletal remains shackled to the walls.", "choices": ["examine the skeletons", "leave the brig"], "transitions": {"examine the skeletons": "ghostShip_clue", "leave the brig": "ghostShip_deck"}, "media": []}, "quarters": {"description": "The captain's quarters are in disarray, as if a struggle took place. A tattered journal lies open on the floor.", "choices": ["read the journal", "search for the map"], "transitions": {"read the journal": "ghostShip_clue", "search for the map": "ghostShip_treasure"}, "media": []}, "clue": {"description": "You piece together clues from the journal and the skeletons. The ghost ship's captain hid the real treasure map to protect it from mutineers.", "choices": ["continue searching"], "transitions": {"continue searching": "ghostShip_treasure"}, "media": []}, "treasure": {"description": "After careful searching, you find a hidden compartment behind a portrait. Inside, a gleaming treasure chest awaits!", "choices": ["open the chest", "take the chest and flee"], "transitions": {"open the chest": "ghostShip_victory", "take the chest and flee": "openSea_return"}, "consequences": {}, "media": []}, "victory": {"description": "The treasure chest overflows with gold, jewels, and artifacts beyond your wildest dreams! You've found the legendary pirate treasure!", "choices": ["claim your prize and set sail"], "transitions": {"claim your prize and set sail": "openSea_return"}, "consequences": {}, "media": []}}, "openSea_return": {"return": {"description": "With the treasure safely stowed on your own ship, you set sail for home, leaving the ghost ship to its eternal slumber.", "choices": ["sail into the sunset"], "transitions": {"sail into the sunset": "end_end"}, "media": []}}, "end": {"end": {"description": "The sun dips below the horizon, painting the sky in hues of gold and crimson. You've cheated death, outsmarted ghosts, and claimed a fortune in pirate treasure. Your legend will be sung in taverns for centuries to come.", "choices": [], "transitions": {}, "media": []}}} + +The user will provide a theme +""" + +WFStage2prompt2 = """Please use the below config as a template to write a story in config format (dont use the template values like masterlocation1): + +{ + "masterlocation1": { + "location1": { + "description": "UI - Experience Bar", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": [], + "developernotes": [] + }, + + There should be 3 master and 10 total sublocations. + The theme is provided by the user +""" + + +MoreRobustWFStage2prompt = """We need to brainstorm how to fit this shape into into JSON format I will provide below. + +Shape = Use this to write a story and explain how it represents the structure - + +graph TD +subgraph "Actual Timeline" +A1[Location 1 - First Visit] +A2[Location 2 - First Visit] +A3[Location 3 - First Visit] +A4[Location 4 - First Visit] +A1 --> A2 --> A3 --> A4 +end +subgraph "Narrative Timeline" +N1[Location 2 - Story Event 1] +N2[Location 4 - Story Event 2] +N3[Location 1 - Story Event 3] +N4[Location 3 - Story Event 4] +N1 --> N2 --> N3 --> N4 +end +subgraph "Hidden Timeline" +H1[Hidden Event 1] +H2[Hidden Event 2] +H3[Hidden Event 3] +H4[Hidden Event 4] +end +A1 -.-> N3 +A2 -.-> N1 +A3 -.-> N4 +A4 -.-> N2 +H1 -.- A1 +H1 -.- A2 +H1 -.- A3 +H1 -.- A4 +H2 -.- A1 +H2 -.- A2 +H2 -.- A3 +H2 -.- A4 +H3 -.- A1 +H3 -.- A2 +H3 -.- A3 +H3 -.- A4 +H4 -.- A1 +H4 -.- A2 +H4 -.- A3 +H4 -.- A4 + + +JSON format = Please use the below config as a template to write a story in config format (dont use the template values like masterlocation1): + +{ + "masterlocation1": { + "location1": { + "description": "UI - Experience Bar", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": [], + "developernotes": [] + }, + + +Loading Code (Dont make media for now) (Avoid Syntax errors when writing the config so the parser loads it) = +class GameSession: + def __init__(self, starting_location='village', starting_state='start'): + self.player = Player() + self.current_location = starting_location + self.current_state = starting_state + self.game_log = [] + + def make_choice(self, choice_index): + state = all_states[self.current_location][self.current_state] + if 0 <= choice_index < len(state['choices']): + choice = state['choices'][choice_index] + next_state = state['transitions'][choice] + + self.game_log.append(f"You chose: {choice}") + self.game_log.append(state['description']) + + if 'consequences' in state and choice in state['consequences']: + if state['consequences'][choice]: + state['consequences'][choice](self.player) + else: + # Handle empty consequence, e.g., log a message or provide a default action + print(f"No consequence for choice: {choice}") + # You can add any default action here if needed + + if '_' in next_state: + self.current_location, self.current_state = next_state.split('_') + else: + self.current_state = next_state + + return self.get_current_state_info() + else: + return "Invalid choice. Please try again." + + def get_current_state_info(self): + state = all_states[self.current_location][self.current_state] + choices = [f"{idx + 1}. {choice}" for idx, choice in enumerate(state['choices'])] + return state['description'], choices, "\n".join(self.game_log) + + def get_current_state_media(self): + media = all_states[self.current_location][self.current_state]['media'] + return media +""" + + +MoreRobustWFStage2promptwithExample = """ +Below is a config based on the mermaid below as a template - You must make it more of a game experience for the user + +{ + "actualtimeline": { + "A1": { + "description": "You arrive at the Windfall Orchard. Soft breezes carry whispers of hidden stories.", + "events": [ + "arrived" + ], + "choices": [ + "continue actual", + "explore orchard story (N3)", + "investigate orchard hidden (H1)" + ], + "transitions": { + "continue actual": "actualtimeline_A2", + "explore orchard story (N3)": "narrativetimeline_N3", + "investigate orchard hidden (H1)": "hiddentimeline_H1" + }, + "media": [], + "developernotes": [ + "Location 1 in Actual Timeline", + "Connects to Narrative N3 and Hidden H1" + ] + }, + "A2": { + "description": "You enter the Misty Bazaar, abuzz with trade and secrets.", + "events": [ + "traveled", + "observed" + ], + "choices": [ + "continue actual", + "uncover bazaar story (N1)", + "probe bazaar hidden (H2)" + ], + "transitions": { + "continue actual": "actualtimeline_A3", + "uncover bazaar story (N1)": "narrativetimeline_N1", + "probe bazaar hidden (H2)": "hiddentimeline_H2" + }, + "media": [], + "developernotes": [ + "Location 2 in Actual Timeline", + "Connects to Narrative N1 and Hidden H2" + ] + }, + "A3": { + "description": "You stand upon the Northern Cliffs, where distant waves roar with mystery.", + "events": [ + "arrived", + "reflected" + ], + "choices": [ + "continue actual", + "witness cliffside story (N4)", + "examine cliffside hidden (H3)" + ], + "transitions": { + "continue actual": "actualtimeline_A4", + "witness cliffside story (N4)": "narrativetimeline_N4", + "examine cliffside hidden (H3)": "hiddentimeline_H3" + }, + "media": [], + "developernotes": [ + "Location 3 in Actual Timeline", + "Connects to Narrative N4 and Hidden H3" + ] + }, + "A4": { + "description": "At last, you reach the Heartwood Temple. An otherworldly calm envelops you.", + "events": [ + "approached", + "pondered" + ], + "choices": [ + "loop actual", + "unearth temple story (N2)", + "search temple hidden (H4)" + ], + "transitions": { + "loop actual": "actualtimeline_A4", + "unearth temple story (N2)": "narrativetimeline_N2", + "search temple hidden (H4)": "hiddentimeline_H4" + }, + "media": [], + "developernotes": [ + "Location 4 in Actual Timeline", + "Connects to Narrative N2 and Hidden H4", + "Loops to itself for demonstration but typically you'd end or branch elsewhere" + ] + } + }, + "narrativetimeline": { + "N1": { + "description": "Narrative Event 1: At the bazaar, you hear a whispered rumor that sparks your quest.", + "events": [ + "heard", + "questioned" + ], + "choices": [ + "continue narrative", + "return to actual A2", + "peek hidden (H2)" + ], + "transitions": { + "continue narrative": "narrativetimeline_N2", + "return to actual A2": "actualtimeline_A2", + "peek hidden (H2)": "hiddentimeline_H2" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A2" + ] + }, + "N2": { + "description": "Narrative Event 2: At the temple, an important secret is revealed.", + "events": [ + "uncovered" + ], + "choices": [ + "continue narrative", + "return to actual A4", + "peek hidden (H4)" + ], + "transitions": { + "continue narrative": "narrativetimeline_N3", + "return to actual A4": "actualtimeline_A4", + "peek hidden (H4)": "hiddentimeline_H4" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A4" + ] + }, + "N3": { + "description": "Narrative Event 3: At the orchard, a forgotten memory emerges from the past.", + "events": [ + "remembered" + ], + "choices": [ + "continue narrative", + "return to actual A1", + "peek hidden (H1)" + ], + "transitions": { + "continue narrative": "narrativetimeline_N4", + "return to actual A1": "actualtimeline_A1", + "peek hidden (H1)": "hiddentimeline_H1" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A1" + ] + }, + "N4": { + "description": "Narrative Event 4: At the cliffside, you discover the final clue of this storyline.", + "events": [ + "realized" + ], + "choices": [ + "finish narrative", + "return to actual A3", + "peek hidden (H3)" + ], + "transitions": { + "finish narrative": "narrativetimeline_N4", + "return to actual A3": "actualtimeline_A3", + "peek hidden (H3)": "hiddentimeline_H3" + }, + "media": [], + "developernotes": [ + "Narrative event linked to Actual A3", + "Finishes or loops for demonstration" + ] + } + }, + "hiddentimeline": { + "H1": { + "description": "Hidden Event 1: A mysterious whisper in the orchard hints at something greater.", + "events": [ + "listened" + ], + "choices": [ + "continue hidden", + "return to A1", + "narrative check (N3)" + ], + "transitions": { + "continue hidden": "hiddentimeline_H2", + "return to A1": "actualtimeline_A1", + "narrative check (N3)": "narrativetimeline_N3" + }, + "media": [], + "developernotes": [ + "Hidden event connected to orchard (A1) and story event (N3)" + ] + }, + "H2": { + "description": "Hidden Event 2: Faint footprints in the bazaar's back alley confirm others are guiding your path.", + "events": [ + "noticed" + ], + "choices": [ + "continue hidden", + "return to A2", + "narrative check (N1)" + ], + "transitions": { + "continue hidden": "hiddentimeline_H3", + "return to A2": "actualtimeline_A2", + "narrative check (N1)": "narrativetimeline_N1" + }, + "media": [], + "developernotes": [ + "Hidden event connected to bazaar (A2) and story event (N1)" + ] + }, + "H3": { + "description": "Hidden Event 3: Carved runes under the cliffs suggest all locations are interwoven.", + "events": [ + "deciphered" + ], + "choices": [ + "continue hidden", + "return to A3", + "narrative check (N4)" + ], + "transitions": { + "continue hidden": "hiddentimeline_H4", + "return to A3": "actualtimeline_A3", + "narrative check (N4)": "narrativetimeline_N4" + }, + "media": [], + "developernotes": [ + "Hidden event connected to cliffs (A3) and story event (N4)" + ] + }, + "H4": { + "description": "Hidden Event 4: A final revelation in the temple ties orchard, bazaar, and cliffs to this hallowed ground.", + "events": [ + "comprehended" + ], + "choices": [ + "close hidden", + "return to A4", + "narrative check (N2)" + ], + "transitions": { + "close hidden": "hiddentimeline_H4", + "return to A4": "actualtimeline_A4", + "narrative check (N2)": "narrativetimeline_N2" + }, + "media": [], + "developernotes": [ + "Hidden event connected to temple (A4) and story event (N2)", + "Loops to itself for demonstration of an end state" + ] + } + } +} + +Shape = Use this to write a story and explain how it represents the structure - + +graph TD +subgraph "Actual Timeline" +A1[Location 1 - First Visit] +A2[Location 2 - First Visit] +A3[Location 3 - First Visit] +A4[Location 4 - First Visit] +A1 --> A2 --> A3 --> A4 +end +subgraph "Narrative Timeline" +N1[Location 2 - Story Event 1] +N2[Location 4 - Story Event 2] +N3[Location 1 - Story Event 3] +N4[Location 3 - Story Event 4] +N1 --> N2 --> N3 --> N4 +end +subgraph "Hidden Timeline" +H1[Hidden Event 1] +H2[Hidden Event 2] +H3[Hidden Event 3] +H4[Hidden Event 4] +end +A1 -.-> N3 +A2 -.-> N1 +A3 -.-> N4 +A4 -.-> N2 +H1 -.- A1 +H1 -.- A2 +H1 -.- A3 +H1 -.- A4 +H2 -.- A1 +H2 -.- A2 +H2 -.- A3 +H2 -.- A4 +H3 -.- A1 +H3 -.- A2 +H3 -.- A3 +H3 -.- A4 +H4 -.- A1 +H4 -.- A2 +H4 -.- A3 +H4 -.- A4 + +------ + +Remember to write in same JSON format after analysing the above so I can test the turn by turn transitions + +""" + +TimeRelatedMermaidStoryAttempttoRefinefromtoJSON = ( +""" +{ + "masterlocation1": { + "location1": { + "description": "The Old Oak Library", + "events": [ + "Sarah discovers an ancient book about the town's lore and hidden history." + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": [], + "developernotes": [ + "This is the starting point of Sarah's journey (A1 in Actual Timeline)" + ] + }, + "location2": { + "description": "The Riverside Café", + "events": [ + "Sarah overhears a conversation about a tragic event in the town's history.", + "She learns about a long-lost love and betrayal." + ], + "choices": [ + "confront patrons", + "continue exploring" + ], + "transitions": { + "confront patrons": "masterlocation2_location1", + "continue exploring": "masterlocation1_location3" + }, + "media": [], + "developernotes": [ + "This location appears in both A2 (Actual Timeline) and N1 (Narrative Timeline)" + ] + }, + "location3": { + "description": "The Abandoned Mill", + "events": [ + "Sarah discovers old photographs, letters, and a diary hinting at a hidden romance." + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location4" + }, + "media": [], + "developernotes": [ + "This location is A3 in the Actual Timeline" + ] + }, + "location4": { + "description": "The Clock Tower", + "events": [ + "Sarah learns about a secret society that protected the town's secrets." + ], + "choices": [ + "investigate further", + "return to library" + ], + "transitions": { + "investigate further": "masterlocation2_location2", + "return to library": "masterlocation2_location3" + }, + "media": [], + "developernotes": [ + "This is A4 in the Actual Timeline and leads to N2 in the Narrative Timeline" + ] + } + }, + "masterlocation2": { + "location1": { + "description": "The Riverside Café - Confrontation", + "events": [ + "Sarah confronts the patrons about their conversation.", + "She learns that the tragic love story involves her great-grandparents." + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation2_location2" + }, + "media": [], + "developernotes": [ + "This is N1 in the Narrative Timeline" + ] + }, + "location2": { + "description": "The Clock Tower - Hidden Room", + "events": [ + "Sarah discovers a hidden room with documents about the secret society.", + "She finds a letter addressed to her great-grandmother from her great-grandfather." + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation2_location3" + }, + "media": [], + "developernotes": [ + "This is N2 in the Narrative Timeline" + ] + }, + "location3": { + "description": "The Old Oak Library - Piecing Together History", + "events": [ + "Sarah connects her family's history to the town's changes.", + "She learns about the establishment of the secret society." + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation2_location4" + }, + "media": [], + "developernotes": [ + "This is N3 in the Narrative Timeline" + ] + }, + "location4": { + "description": "The Abandoned Mill - Uncovering the Truth", + "events": [ + "Sarah re-reads the diary and discovers the truth about the betrayal.", + "She realizes it was a misunderstanding fueled by jealousy and miscommunication." + ], + "choices": [ + "reflect on journey" + ], + "transitions": { + "reflect on journey": "masterlocation3_location1" + }, + "media": [], + "developernotes": [ + "This is N4 in the Narrative Timeline" + ] + } + }, + "masterlocation3": { + "location1": { + "description": "Hidden Timeline Revelations", + "events": [ + "The original betrayal that caused the rift between Sarah's great-grandparents is revealed.", + "The formation of the secret society to protect the town is uncovered.", + "The existence of the lost letter from Sarah's great-grandfather is discovered.", + "The truth about the carefully curated town lore is exposed." + ], + "choices": [ + "conclude journey" + ], + "transitions": { + "conclude journey": "end" + }, + "media": [], + "developernotes": [ + "This represents the Hidden Timeline (H1, H2, H3, H4)", + "These events are not directly experienced but are revealed through Sarah's discoveries" + ] + } + } +} +""", +) + + +#-------------------------#-------------------------#-------------------------#------------------------- + + +MoreRobustTimeline1stPrompt = """Please integrate this story into the format below - + +Story info - + + --- PASTE More Robust JSON here --- + +Format Info - + +JSON format = Please use the below config as a template to write a story in config format (dont use the template values like masterlocation1): + +{ + "masterlocation1": { + "location1": { + "description": "UI - Experience Bar", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": [], + "developernotes": [] + }, + + +Loading Code (Dont make media for now) (Avoid Syntax errors when writing the config so the parser loads it) = +Main error to avoid = Error loading custom configuration: too many values to unpack (expected 2) + +class GameSession: + def __init__(self, starting_location='village', starting_state='start'): + self.player = Player() + self.current_location = starting_location + self.current_state = starting_state + self.game_log = [] + + def make_choice(self, choice_index): + state = all_states[self.current_location][self.current_state] + if 0 <= choice_index < len(state['choices']): + choice = state['choices'][choice_index] + next_state = state['transitions'][choice] + + self.game_log.append(f"You chose: {choice}") + self.game_log.append(state['description']) + + if 'consequences' in state and choice in state['consequences']: + if state['consequences'][choice]: + state['consequences'][choice](self.player) + else: + # Handle empty consequence, e.g., log a message or provide a default action + print(f"No consequence for choice: {choice}") + # You can add any default action here if needed + + if '_' in next_state: + self.current_location, self.current_state = next_state.split('_') + else: + self.current_state = next_state + + return self.get_current_state_info() + else: + return "Invalid choice. Please try again." + + def get_current_state_info(self): + state = all_states[self.current_location][self.current_state] + choices = [f"{idx + 1}. {choice}" for idx, choice in enumerate(state['choices'])] + return state['description'], choices, " +".join(self.game_log) + + def get_current_state_media(self): + media = all_states[self.current_location][self.current_state]['media'] + return media +""" + +Outputof1stMoreRobustJSONcreator = """{ + "steps": [ + { + "step_number": 1, + "location": "The Fiery Caverns Beneath a Sleeping Volcano", + "characters": [ + "Orin", + "Voss" + ], + "interpersonal_event": "They part on bitter terms, trust erodes further", + "relationship_change": -2, + "modification_to_move_forward": "Resolve a longstanding family feud", + "third_party_role": "A beggar listening quietly in the shadows", + "narrative_text": "Step 1: In The Fiery Caverns Beneath a Sleeping Volcano, Orin and Voss experience the following: They part on bitter terms, trust erodes further. Observing from the sidelines: A beggar listening quietly in the shadows. They then proceed to: Resolve a longstanding family feud." + }, + { + "step_number": 2, + "location": "A Haunted Marsh Shrouded in Fog", + "characters": [ + "Orin", + "Voss" + ], + "interpersonal_event": "A surprising confession that changes everything", + "relationship_change": 1, + "modification_to_move_forward": "Recover from a crippling setback", + "third_party_role": "A beggar listening quietly in the shadows", + "narrative_text": "Step 2: In A Haunted Marsh Shrouded in Fog, Orin and Voss experience the following: A surprising confession that changes everything. Observing from the sidelines: A beggar listening quietly in the shadows. They then proceed to: Recover from a crippling setback." + }, + { + "step_number": 3, + "location": "A Remote Mountain Village", + "characters": [ + "Tali", + "Voss" + ], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1, + "modification_to_move_forward": "Escape a dangerous situation unscathed", + "third_party_role": "A curious child fascinated by adventurers", + "narrative_text": "Step 3: In A Remote Mountain Village, Tali and Voss experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: A curious child fascinated by adventurers. They then proceed to: Escape a dangerous situation unscathed." + }, + { + "step_number": 4, + "location": "The Secret Hideout Beneath a Ruined Fort", + "characters": [ + "Nyx", + "Orin" + ], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1, + "modification_to_move_forward": "Acquire a powerful ancient artifact", + "third_party_role": "An inquisitive guard, always seeking the truth", + "narrative_text": "Step 4: In The Secret Hideout Beneath a Ruined Fort, Nyx and Orin experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: An inquisitive guard, always seeking the truth. They then proceed to: Acquire a powerful ancient artifact." + }, + { + "step_number": 5, + "location": "The Fiery Caverns Beneath a Sleeping Volcano", + "characters": [ + "Orin", + "Tali" + ], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1, + "modification_to_move_forward": "Resolve a longstanding family feud", + "third_party_role": "A local gossip eager to spread rumors", + "narrative_text": "Step 5: In The Fiery Caverns Beneath a Sleeping Volcano, Orin and Tali experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: A local gossip eager to spread rumors. They then proceed to: Resolve a longstanding family feud." + } + ], + "relationships": { + "Orin|Voss": -1, + "Tali|Voss": 1, + "Nyx|Orin": 1, + "Orin|Tali": 1 + }, + "chosen_ending": "A New Era Dawns with Cautious Hope" +} + +""" + +Outputof1stMoreRobustPrompt = """{ + "FieryCaverns": { + "step1": { + "description": "Step 1: In The Fiery Caverns Beneath a Sleeping Volcano, Orin and Voss experience the following: They part on bitter terms, trust erodes further. Observing from the sidelines: A beggar listening quietly in the shadows. They then proceed to: Resolve a longstanding family feud.", + "events": [ + { + "characters": ["Orin", "Voss"], + "interpersonal_event": "They part on bitter terms, trust erodes further", + "relationship_change": -2 + }, + { + "third_party_role": "A beggar listening quietly in the shadows" + } + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "HauntedMarsh_step2" + }, + "media": [], + "developernotes": [ + "modification_to_move_forward: Resolve a longstanding family feud" + ] + }, + "step5": { + "description": "Step 5: In The Fiery Caverns Beneath a Sleeping Volcano, Orin and Tali experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: A local gossip eager to spread rumors. They then proceed to: Resolve a longstanding family feud.", + "events": [ + { + "characters": ["Orin", "Tali"], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1 + }, + { + "third_party_role": "A local gossip eager to spread rumors" + } + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "Ending_final" + }, + "media": [], + "developernotes": [ + "modification_to_move_forward: Resolve a longstanding family feud" + ] + } + }, + "HauntedMarsh": { + "step2": { + "description": "Step 2: In A Haunted Marsh Shrouded in Fog, Orin and Voss experience the following: A surprising confession that changes everything. Observing from the sidelines: A beggar listening quietly in the shadows. They then proceed to: Recover from a crippling setback.", + "events": [ + { + "characters": ["Orin", "Voss"], + "interpersonal_event": "A surprising confession that changes everything", + "relationship_change": 1 + }, + { + "third_party_role": "A beggar listening quietly in the shadows" + } + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "RemoteMountainVillage_step3" + }, + "media": [], + "developernotes": [ + "modification_to_move_forward: Recover from a crippling setback" + ] + } + }, + "RemoteMountainVillage": { + "step3": { + "description": "Step 3: In A Remote Mountain Village, Tali and Voss experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: A curious child fascinated by adventurers. They then proceed to: Escape a dangerous situation unscathed.", + "events": [ + { + "characters": ["Tali", "Voss"], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1 + }, + { + "third_party_role": "A curious child fascinated by adventurers" + } + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "SecretHideout_step4" + }, + "media": [], + "developernotes": [ + "modification_to_move_forward: Escape a dangerous situation unscathed" + ] + } + }, + "SecretHideout": { + "step4": { + "description": "Step 4: In The Secret Hideout Beneath a Ruined Fort, Nyx and Orin experience the following: They forge a temporary alliance for mutual gain. Observing from the sidelines: An inquisitive guard, always seeking the truth. They then proceed to: Acquire a powerful ancient artifact.", + "events": [ + { + "characters": ["Nyx", "Orin"], + "interpersonal_event": "They forge a temporary alliance for mutual gain", + "relationship_change": 1 + }, + { + "third_party_role": "An inquisitive guard, always seeking the truth" + } + ], + "choices": [ + "continue" + ], + "transitions": { + "continue": "FieryCaverns_step5" + }, + "media": [], + "developernotes": [ + "modification_to_move_forward: Acquire a powerful ancient artifact" + ] + } + }, + "Ending": { + "final": { + "description": "A New Era Dawns with Cautious Hope", + "events": [], + "choices": [], + "transitions": {}, + "media": [], + "developernotes": [ + "Final relationships:", + "Orin|Voss = -1", + "Tali|Voss = 1", + "Nyx|Orin = 1", + "Orin|Tali = 1" + ] + } + } +} + +""" + +OutputMoreRobustTimeline2ndPrompt = """Here are some ideas and best practices for making your story more believable and immersive—and then weaving in meaningful, player-driven activities at each location: + +--- + +## 1. Deepen Character Motivations + +1. **Flesh Out Personal Motivations** + - Each major character (Orin, Voss, Tali, Nyx) should have something they stand to gain or lose at each location. For instance, maybe Voss is haunted by a personal failure at the Haunted Marsh, so a “surprising confession” ties directly into that trauma. + - Even side characters (like the beggar, gossip, inquisitive guard, etc.) can have small yet relevant reasons for being present that feed into the world’s coherence. + +2. **Connect Past Conflicts and Future Goals** + - Orin and Voss already have deteriorating trust after the fiery caverns. Show how that tension carries over into the Marsh. Maybe they speak in half-truths or withhold critical information to avoid vulnerability. + - Tali may be traveling for a personal quest, such as searching for missing siblings. If that “family feud” is relevant, it can appear in subtle ways (e.g., she overhears gossip or sees a crest in the Hideout that ties to her clan). + +--- + +## 2. Align Environment and Events + +1. **Location-Specific Challenges** + - **Fiery Caverns Beneath a Sleeping Volcano**: Extreme heat, magma flows, or trembling ground force characters to make quick decisions or reveal their personalities in high-stress situations. This environment itself can be the cause of tension or unity. + - **Haunted Marsh Shrouded in Fog**: Low visibility or ghostly apparitions can cause paranoia, spurring confessions or alliances. Perhaps the Marsh is known for revealing illusions of a character’s past regrets, fueling pivotal story revelations. + +2. **Tie Plot to the Location’s Lore** + - When you say “they must recover from a crippling setback” in a Marsh, maybe the Marsh’s local legend is about lost travelers or a hidden cure. This gives the story event a narrative reason for happening *there*. + - If the Ruined Fort has rumors about a powerful artifact, it justifies why Nyx and Orin would forge an alliance to seek it in that very place. + +--- + +## 3. Interweave Third-Party Roles and Consequences + +1. **Third-Party Observers With Agency** + - A beggar who is “quietly listening” might later use that overheard information for blackmail or might warn someone else of the characters’ plans. Don’t let them vanish without consequence. + - The “curious child” in the village might become an unexpected messenger, revealing the party’s presence to foes or letting them in on a local secret. + +2. **NPC Network** + - Give each onlooker or side character a small web of connections to the main cast or the environment. This maintains continuity and a sense that the world reacts to the player’s/characters’ actions. + +--- + +## 4. Introduce Player Agency in Each Location + +Right now, the story steps are fairly linear. You can expand player options by designing activities that support or contrast the main narrative: + +1. **Side Quests / Sub-Activities** + - **Fiery Caverns**: + - **Gather Rare Materials**: The player can explore small side tunnels to collect rare volcanic crystals for trading or forging. This quest can reveal more about Orin’s or Tali’s resourcefulness and the region’s culture. + - **Assist the Beggar**: The beggar might offer a cryptic tip in exchange for help finding safer passage. + + - **Haunted Marsh**: + - **Marsh Trials**: Fog-based illusions make the player confront personal regrets, which might tie into Orin’s or Voss’s confessions. + - **Local Fishing or Foraging**: A “calm moment” to gather supplies or unique Marsh reagents—this can enhance survival elements and make the world feel alive. + +2. **Dialogue Choices** + - During each “interpersonal event,” let the player influence how a conversation unfolds. This can alter relationship changes or immediate next steps (e.g., does Orin soften a bit after hearing Voss’s confession, or does he hold onto anger?). + +3. **Location-Specific Challenges** + - **Environmental Hazards**: Heat exhaustion, sinkholes, hidden traps in ruins—these can become small gameplay loops that reinforce the location’s identity. + - **Stealth vs. Direct Confrontation**: If you have an inquisitive guard, does the player try to sneak past, or talk their way through? Each approach might yield different consequences. + +--- + +## 5. Reinforce Relationship Dynamics with Gameplay + +1. **Team-Based Obstacles** + - Create puzzles or battles that require the skills or trust of both characters involved. For example, Orin’s knowledge of ancient scripts plus Voss’s brute strength might be needed to open a hidden door. If trust is low, they might fail or argue, changing the outcome. + +2. **Branching Relationship Arcs** + - If the relationship meter (e.g., Orin|Voss = -1) goes too low, maybe they fight at inopportune times or sabotage each other. If it climbs high, they might protect each other from certain traps. + - This ensures that relationship changes do more than just appear on a stat screen—they alter the game’s path or difficulty in certain areas. + +--- + +## 6. Maintain Cohesive Narrative Flow + +1. **Chekhov’s Gun Principle** + - If something is mentioned (like a “longstanding family feud” or a “powerful ancient artifact”), make sure it shows up again in a meaningful way, paying it off narratively. + - For example, mention early on that the feud might lead to someone betraying the group, then pay it off by having that tension climax at the final location. + +2. **Ephemeral Continuity** + - Each step influences the next. If Orin left Voss behind in the Marsh, maybe Voss arrives late in the next location with a new grudge or a new ally. That sense of cause and effect keeps the world believable. + +--- + +## 7. Example Outline: Adding Depth and Activities + +Here’s a more fleshed-out partial example focusing on the **Haunted Marsh**: + +- **HauntedMarsh: step2** + - **Description**: Orin and Voss must travel through the fog-enshrouded bog to reach a crucial relic rumored to cleanse curses. + - **Events**: + - “A surprising confession that changes everything” could be spurred by illusions dredging up guilt. + - **Side Quests**: + 1. **Warding Lantern**: A local hermit begs for help collecting phosphorescent mushrooms to craft a lantern that repels marsh spirits. This ties to the environment and helps players survive. + 2. **Marsh Oracle**: The beggar might actually be a visionary who offers cryptic advice if the players bring them a rare “Marsh Lily.” + - **Relationship Mechanics**: + - If Orin is suspicious of Voss, let that seep into how they approach illusions (e.g., Orin might see illusions of Voss betraying him). A dialogue choice can let players choose to trust or deny those illusions. + - **Transitions**: Possibly branch out (stay to do side quests or push on immediately) but eventually funnel back to the next big location. + +--- + +## Final Thoughts + +- **Build each location so it reflects (1) the story’s history and (2) the characters’ personal arcs.** +- **Embed meaningful player actions** that reveal backstory, test alliances, and provide tangible rewards or punishments (equipment, local standing, new information). +- Keep track of relationship metrics **and** how each story beat impacts them, ensuring the effects show up in dialogue, choices, and gameplay consequences. + +By following these steps, you’ll create a more cohesive, believable world where the **story, characters, and gameplay** are closely interwoven—enhancing the player’s sense of agency and immersion. +""" + +SimpleStructureTimeline1stPrompt = """Here is a config skeleton to turn into a story that ends with: The villain gets their comeuppance and is punished for their crimes. + +{ + "masterlocation1": { + "location1": { + "description": "UI - Character Model Viewer", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": [], + "developernotes": [] + }, + "location2": { + "description": "UI - Crafting Progress Bar", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location3" + }, + "media": [], + "developernotes": [] + }, + "location3": { + "description": "UI - Scoreboard", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location4" + }, + "media": [], + "developernotes": [] + }, + "location4": { + "description": "Story: overcomeLoss", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location5" + }, + "media": [], + "developernotes": [] + }, + "location5": { + "description": "Story: formAlliance", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location6" + }, + "media": [], + "developernotes": [] + }, + "location6": { + "description": "Story: makeDecision", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location7" + }, + "media": [], + "developernotes": [] + }, + "location7": { + "description": "Story: exploreLocation", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location8" + }, + "media": [], + "developernotes": [] + }, + "location8": { + "description": "UI - World Map", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location9" + }, + "media": [], + "developernotes": [] + }, + "location9": { + "description": "UI - Crafting Recipe List", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location10" + }, + "media": [], + "developernotes": [] + }, + "location10": { + "description": "Story: faceChallenge", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location11" + }, + "media": [], + "developernotes": [] + }, + "location11": { + "description": "Story: experienceSetback", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location12" + }, + "media": [], + "developernotes": [] + }, + "location12": { + "description": "UI - Skill Path Highlight", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location13" + }, + "media": [], + "developernotes": [] + }, + "location13": { + "description": "Story: makeDiscovery", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location14" + }, + "media": [], + "developernotes": [] + }, + "location14": { + "description": "UI - System Messages", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location15" + }, + "media": [], + "developernotes": [] + }, + "location15": { + "description": "Story: meetCharacter", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location16" + }, + "media": [], + "developernotes": [] + }, + "location16": { + "description": "Story: engageBattle", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location17" + }, + "media": [], + "developernotes": [] + }, + "location17": { + "description": "Story: unlockAbility", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location18" + }, + "media": [], + "developernotes": [] + }, + "location18": { + "description": "UI - Quest Status Indicators", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location19" + }, + "media": [], + "developernotes": [] + }, + "location19": { + "description": "UI - Mini-Map", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "masterlocation1_location20" + }, + "media": [], + "developernotes": [] + }, + "location20": { + "description": "UI - Quick Equip Slots", + "events": [], + "choices": [ + "continue" + ], + "transitions": { + "continue": "end" + }, + "media": [], + "developernotes": [] + }, + "end": { + "description": "The adventure ends here.", + "choices": [ + "restart" + ], + "transitions": { + "restart": "location1" + } + } + } +} +""" + +Outputof1stSimpleStructurePrompt = """{ + "masterlocation1": { + "location1": { + "description": "UI - Character Model Viewer: The hero prepares for the journey.", + "events": ["intro"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location2" + }, + "media": ["hero_model"], + "developernotes": ["Introduce the hero."] + }, + "location2": { + "description": "UI - Crafting Progress Bar: The hero crafts essential tools.", + "events": ["craftItem"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location3" + }, + "media": ["crafting_ui"], + "developernotes": ["Player learns crafting."] + }, + "location3": { + "description": "UI - Scoreboard: The hero reflects on previous challenges.", + "events": ["reflect"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location4" + }, + "media": ["scoreboard"], + "developernotes": ["Introduce past successes and failures."] + }, + "location4": { + "description": "Story: Overcome the loss of an ally during a surprise attack.", + "events": ["overcomeLoss"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location5" + }, + "media": ["battle_scene"], + "developernotes": ["Set up stakes with personal loss."] + }, + "location5": { + "description": "Story: Form an alliance with unlikely allies.", + "events": ["formAlliance"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location6" + }, + "media": ["alliance_image"], + "developernotes": ["Highlight camaraderie."] + }, + "location6": { + "description": "Story: Make a critical decision about pursuing the villain.", + "events": ["makeDecision"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location7" + }, + "media": ["decision_ui"], + "developernotes": ["Player chooses pursuit strategy."] + }, + "location7": { + "description": "Story: Explore the villain’s hideout for clues.", + "events": ["exploreLocation"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location8" + }, + "media": ["hideout_map"], + "developernotes": ["Introduce villain’s presence."] + }, + "location8": { + "description": "UI - World Map: The path to confront the villain is charted.", + "events": ["chartPath"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location9" + }, + "media": ["world_map"], + "developernotes": ["Provide player overview."] + }, + "location9": { + "description": "UI - Crafting Recipe List: Finalize preparations for battle.", + "events": ["prepareGear"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location10" + }, + "media": ["recipes_ui"], + "developernotes": ["Final crafting before the climax."] + }, + "location10": { + "description": "Story: Face the villain’s henchmen in a challenging fight.", + "events": ["faceChallenge"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location11" + }, + "media": ["battle_ui"], + "developernotes": ["Player engages in combat."] + }, + "location11": { + "description": "Story: Experience a setback as the villain escapes.", + "events": ["experienceSetback"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location12" + }, + "media": ["setback_ui"], + "developernotes": ["Raise tension and stakes."] + }, + "location12": { + "description": "UI - Skill Path Highlight: Unlock a hidden ability to counter the villain.", + "events": ["unlockAbility"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location13" + }, + "media": ["skill_ui"], + "developernotes": ["Introduce player growth."] + }, + "location13": { + "description": "Story: Discover a critical weakness in the villain’s plan.", + "events": ["makeDiscovery"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location14" + }, + "media": ["discovery_scene"], + "developernotes": ["Hint at victory."] + }, + "location14": { + "description": "UI - System Messages: Allies send reinforcements.", + "events": ["callReinforcements"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location15" + }, + "media": ["system_ui"], + "developernotes": ["Build momentum for final battle."] + }, + "location15": { + "description": "Story: Confront the villain in their lair.", + "events": ["meetCharacter", "engageBattle"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_location16" + }, + "media": ["final_battle"], + "developernotes": ["Player confronts villain."] + }, + "location16": { + "description": "Story: Defeat the villain and see their crimes revealed.", + "events": ["revealTruth", "punishVillain"], + "choices": ["continue"], + "transitions": { + "continue": "masterlocation1_end" + }, + "media": ["villain_defeat"], + "developernotes": ["Resolution of story."] + }, + "end": { + "description": "The villain gets their comeuppance and is punished for their crimes. Justice is served.", + "choices": ["restart"], + "transitions": { + "restart": "masterlocation1_location1" + }, + "media": ["justice_scene"], + "developernotes": ["Satisfying conclusion."] + } + } +} + +""" + + +#-------------------------#-------------------------#-------------------------#------------------------- + +#-------------------------#-------------------------#-------------------------#------------------------- diff --git a/relatively_constant_variables.py b/relatively_constant_variables.py index f7db278b49957ba466682d13a198bd1b93df40e1..52c7ea6237dce291685dcbe00c8bc11390afcbc2 100644 --- a/relatively_constant_variables.py +++ b/relatively_constant_variables.py @@ -1,2794 +1,25 @@ -""" Variables moved to make reading a bit easier -ctrl+f - #-------------------------#-------------------------#-------------------------#------------------------- -Game Ideas for LLM prompts -FAQ and RAG data and prompt engineering -Default Config - JS config -Autostructure suggestor? -Initial Game Config -Comporation between inital and edited - with suggestions in between -Test if HTML can load in HTML gradio component - - - -""" - -GameIdeasForPromptstodiscusswithSOTALLM = ["Programming Interview", "Coaching an NPC into the main character", "High pressure environment eg. Chefs Kitchen, Pitstop", "Moral Injury"] - -#------------------------------------------------------------------------------------------------------------------------- - -#Supposed to be part of FAQ buttons -Frontendpromptengforguide = """Suggest descriptions for media to fill the empty media fields - - -{ - "village": { - "start": { - "description": "You wake up in a small village. You hear a rumor about a lost treasure.", - "choices": [ - "explore village", - "gather supplies", - "rest" - ], - "transitions": { - "explore village": "village_rumor", - "gather supplies": "village_supplies", - "rest": "village_start" - }, - "consequences": { - "gather supplies": {} - }, - "media": [] - }, - "rumor": { - "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", - "choices": [ - "decide to go", - "ignore" - ], - "transitions": { - "decide to go": "village_supplies", - "ignore": "village_start" - }, - "consequences": { - "decide to go": {} - }, - "media": [] - }, - "supplies": { - "description": "You gather supplies for your journey.", - "choices": [ - "head to forest", - "stay in village" - ], - "transitions": { - "head to forest": "forest_forest", - "stay in village": "village_start" - }, - "media": [] - } - }, - "forest": { - "forest": { - "description": "You enter the dense forest, heading towards the ruins.", - "choices": [ - "travel further", - "return to village" - ], - "transitions": { - "travel further": "ruins_ruins", - "return to village": "village_start" - }, - "media": [] - } - }, - "ruins": { - "ruins": { - "description": "You reach the ancient ruins. The entrance is dark and eerie.", - "choices": [ - "enter ruins", - "return to forest" - ], - "transitions": { - "enter ruins": "ruins_explore", - "return to forest": "forest_forest" - }, - "media": [] - }, - "explore": { - "description": "You explore the ruins, encountering traps and puzzles.", - "choices": [ - "solve puzzle", - "avoid traps" - ], - "transitions": { - "solve puzzle": "ruins_hiddenPassage", - "avoid traps": "ruins_ruins" - }, - "media": [] - }, - "hiddenPassage": { - "description": "You solve a challenging puzzle and unlock a hidden passage.", - "choices": [ - "enter passage", - "go back" - ], - "transitions": { - "enter passage": "ruins_treasureRoom", - "go back": "ruins_explore" - }, - "media": [] - }, - "treasureRoom": { - "description": "You enter the treasure room and find the treasure chest.", - "choices": [ - "take treasure", - "leave" - ], - "transitions": { - "take treasure": "ruins_celebrate", - "leave": "ruins_ruins" - }, - "consequences": { - "take treasure": {} - }, - "media": [] - }, - "celebrate": { - "description": "You celebrate your discovery and decide to bring the treasure back to the village.", - "choices": [ - "return to village" - ], - "transitions": { - "return to village": "village_return" - }, - "media": [] - } - }, - "village_return": { - "village_return": { - "description": "You return to the village with the treasure and share it with the villagers.", - "choices": [ - "end adventure" - ], - "transitions": { - "end adventure": "end_end" - }, - "media": [] - } - }, - "end": { - "end": { - "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", - "choices": [], - "transitions": {}, - "media": [] - } - } -} -""" - -FAQAllprompts = { - "Example Workflow": [ - ("I am writing a story about a chef. Please write dishes to appear on the menu.", " - I am writing a story about a chef. Please write dishes to appear on the menu."), - ("Lets brainstorm some decisions a chef would take", " - What are the most common decisions that a chef story would include?"), - ("Lets brainstorm some problems a chef would face", " - What are the kinds problems that a chef story would include?"), - ("Lets brainstorm some goals the chef would have", " - What are the kinds of out of reach goals that a chef story would include?"), - ("Please help me add to config that I pasted", " - Continue the pasted config from any complete block."), - ("Help me think of images for a chef story", ". Help me think of images for a chef story") - ], - "FAQ": [ - ("Help me make a story", "- Explain to the user that they need to come up with a list of decisions, and then choose themes and then do the setting in characters and add some examples for each step:"), - ], - "Brainstorming for this space": [ - ("Help me make a story", "- Explain to the user that they need to come up with a list of decisions, and then choose themes and then do the setting in characters and add some examples for each step:"), - ], - "Expansive Problem solving": [ - ("My problem to solve is", "- please make 10 sub problems have to solve from this:"), - ("My process to solve is", "- please make 10 sub processes have to solve from this:"), - ("My goal to solve is", "- please make 10 sub goals have to solve from this:"), - ("My task to solve is", "- please make 10 sub tasks have to solve from this:"), - ("My phase to solve is", "- please make 10 sub phases have to solve from this:"), - ("My component to solve is", "- please make 10 sub components have to solve from this:"), - ("My element to solve is", "- please make 10 sub elements have to solve from this:"), - ("A brief description of my current situation:", "- please list the most important task to pay attention to:"), - ("A brief description of my current situation to analyse:", "- please conduct a situational analysis:"), - ("A brief description of my current situation to decompose:", "- please conduct a problem decomposition:"), - ], -} - -Storycraftprompts = ["Write a 3 sentence story", -"Great, Lets rewrite the story (keep as much of it as possible) with F1 as our thematic source", -"Great, Next for each sentence make decisions for a player in a text game"] - - -# Sample knowledge base (replace with your own data) -knowledge_base = [ - {"id": "doc1", "content": "The capital of France is Paris."}, - {"id": "doc2", "content": "Python is a popular programming language."}, - {"id": "doc3", "content": "Machine learning is a subset of artificial intelligence."}, - {"id": "doc4", "content": "The Earth orbits around the Sun."}, - {"id": "doc5", "content": "Orbits is the name of a korean fangroup"}, - {"id": "doc6", "content": "The capital of France is Paris. It's known for the Eiffel Tower."}, - {"id": "doc7", "content": "The capital of Italy is Rome. It's famous for the Colosseum."}, - {"id": "doc8", "content": "Python is a popular programming language, known for its simplicity."}, - {"id": "doc9", "content": "Java is a widely-used programming language, valued for its portability."}, - {"id": "doc10", "content": "Deep learning is a part of machine learning based on artificial neural networks."}, - {"id": "doc11", "content": "Law is a Tekken character"}, - {"id": "doc12", "content": "The law is very complicated"}, -] - -#-------------------------#-------------------------#-------------------------#------------------------- - -# Default configuration template -default_config = { - 'background': '/AutoGameBackgrounds/1stGameLoc123.png', - 'inventory': [], - 'skills': [], - 'objectives': [], - 'targets': [], - 'story': [], - 'actions': {} -} - -#-------------------------#-------------------------#-------------------------#------------------------- - -# List of player engagement UI elements -player_engagement_items = [ - "Health Bar", "Mana/Energy Bar", "Experience Bar", "Mini-Map", "Quest Tracker", - "Inventory Quick Access Slots", "Skill Cooldown Indicators", "Currency Display", - "Compass", "Timer/Clock", "Action Prompts", "Reticle/Crosshair", "Enemy Health Bars", - "Score Display", "Inventory Grid", "Item Tooltip", "Item Categories/Tabs", - "Equip/Unequip Buttons", "Item Sorting Options", "Character Stats Panel", - "Character Model Viewer", "Quick Equip Slots", "Dialogue Box", "Dialogue Choices", - "Character Portraits", "Character Name Display", "Subtitle Text", "World Map", - "Local Map", "Fast Travel Points", "Map Markers", "Map Legend", "Crafting Recipe List", - "Ingredient Slots", "Craft Button", "Crafting Progress Bar", "Skill Nodes", - "Skill Descriptions", "Skill Point Counter", "Unlock Button", "Skill Path Highlight", - "Quest List", "Quest Details Panel", "Quest Objectives", "Quest Rewards", - "Quest Status Indicators", "Chat Box", "Player List", "Voice Chat Indicators", - "Ping/Latency Indicator", "Party/Team UI", "Scoreboard", "Matchmaking Screen", - "Pop-up Notifications", "Achievement Unlocks", "System Messages", - "On-screen Key Prompts", "Environmental Interaction Icons", "Save/Auto-Save Indicators", - "Credits Screen" -] - -# List of character-based story events -story_events = [ - 'exploreLocation', 'discoverClue', 'meetCharacter', 'solveRiddle', 'findItem', - 'faceChallenge', 'makeDecision', 'engageBattle', 'unlockAbility', 'learnSecret', - 'completeMission', 'experienceSetback', 'earnReward', 'uncoverMystery', 'formAlliance', - 'faceBetrayal', 'confrontNemesis', 'makeDiscovery', 'overcomeLoss', 'achieveVictory' -] - - -#-------------------------#-------------------------#-------------------------#------------------------- - - -# Define the states -all_states = { - 'village': { - 'start': { - "description": "You wake up in a small village. You hear a rumor about a lost treasure.", - "choices": ['explore village', 'gather supplies', 'rest'], - "transitions": {'explore village': 'village_rumor', 'gather supplies': 'village_supplies', 'rest': 'village_start'}, - "consequences": { - 'gather supplies': lambda player: player.add_item('basic supplies') - }, - "media": [] - }, - 'rumor': { - "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", - "choices": ['decide to go', 'ignore'], - "transitions": {'decide to go': 'village_supplies', 'ignore': 'village_start'}, - "consequences": { - 'decide to go': lambda player: player.update_knowledge('treasure location') - }, - "media": [] - }, - 'supplies': { - "description": "You gather supplies for your journey.", - "choices": ['head to forest', 'stay in village'], - "transitions": {'head to forest': 'forest_forest', 'stay in village': 'village_start'}, - "media": [] - }, - }, - 'forest': { - 'forest': { - "description": "You enter the dense forest, heading towards the ruins.", - "choices": ['travel further', 'return to village'], - "transitions": {'travel further': 'ruins_ruins', 'return to village': 'village_start'}, - "media": [] - }, - }, - 'ruins': { - 'ruins': { - "description": "You reach the ancient ruins. The entrance is dark and eerie.", - "choices": ['enter ruins', 'return to forest'], - "transitions": {'enter ruins': 'ruins_explore', 'return to forest': 'forest_forest'}, - "media": [] - }, - 'explore': { - "description": "You explore the ruins, encountering traps and puzzles.", - "choices": ['solve puzzle', 'avoid traps'], - "transitions": {'solve puzzle': 'ruins_hiddenPassage', 'avoid traps': 'ruins_ruins'}, - "media": [] - }, - 'hiddenPassage': { - "description": "You solve a challenging puzzle and unlock a hidden passage.", - "choices": ['enter passage', 'go back'], - "transitions": {'enter passage': 'ruins_treasureRoom', 'go back': 'ruins_explore'}, - "media": [] - }, - 'treasureRoom': { - "description": "You enter the treasure room and find the treasure chest.", - "choices": ['take treasure', 'leave'], - "transitions": {'take treasure': 'ruins_celebrate', 'leave': 'ruins_ruins'}, - "consequences": { - 'take treasure': lambda player: player.add_item('treasure') - }, - "media": [] - }, - 'celebrate': { - "description": "You celebrate your discovery and decide to bring the treasure back to the village.", - "choices": ['return to village'], - "transitions": {'return to village': 'village_return'}, - "media": [] - }, - }, - 'village_return': { - 'village_return': { - "description": "You return to the village with the treasure and share it with the villagers.", - "choices": ['end adventure'], - "transitions": {'end adventure': 'end_end'}, - "media": [] - }, - }, - 'end': { - 'end': { - "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", - "choices": [], - "transitions": {}, - "media": [] - }, - } -} - -#-------------------------#-------------------------#-------------------------#------------------------- - -#actual is not screen ready -originalconfigatbeinningofthisspace = """{ - "village": { - "start": { - "description": "You wake up in a small village. You hear a rumor about a lost treasure.", - "choices": ['explore village', 'gather supplies', 'rest'], - "transitions": {'explore village': 'village_rumor', 'gather supplies': 'village_supplies', 'rest': 'village_start'}, - "consequences": { - 'gather supplies': lambda player: player.add_item('basic supplies') - }, - "media": [] - }, - "rumor": { - "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", - "choices": ['decide to go', 'ignore'], - "transitions": {'decide to go': 'village_supplies', 'ignore': 'village_start'}, - "consequences": { - 'decide to go': lambda player: player.update_knowledge('treasure location') - }, - "media": [] - }, - "supplies": { - "description": "You gather supplies for your journey.", - "choices": ['head to forest', 'stay in village'], - "transitions": {'head to forest': 'forest_forest', 'stay in village': 'village_start'}, - "media": [] - }, - }, - "forest": { - "forest": { - "description": "You enter the dense forest, heading towards the ruins.", - "choices": ['travel further', 'return to village'], - "transitions": {'travel further': 'ruins_ruins', 'return to village': 'village_start'}, - "media": [] - }, - }, - "ruins": { - "ruins": { - "description": "You reach the ancient ruins. The entrance is dark and eerie.", - "choices": ['enter ruins', 'return to forest'], - "transitions": {'enter ruins': 'ruins_explore', 'return to forest': 'forest_forest'}, - "media": [] - }, - "explore": { - "description": "You explore the ruins, encountering traps and puzzles.", - "choices": ['solve puzzle', 'avoid traps'], - "transitions": {'solve puzzle': 'ruins_hiddenPassage', 'avoid traps': 'ruins_ruins'}, - "media": [] - }, - "hiddenPassage": { - "description": "You solve a challenging puzzle and unlock a hidden passage.", - "choices": ['enter passage', 'go back'], - "transitions": {'enter passage': 'ruins_treasureRoom', 'go back': 'ruins_explore'}, - "media": [] - }, - "treasureRoom": { - "description": "You enter the treasure room and find the treasure chest.", - "choices": ['take treasure', 'leave'], - "transitions": {'take treasure': 'ruins_celebrate', 'leave': 'ruins_ruins'}, - "consequences": { - 'take treasure': lambda player: player.add_item('treasure') - }, - "media": [] - }, - "celebrate": { - "description": "You celebrate your discovery and decide to bring the treasure back to the village.", - "choices": ['return to village'], - "transitions": {'return to village': 'village_return'}, - "media": [] - }, - }, - "village_return": { - "village_return": { - "description": "You return to the village with the treasure and share it with the villagers.", - "choices": ['end adventure'], - "transitions": {'end adventure': 'end_end'}, - "media": [] - }, - }, - "end": { - "end": { - "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", - "choices": [], - "transitions": {}, - "media": [] - }, - } -}""" - #Analyzing Game Configuration Metrics - was the thread name of where the -claude3_5_06072024configtips = """Example Config Development Tab - Claude 3.5 Sonnet as the LLM Assitant - -This config is for a basic text based game engine I made with help many llms including you. -Thanks to all the help it loads which presented another problem, I dont have any structural metrics to assess the quality of the config. -What JSON things can we look at to see if it may be too bland for a person testing the game - - -Text-Based Game Configuration Analysis -1. State Variety - -Total number of states: 13 -Number of unique locations: 5 (village, forest, ruins, village_return, end) - -2. Choice Diversity - -Average choices per state: 1.92 -States with no choices: 1 (end state) -States with most choices: 3 (village_start with 3 choices) -Unique choices across all states: 17 - -3. Transition Complexity - -Total number of transitions: 25 -Average transitions per state: 1.92 -States with most transitions: 3 (village_start with 3 transitions) - -4. Consequence Implementation - -States with consequences: 3 -Total number of consequences: 3 -Types of consequences: Adding items, Updating knowledge - -5. Description Length - -Average description length: 13.15 words -Longest description: 24 words (village_return) -Shortest description: 4 words (village_supplies) - -6. Cyclic Paths - -Presence of loops: Yes (e.g., village_start -> village_supplies -> village_start) - -7. Dead Ends - -Number of dead ends: 1 (end state) - -8. Media Integration - -States with media: 0 -Total media elements: 0 - -9. Narrative Progression - -Clear beginning (village_start) and end (end) states -Multiple paths to reach the end - -10. Player Agency - -Choices affect game progression -Some choices have consequences (3 instances) - -11. Game Length - -Minimum steps to complete: 6 (village_start -> village_supplies -> forest_forest -> ruins_ruins -> ruins_explore -> ruins_hiddenPassage -> ruins_treasureRoom -> ruins_celebrate -> village_return -> end) -Maximum steps (including loops): Potentially infinite due to cycles - -12. Replayability - -Multiple paths through the game -Some variability in outcomes based on choices - -#============================================= - -To assess the quality and potential blandness of your game configuration, we can analyze several structural metrics within the JSON. Here are some aspects we can examine: -Text-Based Game Configuration AnalysisClick to open document -Based on this analysis, here are some observations about the potential blandness of the game: - -Limited state variety: With only 13 states across 5 locations, the game might feel short and lacking in diversity. Consider adding more locations or states within existing locations. -Low average choices: An average of 1.92 choices per state might make the game feel linear. Increasing the number of choices in more states could improve player engagement. -Limited consequences: Only 3 states have consequences, which might make player choices feel less impactful. Adding more consequences could increase the sense of agency. -Short descriptions: The average description length of 13.15 words might not provide enough detail to immerse players. Consider expanding descriptions to create a richer narrative. -Lack of media: No states currently use media elements, which could make the game feel less engaging. Adding images, sound effects, or other media could enhance the player experience. -Limited narrative branching: While there are some loops and choices, the overall structure is relatively linear. Adding more branching paths could increase replayability and player interest. - -To make the game less bland, consider: - -Adding more states and locations -Increasing the number of choices in each state -Implementing more consequences for player actions -Expanding descriptions to create a richer narrative -Incorporating media elements -Creating more diverse paths through the game - -#=============================================== - - -# Text-Based Game Improvement Suggestions - -## 1. Expand State Variety -- Add a new location: "Mountain Pass" - - Include states like "steep_climb", "avalanche_risk", "hidden_cave" -- Expand the "forest" location: - - Add states like "dense_thicket", "mysterious_clearing", "ancient_tree" - -## 2. Increase Choice Diversity -- Add more choices to existing states. For example, in "village_start": - - "talk to elder" - - "visit market" - - "investigate strange noises" -- Ensure most states have at least 3 choices - -## 3. Enhance Transition Complexity -- Create more interconnected states. For example: - - Allow travel between "forest" and "mountain_pass" - - Add shortcuts or secret passages between non-adjacent locations - -## 4. Implement More Consequences -- Add consequences to more choices. For example: - - In "forest_forest": - - "forage for food" -> `lambda player: player.add_item('wild berries')` - - "take shortcut" -> `lambda player: player.update_status('lost')` - -## 5. Enrich Descriptions -- Expand existing descriptions with more vivid details -- Example for "village_start": - ```python - "description": "You wake up in a small village nestled between rolling hills. The air is filled with the scent of freshly baked bread and the sound of a distant waterfall. Villagers bustle about, whispering excitedly about a rumor of lost treasure." - ``` - -## 6. Introduce Cyclic Content with Variations -- Add time-based changes to revisited locations -- Example: - ```python - 'village_start': { - "description": lambda player: f"You return to the village square. It's now {player.time_of_day()}. " + - ("The market is bustling with activity." if player.time_of_day() == "day" else - "The streets are quiet, lit by flickering torches.") - } - ``` - -## 7. Create Meaningful Dead Ends -- Add optional quests or side adventures -- Example: "help_lost_child" state in the village that doesn't progress main story but provides rewards - -## 8. Integrate Media Elements -- Add placeholder media to enhance immersion: - ```python - "media": [ - {"type": "image", "url": "/api/placeholder/400/300", "alt": "A picturesque village at dawn"}, - {"type": "sound", "url": "village_ambience.mp3"} - ] - ``` - -## 9. Enhance Narrative Progression -- Add a main quest storyline with multiple stages -- Introduce side quests for additional depth - -## 10. Increase Player Agency -- Add more impactful choices that significantly alter the game's direction -- Implement a simple inventory or skill system that affects available choices - -## 11. Extend Game Length -- Add more locations and states to create a longer, more involved adventure -- Implement quest chains that span multiple locations - -## 12. Improve Replayability -- Add random events that can occur in each location -- Implement multiple endings based on player choices and achievements - - -#=============================================== - -# Updated Text-Based Game Configuration with Side Quest - -## New State: help_lost_child - -Add this new state to the 'village' section of your `all_states` dictionary: - -```python -'village': { - # ... existing states ... - - 'help_lost_child': { - "description": "You encounter a crying child in the village square. They seem to have lost their way home.", - "choices": ['help child', 'ignore and continue'], - "transitions": { - 'help child': 'village_child_quest', - 'ignore and continue': 'village_start' - }, - "consequences": { - 'help child': lambda player: player.update_status('helping_child') - }, - "media": [] - }, - 'child_quest': { - "description": "You decide to help the lost child. After asking around and searching the village, you finally locate their home.", - "choices": ['return child', 'ask for reward'], - "transitions": { - 'return child': 'village_child_reunion', - 'ask for reward': 'village_child_reward' - }, - "media": [] - }, - 'child_reunion': { - "description": "You return the child to their grateful parents. They thank you profusely for your kindness.", - "choices": ['accept thanks', 'request reward'], - "transitions": { - 'accept thanks': 'village_start', - 'request reward': 'village_child_reward' - }, - "consequences": { - 'accept thanks': lambda player: player.update_status('local_hero') - }, - "media": [] - }, - 'child_reward': { - "description": "The child's parents offer you a small pouch of coins as a reward for your help.", - "choices": ['accept reward', 'decline reward'], - "transitions": { - 'accept reward': 'village_start', - 'decline reward': 'village_start' - }, - "consequences": { - 'accept reward': lambda player: player.add_item('coin pouch'), - 'decline reward': lambda player: player.update_status('selfless_hero') - }, - "media": [] - }, -} -``` - -## Updated Transitions - -To incorporate this side quest into the existing game flow, we need to update some transitions in the 'village_start' state: - -```python -'village': { - 'start': { - "description": "You're in the village square. The usual bustle of village life surrounds you.", - "choices": ['explore village', 'gather supplies', 'rest', 'look around'], # Added 'look around' - "transitions": { - 'explore village': 'village_rumor', - 'gather supplies': 'village_supplies', - 'rest': 'village_start', - 'look around': 'village_help_lost_child' # New transition - }, - "consequences": { - 'gather supplies': lambda player: player.add_item('basic supplies') - }, - "media": [] - }, - # ... other states ... -} -``` - -## Explanation of Changes - -1. New States: - - `help_lost_child`: Introduces the side quest - - `child_quest`: Represents the process of helping the child - - `child_reunion`: Concludes the quest if you don't ask for a reward - - `child_reward`: Offers a choice to accept or decline a reward - -2. Updated `village_start`: - - Added a new choice: 'look around' - - Added a new transition to `village_help_lost_child` - -3. Consequences: - - Added status updates: 'helping_child', 'local_hero', 'selfless_hero' - - Added item reward: 'coin pouch' - -These changes create a meaningful dead end by: -1. Providing an optional quest that doesn't directly progress the main treasure hunt story -2. Offering multiple choices within the side quest, allowing for different outcomes -3. Implementing consequences that affect the player's status or inventory, providing rewards for engagement -4. Allowing the player to return to the main story flow after completing (or ignoring) the side quest - -This side quest adds depth to the village location, gives the player more to do, and provides character-building opportunities without derailing the main narrative. It also demonstrates how you can implement similar side quests or meaningful dead ends in other locations of your game. - -#================================================================================ - -Certainly! Let's implement the "help_lost_child" side quest as a meaningful dead end in the village. This will add depth to the game without necessarily progressing the main story. Here's how we can incorporate this into your current configuration: -Updated Text-Based Game Configuration with Side QuestClick to open document -To further improve this side quest and make it even more engaging, you could consider: - -Time sensitivity: Make the quest available only at certain times or for a limited number of turns. -Skill checks: If your game has a skill system, you could add checks for persuasion or perception that affect the outcome. -Long-term consequences: The status changes (like 'local_hero') could affect how NPCs interact with the player later in the game. -Branching outcomes: You could add more complex branches, such as the child being in actual danger, leading to a mini-adventure. - -Would you like me to elaborate on any of these ideas or show how to implement them in your configuration? - """ +Variables moved to make reading a bit easier - NOW SPLIT INTO MODULES -tipsupdatedconfigatbeinningofthisspace = """{ - "village": { - "start": { - "description": "You wake up in a small village. You hear a rumor about a lost treasure.", - "choices": ['explore village', 'gather supplies', 'rest', 'look around'], - "transitions": {'explore village': 'village_rumor', 'gather supplies': 'village_supplies', 'rest': 'village_start', 'look around': 'village_help_lost_child'}, - "consequences": { - 'gather supplies': lambda player: player.add_item('basic supplies') - }, - "media": [] - }, - "rumor": { - "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", - "choices": ['decide to go', 'ignore'], - "transitions": {'decide to go': 'village_supplies', 'ignore': 'village_start'}, - "consequences": { - 'decide to go': lambda player: player.update_knowledge('treasure location') - }, - "media": [] - }, - "supplies": { - "description": "You gather supplies for your journey.", - "choices": ['head to forest', 'stay in village'], - "transitions": {'head to forest': 'forest_forest', 'stay in village': 'village_start'}, - "media": [] - }, - "help_lost_child": { - "description": "You encounter a crying child in the village square. They seem to have lost their way home.", - "choices": ['help child', 'ignore and continue'], - "transitions": { - 'help child': 'village_child_quest', - 'ignore and continue': 'village_start' - }, - "consequences": { - 'help child': lambda player: player.update_status('helping_child') - }, - "media": [] - }, - "child_quest": { - "description": "You decide to help the lost child. After asking around and searching the village, you finally locate their home.", - "choices": ['return child', 'ask for reward'], - "transitions": { - 'return child': 'village_child_reunion', - 'ask for reward': 'village_child_reward' - }, - "media": [] - }, - "child_reunion": { - "description": "You return the child to their grateful parents. They thank you profusely for your kindness.", - "choices": ['accept thanks', 'request reward'], - "transitions": { - 'accept thanks': 'village_start', - 'request reward': 'village_child_reward' - }, - "consequences": { - 'accept thanks': lambda player: player.update_status('local_hero') - }, - "media": [] - }, - "child_reward": { - "description": "The child's parents offer you a small pouch of coins as a reward for your help.", - "choices": ['accept reward', 'decline reward'], - "transitions": { - 'accept reward': 'village_start', - 'decline reward': 'village_start' - }, - "consequences": { - 'accept reward': lambda player: player.add_item('coin pouch'), - 'decline reward': lambda player: player.update_status('selfless_hero') - }, - "media": [] - }, - }, - "forest": { - "forest": { - "description": "You enter the dense forest, heading towards the ruins.", - "choices": ['travel further', 'return to village'], - "transitions": {'travel further': 'ruins_ruins', 'return to village': 'village_start'}, - "media": [] - }, - }, - "ruins": { - "ruins": { - "description": "You reach the ancient ruins. The entrance is dark and eerie.", - "choices": ['enter ruins', 'return to forest'], - "transitions": {'enter ruins': 'ruins_explore', 'return to forest': 'forest_forest'}, - "media": [] - }, - "explore": { - "description": "You explore the ruins, encountering traps and puzzles.", - "choices": ['solve puzzle', 'avoid traps'], - "transitions": {'solve puzzle': 'ruins_hiddenPassage', 'avoid traps': 'ruins_ruins'}, - "media": [] - }, - "hiddenPassage": { - "description": "You solve a challenging puzzle and unlock a hidden passage.", - "choices": ['enter passage', 'go back'], - "transitions": {'enter passage': 'ruins_treasureRoom', 'go back': 'ruins_explore'}, - "media": [] - }, - "treasureRoom": { - "description": "You enter the treasure room and find the treasure chest.", - "choices": ['take treasure', 'leave'], - "transitions": {'take treasure': 'ruins_celebrate', 'leave': 'ruins_ruins'}, - "consequences": { - 'take treasure': lambda player: player.add_item('treasure') - }, - "media": [] - }, - 'celebrate': { - "description": "You celebrate your discovery and decide to bring the treasure back to the village.", - "choices": ['return to village'], - "transitions": {'return to village': 'village_return'}, - "media": [] - }, - }, - "village_return": { - "village_return": { - "description": "You return to the village with the treasure and share it with the villagers.", - "choices": ['end adventure'], - "transitions": {'end adventure': 'end_end'}, - "media": [] - }, - }, - "end": { - "end": { - "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", - "choices": [], - "transitions": {}, - "media": [] - }, - } -}""" - -#-------------------------#-------------------------#-------------------------#------------------------- - -TestmedialoadinHTML = """ -
-

This is a placeholder to test use as an all-media loader for ease of prototyping. Seems gradio HTML doesnt support loading local assets or my file path is wrong

-
-
-
- Random squiggles as roads -
-
- -
-
- -
-
- """ - - -#-------------------------#-------------------------#-------------------------#------------------------- - -timeline_components = [ - "Story progression tracker", - "Inventory management system", - "Character stat system", - "Quest log", - "Achievement system", - "NPC relationship tracker", - "Time and weather simulation", - "Economy and trading system", - "Crafting system", - "Skill tree", - "Dialogue manager", - "Combat system", - "Save/Load system", - "Random event generator", - "Faction reputation system" -] - -# Decisions (and context explanation) -timeline_decision_features = [ - "Moral alignment system (good/neutral/evil)", - "Consequence ripple effect (choices affect multiple future events)", - "Timed decisions (limited time to choose)", - "Hidden choices (unlocked by specific conditions)", - "Context-sensitive options (choices change based on character stats/items)", - "Dialogue trees with branching paths", - "Risk/reward choices (high risk, high reward options)", - "Character personality development through choices", - "Relationship-altering decisions", - "Story-altering key decisions", - "Multi-step decision processes", - "Decision journals (recap of major choices)", - "Faction-based decisions", - "Profession or class-specific choices", - "Ethical dilemmas with no clear 'right' answer" -] - -# Nested Sections -timeline_nested_section_ideas = [ - "Multilevel dungeons", - "City districts with sub-locations", - "Planetary system with multiple explorable planets", - "Dreamworld with nested dreams", - "Time periods (past, present, future) with sub-events", - "Parallel universes with alternate versions of locations", - "Miniature worlds inside objects", - "Body systems (for a medical game or body-exploration adventure)", - "Nested memories or flashbacks", - "Virtual reality levels within the game world", - "Dimensional rifts with sub-dimensions", - "Organizational hierarchies (e.g., crime family structure)", - "Ecosystem layers (e.g., forest canopy, understory, forest floor)", - "Nested puzzles (solving one unlocks another)", - "Matryoshka doll-like structures with hidden secrets" -] - -# Media (Especially to affect decisions) -timeline_media_integration = [ - "Dynamic background music changing with mood/tension", - "Character portrait changes reflecting emotional states", - "Environmental sound effects influencing player choices", - "Visual cues (color schemes, lighting) hinting at outcomes", - "Cinematic cutscenes revealing crucial information", - "Interactive maps updating with player discoveries", - "Voice acting with tone influencing trustworthiness", - "Symbolic imagery foreshadowing consequences", - "Augmented reality overlays providing decision hints", - "Character theme motifs indicating importance of NPCs", - "Weather effects reflecting story mood", - "Dream sequences visualizing potential outcomes", - "Time-lapse visuals showing long-term effects of choices", - "Split-screen effects for simultaneous events", - "Flashback scenes providing context for decisions" -] - -# Replayability (GTA and Tekken type mechanics in text form) -timeline_replayability_features = [ - "Multiple character origins with unique storylines", - "Randomized side quests and events", - "Unlockable skills and abilities for subsequent playthroughs", - "New Game+ mode with increased difficulty and rewards", - "Alternative routes to objectives", - "Hidden endings based on specific choices or achievements", - "Character customization affecting story and gameplay", - "Faction system with different allegiances each playthrough", - "Time-based events that change on each playthrough", - "Roguelike elements (permadeath, procedural generation)", - "Challenge modes (speed runs, minimalist runs)", - "Branching skill trees with mutually exclusive paths", - "Collectibles that unlock new content or lore", - "Dynamic world events that evolve differently each time", - "Secret characters or modes to unlock" -] - -# Theme integration (Modified Variables that affect UI or config order) -timeline_theme_integration = [ - "Dynamic UI color schemes based on location or story progress", - "Font styles changing with different eras or dimensions", - "Adaptive music system reflecting current theme", - "UI element shapes and designs matching the current environment", - "Loading screen tips and lore adapted to current theme", - "Cursor or pointer design changing with themes", - "Theme-specific animations for transitions between scenes", - "Variable text descriptors (e.g., 'cold' vs 'scorching' based on environment)", - "Themed icons for inventory and skills", - "Background textures or patterns reflecting current setting", - "Character portrait frames styled to match the theme", - "Theme-specific sound effects for UI interactions", - "Adaptive difficulty adjustments based on chosen theme", - "Mini-map or navigation elements styled to fit the theme", - "Theme-specific filters or overlays for the game view" -] - -# GTA Heists - Same Map with overlapping branching narratives -gta_heist_inspired = [ - "Multiple missions/quests available in the same location", - "Choices in one quest affect options in others", - "Time-sensitive missions that change the environment", - "Characters that appear in multiple storylines with different roles", - "Hidden connections between seemingly unrelated quests", - "Replay value through different approaches to the same heist", - "Dynamic difficulty based on previous mission outcomes", - "Unlockable areas within the same map as story progresses", - "Consequences of actions affecting the game world and future missions", - "Team assembly mechanics for different skill combinations", - "Planning phase before execution of major quests", - "Risk/reward choices for mission approach (stealth vs. aggressive)", - "Easter eggs and secrets hidden within the familiar environment", - "Alternate endings based on cumulative choices across multiple quests", - "NPC relationships affected by actions in different storylines" -] - -# Battlefront - Elites amongst Commoners -battlefront_inspired = [ - "Player can switch between 'grunt' and 'hero' modes", - "Special abilities or items only available to 'elite' characters", - "Earning 'elite' status through exemplary performance", - "Time-limited elite transformations", - "Missions where player commands both elites and regular units", - "Elites with unique storylines and dialogue options", - "Risk of losing elite status through poor performance", - "Elite characters influencing morale of common units", - "Unlockable elite characters with diverse abilities", - "Missions requiring coordination between elites and common units", - "Elite vs Elite showdowns as climactic events", - "Common units gaining experience to become elites over time", - "Elites having a larger impact on the story and world events", - "Special elite-only areas or quests", - "Dynamic difficulty adjustment when playing as elite characters" -] - -# Tekken Casino (one mistake = 1/2 or 1/3 of your Resources) -tekken_casino_inspired = [ - "High-stakes dialogue options with major resource loss for wrong choices", - "Timed decision-making with harsh penalties for indecision", - "Mini-games where a single mistake can cost significant resources", - "Opportunities to 'double or nothing' on quest rewards", - "Character skills affecting the odds of success in risky situations", - "Resource management system with high-risk, high-reward options", - "Combo system for dialogue or actions, breaking combo loses resources", - "Unlockable 'retry' options at the cost of resources", - "Progressive difficulty increase, with higher stakes for each level", - "Special 'all-in' options for desperate situations", - "Random events that can dramatically increase or decrease resources", - "Skill-based challenges with variable resource rewards/penalties", - "NPC interactions where trust is a resource that can be rapidly lost", - "Timed exploration with resource drain for inefficiency", - "Boss encounters with escalating stakes for each phase" -] - -# Turn-based: 'Tactics' type nintendo games -tactics_inspired = [ - "Grid-based movement and action system", - "Unit positioning affecting attack effectiveness and vulnerability", - "Diverse character classes with unique abilities and weaknesses", - "Terrain effects on movement and combat", - "Action points system for balancing multiple actions per turn", - "Status effects persisting across multiple turns", - "Synergy bonuses for complementary unit combinations", - "Permadeath or severe consequences for unit loss", - "Weapon triangle or elemental weakness system", - "Experience and leveling system for units", - "Limited resource management (e.g., mana, ammo) across battles", - "Multi-stage battles with changing objectives", - "Recruitment system for expanding team roster", - "Weather or time-of-day effects on battlefield conditions", - "Morale system affecting unit performance" -] - -# Chess (and any other tile-based game) -chess_inspired = [ - "Different 'pieces' with unique movement and action patterns", - "Objective-based victory conditions (e.g., capturing the 'king')", - "Limited 'board' space creating positioning challenges", - "Sacrificial strategies for long-term advantage", - "Opening, mid-game, and end-game phases with different strategies", - "Promotion system for 'pieces' reaching certain positions", - "Check and checkmate concepts for imminent defeat conditions", - "En passant-like special moves under specific circumstances", - "Castling-inspired defensive or positioning special actions", - "Pawn-like expendable units with potential for growth", - "Stalemate possibilities leading to draw outcomes", - "Time control mechanics for decision-making", - "Algebraic notation for recording and replaying game states", - "Handicap system for balancing player skill differences", - "Puzzle-like scenarios with optimal solution paths" -] - -# Cyberpunk RED (adapting tabletop RPG rules for text-based gameplay) -cyberpunk_red_inspired = [ - "Skill-based character creation with points allocation", - "Netrunning mechanics for hacking and virtual world exploration", - "Cybernetic enhancements with both benefits and drawbacks", - "Social status and reputation systems affecting interactions", - "Complex crafting system for weapons and gadgets", - "Trauma Team-like emergency services as game mechanic", - "Corporation-controlled zones with varying rule sets", - "Lifepath system affecting character background and abilities", - "Street slang glossary influencing dialogue options", - "Critical injury system with lasting effects", - "Improvised weapon creation from environment", - "Data mining and information brokering as key gameplay elements", - "Cyberpsychosis risk for excessive augmentation", - "Night City-inspired district exploration with distinct subcultures", - "Fixer-based job acquisition and reputation building" -] - -# Community playthrough - Tally of players' choices -community_playthrough = [ - "Global choice statistics visible to all players", - "Weekly community challenges based on least-chosen options", - "Adaptive storytelling influenced by majority choices", - "Community-unlocked content when certain choice thresholds are met", - "Leaderboards for most unique choice combinations", - "Time-limited voting on major story decisions", - "Character popularity contests affecting NPC prominence", - "Community-driven side quest creation", - "Collaborative puzzle solving with choice aggregation", - "Dynamic difficulty adjustment based on community performance", - "Seasonal events with community-wide consequences", - "Faction wars where player choices determine outcomes", - "Community achievements unlocking special game modes", - "Player choice heat maps for game areas", - "Periodic resets allowing community to reshape the game world" -] - -# Random item placed in a random location - first person to get it wins -random_item_hunt = [ - "Daily randomized legendary item hunts", - "Clue system hinting at item location based on player actions", - "Item location narrowing over time to increase tension", - "Multiple item tiers with varying rarity and win conditions", - "Limited-time windows for item discovery", - "Collaborative and competitive modes for item hunting", - "Randomized item powers affecting gameplay upon discovery", - "Cursed items with negative effects to add risk", - "Community-created hiding spots for items", - "Item hunt leaderboards with speed and efficiency metrics", - "Seasonal themed items with unique properties", - "Hint sharing system among players", - "Progressive clue unlocks for players lagging behind", - "Randomized environmental hazards during item hunts", - "Special game modes unlocked by finding rare items" -] - -# Survival by location or characters met -survival_mechanics = [ - "Location-based resource scarcity affecting survival strategies", - "Character relationships influencing survival odds", - "Dynamic weather system impacting survival difficulty", - "Randomized disease outbreaks requiring specific remedies", - "Crafting system adapted to location-specific materials", - "NPC trust system affecting trade and assistance availability", - "Time-based challenges for reaching safe locations", - "Unique survival skills unlocked by meeting specific characters", - "Randomized events testing different survival aspects", - "Permadeath mode with character legacy system", - "Base building mechanics adapted to different environments", - "Seasonal changes affecting survival strategies", - "Character-specific weaknesses requiring team-based survival", - "Morale system influenced by character interactions and environment", - "Scavenging missions with risk/reward balancing" -] - -# Additional community and randomization features -bonus_features = [ - "Player-created content voting and integration system", - "Randomized daily quests with community-wide rewards", - "Collaborative world-building events", - "Dynamic economy based on player actions and random events", - "Community-driven storyline branching", - "Randomized character backstories affecting gameplay", - "Player-run factions with territory control mechanics", - "Periodic world resets with randomized layouts", - "Community challenges requiring diverse skill combinations", - "Randomized NPC behavior and dialogue options", - "Player-created lore integration system", - "Dynamic difficulty scaling based on community skill level", - "Randomized ethical dilemmas with community-wide consequences", - "Collaborative raid events against procedurally generated dungeons", - "Community-driven development roadmap voting system" -] - -multiplayer_features = [ - "Real-time cooperative puzzle solving", - "Competitive leaderboards for speed runs or high scores", - "Shared world events affecting all players simultaneously", - "Player-vs-player mini-games or challenges", - "Guild or clan system for group activities", - "Trading system for in-game items or resources", - "Multiplayer-exclusive quests or storylines", - "Dynamic difficulty scaling based on group size and composition", - "Shared base-building or territory control mechanics", - "Cross-player skill or ability synergies" -] - -# Categorized lists -existing_game_inspirations = [ - gta_heist_inspired, - battlefront_inspired, - tekken_casino_inspired, - tactics_inspired, - chess_inspired, - cyberpunk_red_inspired -] - -feature_lists = [ - community_playthrough, - random_item_hunt, - survival_mechanics, - bonus_features -] - -# All lists combined -all_idea_lists = existing_game_inspirations + feature_lists + [multiplayer_features] - -# Create a list of all the idea lists for easy import and use -all_idea_lists = [ - gta_heist_inspired, - battlefront_inspired, - tekken_casino_inspired, - tactics_inspired, - chess_inspired, - cyberpunk_red_inspired, - community_playthrough, - random_item_hunt, - survival_mechanics, - bonus_features -] - -# List names for reference -list_names = [ - "GTA Heists observations", "Battlefront observations", "Tekken 'Casino' observations", "Tactics Games observations", "Chess observations", - "Cyberpunk RED observations", "Community Playthrough observations", "Random Item Hunt observations", - "Survival Mechanics observations", "Bonus Features observations", "Multiplayer Features observations" -] - -#-------------------------#-------------------------#-------------------------#------------------------- - -mermaidstorystructures = { - "Hidden Progression 1": - """graph TD - subgraph "Actual Timeline" - A1[Location 1 - First Visit] - A2[Location 2 - First Visit] - A3[Location 3 - First Visit] - A4[Location 4 - First Visit] - A1 --> A2 --> A3 --> A4 - end - subgraph "Narrative Timeline" - N1[Location 2 - Story Event 1] - N2[Location 4 - Story Event 2] - N3[Location 1 - Story Event 3] - N4[Location 3 - Story Event 4] - N1 --> N2 --> N3 --> N4 - end - subgraph "Hidden Timeline" - H1[Hidden Event 1] - H2[Hidden Event 2] - H3[Hidden Event 3] - H4[Hidden Event 4] - end - A1 -.-> N3 - A2 -.-> N1 - A3 -.-> N4 - A4 -.-> N2 - H1 -.- A1 - H1 -.- A2 - H1 -.- A3 - H1 -.- A4 - H2 -.- A1 - H2 -.- A2 - H2 -.- A3 - H2 -.- A4 - H3 -.- A1 - H3 -.- A2 - H3 -.- A3 - H3 -.- A4 - H4 -.- A1 - H4 -.- A2 - H4 -.- A3 - H4 -.- A4""", - - "Hidden Progression 2": - """graph LR - subgraph "Actual Timeline" - A1[Location 1 - First Visit] - A2[Location 2 - First Visit] - A3[Location 3 - First Visit] - A4[Location 4 - First Visit] - A1 --> A2 --> A3 --> A4 - end - subgraph "Narrative Timeline" - N1[Location 2 - Story Event 1] - N2[Location 4 - Story Event 2] - N3[Location 1 - Story Event 3] - N4[Location 3 - Story Event 4] - N1 --> N2 --> N3 --> N4 - end - subgraph "Hidden Timeline" - H1[Hidden Event 1] - H2[Hidden Event 2] - H3[Hidden Event 3] - H4[Hidden Event 4] - H1 --> H2 --> H3 --> H4 - end - A1 -.-> N3 - A2 -.-> N1 - A3 -.-> N4 - A4 -.-> N2 - H1 -.- N1 - H2 -.- N2 - H3 -.- N3 - H4 -.- N4""", - - "Hidden Progression 3": - """graph LR - subgraph "Actual Timeline" - A1[Location 1 - First Visit] - A2[Location 2 - First Visit] - A3[Location 3 - First Visit] - A4[Location 4 - First Visit] - A1 --> A2 --> A3 --> A4 - end - subgraph "Narrative Timeline" - N1[Location 2 - Story Event 1] - N2[Location 4 - Story Event 2] - N3[Location 1 - Story Event 3] - N4[Location 3 - Story Event 4] - N1 --> N2 --> N3 --> N4 - end - A1 -.-> N3 - A2 -.-> N1 - A3 -.-> N4 - A4 -.-> N2""", - - "Parallel Storylines Progression": - """graph TD - A[Start] --> B[Character 1 Introduction] - A --> C[Character 2 Introduction] - B --> D[Character 1 Event 1] - C --> E[Character 2 Event 1] - D --> F[Character 1 Event 2] - E --> G[Character 2 Event 2] - F --> H[Characters Meet] - G --> H - H --> I[Climax] - I --> J[Resolution] - """, - } - -examplemermaidconceptblendingstrutures = { - "Star Wars Bakery Example": - """flowchart TD - - A[Bakery] - B[Star Wars] - C[Adjectives] - D[Combinatorics] - E[Seed Ideas] - F[LLM Processing] - - A -- has attributes --> C - B -- has attributes --> C - C -- generates --> D - D -- creates --> E - E -- inputs to --> F - - subgraph Bakery Attributes - direction TB - A1[Sweet] - A2[Warm] - A3[Comforting] - A4[Artisanal] - A -- includes --> A1 - A -- includes --> A2 - A -- includes --> A3 - A -- includes --> A4 - end - - subgraph Star Wars Attributes - direction TB - B1[Galactic] - B2[Epic] - B3[Heroic] - B4[Dramatic] - B -- includes --> B1 - B -- includes --> B2 - B -- includes --> B3 - B -- includes --> B4 - end - - subgraph Combinatorics - direction TB - C1[Sweet Galactic] - C2[Warm Epic] - C3[Comforting Heroic] - C4[Artisanal Dramatic] - D -- generates --> C1 - D -- generates --> C2 - D -- generates --> C3 - D -- generates --> C4 - end - - subgraph Seed Ideas - direction TB - E1[Galactic Pastries] - E2[Epic Bread Battles] - E3[Heroic Baker's Quest] - E4[Dramatic Cake Decor] - E -- includes --> E1 - E -- includes --> E2 - E -- includes --> E3 - E -- includes --> E4 - end""" -} - -#-------------------------#-------------------------#-------------------------#------------------------- - -ExampleGameConfigs = { - "Racinggametest": { - "pitLane": { - "start": { - "description": "You're in the pit lane of a famous racing circuit. There's buzz about a legendary race coming up.", - "choices": [ - "check car", - "talk to crew", - "rest" - ], - "transitions": { - "check car": "pitLane_carPrep", - "talk to crew": "pitLane_crewInfo", - "rest": "pitLane_start" - }, - "consequences": { - "check car": {} - }, - "media": [] - }, - "crewInfo": { - "description": "Your crew tells you about the challenging track and the fierce competition.", - "choices": [ - "start preparations", - "ignore advice" - ], - "transitions": { - "start preparations": "pitLane_carPrep", - "ignore advice": "pitLane_start" - }, - "consequences": { - "start preparations": {} - }, - "media": [] - }, - "carPrep": { - "description": "You and your crew prepare the car for the race.", - "choices": [ - "head to grid", - "more tuning" - ], - "transitions": { - "head to grid": "track_startingGrid", - "more tuning": "pitLane_start" - }, - "media": [] - } - }, - "track": { - "startingGrid": { - "description": "You're on the starting grid. The atmosphere is tense as drivers prepare for the race.", - "choices": [ - "start race", - "return to pit" - ], - "transitions": { - "start race": "track_firstLap", - "return to pit": "pitLane_start" - }, - "media": [] - }, - "firstLap": { - "description": "The race begins! You navigate through the first lap, jostling for position.", - "choices": [ - "overtake", - "maintain position" - ], - "transitions": { - "overtake": "track_midRace", - "maintain position": "track_midRace" - }, - "media": [] - }, - "midRace": { - "description": "You're in the middle of the race, facing challenges and making split-second decisions.", - "choices": [ - "push harder", - "conserve tires" - ], - "transitions": { - "push harder": "track_finalLaps", - "conserve tires": "track_finalLaps" - }, - "media": [] - }, - "finalLaps": { - "description": "The final laps approach. Victory is within reach, but so is the risk of a mistake.", - "choices": [ - "give it all", - "play it safe" - ], - "transitions": { - "give it all": "track_finishLine", - "play it safe": "track_finishLine" - }, - "consequences": { - "give it all": {} - }, - "media": [] - }, - "finishLine": { - "description": "You cross the finish line! The results are in.", - "choices": [ - "check results", - "return to pit" - ], - "transitions": { - "check results": "podium_ceremony", - "return to pit": "pitLane_start" - }, - "media": [] - } - }, - "podium": { - "ceremony": { - "description": "The podium ceremony begins. Your performance is recognized.", - "choices": [ - "celebrate", - "prepare for next race" - ], - "transitions": { - "celebrate": "end_celebration", - "prepare for next race": "pitLane_start" - }, - "consequences": { - "celebrate": {} - }, - "media": [] - } - }, - "end": { - "celebration": { - "description": "You celebrate your achievement. Your team and fans are overjoyed.", - "choices": [ - "end race day" - ], - "transitions": { - "end race day": "end_end" - }, - "media": [] - }, - "end": { - "description": "Your racing day ends here. Your performance has impacted your standing in the championship.", - "choices": [], - "transitions": {}, - "media": [] - } - } - }, - - "Eventplanningstortytest": { - "tavern": { - "start": { - "description": "The air crackles with anticipation as you step into the bustling event planning expo. Caterers offer tantalizing samples, florists boast breathtaking arrangements, and a charismatic DJ promises an unforgettable party atmosphere.", - "choices": ["approach the DJ", "sample the catering", "admire the floral displays"], - "transitions": { - "approach the DJ": "tavern_rumor", - "sample the catering": "tavern_grog", - "admire the floral displays": "tavern_start" - }, - "consequences": {}, - "media": [] - }, - "rumor": { - "description": "The DJ, a master of hype, spins tales of legendary events he's orchestrated. He mentions a client, a mysterious socialite, planning an extravagant masked ball with a budget that could make your career.", - "choices": ["express your interest", "dismiss him as a showman"], - "transitions": { - "express your interest": "tavern_map", - "dismiss him as a showman": "tavern_start" - }, - "consequences": {}, - "media": [] - }, - "grog": { - "description": "You savor the exquisite flavors of miniature gourmet dishes. The caterer, a true artist, describes their vision for a multi-sensory dining experience.", - "choices": ["inquire about their rates", "network with other attendees"], - "transitions": { - "inquire about their rates": "tavern_grog", - "network with other attendees": "tavern_start" - }, - "media": [] - }, - "map": { - "description": "The DJ, sensing your ambition, offers a tantalizing clue: the socialite is looking for a fresh perspective. He suggests you submit a proposal, highlighting your unique vision.", - "choices": ["propose a themed experience", "focus on logistics and budget"], - "transitions": { - "propose a themed experience": "docks_prepare", - "focus on logistics and budget": "tavern_haggle" - }, - "consequences": {}, - "media": [] - }, - "haggle": { - "description": "The DJ warns you that the socialite is notoriously demanding. \"She expects perfection,\" he cautions.", - "choices": ["assure him you deliver excellence", "reconsider your options"], - "transitions": { - "assure him you deliver excellence": "docks_prepare", - "reconsider your options": "tavern_start" - }, - "consequences": {}, - "media": [] - }, - "fight": { - "description": "You overhear another event planner bragging about securing the masked ball contract. Jealousy burns, and you're tempted to challenge their claim.", - "choices": ["confront the planner", "stay focused on your goals"], - "transitions": { - "confront the planner": "tavern_victory", - "stay focused on your goals": "tavern_defeat" - }, - "consequences": {}, - "media": [] - }, - "victory": { - "description": "You confront the planner, exposing their lies. The truth prevails, and word of your integrity reaches the socialite.", - "choices": ["prepare your proposal"], - "transitions": { - "prepare your proposal": "docks_prepare" - }, - "consequences": {}, - "media": [] - }, - "defeat": { - "description": "You decide not to stoop to their level. Trusting your instincts, you continue networking and refining your pitch.", - "choices": ["continue networking"], - "transitions": { - "continue networking": "tavern_start" - }, - "consequences": {}, - "media": [] - } - }, - "docks": { - "prepare": { - "description": "Armed with a killer concept, you retreat to your studio to craft the perfect proposal. It's time to gather your team: the best florists, caterers, entertainers...", - "choices": ["contact your florist", "secure a caterer"], - "transitions": { - "contact your florist": "docks_ship", - "secure a caterer": "docks_crew" - }, - "media": [] - }, - "ship": { - "description": "Your trusted florist presents a stunning array of options, from elegant orchids to whimsical fairy lights. You envision transforming the venue into a magical landscape.", - "choices": ["finalize the floral arrangements", "explore additional decor"], - "transitions": { - "finalize the floral arrangements": "docks_captain", - "explore additional decor": "docks_prepare" - }, - "media": [] - }, - "crew": { - "description": "You browse through portfolios of caterers, each offering a unique culinary experience. The decision is crucial: the food must be as unforgettable as the event itself.", - "choices": ["choose a gourmet caterer", "opt for a themed buffet"], - "transitions": { - "choose a gourmet caterer": "docks_captain", - "opt for a themed buffet": "docks_captain" - }, - "consequences": {}, - "media": [] - }, - "captain": { - "description": "With each element falling into place, your vision for the masked ball comes alive. The proposal is complete - a masterpiece of creativity and meticulous planning.", - "choices": ["submit your proposal"], - "transitions": { - "submit your proposal": "openSea_start" - }, - "media": [] - } - }, - "openSea": { - "start": { - "description": "Days turn into nights as you await a response. The uncertainty is agonizing, your hopes rising and falling with every ring of the phone.", - "choices": ["check your emails obsessively", "distract yourself with another project"], - "transitions": { - "check your emails obsessively": "openSea_storm", - "distract yourself with another project": "openSea_mutiny" - }, - "media": [] - }, - "storm": { - "description": "A wave of despair crashes over you as you receive a generic rejection email. Your carefully crafted proposal, your dreams... dashed.", - "choices": ["analyze the email for feedback", "move on to the next opportunity"], - "transitions": { - "analyze the email for feedback": "openSea_ghostShip", - "move on to the next opportunity": "openSea_start" - }, - "media": [] - }, - "mutiny": { - "description": "Your phone vibrates - an unknown number. Your heart races as you answer. It's the socialite's assistant, and they're impressed.", - "choices": ["accept their invitation to meet", "negotiate your terms"], - "transitions": { - "accept their invitation to meet": "openSea_ghostShip", - "negotiate your terms": "openSea_start" - }, - "consequences": {}, - "media": [] - }, - "ghostShip": { - "description": "You meet the socialite - an enigmatic figure radiating elegance and power. They're intrigued by your vision, your passion.", - "choices": ["present your ideas in detail", "listen to their expectations"], - "transitions": { - "present your ideas in detail": "ghostShip_deck", - "listen to their expectations": "openSea_start" - }, - "media": [] - } - }, - "ghostShip": { - "deck": { - "description": "The socialite listens intently as you unveil your plans, their eyes sparkling with approval. They love your innovative concepts, your attention to detail.", - "choices": ["discuss the budget and timeline", "negotiate your fee"], - "transitions": { - "discuss the budget and timeline": "ghostShip_brig", - "negotiate your fee": "ghostShip_quarters" - }, - "media": [] - }, - "brig": { - "description": "Negotiations are swift and decisive. The socialite appreciates your professionalism and agrees to your terms. The masked ball is yours to create!", - "choices": ["celebrate your success", "start planning immediately"], - "transitions": { - "celebrate your success": "ghostShip_clue", - "start planning immediately": "ghostShip_treasure" - }, - "media": [] - }, - "quarters": { - "description": "You secure a contract that exceeds your wildest dreams. The masked ball, a testament to your talent and dedication, will be your masterpiece.", - "choices": ["assemble your dream team", "order the first batch of champagne"], - "transitions": { - "assemble your dream team": "ghostShip_clue", - "order the first batch of champagne": "ghostShip_treasure" - }, - "media": [] - }, - "clue": { - "description": "Excitement courses through you as you share the good news with your team. The best in the business, they're ready to help you execute this extraordinary event.", - "choices": ["begin the preparations"], - "transitions": { - "begin the preparations": "ghostShip_treasure" - }, - "media": [] - }, - "treasure": { - "description": "Weeks of tireless work culminate in the grand opening of the masked ball. It's a triumph - a symphony of elegance, enchantment, and pure joy.", - "choices": ["bask in the glory of your success", "network with potential clients"], - "transitions": { - "bask in the glory of your success": "ghostShip_victory", - "network with potential clients": "openSea_return" - }, - "consequences": {}, - "media": [] - }, - "victory": { - "description": "The masked ball becomes legendary, exceeding all expectations. Your name is on everyone's lips, synonymous with unforgettable events.", - "choices": ["claim your place as an event icon"], - "transitions": { - "claim your place as an event icon": "openSea_return" - }, - "consequences": {}, - "media": [] - } - }, - "openSea_return": { - "return": { - "description": "Your star continues to rise as you orchestrate one successful event after another. Your creativity knows no bounds, your reputation solidified.", - "choices": ["embrace your destiny as an event maestro"], - "transitions": { - "embrace your destiny as an event maestro": "end_end" - }, - "media": [] - } - }, - "end": { - "end": { - "description": "From humble beginnings at an industry expo, you've become an icon, transforming dreams into dazzling reality. The world is your canvas, and every event is a masterpiece.", - "choices": [], - "transitions": {}, - "media": [] - } - } - }, - -} - -#-------------------------#-------------------------#-------------------------#------------------------- - -Adjectivebasedcombinationideatexts = """For control over the flow of the game first need to solve chicken egg of story vs json then for each concept to mixed need to do the same. +This file re-exports everything from the split modules for backwards compatibility. +All existing `from relatively_constant_variables import *` will continue to work. -Gemini-test (23/07/2023) - -The kind of analysis you're describing, where you're looking for shared qualities and connections between seemingly disparate concepts like storytelling and JSON, can be referred to by several terms, depending on the specific focus and context: - -General Terms: - -Comparative Analysis: This is a broad term that encompasses any analysis where you're comparing and contrasting two or more things. -Analogical Reasoning: This involves identifying similarities between two things and using those similarities to understand or explain one in terms of the other. -Conceptual Blending: This refers to the cognitive process of combining different concepts to create new meanings or understandings. -Interdisciplinary Thinking: This involves drawing connections and insights from different fields of knowledge. -More Specific Terms (depending on the goal): - -Metaphorical Analysis: If you're focusing on the metaphorical connections between the concepts, this term would be appropriate. -Cross-Domain Mapping: This term is often used in cognitive science and artificial intelligence to describe the process of finding correspondences between different domains of knowledge. -Design Thinking: If your goal is to use these insights to create something new or solve a problem, this term might be relevant. -In the context of your specific example (storytelling and JSON): - -You could describe this analysis as: - -Finding Shared Principles of Effective Communication: Both storytelling and JSON are ultimately about conveying information effectively. -Exploring the Art of Structure and Organization: Both storytelling and JSON rely on clear structure and organization to achieve their respective goals. -Bridging the Gap Between Narrative and Data: This analysis highlights the unexpected connections between the seemingly disparate worlds of storytelling and data representation. -To further refine your search for resources and methods, consider these questions: - -What is your ultimate goal in merging these concepts? Are you trying to understand one better through the lens of the other? Are you trying to create something new? -What specific aspects of each concept are you most interested in comparing? Are you focusing on structure, clarity, engagement, or something else? -By clarifying your goals and focusing your analysis, you can more effectively leverage these different approaches and find the most relevant resources to guide your exploration.""" - - -Adjectivebasedcombinationideatextsv2 = """Nemotron 4 340B Perplexity Labs - -Original Prompt -Please help me. If I want to merge the idea of a bakery with the plot of star wars I need to find points where they match and dont match. I decided to use adjectives as a proxy so the combinatorics is initiated by close adjectives to generate seeds for combination ideas that we can send to an llm for each seed. How would this look in mermaid - - - -Mermaid Struxture - - -The workflow or process you've described can be generally categorized as a "concept blending" or "idea fusion" approach, which involves combining elements from two or more distinct concepts to generate new ideas. In your specific case, you're using adjectives as a proxy to facilitate the blending of a bakery concept with the plot of Star Wars. This process can also be considered a form of "creative problem-solving" or "design thinking," where you're systematically exploring and combining elements to generate innovative solutions or ideas. -More specifically, the steps in your workflow can be broken down into the following stages: -Concept Identification: Identifying the two distinct concepts (bakery and Star Wars) to be blended. -Attribute Extraction: Extracting relevant attributes (adjectives) from each concept. -Combinatorics: Systematically combining the attributes from both concepts to generate new combinations. -Seed Idea Generation: Translating the combined attributes into seed ideas that merge the two concepts. -LLM Processing: Utilizing a Large Language Model (LLM) to further develop and refine the seed ideas into more detailed and cohesive concepts. -This workflow can be applied to various creative problem-solving scenarios, not just limited to the bakery and Star Wars example - - - -Indeed, design can be viewed as a search algorithm, where you explore a vast space of possibilities to find optimal or innovative solutions. Here's how you can apply this idea to five more scenarios: -Merging a Library with a Music Festival -Concept Identification: Library and Music Festival -Attribute Extraction: Quiet, Knowledgeable, Organized (Library) vs. Loud, Energetic, Creative (Music Festival) -Combinatorics: Quiet Creative Spaces, Knowledgeable Music Workshops, Organized Jam Sessions -Seed Idea Generation: Silent Disco Library, Music Theory Workshops, Collaborative Songwriting Sessions -LLM Processing: Develop detailed event plans, marketing materials, and logistical considerations -Blending a Fitness Center with a Video Game Arcade -Concept Identification: Fitness Center and Video Game Arcade -Attribute Extraction: Active, Healthy, Competitive (Fitness Center) vs. Interactive, Fun, Social (Video Game Arcade) -Combinatorics: Active Interactive Games, Healthy Fun Challenges, Competitive Social Workouts -Seed Idea Generation: Exergaming Stations, Virtual Reality Fitness Challenges, Multiplayer Workout Games -LLM Processing: Create game concepts, rules, and scoring systems, as well as marketing and branding strategies -Fusing a Vegan Restaurant with a Science Museum -Concept Identification: Vegan Restaurant and Science Museum -Attribute Extraction: Plant-Based, Sustainable, Healthy (Vegan Restaurant) vs. Educational, Interactive, Innovative (Science Museum) -Combinatorics: Plant-Based Interactive Exhibits, Sustainable Educational Dining, Healthy Innovative Cuisine -Seed Idea Generation: Edible Plant Exhibits, Cooking Classes with a Scientific Twist, Molecular Gastronomy Menu -LLM Processing: Develop exhibit designs, class curricula, and menu items, as well as marketing and branding strategies -Combining a Co-working Space with a Pet Store -Concept Identification: Co-working Space and Pet Store -Attribute Extraction: Collaborative, Productive, Professional (Co-working Space) vs. Friendly, Playful, Nurturing (Pet Store) -Combinatorics: Collaborative Pet-Friendly Spaces, Productive Play Areas, Professional Pet Care Services -Seed Idea Generation: Pet-Friendly Co-working Desks, On-site Pet Daycare, Pet-themed Networking Events -LLM Processing: Create space layouts, pet care service offerings, and event plans, as well as marketing and branding strategies -Integrating a Fashion Boutique with an Art Gallery -Concept Identification: Fashion Boutique and Art Gallery -Attribute Extraction: Stylish, Trendy, Creative (Fashion Boutique) vs. Artistic, Thought-provoking, Inspirational (Art Gallery) -Combinatorics: Stylish Artistic Installations, Trendy Thought-provoking Fashion, Creative Inspirational Clothing -Seed Idea Generation: Art-inspired Fashion Collections, Interactive Fashion Exhibits, Artist-designed Clothing Lines -LLM Processing: Develop fashion collection concepts, exhibit designs, and artist collaborations, as well as marketing and branding strategies - -""" - -Nonlinearprogressionideas = """Llama 405B - -Non-linear storytelling involves presenting events out of chronological order. This technique can be used in literature, film, and other forms of narrative to create suspense, surprise, or to highlight certain themes or character developments. Here are some common examples of non-linear storytelling events: - -1. Flashbacks: These are scenes that take the story back in time, often used to explain a character's past or to provide context for the current situation. - -2. Flashforwards: Similar to flashbacks, but these scenes take the story forward in time, often used to create suspense or to show the consequences of current actions. - -3. Parallel storylines: This involves telling multiple stories that happen at the same time but are presented in a non-linear fashion, often intersecting or influencing each other at certain points. - -4. Reverse chronology: The story is presented in reverse order, starting with the conclusion and moving backward in time. - -5. Non-chronological chapters or scenes: The story is presented in a non-linear order, with each chapter or scene taking place at a different point in time. - -6. Time loops: A character relives the same period of time repeatedly, often with slight variations. - -7. Unreliable narrators: The narrator withholds or distorts information, leading to a non-linear presentation of events. - -8. Dream sequences: The story jumps into a character's dreams or imagination, which can be presented in a non-linear fashion. - -These are just a few examples of non-linear storytelling events. The possibilities are endless, and the technique can be adapted to fit the needs of the story and the creativity of the storyteller. +Modules: +- knowledge_base_data.py: RAG knowledge base +- story_elements.py: Player engagement items, story events, conflict ideas +- timeline_and_features.py: Timeline features, game inspirations, mermaid structures +- game_configs.py: Game states, configs, ExampleGameConfigs +- prompts_and_outputs.py: LLM prompts and their outputs +- platform_templates.py: PlayCanvas, Twine, HTML templates """ -TimeRelatedConceptsForIdeaGeneration = """Sonnet 3.5 - -Let's explore some other ways we can conceptualize time tangibly for storytelling purposes: - -Time as a Currency: -Imagine a world where time itself is a tradable resource. People can buy, sell, or exchange units of time. -Time as a Landscape: -Time could be visualized as a physical terrain that characters can traverse, with different eras represented by distinct geographical features. -Time as a Library: -Similar to the warehouse concept, but each moment in time is a book that can be read, written, or edited. -Time as a Network: -Envision time as an interconnected web of nodes, where each node represents an event or moment that can be accessed non-linearly. -Time as a Crafting Material: -In this concept, time could be molded, shaped, or combined like clay to create new realities or alter existing ones. -Time as a Ecosystem: -Here, different time periods interact like species in an ecosystem, with cause-and-effect relationships spanning across eras. -Time as a Computer Program: -Time could be seen as a vast codebase, where altering a line of code in the past changes the program's output in the future. -Time as a Painting: -Imagine time as a giant canvas where past, present, and future are all visible simultaneously, and characters can step into different parts of the painting. -Time as a Puzzle: -Each moment in time is a puzzle piece that can be rearranged to create different realities or outcomes. -Time as a Musical Composition: -Different time periods are like instruments in an orchestra, and changing the past is like altering the score, creating ripple effects through the entire composition. - -Investing (as you mentioned): - -Go back in time to invest in successful companies early -Preview future market trends - - -Law (as you mentioned): - -Go back in time to gather evidence or witness events firsthand -Preview potential outcomes of different legal strategies - - -Science (as you mentioned): - -Go to the future to see long-term experimental results -Observe long-term environmental changes - - -Medicine: - -Travel to the future to see long-term effects of treatments -Go back in time to prevent diseases before they start +# Re-export everything from split modules +from knowledge_base_data import * +from story_elements import * +from timeline_and_features import * +from game_configs import * +from prompts_and_outputs import * +from platform_templates import * - -Archaeology: - -Travel to the past to directly observe historical events -Verify theories about ancient civilizations - - -Journalism: - -Witness and report on historical events firsthand -Preview future events to prepare in-depth coverage - - -Environmental Conservation: - -Go to the future to see the impacts of current policies -Travel to the past to preserve extinct species - - -Engineering: - -Test structures over centuries in a matter of days -Go back in time to prevent disasters by fixing design flaws - - -Agriculture: - -Observe full growth cycles of crops in an instant -Travel to the future to develop crops resistant to future climate conditions - - -Criminal Investigation: - -Witness crimes as they happen in the past -Prevent crimes by foreseeing them in the future - - -Art Restoration: - -Go back in time to see artwork in its original state -Travel to the future to see how current restoration techniques hold up - - -Education: - -Take students on field trips to historical events -Preview future job markets to tailor current curricula - - -Disaster Management: - -Go back in time to implement better preventive measures -Travel to the future to assess long-term impacts of disasters - - -Politics: - -See the long-term effects of policies instantly -Go back in time to prevent international conflicts - - -Sports Coaching: - -Observe an athlete's entire career to optimize training from the start -Preview future competitions to develop winning strategies - - - -From this exercise, we can see some common themes emerging: - -Verification of past events or theories -Prevention of negative outcomes -Optimization of current processes -Long-term impact assessment -Preparation for future events - - - -This table outlines potential risk elements and game ideas for each of the themes we identified. Let's discuss these in more detail: - -Verification of past events: - -Risk: The butterfly effect. Even small interactions in the past could dramatically alter the present. -Game Idea: Players could be historians or detectives, tasked with observing past events to solve present-day mysteries. The challenge is to gather information without interfering with the past. - - -Prevention of negative outcomes: - -Risk: Unintended consequences. Preventing one disaster might lead to an even worse one. -Game Idea: Players manage multiple timelines, trying to create the best overall outcome. They must balance preventing catastrophes in one timeline against potential negative impacts in others. - - -Optimization of current processes: - -Risk: Over-optimization leading to system collapse. -Game Idea: Players could manage a company or even a country, tweaking past decisions to improve the present. However, pushing too hard for efficiency or profit could lead to unforeseen problems or total collapse. - - -Long-term impact assessment: - -Risk: Increasing uncertainty and danger the further into the future you go. -Game Idea: Players explore future timelines to gather data, but risk getting trapped in increasingly divergent and potentially hostile futures. The challenge is to gather enough information without going so far that you can't return. - - -Preparation for future events: - -Risk: Self-fulfilling prophecies. Preparing for a predicted future might be what causes it to happen. -Game Idea: Players navigate through potential futures, trying to prepare for or prevent them without inadvertently causing them. The game could involve complex cause-and-effect chains where players must carefully consider the implications of their actions. - - - -To further develop these into game concepts, we could consider: - -Resource management: Time itself could be a limited resource that players must manage carefully. -Paradox potential: Create tension by introducing the risk of paradoxes that could erase the player from existence. -Competing factions: Introduce other time travelers with different agendas, creating conflict and competition. -Technological limitations: Time travel devices could have cooldown periods, limited charges, or other restrictions that force strategic use. -Ethical dilemmas: Present players with morally ambiguous choices that have far-reaching consequences across time. -""" - -WFStage1prompt = """Use this to write a story and explain how it represents the structure - - -graph TD -subgraph "Actual Timeline" -A1[Location 1 - First Visit] -A2[Location 2 - First Visit] -A3[Location 3 - First Visit] -A4[Location 4 - First Visit] -A1 --> A2 --> A3 --> A4 -end -subgraph "Narrative Timeline" -N1[Location 2 - Story Event 1] -N2[Location 4 - Story Event 2] -N3[Location 1 - Story Event 3] -N4[Location 3 - Story Event 4] -N1 --> N2 --> N3 --> N4 -end -subgraph "Hidden Timeline" -H1[Hidden Event 1] -H2[Hidden Event 2] -H3[Hidden Event 3] -H4[Hidden Event 4] -end -A1 -.-> N3 -A2 -.-> N1 -A3 -.-> N4 -A4 -.-> N2 -H1 -.- A1 -H1 -.- A2 -H1 -.- A3 -H1 -.- A4 -H2 -.- A1 -H2 -.- A2 -H2 -.- A3 -H2 -.- A4 -H3 -.- A1 -H3 -.- A2 -H3 -.- A3 -H3 -.- A4 -H4 -.- A1 -H4 -.- A2 -H4 -.- A3 -H4 -.- A4""" - -TimeRelatedMermaidStoryAttempttoRefinefrom = ("""gpt-4o - -Once upon a time in a land where time flowed like a river with hidden currents, there was a story that unfolded across three intertwined timelines: the Actual Timeline, the Narrative Timeline, and the Hidden Timeline. - -Actual Timeline -The protagonist, Elena, embarked on a journey through four distinct locations, each holding a piece of a grand puzzle she needed to solve. - -Location 1 - First Visit: Elena started her journey here, uncovering an ancient map that hinted at a deeper mystery. -Location 2 - First Visit: She traveled to a bustling marketplace, where she found a cryptic message carved into a stone. -Location 3 - First Visit: In an abandoned library, Elena discovered a journal detailing strange occurrences and hidden events. -Location 4 - First Visit: Her journey led her to a serene forest where she encountered an enigmatic figure who offered her a crucial clue. -Narrative Timeline -As Elena pieced together the clues, the story she narrated to her friends followed a different sequence, highlighting the events that made the most sense from her perspective at the time. - -Location 2 - Story Event 1: Elena recalled the marketplace first, sharing the mystery of the cryptic message. -Location 4 - Story Event 2: She then described her encounter in the serene forest, emphasizing the importance of the clue she received. -Location 1 - Story Event 3: Elena recounted the discovery of the ancient map, giving context to the overall mystery. -Location 3 - Story Event 4: Lastly, she talked about the abandoned library and the journal, which tied all the events together. -Hidden Timeline -Unbeknownst to Elena, there was a hidden layer of events influencing her journey. - -Hidden Event 1: In the shadows, an ancient order monitored her progress from the moment she found the map, ensuring she remained on the right path. -Hidden Event 2: They subtly guided her towards the marketplace, planting the cryptic message for her to find. -Hidden Event 3: The journal in the library was placed there by the order, containing information they wanted her to uncover. -Hidden Event 4: The enigmatic figure in the forest was a member of the order, providing her with the final piece of the puzzle. -Interconnections -Actual Timeline to Narrative Timeline: -Location 1 (First Visit) led to Story Event 3 (Elena's recollection of the map). -Location 2 (First Visit) led to Story Event 1 (marketplace mystery). -Location 3 (First Visit) led to Story Event 4 (the journal). -Location 4 (First Visit) led to Story Event 2 (forest encounter). -Actual Timeline to Hidden Timeline: -Each visit in the Actual Timeline was influenced by the Hidden Events, which shaped Elena's experiences without her knowledge. -Story Representation -This structure represents the complexity of storytelling, where the sequence of events (Actual Timeline) is often different from how the story is told (Narrative Timeline), and there are unseen forces (Hidden Timeline) that influence the course of events. Elena’s journey through the four locations, her recollection of events, and the hidden manipulations all intertwine to create a rich, multi-layered narrative. - -In this story, the Actual Timeline shows Elena’s chronological journey. The Narrative Timeline reveals how she chose to share her experiences, emphasizing different aspects of her journey. The Hidden Timeline uncovers the secret influences that guided her path, adding depth and intrigue to the overall tale. - - - -Great attempt. Lets see how the story changes if we assume the main character goes to every location twice - - - -Certainly! Let's expand the story to include two visits to each location, which will add more depth and complexity to the narrative. - -Actual Timeline -Elena revisits each location, uncovering new layers of the mystery with each visit. - -Location 1 - First Visit: Elena starts her journey here, uncovering an ancient map that hints at a deeper mystery. -Location 2 - First Visit: She travels to a bustling marketplace, where she finds a cryptic message carved into a stone. -Location 3 - First Visit: In an abandoned library, Elena discovers a journal detailing strange occurrences and hidden events. -Location 4 - First Visit: Her journey leads her to a serene forest where she encounters an enigmatic figure who offers her a crucial clue. -Location 1 - Second Visit: Elena returns to the first location, where she deciphers the ancient map, revealing a hidden passage. -Location 2 - Second Visit: Back at the marketplace, she meets a mysterious merchant who provides additional information about the cryptic message. -Location 3 - Second Visit: In the library, she finds a hidden compartment in the journal containing a key. -Location 4 - Second Visit: Finally, in the forest, the enigmatic figure reappears, guiding her to the entrance of an ancient temple. -Narrative Timeline -Elena pieces together her adventure in a sequence that makes the most sense to her story. - -Location 2 - Story Event 1: Elena recalls the marketplace first, sharing the mystery of the cryptic message and the merchant’s information. -Location 4 - Story Event 2: She then describes her encounters in the serene forest, focusing on both the clue and the guidance to the temple. -Location 1 - Story Event 3: Elena recounts the discovery of the ancient map and the hidden passage. -Location 3 - Story Event 4: Lastly, she talks about the abandoned library, the journal, and the key. -Hidden Timeline -The ancient order continues to guide Elena’s journey, intervening subtly. - -Hidden Event 1: The order monitors Elena from the moment she finds the map, ensuring she is on the right path. -Hidden Event 2: They guide her towards the marketplace, planting the cryptic message and arranging the merchant’s appearance. -Hidden Event 3: The journal in the library is placed by the order, with the hidden compartment and key added during her first visit. -Hidden Event 4: The enigmatic figure in the forest is a member of the order, orchestrating both encounters to provide the necessary guidance. -Interconnections -Actual Timeline to Narrative Timeline: -Location 1 First Visit (map discovery) leads to Story Event 3 (hidden passage). -Location 2 First Visit (cryptic message) and Second Visit (merchant) combine into Story Event 1 (marketplace mystery). -Location 3 First Visit (journal) and Second Visit (key) combine into Story Event 4 (library findings). -Location 4 First Visit (clue) and Second Visit (temple guidance) combine into Story Event 2 (forest encounters). -Actual Timeline to Hidden Timeline: -Each visit in the Actual Timeline is influenced by the Hidden Events, shaping Elena’s experiences without her knowledge. -Expanded Story Representation -In this expanded story, the Actual Timeline now includes two visits to each location, reflecting Elena's deepening understanding of the mystery. The Narrative Timeline still presents her recollection of events, but now with added complexity as she integrates discoveries from both visits. The Hidden Timeline continues to reveal the secret influences that guide her path, adding further depth and intrigue. - -Elena’s repeated visits to each location allow her to uncover additional layers of the mystery, with each visit building on the previous one. This structure illustrates how stories can evolve with new information and perspectives, and how unseen forces can shape the journey in subtle but significant ways. -""", - -"""meta-llama-3.1-405b-instruct - -Here's a story based on the graph structure: - -Emily had always been fascinated by the mysterious disappearances that had taken place in the four locations she was about to visit. She packed her bags, said goodbye to her family, and set off on her journey. - -Her first stop was Location 1, an abandoned mansion on the outskirts of town. As she walked through the dusty halls, she stumbled upon a cryptic message scrawled on the wall: "The eyes that see are not the ones that look." Emily was intrigued but couldn't decipher the meaning. - -Next, she visited Location 2, an old bookstore in the city. While browsing through the shelves, she met the owner, an eccentric old man who told her about a mysterious book that was said to hold the key to the disappearances. Emily was skeptical, but the old man's words lingered in her mind. - -From there, she traveled to Location 3, a secluded monastery in the mountains. The monks welcomed her warmly and shared stories of a long-forgotten event that had taken place in the monastery's past. Emily sensed that there was more to the story, but the monks were tight-lipped about the details. - -Finally, she arrived at Location 4, a small island with a reputation for being cursed. As she explored the island, she stumbled upon a journal belonging to one of the disappeared individuals. The journal hinted at a dark secret, but the writer's words were cryptic and unclear. - -As Emily returned home, she began to piece together the events of her journey. She realized that the disappearances were not random events, but rather, they were all connected. The message on the wall in Location 1 was a warning, the book in Location 2 held a vital clue, the monastery's past was crucial to understanding the present, and the journal on the island was the key to unlocking the mystery. - -However, what Emily didn't know was that her journey was not just about uncovering the truth about the disappearances. Unbeknownst to her, a series of hidden events had been set in motion, influencing her actions and guiding her towards a specific outcome. These events, though invisible to Emily, were the true driving force behind the story. - -The story structure represented by the graph is non-linear. The actual timeline shows Emily visiting the four locations in a linear sequence (A1 -> A2 -> A3 -> A4). However, the narrative timeline is different, with the story events being presented out of chronological order (N1 -> N2 -> N3 -> N4). This non-linearity allows the story to build suspense and create a sense of mystery. - -The hidden timeline represents the unseen forces that shape the story. These events are not directly revealed to the reader, but they have a profound impact on the narrative. The connections between the hidden events and the actual timeline suggest that the story is more complex and layered than initially apparent. - -The graph structure highlights the relationships between the different timelines and events, demonstrating how a non-linear narrative can be constructed to create a engaging and mysterious story. - - - -Very Good Attempt. Lets see how your story will change under the assumption that each location is visited twice - - - -That's an interesting twist. Here's a revised story based on the assumption that each location is visited twice: - -Emily had always been fascinated by the mysterious disappearances that had taken place in the four locations she was about to visit. She packed her bags, said goodbye to her family, and set off on her journey. - -Her first stop was Location 1, an abandoned mansion on the outskirts of town. As she walked through the dusty halls, she stumbled upon a cryptic message scrawled on the wall: "The eyes that see are not the ones that look." Emily was intrigued but couldn't decipher the meaning. - -From there, she visited Location 2, an old bookstore in the city. While browsing through the shelves, she met the owner, an eccentric old man who told her about a mysterious book that was said to hold the key to the disappearances. Emily was skeptical, but the old man's words lingered in her mind. - -Next, she traveled to Location 3, a secluded monastery in the mountains. The monks welcomed her warmly and shared stories of a long-forgotten event that had taken place in the monastery's past. Emily sensed that there was more to the story, but the monks were tight-lipped about the details. - -Finally, she arrived at Location 4, a small island with a reputation for being cursed. As she explored the island, she stumbled upon a journal belonging to one of the disappeared individuals. The journal hinted at a dark secret, but the writer's words were cryptic and unclear. - -However, Emily's journey didn't end there. She felt that she had only scratched the surface of the mystery, and so she decided to revisit each location, hoping to uncover more clues. - -Upon her second visit to Location 1, Emily discovered a hidden room deep within the mansion's basement. Inside, she found a series of letters exchanged between the mansion's former occupants, revealing a dark family secret that seemed to be connected to the disappearances. - -Her second visit to Location 2 led her to a hidden compartment in the bookstore's shelves, where she found the mysterious book the old man had told her about. As she flipped through its pages, she realized that the book was more than just a simple novel – it was a guide to unlocking the secrets of the disappearances. - -During her second visit to Location 3, Emily managed to gain the trust of one of the monks, who shared with her the true story of the monastery's past. The monk's words revealed a shocking connection between the monastery and the disappearances, and Emily began to piece together the puzzle. - -Finally, her second visit to Location 4 took her to a hidden cave on the island, where she found a series of cryptic messages etched into the walls. The messages seemed to be a code, and Emily realized that they were the key to unlocking the secrets of the journal she had found during her first visit. - -As Emily returned home, she began to piece together the events of her journey. She realized that the disappearances were not random events, but rather, they were all connected. The message on the wall in Location 1 was a warning, the book in Location 2 held a vital clue, the monastery's past was crucial to understanding the present, and the journal on the island was the key to unlocking the mystery. - -However, what Emily didn't know was that her journey was not just about uncovering the truth about the disappearances. Unbeknownst to her, a series of hidden events had been set in motion, influencing her actions and guiding her towards a specific outcome. These events, though invisible to Emily, were the true driving force behind the story. - -The story structure remains non-linear, with the narrative timeline presenting the story events out of chronological order. The actual timeline shows Emily visiting each location twice, with the second visit revealing more clues and pieces of the puzzle. The hidden timeline continues to represent the unseen forces that shape the story, influencing Emily's actions and guiding her towards a specific outcome. - -Here's an updated graph to reflect the changes: - -graph TD -subgraph "Actual Timeline" -A1[Location 1 - First Visit] -A2[Location 2 - First Visit] -A3[Location 3 - First Visit] -A4[Location 4 - First Visit] -A5[Location 1 - Second Visit] -A6[Location 2 - Second Visit] -A7[Location 3 - Second Visit] -A8[Location 4 - Second Visit] -A1 --> A2 --> A3 --> A4 --> A5 --> A6 --> A7 --> A8 -end -subgraph "Narrative Timeline" -N1[Location 2 - Story Event 1] -N2[Location 4 - Story Event 2] -N3[Location 1 - Story Event 3] -N4[Location 3 - Story Event 4] -N5[Location 1 - Story Event 5] -N -""", - -"""mistral-large-2407 - -Story: The Unraveling of Time -Actual Timeline -In the quaint town of Harborside, four locations form the backbone of daily life: the Library (Location 1), the Market (Location 2), the Park (Location 3), and the Lighthouse (Location 4). The story unfolds as the protagonist, Emily, visits these locations in sequence. - -Location 1 - First Visit (Library): Emily starts her day at the library, researching old town records. -Location 2 - First Visit (Market): She then heads to the market, picking up fresh produce and chatting with the locals. -Location 3 - First Visit (Park): After lunch, Emily takes a leisurely stroll in the park, enjoying the serene environment. -Location 4 - First Visit (Lighthouse): Finally, she visits the lighthouse, where she meets the old lighthouse keeper who tells her stories about the town's past. -Narrative Timeline -The story, however, is not told in the sequence of Emily's visits. Instead, it jumps between events in a non-linear fashion, creating a sense of mystery and intrigue. - -Location 2 - Story Event 1 (Market): The story begins with a mysterious theft at the market, where Emily witnesses a valuable artifact being stolen. -Location 4 - Story Event 2 (Lighthouse): The narrative then shifts to the lighthouse, where Emily discovers hidden letters that hint at a decades-old secret. -Location 1 - Story Event 3 (Library): The story then flashes back to Emily's visit to the library, where she uncovers ancient documents that reveal a dark history of the town. -Location 3 - Story Event 4 (Park): The climax of the story takes place in the park, where Emily confronts the thief and unveils the truth behind the town's secrets. -Hidden Timeline -Underlying the visible events is a hidden timeline that influences the narrative without being directly visible. These hidden events represent the dark history of the town, hinted at but not fully revealed until the end. - -Hidden Event 1: A series of unexplained disappearances that occurred decades ago. -Hidden Event 2: A forgotten curse placed on the town by a disgruntled resident. -Hidden Event 3: A secret society formed to protect the town's dark secrets. -Hidden Event 4: A powerful artifact that holds the key to understanding the town's past. -Explanation of the Structure -The structure of the story is represented by the graph, which consists of three timelines: - -Actual Timeline: This represents the chronological order of Emily's visits to the locations. It shows the sequence of events as they actually happened. -Narrative Timeline: This represents the order in which the story is told. The events are not presented in chronological order but are arranged to build suspense and reveal information gradually. -Hidden Timeline: This represents the underlying events that influence the story but are not directly visible. These hidden events provide depth and complexity to the narrative. -The connections between the timelines (indicated by dashed lines) show how the actual events are linked to the narrative events and how the hidden events influence the overall story. This non-linear structure allows for a more engaging and mysterious narrative, keeping the reader intrigued until the final revelation. - - - -Very Good Attempt. Lets see how your story will change under the assumption that each location is visited twice - - - -Story: The Unraveling of Time (Revisited) -Actual Timeline -In the quaint town of Harborside, four locations form the backbone of daily life: the Library (Location 1), the Market (Location 2), the Park (Location 3), and the Lighthouse (Location 4). The story unfolds as the protagonist, Emily, visits these locations in sequence, but this time, she visits each location twice. - -Location 1 - First Visit (Library): Emily starts her day at the library, researching old town records. -Location 2 - First Visit (Market): She then heads to the market, picking up fresh produce and chatting with the locals. -Location 3 - First Visit (Park): After lunch, Emily takes a leisurely stroll in the park, enjoying the serene environment. -Location 4 - First Visit (Lighthouse): Finally, she visits the lighthouse, where she meets the old lighthouse keeper who tells her stories about the town's past. -Location 1 - Second Visit (Library): Emily returns to the library, this time finding a hidden compartment in an old book. -Location 2 - Second Visit (Market): She revisits the market and notices a strange symbol carved into one of the stalls. -Location 3 - Second Visit (Park): Emily returns to the park and discovers a hidden underground tunnel. -Location 4 - Second Visit (Lighthouse): She goes back to the lighthouse and finds a secret room containing old photographs and letters. -Narrative Timeline -The story is not told in the sequence of Emily's visits. Instead, it jumps between events in a non-linear fashion, creating a sense of mystery and intrigue. - -Location 2 - Story Event 1 (Market): The story begins with a mysterious theft at the market, where Emily witnesses a valuable artifact being stolen. -Location 4 - Story Event 2 (Lighthouse): The narrative then shifts to the lighthouse, where Emily discovers hidden letters that hint at a decades-old secret. -Location 1 - Story Event 3 (Library): The story then flashes back to Emily's visit to the library, where she uncovers ancient documents that reveal a dark history of the town. -Location 3 - Story Event 4 (Park): The climax of the story takes place in the park, where Emily confronts the thief and unveils the truth behind the town's secrets. -Location 1 - Story Event 5 (Library): Emily finds a hidden compartment in an old book, leading her to a cryptic map. -Location 2 - Story Event 6 (Market): She notices a strange symbol carved into one of the stalls, matching the symbol on the map. -Location 3 - Story Event 7 (Park): Emily discovers a hidden underground tunnel, which she believes is connected to the town's dark past. -Location 4 - Story Event 8 (Lighthouse): She finds a secret room containing old photographs and letters that provide the final pieces of the puzzle. -Hidden Timeline -Underlying the visible events is a hidden timeline that influences the narrative without being directly visible. These hidden events represent the dark history of the town, hinted at but not fully revealed until the end. - -Hidden Event 1: A series of unexplained disappearances that occurred decades ago. -Hidden Event 2: A forgotten curse placed on the town by a disgruntled resident. -Hidden Event 3: A secret society formed to protect the town's dark secrets. -Hidden Event 4: A powerful artifact that holds the key to understanding the town's past. -Hidden Event 5: The existence of a hidden map that leads to a secret underground chamber. -Hidden Event 6: The symbol that marks the entrance to the hidden chamber. -Hidden Event 7: The underground tunnel that connects all the locations. -Hidden Event 8: The secret room in the lighthouse that contains the final clues. -Explanation of the Structure -The structure of the story is represented by the graph, which consists of three timelines: - -Actual Timeline: This represents the chronological order -""", - -"""gpt-4o-mini-2024-07-18 - -Title: The Hidden Threads of Time - -Story: -In the quiet town of Eldridge, four locations held the secrets of a past intertwined with the present: The Old Oak Library, The Riverside Café, The Abandoned Mill, and The Clock Tower. Over a single summer, Sarah, a curious young woman, embarked on a journey that would reveal the layers of time hidden beneath the surface of her seemingly mundane life. - -Actual Timeline: A Journey Begins -Location 1: The Old Oak Library (A1) -Sarah’s adventure began at the Old Oak Library. As she browsed through the dusty shelves, she stumbled upon an ancient book that spoke of the town’s lore and the hidden events that shaped its history. Intrigued, she decided to explore more. - -Location 2: The Riverside Café (A2) -Her next stop was the Riverside Café, where she overheard a conversation between two elderly patrons discussing a tragic event tied to the town's history. They spoke in hushed tones about a long-lost love and a betrayal that had forever altered the lives of those involved. - -Location 3: The Abandoned Mill (A3) -The following day, Sarah ventured to the Abandoned Mill. There, she discovered remnants of a past life: photographs, letters, and a diary that hinted at a hidden romance. She felt a strange connection to the stories within these walls. - -Location 4: The Clock Tower (A4) -Finally, she arrived at the Clock Tower, where she learned about a secret society that once met there to protect the town’s secrets. This location felt alive with energy, as if the whispers of the past were echoing through time. - -Narrative Timeline: Unraveling the Mystery -Story Event 1: The Riverside Café (N1) -At the café, Sarah confronted the two patrons about their conversation. She learned that the tragic love story involved her great-grandparents, whose destinies were forever altered by a choice made in that very café. - -Story Event 2: The Clock Tower (N2) -Back at the Clock Tower, she discovered a hidden room filled with documents that detailed the secret society's attempts to mend the heartbreak caused by her ancestors. Here, she found a letter addressed to her great-grandmother, containing a heartfelt apology from her great-grandfather. - -Story Event 3: The Old Oak Library (N3) -Returning to the library, Sarah pieced together her family’s history through the ancient book. It revealed that the love story had been a catalyst for change in Eldridge, leading to the establishment of the society that aimed to protect the town from repeating past mistakes. - -Story Event 4: The Abandoned Mill (N4) -In the mill, Sarah re-read the diary and uncovered the truth: the betrayal was a misunderstanding fueled by jealousy and miscommunication. Understanding this allowed Sarah to forgive her ancestors and break the cycle of pain that had lingered in her family for generations. - -Hidden Timeline: The Secrets Beneath -Throughout her journey, Sarah was unaware of the Hidden Events that had shaped her family’s history and the town of Eldridge: - -Hidden Event 1 (H1): A Betrayal -Long ago, a betrayal occurred that led to the rift between Sarah’s great-grandparents. This event was the catalyst for the tragic love story that would echo through time. - -Hidden Event 2 (H2): The Secret Society's Formation -In the wake of the betrayal, a secret society formed to protect the town from the repercussions of the past, striving to mend what was broken. - -Hidden Event 3 (H3): The Lost Letter -A letter written by Sarah’s great-grandfather remained hidden for decades, containing apologies and hopes for reconciliation that were never delivered. - -Hidden Event 4 (H4): The Town’s Lore -The stories of Eldridge were carefully curated, with certain truths obscured to protect the living from the pain of the past. - -Structure Explanation: -The story’s structure represents three distinct timelines—Actual Timeline, Narrative Timeline, and Hidden Timeline—which interact in complex ways: - -Actual Timeline: This is the chronological order of Sarah’s visits to the four locations, marking her physical journey through the town. It serves as the backbone of the story, grounding it in reality and providing a structure for her exploration. -Narrative Timeline: This timeline unfolds the key story events that arise from Sarah’s discoveries at each location. Unlike the Actual Timeline, the Narrative Timeline does not follow a linear path but instead reveals the emotional and thematic significance of her journey, highlighting the connections between past and present. -Hidden Timeline: This timeline consists of events and truths that remain obscured until Sarah unearths them through her exploration. The hidden events symbolize the burdens of history that affect the present, illustrating how the past can shape one’s -""", - -"""athene-70b - -Story: The Cartographer's Enigma - -Protagonist Emilia, a skilled cartographer, receives a cryptic atlas from her reclusive mentor, featuring four locations: Ravenshire, Luminaria, Celestia's Peak, and Argentum Falls. The atlas hints at a centuries-old conspiracy, weaving together seemingly unrelated events across these sites. - -Narrative Timeline: - -Luminaria - The Celestial Alignment (N1): Emilia discovers an ancient celestial map etched into Luminaria's observatory walls, forecasting a rare alignment that unlocks a hidden chamber beneath Argentum Falls (A4). -Argentum Falls - The Forgotten Treaty (N2): Within the chamber, Emilia uncovers a long-lost treaty between ancient nations, which was negotiated at Ravenshire (A1) and ratified at Celestia's Peak (A3). -Ravenshire - The Cartographer's Legacy (N3): Emilia finds her mentor's hidden journals in Ravenshire, detailing the atlas's creation and the significance of Celestia's Peak, where an ancient order once protected the treaty. -Celestia's Peak - The Order's Reckoning (N4): At the peak, Emilia confronts a descendant of the ancient order, who reveals that they had been safeguarding the treaty against rogue factions. The descendant hands over a crucial, annotated atlas page, exposing the hidden events. -Actual Timeline (Emilia's Journey): - -Emilia visits the locations in the following order: Ravenshire (A1), Luminaria (A2), Celestia's Peak (A3), and Argentum Falls (A4). Each location builds upon her understanding, though the narrative jumps between them to create suspense and reveal connections. - -Hidden Timeline (Influencing Events): - -Hidden Event 1: A centuries-old astronomer in Luminaria (H1) calculates the celestial alignment, inspiring the treaty. -Hidden Event 2: A rogue faction infiltrates the ancient order at Celestia's Peak (H2), seeding corruption. -Hidden Event 3: Emilia's mentor, aware of the conspiracy, crafts the atlas (H3) to guide a worthy successor. -Hidden Event 4: The ancient order's remnants secretly aid Emilia's journey (H4), ensuring her discovery of the truth. -Structure Representation: - -Non-linear narrative: The narrative timeline (N1-N4) rearranges Emilia's actual visits (A1-A4) to build tension, reveal connections, and create an engaging mystery. -Hidden influences: The hidden events (H1-H4) permeate the actual timeline, motivating character actions, and informing the narrative's twists. Dotted lines illustrate how these events subtly shape Emilia's journey, enriching the story's depth and complexity. -Interconnectedness: The graph visually demonstrates how each location, narrative beat, and hidden event intertwines, producing a rich, layered narrative that rewards close attention and reflection. As Emilia unravels the enigma, the reader experiences a satisfying convergence of threads, highlighting the power of deliberate, non-linear storytelling. -""" - -) - -WFStage2prompt = """Please use the below config as a template to convert the story to config format (dont use the template values like masterlocation1): - -{ - "masterlocation1": { - "location1": { - "description": "UI - Experience Bar", - "events": [], - "choices": [ - "continue" - ], - "transitions": { - "continue": "masterlocation1_location2" - }, - "media": [], - "developernotes": [] - }, - - eg. {"tavern": {"start": {"description": "You wake up in a dimly lit tavern, the smell of ale and salt heavy in the air. A grizzled sailor at the bar boasts about a hidden island overflowing with \"more gold than ye can dream of!\" ", "choices": ["approach sailor", "ignore sailor", "order grog"], "transitions": {"approach sailor": "tavern_rumor", "ignore sailor": "tavern_start", "order grog": "tavern_grog"}, "consequences": {}, "media": []}, "rumor": {"description": "The sailor, three sheets to the wind, spins a tale of treacherous waters, ancient curses, and a map hidden on a ghost ship. He claims to know the way.", "choices": ["offer to buy him another drink", "dismiss him as a drunkard"], "transitions": {"offer to buy him another drink": "tavern_map", "dismiss him as a drunkard": "tavern_start"}, "consequences": {}, "media": []}, "grog": {"description": "The tavern keeper slides you a tankard of potent grog. It burns pleasantly as it goes down, and you feel your senses dull.", "choices": ["order another", "eavesdrop on nearby pirates"], "transitions": {"order another": "tavern_grog", "eavesdrop on nearby pirates": "tavern_start"}, "media": []}, "map": {"description": "With a wink and a hiccup, the sailor produces a crumpled piece of parchment. It's stained with salt and what looks suspiciously like blood, but the markings... they resemble a map! ", "choices": ["propose a partnership", "try to buy the map", "steal the map"], "transitions": {"propose a partnership": "docks_prepare", "try to buy the map": "tavern_haggle", "steal the map": "tavern_fight"}, "consequences": {}, "media": []}, "haggle": {"description": "The sailor eyes you shrewdly. \"This here map's worth a king's ransom,\" he slurs.", "choices": ["offer a pouch of gold", "walk away"], "transitions": {"offer a pouch of gold": "docks_prepare", "walk away": "tavern_start"}, "consequences": {}, "media": []}, "fight": {"description": "You lunge for the map, but the sailor's quicker than he looks! A brawl erupts, tables are overturned, and mugs fly through the air.", "choices": ["fight dirty", "try to reason with him"], "transitions": {"fight dirty": "tavern_victory", "try to reason with him": "tavern_defeat"}, "consequences": {}, "media": []}, "victory": {"description": "With a final blow, you knock the sailor unconscious. You grab the map and make a hasty exit.", "choices": ["head to the docks"], "transitions": {"head to the docks": "docks_prepare"}, "consequences": {}, "media": []}, "defeat": {"description": "The sailor and a couple of his cronies land some solid punches. You're tossed out of the tavern, bruised and map-less.", "choices": ["nurse your wounds"], "transitions": {"nurse your wounds": "tavern_start"}, "consequences": {}, "media": []}}, "docks": {"prepare": {"description": "The salty air of the docks fills your lungs. You can practically taste the adventure on the horizon. But first, you need a ship and a crew...", "choices": ["find a ship", "assemble a crew"], "transitions": {"find a ship": "docks_ship", "assemble a crew": "docks_crew"}, "media": []}, "ship": {"description": "You spot a sturdy-looking galleon with a \"For Sale\" sign hanging precariously from its mast.", "choices": ["inspect the ship", "look for another vessel"], "transitions": {"inspect the ship": "docks_captain", "look for another vessel": "docks_prepare"}, "media": []}, "crew": {"description": "The docks are teeming with salty dogs of all shapes and sizes.", "choices": ["recruit seasoned sailors", "round up some desperate souls"], "transitions": {"recruit seasoned sailors": "docks_captain", "round up some desperate souls": "docks_captain"}, "consequences": {}, "media": []}, "captain": {"description": "With a ship and a crew (of varying competence), you're ready to set sail! ", "choices": ["set sail for the open sea!"], "transitions": {"set sail for the open sea!": "openSea_start"}, "media": []}}, "openSea": {"start": {"description": "The open sea stretches before you, vast and unforgiving. The wind whips at your sails as you set course for the uncharted waters where the treasure awaits.", "choices": ["consult the map", "enjoy the voyage"], "transitions": {"consult the map": "openSea_storm", "enjoy the voyage": "openSea_mutiny"}, "media": []}, "storm": {"description": "A storm gathers on the horizon, dark clouds swirling ominously. Waves crash against the hull, threatening to swallow you whole.", "choices": ["weather the storm", "change course"], "transitions": {"weather the storm": "openSea_ghostShip", "change course": "openSea_start"}, "media": []}, "mutiny": {"description": "Your crew, a motley bunch at best, begin to grumble. They're growing impatient and greedy, their eyes glinting with mutiny.", "choices": ["quell the mutiny", "reason with them"], "transitions": {"quell the mutiny": "openSea_ghostShip", "reason with them": "openSea_start"}, "consequences": {}, "media": []}, "ghostShip": {"description": "Through the mist and fog, a ghostly silhouette emerges. A ship, its sails in tatters, manned by skeletal figures. This... is the ghost ship.", "choices": ["board the ghost ship", "flee"], "transitions": {"board the ghost ship": "ghostShip_deck", "flee": "openSea_start"}, "media": []}}, "ghostShip": {"deck": {"description": "You step aboard the ghost ship, the air heavy with the stench of decay. Eerie silence hangs over the vessel.", "choices": ["explore the ship", "search for the captain's quarters"], "transitions": {"explore the ship": "ghostShip_brig", "search for the captain's quarters": "ghostShip_quarters"}, "media": []}, "brig": {"description": "You stumble upon the ship's brig, its cells still occupied by skeletal remains shackled to the walls.", "choices": ["examine the skeletons", "leave the brig"], "transitions": {"examine the skeletons": "ghostShip_clue", "leave the brig": "ghostShip_deck"}, "media": []}, "quarters": {"description": "The captain's quarters are in disarray, as if a struggle took place. A tattered journal lies open on the floor.", "choices": ["read the journal", "search for the map"], "transitions": {"read the journal": "ghostShip_clue", "search for the map": "ghostShip_treasure"}, "media": []}, "clue": {"description": "You piece together clues from the journal and the skeletons. The ghost ship's captain hid the real treasure map to protect it from mutineers.", "choices": ["continue searching"], "transitions": {"continue searching": "ghostShip_treasure"}, "media": []}, "treasure": {"description": "After careful searching, you find a hidden compartment behind a portrait. Inside, a gleaming treasure chest awaits!", "choices": ["open the chest", "take the chest and flee"], "transitions": {"open the chest": "ghostShip_victory", "take the chest and flee": "openSea_return"}, "consequences": {}, "media": []}, "victory": {"description": "The treasure chest overflows with gold, jewels, and artifacts beyond your wildest dreams! You've found the legendary pirate treasure!", "choices": ["claim your prize and set sail"], "transitions": {"claim your prize and set sail": "openSea_return"}, "consequences": {}, "media": []}}, "openSea_return": {"return": {"description": "With the treasure safely stowed on your own ship, you set sail for home, leaving the ghost ship to its eternal slumber.", "choices": ["sail into the sunset"], "transitions": {"sail into the sunset": "end_end"}, "media": []}}, "end": {"end": {"description": "The sun dips below the horizon, painting the sky in hues of gold and crimson. You've cheated death, outsmarted ghosts, and claimed a fortune in pirate treasure. Your legend will be sung in taverns for centuries to come.", "choices": [], "transitions": {}, "media": []}}} - -The user will provide a theme -""" - -WFStage2prompt2 = """Please use the below config as a template to convert the story to config format (dont use the template values like masterlocation1): - -{ - "masterlocation1": { - "location1": { - "description": "UI - Experience Bar", - "events": [], - "choices": [ - "continue" - ], - "transitions": { - "continue": "masterlocation1_location2" - }, - "media": [], - "developernotes": [] - }, - - There should be 3 master and 10 total sublocations. - The theme is provided by the user -""" - - -TimeRelatedMermaidStoryAttempttoRefinefromtoJSON = ( -""" -{ - "masterlocation1": { - "location1": { - "description": "The Old Oak Library", - "events": [ - "Sarah discovers an ancient book about the town's lore and hidden history." - ], - "choices": [ - "continue" - ], - "transitions": { - "continue": "masterlocation1_location2" - }, - "media": [], - "developernotes": [ - "This is the starting point of Sarah's journey (A1 in Actual Timeline)" - ] - }, - "location2": { - "description": "The Riverside Café", - "events": [ - "Sarah overhears a conversation about a tragic event in the town's history.", - "She learns about a long-lost love and betrayal." - ], - "choices": [ - "confront patrons", - "continue exploring" - ], - "transitions": { - "confront patrons": "masterlocation2_location1", - "continue exploring": "masterlocation1_location3" - }, - "media": [], - "developernotes": [ - "This location appears in both A2 (Actual Timeline) and N1 (Narrative Timeline)" - ] - }, - "location3": { - "description": "The Abandoned Mill", - "events": [ - "Sarah discovers old photographs, letters, and a diary hinting at a hidden romance." - ], - "choices": [ - "continue" - ], - "transitions": { - "continue": "masterlocation1_location4" - }, - "media": [], - "developernotes": [ - "This location is A3 in the Actual Timeline" - ] - }, - "location4": { - "description": "The Clock Tower", - "events": [ - "Sarah learns about a secret society that protected the town's secrets." - ], - "choices": [ - "investigate further", - "return to library" - ], - "transitions": { - "investigate further": "masterlocation2_location2", - "return to library": "masterlocation2_location3" - }, - "media": [], - "developernotes": [ - "This is A4 in the Actual Timeline and leads to N2 in the Narrative Timeline" - ] - } - }, - "masterlocation2": { - "location1": { - "description": "The Riverside Café - Confrontation", - "events": [ - "Sarah confronts the patrons about their conversation.", - "She learns that the tragic love story involves her great-grandparents." - ], - "choices": [ - "continue" - ], - "transitions": { - "continue": "masterlocation2_location2" - }, - "media": [], - "developernotes": [ - "This is N1 in the Narrative Timeline" - ] - }, - "location2": { - "description": "The Clock Tower - Hidden Room", - "events": [ - "Sarah discovers a hidden room with documents about the secret society.", - "She finds a letter addressed to her great-grandmother from her great-grandfather." - ], - "choices": [ - "continue" - ], - "transitions": { - "continue": "masterlocation2_location3" - }, - "media": [], - "developernotes": [ - "This is N2 in the Narrative Timeline" - ] - }, - "location3": { - "description": "The Old Oak Library - Piecing Together History", - "events": [ - "Sarah connects her family's history to the town's changes.", - "She learns about the establishment of the secret society." - ], - "choices": [ - "continue" - ], - "transitions": { - "continue": "masterlocation2_location4" - }, - "media": [], - "developernotes": [ - "This is N3 in the Narrative Timeline" - ] - }, - "location4": { - "description": "The Abandoned Mill - Uncovering the Truth", - "events": [ - "Sarah re-reads the diary and discovers the truth about the betrayal.", - "She realizes it was a misunderstanding fueled by jealousy and miscommunication." - ], - "choices": [ - "reflect on journey" - ], - "transitions": { - "reflect on journey": "masterlocation3_location1" - }, - "media": [], - "developernotes": [ - "This is N4 in the Narrative Timeline" - ] - } - }, - "masterlocation3": { - "location1": { - "description": "Hidden Timeline Revelations", - "events": [ - "The original betrayal that caused the rift between Sarah's great-grandparents is revealed.", - "The formation of the secret society to protect the town is uncovered.", - "The existence of the lost letter from Sarah's great-grandfather is discovered.", - "The truth about the carefully curated town lore is exposed." - ], - "choices": [ - "conclude journey" - ], - "transitions": { - "conclude journey": "end" - }, - "media": [], - "developernotes": [ - "This represents the Hidden Timeline (H1, H2, H3, H4)", - "These events are not directly experienced but are revealed through Sarah's discoveries" - ] - } - } -} -""", -) - - -#-------------------------#-------------------------#-------------------------#------------------------- - -finished_product_demo = { - 'village': { - 'start': { - "description": "You wake up in a small village. You hear a rumor about a lost treasure.", - "choices": ['explore village', 'gather supplies', 'rest'], - "transitions": {'explore village': 'village_rumor', 'gather supplies': 'village_supplies', 'rest': 'village_start'}, - "consequences": { - 'gather supplies': lambda player: player.add_item('basic supplies') - }, - "media": ["testmedia/fpe_small_village.webp", "testmedia/fpe_rumor_about_a_lost_treasure.webp", "testmedia/fpe_gathering_supplies_in_a_village.webp"] - }, - 'rumor': { - "description": "You hear more details about the treasure hidden in the ancient ruins nearby.", - "choices": ['decide to go', 'ignore'], - "transitions": {'decide to go': 'village_supplies', 'ignore': 'village_start'}, - "consequences": { - 'decide to go': lambda player: player.update_knowledge('treasure location') - }, - "media": ["testmedia/fpe_rumor_about_a_lost_treasure.webp"] - }, - 'supplies': { - "description": "You gather supplies for your journey.", - "choices": ['head to forest', 'stay in village'], - "transitions": {'head to forest': 'forest_forest', 'stay in village': 'village_start'}, - "media": ["testmedia/fpe_person_looking_away_Listening_I_cant_hear_you_pose.webp", "testmedia/fpr_person_looking_away_Listening_hand_over_ear.webp", "testmedia/fpe_You_gather_supplies_for_your_journey.webp", "testmedia/fpe_head_to_forest.webp"] - }, - }, - 'forest': { - 'forest': { - "description": "You enter the dense forest, heading towards the ruins.", - "choices": ['travel further', 'return to village'], - "transitions": {'travel further': 'ruins_ruins', 'return to village': 'village_start'}, - "media": ["testmedia/fpe_village_start_collage.png", ] - }, - }, - 'ruins': { - 'ruins': { - "description": "You reach the ancient ruins. The entrance is dark and eerie.", - "choices": ['enter ruins', 'return to forest'], - "transitions": {'enter ruins': 'ruins_explore', 'return to forest': 'forest_forest'}, - "media": [] - }, - 'explore': { - "description": "You explore the ruins, encountering traps and puzzles.", - "choices": ['solve puzzle', 'avoid traps'], - "transitions": {'solve puzzle': 'ruins_hiddenPassage', 'avoid traps': 'ruins_ruins'}, - "media": [] - }, - 'hiddenPassage': { - "description": "You solve a challenging puzzle and unlock a hidden passage.", - "choices": ['enter passage', 'go back'], - "transitions": {'enter passage': 'ruins_treasureRoom', 'go back': 'ruins_explore'}, - "media": [] - }, - 'treasureRoom': { - "description": "You enter the treasure room and find the treasure chest.", - "choices": ['take treasure', 'leave'], - "transitions": {'take treasure': 'ruins_celebrate', 'leave': 'ruins_ruins'}, - "consequences": { - 'take treasure': lambda player: player.add_item('treasure') - }, - "media": [] - }, - 'celebrate': { - "description": "You celebrate your discovery and decide to bring the treasure back to the village.", - "choices": ['return to village'], - "transitions": {'return to village': 'village_return'}, - "media": [] - }, - }, - 'village_return': { - 'village_return': { - "description": "You return to the village with the treasure and share it with the villagers.", - "choices": ['end adventure'], - "transitions": {'end adventure': 'end_end'}, - "media": [] - }, - }, - 'end': { - 'end': { - "description": "Your adventure ends here. The villagers are grateful and everyone's lives improve.", - "choices": [], - "transitions": {}, - "media": [] - }, - } -} - - -#-------------------------#-------------------------#-------------------------#------------------------- - - -#-------------------------#-------------------------#-------------------------#------------------------- - -#-------------------------#-------------------------#-------------------------#------------------------- - -#-------------------------#-------------------------#-------------------------#------------------------- +# Misc items that didn't fit elsewhere +GameIdeasForPromptstodiscusswithSOTALLM = ["Programming Interview", "Coaching an NPC into the main character", "High pressure environment eg. Chefs Kitchen, Pitstop", "Moral Injury"] diff --git a/requirements.txt b/requirements.txt index acb1435f97e817b07b4f025331f8df86e88fc14b..af22ca2ce8f8b30694e1ef86ffb9a4250a4d6396 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,19 @@ -pydantic==2.10.6 +huggingface_hub bitsandbytes -transformers +transformers>=4.51.0 accelerate sentence_transformers -diffusers -sentencepiece \ No newline at end of file +diffusers>=0.30.0,<0.36.0 +# Video export (required by diffusers export_to_video) +imageio[ffmpeg] +sentencepiece +openai +scipy +soundfile +# Local TTS +kokoro>=0.9.2 +supertonic +# Local 3D generation (Shap-E ~300M params, ~6GB VRAM) +git+https://github.com/openai/shap-e.git +trimesh +# Note: Talking Head uses API only (SadTalker deps like dlib cause build timeout) \ No newline at end of file diff --git a/sandwich_quest_demo.json b/sandwich_quest_demo.json new file mode 100644 index 0000000000000000000000000000000000000000..b229c8670ada4c5ef9b19c75245cdb322cc7f87c --- /dev/null +++ b/sandwich_quest_demo.json @@ -0,0 +1,1794 @@ +{ + "home": { + "wake_up": { + "description": "Sunlight streams through your window. You stretch and yawn, then suddenly bolt upright. Wait... what day is it? You check your calendar and gasp.", + "choices": ["Check the calendar closely", "Go back to sleep"], + "transitions": { + "Check the calendar closely": "birthday_realization", + "Go back to sleep": "wake_up_again" + }, + "media": {} + }, + "wake_up_again": { + "description": "You pull the covers over your head, but something nags at you. The date... there's something important about today. Your eyes snap open.", + "choices": ["Fine, check the calendar"], + "transitions": { + "Fine, check the calendar": "birthday_realization" + }, + "media": {} + }, + "birthday_realization": { + "description": "IT'S YOUR BIRTHDAY! The big day! You've been planning this for months. Today is the day you finally make... THE PERFECT SANDWICH. Not just any sandwich - the legendary combination that your grandmother spoke of in hushed tones.", + "choices": ["Find Grandma's recipe", "Question your life choices"], + "transitions": { + "Find Grandma's recipe": "get_recipe", + "Question your life choices": "question_choices" + }, + "media": {} + }, + "question_choices": { + "description": "You briefly wonder if spending your birthday hunting for sandwich ingredients is a sign of something. Then you remember: this sandwich is said to grant perfect contentment to whoever eats it on their birthday. That's worth any quest!", + "choices": ["Find Grandma's recipe"], + "transitions": { + "Find Grandma's recipe": "get_recipe" + }, + "media": {} + }, + "get_recipe": { + "description": "You retrieve the ancient, slightly grease-stained recipe card from its sacred place (the junk drawer). The ingredients read like a quest log:\n\n- Golden Bread from Crispin's Bakery\n- Aged Cave Cheese from the Mountain Monks\n- Rainbow Lettuce from the Enchanted Garden\n- Sun-Ripened Tomatoes from Sunny Farm\n- Legendary Ham from the Hunter's Lodge\n- Secret Sauce from the Hidden Restaurant\n- Ancient Pickles from the Pickle Temple\n- Crystal Onions from the Caverns\n\nThis won't be easy. But nothing worth having ever is.", + "choices": ["Head to the village to begin your quest"], + "transitions": { + "Head to the village to begin your quest": "village/hub" + }, + "media": {} + } + }, + + "village": { + "hub": { + "description": "The village square bustles with morning activity. From here, paths lead in all directions. A helpful signpost stands in the center, and various villagers go about their business. You clutch your recipe card tightly. Eight ingredients. Eight adventures. One perfect sandwich.", + "choices": ["Read the signpost", "Talk to villagers for tips", "Head directly to a destination"], + "transitions": { + "Read the signpost": "signpost", + "Talk to villagers for tips": "villager_tips", + "Head directly to a destination": "choose_destination" + }, + "media": {} + }, + "signpost": { + "description": "The signpost has eight weathered wooden arrows:\n\n-> BAKERY DISTRICT (Golden Bread)\n-> MOUNTAIN CAVES (Aged Cheese)\n-> ENCHANTED GARDEN (Rainbow Lettuce)\n-> SUNNY FARM (Sun Tomatoes)\n-> HUNTER'S LODGE (Legendary Ham)\n-> HIDDEN RESTAURANT (Secret Sauce)\n-> PICKLE TEMPLE (Ancient Pickles)\n-> CRYSTAL CAVERNS (Crystal Onions)\n\nSomeone has scrawled 'Good luck, you'll need it!' at the bottom.", + "choices": ["Choose a destination", "Talk to villagers first"], + "transitions": { + "Choose a destination": "choose_destination", + "Talk to villagers first": "villager_tips" + }, + "media": {} + }, + "villager_tips": { + "description": "An elderly woman notices your recipe card and chuckles. 'Ah, attempting the Perfect Sandwich, are we? My advice: each ingredient guardian has a problem. Help them, and they'll help you. Oh, and the Cheese Monk speaks in riddles - don't let it frustrate you. The Pickle Wizard is worse.'", + "choices": ["Thank her and choose a destination", "Ask for more advice"], + "transitions": { + "Thank her and choose a destination": "choose_destination", + "Ask for more advice": "more_advice" + }, + "media": {} + }, + "more_advice": { + "description": "'Well,' she continues, 'the Baker's having ghost troubles, the Farmer's fighting crows, and Chef Mysterio... well, he's always been dramatic. Just be patient and kind. These folks have been guarding their ingredients for generations. Earn their trust.'\n\nShe pats your shoulder. 'Now go make that sandwich!'", + "choices": ["Choose a destination"], + "transitions": { + "Choose a destination": "choose_destination" + }, + "media": {} + }, + "choose_destination": { + "description": "Where would you like to go first? The quest for the Perfect Sandwich awaits!", + "choices": [ + "Bakery District (Golden Bread)", + "Mountain Caves (Aged Cheese)", + "Enchanted Garden (Rainbow Lettuce)", + "Sunny Farm (Sun Tomatoes)", + "Hunter's Lodge (Legendary Ham)", + "Hidden Restaurant (Secret Sauce)", + "Pickle Temple (Ancient Pickles)", + "Crystal Caverns (Crystal Onions)" + ], + "transitions": { + "Bakery District (Golden Bread)": "bakery_district/entrance", + "Mountain Caves (Aged Cheese)": "mountain_caves/trail_start", + "Enchanted Garden (Rainbow Lettuce)": "enchanted_garden/garden_gate", + "Sunny Farm (Sun Tomatoes)": "sunny_farm/farm_entrance", + "Hunter's Lodge (Legendary Ham)": "hunters_lodge/forest_path", + "Hidden Restaurant (Secret Sauce)": "hidden_restaurant/search_begin", + "Pickle Temple (Ancient Pickles)": "pickle_temple/temple_approach", + "Crystal Caverns (Crystal Onions)": "crystal_caverns/cavern_entrance" + }, + "media": {} + }, + "return_hub": { + "description": "You return to the village square, one step closer to sandwich perfection. The recipe card feels a little lighter with each ingredient collected.", + "choices": ["Check remaining ingredients", "Continue to next destination"], + "transitions": { + "Check remaining ingredients": "choose_destination", + "Continue to next destination": "choose_destination" + }, + "media": {} + } + }, + + "bakery_district": { + "entrance": { + "description": "The aroma of fresh bread fills the air - or it should. Instead, there's a strange burnt smell mixed with something... spectral? A wooden sign reads 'Crispin's Golden Loaves - Est. 1847'. The bakery door is slightly ajar, and flickering blue light spills onto the cobblestones.", + "choices": ["Enter the bakery", "Look around outside first"], + "transitions": { + "Enter the bakery": "meet_baker", + "Look around outside first": "bakery_exterior" + }, + "media": {} + }, + "bakery_exterior": { + "description": "The bakery windows are fogged up from the inside, but you can see shadows moving frantically. A small alley runs alongside the building. Flour footprints - glowing flour footprints - lead down it.", + "choices": ["Go inside now", "Investigate the alley"], + "transitions": { + "Go inside now": "meet_baker", + "Investigate the alley": "alley_discovery" + }, + "media": {} + }, + "alley_discovery": { + "description": "Following the glowing footprints, you find an old gravestone wedged behind some barrels. It reads: 'Here lies the Original Recipe of Master Baker Aldous - May His Sourdough Rise Eternal.' Someone has recently disturbed the dirt around it.", + "choices": ["Interesting... go inside", "Touch the gravestone"], + "transitions": { + "Interesting... go inside": "meet_baker", + "Touch the gravestone": "ghost_clue" + }, + "media": {} + }, + "ghost_clue": { + "description": "The moment you touch the stone, you hear a whisper: 'My recipe... forgotten... bread without soul...' The voice fades. You now understand - this ghost has unfinished business with bread. Armed with this knowledge, you head inside.", + "choices": ["Enter the bakery"], + "transitions": { + "Enter the bakery": "meet_baker_informed" + }, + "media": {} + }, + "meet_baker": { + "description": "Behind the counter stands a flour-dusted man with deeply tired eyes and a nervous twitch. This must be Crispin. 'W-welcome! Um, welcome... though I'm afraid I c-can't sell you much today. The oven... it's, um...' He shivers. 'Haunted. Very haunted.'", + "choices": ["Tell me about this ghost", "I'm here to help!", "I just need some Golden Bread"], + "transitions": { + "Tell me about this ghost": "ghost_story", + "I'm here to help!": "offer_help", + "I just need some Golden Bread": "no_bread_yet" + }, + "media": {} + }, + "meet_baker_informed": { + "description": "Crispin looks up with desperate hope. 'Oh! A customer! Though I'm afraid- wait.' He squints at you. 'You have that look. The look of someone who's seen something in the alley.' His voice drops. 'You know about the ghost, don't you?'", + "choices": ["Tell him what you found", "Ask about the Golden Bread"], + "transitions": { + "Tell him what you found": "share_discovery", + "Ask about the Golden Bread": "no_bread_yet" + }, + "media": {} + }, + "share_discovery": { + "description": "Crispin's eyes widen as you describe the gravestone. 'Master Aldous! Of course! When I found his old recipe book in the walls during renovation, I just... used the recipes without thinking. I never honored him!' He clasps your hands. 'Will you help me make peace with his spirit?'", + "choices": ["Yes, let's do this!", "How do we make peace with a ghost?"], + "transitions": { + "Yes, let's do this!": "investigate_oven", + "How do we make peace with a ghost?": "ghost_explanation" + }, + "media": {} + }, + "ghost_story": { + "description": "'It started three weeks ago,' Crispin whispers. 'I was renovating and found an old recipe book in the walls. Amazing recipes! But since I started using them...' He gestures at the oven, which flickers with ethereal blue flame. 'HE doesn't like how I make his bread.'", + "choices": ["His bread? Whose bread?", "I'll help you solve this"], + "transitions": { + "His bread? Whose bread?": "ghost_identity", + "I'll help you solve this": "offer_help" + }, + "media": {} + }, + "ghost_identity": { + "description": "'Master Baker Aldous. The original owner, from 1847. I think... I think he's upset that I use his recipes but never acknowledged him. Never honored his memory.' Crispin wrings his hands. 'The bread comes out wrong now. Angry bread. Ghost bread.'", + "choices": ["I'll help you make peace with him"], + "transitions": { + "I'll help you make peace with him": "offer_help" + }, + "media": {} + }, + "offer_help": { + "description": "Crispin's face floods with relief. 'You will? Oh, thank you! I think... I think we need to properly honor Master Aldous. Maybe bake his original recipe the way HE did it, and, um, apologize?' He fidgets. 'The ghost is in the oven. You'll need to talk to him.'", + "choices": ["Let's investigate the oven"], + "transitions": { + "Let's investigate the oven": "investigate_oven" + }, + "media": {} + }, + "ghost_explanation": { + "description": "'Well, I think he wants his legacy honored properly. Not just his recipes used, but his NAME remembered. His METHODS respected.' Crispin pulls out an old, yellowed book. 'This is his recipe book. Maybe if we bake his signature loaf HIS way...'", + "choices": ["Let's investigate the oven"], + "transitions": { + "Let's investigate the oven": "investigate_oven" + }, + "media": {} + }, + "no_bread_yet": { + "description": "Crispin shakes his head sadly. 'I can't make any Golden Bread while the oven is possessed! Every loaf comes out either burnt to ash or... or SCREAMING. Please, won't you help me with my ghost problem first?'", + "choices": ["Tell me about the ghost", "Fine, I'll help"], + "transitions": { + "Tell me about the ghost": "ghost_story", + "Fine, I'll help": "offer_help" + }, + "media": {} + }, + "investigate_oven": { + "description": "You approach the old brick oven. It pulses with an unearthly blue glow. The temperature fluctuates wildly - freezing cold one moment, scorching the next. From within, you hear a low moan: 'Proooove the dooooough... respect the riiiiise...'", + "choices": ["Open the oven door", "Speak to the ghost first"], + "transitions": { + "Open the oven door": "ghost_appears", + "Speak to the ghost first": "ghost_dialogue" + }, + "media": {} + }, + "ghost_dialogue": { + "description": "'Who dares approach my oven?' The voice echoes. 'Are you another fool who uses my recipes without understanding? WITHOUT RESPECTING THE CRAFT?' The blue flames flicker angrily.", + "choices": ["We want to honor you properly, Master Aldous", "I'm just here for bread, ghost"], + "transitions": { + "We want to honor you properly, Master Aldous": "ghost_listens", + "I'm just here for bread, ghost": "ghost_angry" + }, + "media": {} + }, + "ghost_angry": { + "description": "The oven EXPLODES open and a flour-covered specter bursts out! 'JUST HERE FOR BREAD?! MY BREAD IS ART! MY BREAD IS LEGACY!' Pots and pans fly around the room. Crispin ducks behind the counter.", + "choices": ["Run for cover!", "Wait! I'm sorry!"], + "transitions": { + "Run for cover!": "chase_sequence", + "Wait! I'm sorry!": "ghost_apologize" + }, + "media": {} + }, + "ghost_appears": { + "description": "You pull open the oven door and a blast of spectral energy knocks you back! A flour-covered ghost rises from the flames, translucent and furious. 'WHO DISTURBS THE REST OF MASTER ALDOUS?!'", + "choices": ["We come in peace!", "Duck and cover!"], + "transitions": { + "We come in peace!": "ghost_negotiate", + "Duck and cover!": "chase_sequence" + }, + "media": {} + }, + "ghost_listens": { + "description": "The flames dim slightly. 'Honor... me?' The ghost's voice softens from a howl to a whisper. 'It has been so long since anyone spoke my name with respect. Tell me... what do you propose?'", + "choices": ["We'll bake your signature recipe together, your way"], + "transitions": { + "We'll bake your signature recipe together, your way": "ghost_deal" + }, + "media": {} + }, + "ghost_apologize": { + "description": "The flying kitchenware pauses mid-air. The ghost tilts his spectral head. 'You... apologize? Hmm. It has been 150 years since anyone showed me respect. Perhaps I was too hasty.' The pans lower gently.", + "choices": ["Can we start over? We want to honor your legacy"], + "transitions": { + "Can we start over? We want to honor your legacy": "ghost_negotiate" + }, + "media": {} + }, + "ghost_negotiate": { + "description": "The ghost crosses his translucent arms. 'You seek my Golden Bread, don't you? Everyone does. Very well. But my bread requires SOUL. Bake my original recipe with RESPECT, and I shall grant you the finest loaf ever made. Fail, and this bakery stays haunted FOREVER.'", + "choices": ["We accept your challenge"], + "transitions": { + "We accept your challenge": "ghost_deal" + }, + "media": {} + }, + "chase_sequence": { + "description": "You scramble around the bakery as the ghost hurls baguettes like javelins! 'RESPECT THE BREAD!' he wails. Crispin crawls under a table. You need to calm this spirit down somehow!", + "choices": ["Hide in the pantry", "Grab a flour bag for defense", "Shout an apology"], + "transitions": { + "Hide in the pantry": "pantry_trap", + "Grab a flour bag for defense": "flour_defense", + "Shout an apology": "ghost_apologize" + }, + "media": {} + }, + "pantry_trap": { + "description": "You dive into the pantry - right into a pile of Master Aldous's original ingredients! The ghost follows but stops short. 'My... my heritage flour. My wild yeast culture.' His rage flickers. 'You found... my supplies.'", + "choices": ["Crispin could use these to bake YOUR way"], + "transitions": { + "Crispin could use these to bake YOUR way": "ghost_softens" + }, + "media": {} + }, + "flour_defense": { + "description": "You grab a flour bag and it GLOWS in your hands - this is the ghost's special heritage flour! The ghost gasps. 'My flour! You hold it like... like you understand its importance.' His anger wavers.", + "choices": ["We should use this to bake YOUR recipe, YOUR way"], + "transitions": { + "We should use this to bake YOUR recipe, YOUR way": "ghost_softens" + }, + "media": {} + }, + "ghost_softens": { + "description": "The ghost's fury melts into something softer - nostalgia? 'You truly wish to honor my methods? To bake as I baked?' He looks at Crispin, who nods vigorously. 'Then... then let us bake together. One final loaf.'", + "choices": ["Let's do this"], + "transitions": { + "Let's do this": "ghost_deal" + }, + "media": {} + }, + "ghost_deal": { + "description": "Master Aldous's ghost guides you and Crispin through his ancient technique. Crispin finally understands - the long rise, the gentle handling, the patience. 'I was rushing,' Crispin admits. 'Treating your recipes like factory instructions, not art.' The ghost nods approvingly.", + "choices": ["Complete the bake"], + "transitions": { + "Complete the bake": "ghost_resolved" + }, + "media": {} + }, + "ghost_resolved": { + "description": "As the final loaf emerges from the oven - golden, perfect, GLOWING with warmth - Master Aldous begins to fade. 'My legacy... lives on... properly...' He turns to you. 'Thank you, young one. Take this bread. It carries my blessing.' With a peaceful smile, he vanishes.", + "choices": ["Receive the Golden Bread"], + "transitions": { + "Receive the Golden Bread": "receive_bread" + }, + "media": {} + }, + "receive_bread": { + "description": "Crispin, tears in his eyes, presents you with the most magnificent loaf you've ever seen. It practically glows with golden perfection. 'The Golden Bread,' he says reverently. 'Baked with love, patience, and one ghost's blessing. I'll never forget what you taught us today.'", + "choices": ["Thank Crispin and continue your quest"], + "transitions": { + "Thank Crispin and continue your quest": "exit_bakery" + }, + "media": {} + }, + "exit_bakery": { + "description": "You step out of Crispin's Bakery with the Golden Bread safely wrapped. The morning sun feels warmer somehow. One ingredient down, seven to go. The Perfect Sandwich is becoming more real with every step.", + "choices": ["Return to village"], + "transitions": { + "Return to village": "village/return_hub" + }, + "media": {} + } + }, + + "mountain_caves": { + "trail_start": { + "description": "The path to the Mountain Caves winds upward through misty pines. A wooden sign reads: 'CAVE OF CONTEMPLATION - Home of the Cheese Monks. Please maintain silence and open mind.' You can smell something sharp and delicious on the breeze.", + "choices": ["Begin the climb", "Read the smaller sign below"], + "transitions": { + "Begin the climb": "mountain_path", + "Read the smaller sign below": "warning_sign" + }, + "media": {} + }, + "warning_sign": { + "description": "A smaller, hand-painted sign adds: 'Brother Gouda is currently accepting visitors. Warning: He speaks only in riddles and cheese puns. We apologize for nothing.' Someone has drawn a small wedge of cheese with a halo.", + "choices": ["Begin the climb anyway"], + "transitions": { + "Begin the climb anyway": "mountain_path" + }, + "media": {} + }, + "mountain_path": { + "description": "The trail grows steeper. You pass small meditation alcoves carved into the rock, each holding a tiny wheel of aging cheese. Prayer flags made of cheesecloth flutter in the wind. Ahead, you hear chanting: 'Oooommm-embert... Oooommm-embert...'", + "choices": ["Continue toward the chanting", "Examine a cheese alcove"], + "transitions": { + "Continue toward the chanting": "cave_entrance", + "Examine a cheese alcove": "cheese_alcove" + }, + "media": {} + }, + "cheese_alcove": { + "description": "The small cheese wheel sits on a bed of herbs, slowly aging to perfection. A tiny plaque reads: 'In caves we trust. In time we must. Good things come to those who wait. This cheese ready: 2027.' Patience, indeed.", + "choices": ["Continue toward the chanting"], + "transitions": { + "Continue toward the chanting": "cave_entrance" + }, + "media": {} + }, + "cave_entrance": { + "description": "The cave mouth yawns before you, cool air carrying the complex aroma of aged cheese. Inside, you can see the faint glow of crystals - and a robed figure sitting in lotus position atop a giant wheel of cheese.", + "choices": ["Enter respectfully", "Call out a greeting"], + "transitions": { + "Enter respectfully": "meet_gouda", + "Call out a greeting": "gouda_responds" + }, + "media": {} + }, + "gouda_responds": { + "description": "Your greeting echoes through the cave. The robed figure slowly opens one eye. 'What is the sound of one cheese aging?' he asks calmly. 'Enter, seeker. The cave welcomes those who question.'", + "choices": ["Enter the cave"], + "transitions": { + "Enter the cave": "meet_gouda" + }, + "media": {} + }, + "meet_gouda": { + "description": "Brother Gouda sits serenely, his orange robes matching his namesake. He is bald, peaceful, and smells faintly of aged dairy. 'Ah,' he says, eyes still closed. 'You seek the Cave Cheese, aged one hundred years in crystal resonance. But first - a question. Why does one seek the perfect meal?'", + "choices": ["It's my birthday, and I want to make something special", "I seek the Perfect Sandwich"], + "transitions": { + "It's my birthday, and I want to make something special": "gouda_approves", + "I seek the Perfect Sandwich": "gouda_curious" + }, + "media": {} + }, + "gouda_curious": { + "description": "'The Perfect Sandwich...' Gouda smiles. 'Brie-lliant goal. But perfection is not a destination - it is a journey of many cheesy steps. Tell me, what makes a sandwich perfect?'", + "choices": ["The ingredients, gathered with care", "The love put into making it"], + "transitions": { + "The ingredients, gathered with care": "gouda_approves", + "The love put into making it": "gouda_approves" + }, + "media": {} + }, + "gouda_approves": { + "description": "'A gouda answer,' he chuckles. 'You understand that the journey matters as much as the meal. Very well. I shall grant you access to our aged cheese - but there is a problem.' He sighs. 'I have lost my meditation crystals. Without them, I cannot properly age the final wheel.'", + "choices": ["I'll help you find them!", "Where did you lose them?"], + "transitions": { + "I'll help you find them!": "crystal_quest", + "Where did you lose them?": "crystals_explanation" + }, + "media": {} + }, + "crystals_explanation": { + "description": "'Three crystals, hidden in three chambers of this cave. Each resonates with a different aspect of cheese: the Crystal of Age, the Crystal of Flavor, and the Crystal of Aroma.' He opens his eyes, looking embarrassed. 'I was sleep-walking. Monks have stress too, you know.'", + "choices": ["I'll find them for you"], + "transitions": { + "I'll find them for you": "crystal_quest" + }, + "media": {} + }, + "crystal_quest": { + "description": "'Grate-ful I am,' Gouda bows. 'The three chambers lie deeper in the cave. Trust your senses - Age smells of time, Flavor tastes of truth, and Aroma... well, it smells of aroma. That one's obvious. Go now, with cheese-speed.'", + "choices": ["Enter the first chamber"], + "transitions": { + "Enter the first chamber": "chamber_age" + }, + "media": {} + }, + "chamber_age": { + "description": "The Chamber of Age is filled with ancient cheese wheels, some crumbling, others perfect. Dust motes dance in shafts of light. In the corner, you spot something glinting beneath a very old, very impressive wheel.", + "choices": ["Carefully move the old wheel", "Look around more first"], + "transitions": { + "Carefully move the old wheel": "find_age_crystal", + "Look around more first": "age_chamber_explore" + }, + "media": {} + }, + "age_chamber_explore": { + "description": "The chamber holds cheese that predates your grandparents. One wheel has a tag: 'Reserved for wedding of King Harold VII. 1543.' Some of these cheeses might be sentient by now. Better get that crystal and leave them to their contemplation.", + "choices": ["Carefully move the impressive wheel"], + "transitions": { + "Carefully move the impressive wheel": "find_age_crystal" + }, + "media": {} + }, + "find_age_crystal": { + "description": "Beneath the ancient wheel, you find a pale yellow crystal that seems to shimmer with the weight of years. When you touch it, you feel a brief flash of patience - the understanding that some things cannot be rushed. The Crystal of Age is yours.", + "choices": ["Continue to the Flavor chamber"], + "transitions": { + "Continue to the Flavor chamber": "chamber_flavor" + }, + "media": {} + }, + "chamber_flavor": { + "description": "The Chamber of Flavor assaults your taste buds just by BEING here. Sharp cheddars, pungent blues, creamy bries - the air itself is flavorful. Several crystal formations jut from the walls, but which is THE crystal?", + "choices": ["Taste the air near each crystal", "Look for one that stands out"], + "transitions": { + "Taste the air near each crystal": "flavor_test", + "Look for one that stands out": "flavor_search" + }, + "media": {} + }, + "flavor_search": { + "description": "Most crystals are clear or white, but one near the back has a golden hue and seems to pulse gently. As you approach it, you can almost taste cheddar, gouda, and manchego all at once.", + "choices": ["Take the golden crystal"], + "transitions": { + "Take the golden crystal": "find_flavor_crystal" + }, + "media": {} + }, + "flavor_test": { + "description": "You walk the chamber, mouth slightly open (you feel silly but it works). Near a golden crystal, the flavors intensify dramatically - complex, layered, perfect. This must be it!", + "choices": ["Take the golden crystal"], + "transitions": { + "Take the golden crystal": "find_flavor_crystal" + }, + "media": {} + }, + "find_flavor_crystal": { + "description": "The Crystal of Flavor warms in your hand, and for a moment every cheese you've ever tasted returns to your memory in perfect clarity. Your first grilled cheese. Your grandmother's mac and cheese. The gas station nacho cheese you're not proud of. All flavors, honored.", + "choices": ["Continue to the Aroma chamber"], + "transitions": { + "Continue to the Aroma chamber": "chamber_aroma" + }, + "media": {} + }, + "chamber_aroma": { + "description": "The Chamber of Aroma is overwhelming. Your eyes water. Your nose burns. Your soul questions its choices. Somewhere in this pungent fog is the final crystal. A sign reads: 'If you can still smell after 5 minutes, the Crystal has chosen you.'", + "choices": ["Push through the smell", "Take shallow breaths and search"], + "transitions": { + "Push through the smell": "aroma_search", + "Take shallow breaths and search": "aroma_search" + }, + "media": {} + }, + "aroma_search": { + "description": "Your nose adapts (or dies, hard to tell). Past the initial assault, you begin to detect... beauty? The complexity of aged cheese, the earthiness, the life of bacterial cultures working in harmony. In the center of the chamber, a crystal pulses with an almost visible scent.", + "choices": ["Claim the Aroma crystal"], + "transitions": { + "Claim the Aroma crystal": "find_aroma_crystal" + }, + "media": {} + }, + "find_aroma_crystal": { + "description": "The Crystal of Aroma seems to purify the air around it as you pick it up. Suddenly, you can smell everything clearly - the cave, the mountain air, and distantly, the perfect cheese waiting to be aged. Three crystals gathered. Time to return.", + "choices": ["Return to Brother Gouda"], + "transitions": { + "Return to Brother Gouda": "return_to_gouda" + }, + "media": {} + }, + "return_to_gouda": { + "description": "Brother Gouda remains in his meditation pose, but his smile widens as you approach. 'I sense... yes! The three crystals, reunited! You have walked the path of cheese enlightenment.' He stands, joints cracking. 'Now, let us complete the aging.'", + "choices": ["Watch the ritual"], + "transitions": { + "Watch the ritual": "cheese_ritual" + }, + "media": {} + }, + "cheese_ritual": { + "description": "Gouda arranges the crystals around a special wheel of cheese. He begins to chant: 'As whey separates from curd, let time be our reward. Crystal resonance, complete the aging dance!' The crystals GLOW, and the cheese transforms before your eyes - from young and bland to magnificently aged.", + "choices": ["That was amazing!"], + "transitions": { + "That was amazing!": "receive_cheese" + }, + "media": {} + }, + "receive_cheese": { + "description": "'This,' Brother Gouda says, cutting you a generous wedge, 'is Cave Cheese aged to perfection. One hundred years of flavor, condensed through crystal magic into one wheel.' He winks. 'A little cheesy, perhaps, but that's kind of our thing.'", + "choices": ["Thank you, Brother Gouda"], + "transitions": { + "Thank you, Brother Gouda": "gouda_farewell" + }, + "media": {} + }, + "gouda_farewell": { + "description": "'Remember,' Gouda says as you prepare to leave, 'the best things in life are worth waiting for. Your sandwich will be no different. Each ingredient is a lesson, each step a meditation.' He bows. 'May your bread be crusty and your cheese be melty.'", + "choices": ["Return to village"], + "transitions": { + "Return to village": "village/return_hub" + }, + "media": {} + } + }, + + "enchanted_garden": { + "garden_gate": { + "description": "A wrought-iron gate covered in flowering vines marks the entrance to the Enchanted Garden. Through the bars, you see colors you didn't know existed - flowers that seem to hum, vegetables that sparkle, and in the distance, a shimmer of rainbow-colored leaves. A tiny sign reads: 'Please don't pick the singing tulips. They're in rehearsal.'", + "choices": ["Enter the garden", "Examine the gate more closely"], + "transitions": { + "Enter the garden": "garden_path", + "Examine the gate more closely": "gate_detail" + }, + "media": {} + }, + "gate_detail": { + "description": "The gate's ironwork depicts vegetables and fruits growing in harmony with magical creatures. A smaller plaque notes: 'Tended by Fern the Garden Fairy since... well, she's never told us her age. That would be un-vine-ted.' The pun physically hurts.", + "choices": ["Enter the garden"], + "transitions": { + "Enter the garden": "garden_path" + }, + "media": {} + }, + "garden_path": { + "description": "The garden path winds between beds of impossible plants. A tomato bush waves its leaves at you. Carrots hum in harmony. And there, floating near a patch of shimmering lettuce, is a tiny glowing figure with dragonfly wings - Fern the Garden Fairy.", + "choices": ["Approach Fern", "Explore the garden first"], + "transitions": { + "Approach Fern": "meet_fern", + "Explore the garden first": "garden_explore" + }, + "media": {} + }, + "garden_explore": { + "description": "The garden is a marvel! Strawberries giggle when you pass, beans race each other up their poles, and a particularly snooty rose critiques your outfit. But you also notice something troubling - aggressive weeds are encroaching from the edges, and Fern looks exhausted.", + "choices": ["Approach Fern about the weeds"], + "transitions": { + "Approach Fern about the weeds": "meet_fern_informed" + }, + "media": {} + }, + "meet_fern": { + "description": "'Oh! A visitor!' Fern zips over to you, trailing sparkles. She's tiny, green-haired, and speaks at approximately 100 words per minute. 'Welcome-to-the-Enchanted-Garden-home-of-the-magical-Rainbow-Lettuce-I'm-Fern-what-brings-you-here?!' She pauses to breathe. 'Is it the lettuce? Please say it's the lettuce. I love giving people lettuce!'", + "choices": ["Yes, I need Rainbow Lettuce!", "Why do you seem so stressed?"], + "transitions": { + "Yes, I need Rainbow Lettuce!": "lettuce_request", + "Why do you seem so stressed?": "fern_problem" + }, + "media": {} + }, + "meet_fern_informed": { + "description": "Fern looks up as you approach, her glow dimming with fatigue. 'Oh, you noticed the weeds. They're TERRIBLE. Usually I can keep them under control but lately they've been... aggressive. Like they have a grudge.' She sniffles. 'My poor garden is being invaded!'", + "choices": ["Let me help with the weeds!"], + "transitions": { + "Let me help with the weeds!": "weed_quest" + }, + "media": {} + }, + "lettuce_request": { + "description": "Fern claps her tiny hands! 'The Rainbow Lettuce! Yes! It's right over- oh.' Her face falls. 'The weeds. The horrible, no-good, very bad weeds are CHOKING my lettuce patch! I've been fighting them all week but they keep growing back!' Her wings droop. 'I can't harvest until they're gone.'", + "choices": ["I'll help you with the weeds!"], + "transitions": { + "I'll help you with the weeds!": "weed_quest" + }, + "media": {} + }, + "fern_problem": { + "description": "'Oh, is it that obvious?' Fern sighs, landing on a mushroom. 'These weeds just appeared one day - magical weeds, resistant to my usual tricks. They're called Grudge Vines. Someone must have planted them out of spite! Who would hate a GARDEN?!' She looks genuinely hurt.", + "choices": ["Let me help pull the weeds"], + "transitions": { + "Let me help pull the weeds": "weed_quest" + }, + "media": {} + }, + "weed_quest": { + "description": "'You'd REALLY help?!' Fern perks up instantly. 'Oh, you're the BEST! Okay, there are three main Grudge Vine clusters - one in the root vegetable section, one in the fruit grove, and one RIGHT next to my lettuce! Pull them out by the roots and I'll reward you with the BEST Rainbow Lettuce ever!'", + "choices": ["Let's start with the root vegetables"], + "transitions": { + "Let's start with the root vegetables": "weed_cluster_1" + }, + "media": {} + }, + "weed_cluster_1": { + "description": "The root vegetable section is chaos. Aggressive vines have wrapped around the carrots (who are screaming) and are actively fighting with the potatoes (who are losing). A central vine, thick and purple, seems to be the leader. It hisses at you.", + "choices": ["Grab the main vine!", "Free the carrots first"], + "transitions": { + "Grab the main vine!": "pull_weed_1", + "Free the carrots first": "free_carrots" + }, + "media": {} + }, + "free_carrots": { + "description": "You carefully untangle the carrots, who thank you in high-pitched squeaks. Now they point at the main vine with their leafy tops: 'That one! Get that one! It called us names!' The vine seems offended at being ratted out.", + "choices": ["Grab the main vine"], + "transitions": { + "Grab the main vine": "pull_weed_1" + }, + "media": {} + }, + "pull_weed_1": { + "description": "You grab the purple vine firmly and PULL! It resists, thrashing, but you're determined. With a satisfying RRRIP, it comes free - roots and all! The vine shrieks, shrivels, and dissolves into purple dust. The root vegetables cheer!", + "choices": ["On to the fruit grove!"], + "transitions": { + "On to the fruit grove!": "weed_cluster_2" + }, + "media": {} + }, + "weed_cluster_2": { + "description": "The fruit grove is under siege. Grape vines (the good kind) are tangled with Grudge Vines (the bad kind). Apple trees shake with distress. A particularly bold Grudge Vine has climbed a peach tree and is making rude gestures with its leaves.", + "choices": ["Climb up and get it!", "Shake the tree first"], + "transitions": { + "Climb up and get it!": "pull_weed_2_climb", + "Shake the tree first": "shake_tree" + }, + "media": {} + }, + "shake_tree": { + "description": "You shake the peach tree vigorously! Fruit rains down, and the Grudge Vine loses its grip, tumbling to the ground in an undignified heap. It tries to slither away but you're faster.", + "choices": ["Grab it!"], + "transitions": { + "Grab it!": "pull_weed_2" + }, + "media": {} + }, + "pull_weed_2_climb": { + "description": "You scale the peach tree (careful of the fruit) and grab the Grudge Vine directly! It wraps around your arm, trying to intimidate you, but you've dealt with ghost bread - a weed doesn't scare you.", + "choices": ["Pull it out!"], + "transitions": { + "Pull it out!": "pull_weed_2" + }, + "media": {} + }, + "pull_weed_2": { + "description": "Another satisfying RRRRIP! The second Grudge Vine comes loose, flails dramatically, and crumbles to dust. The fruit trees rustle their leaves in applause. One peach falls into your pocket as thanks. Only one vine remains - the one threatening the lettuce.", + "choices": ["To the lettuce patch!"], + "transitions": { + "To the lettuce patch!": "weed_cluster_3" + }, + "media": {} + }, + "weed_cluster_3": { + "description": "The final Grudge Vine has wrapped itself around the entire Rainbow Lettuce patch, squeezing tight. The lettuce leaves flicker weakly, their colors fading. This vine is the biggest yet, and it's got a face - an angry, gnarled knot that glares at you.", + "choices": ["Talk to the vine", "Just pull it!"], + "transitions": { + "Talk to the vine": "vine_talk", + "Just pull it!": "vine_battle" + }, + "media": {} + }, + "vine_talk": { + "description": "'What do YOU want?' the vine growls (yes, growls). 'This garden humiliated my kind for YEARS! Always pulling us, poisoning us, calling us WEEDS! Well who's the weed NOW, huh?!' It squeezes the lettuce tighter. 'This garden will be OURS!'", + "choices": ["I understand your pain, but this isn't the way", "You're hurting innocent lettuce!"], + "transitions": { + "I understand your pain, but this isn't the way": "vine_compassion", + "You're hurting innocent lettuce!": "vine_battle" + }, + "media": {} + }, + "vine_compassion": { + "description": "The vine pauses. 'You... understand?' Its grip loosens slightly. 'No one's ever said that before. They just pull and spray and...' It looks almost sad. 'But it's too late. We've been hurt too much. We can't forgive.' The vine tightens again. 'We CAN'T!'", + "choices": ["Then I'm sorry, but I have to stop you"], + "transitions": { + "Then I'm sorry, but I have to stop you": "vine_battle" + }, + "media": {} + }, + "vine_battle": { + "description": "The Grudge Vine lashes out! You dodge, grab, pull! It's a tug-of-war between you and centuries of botanical resentment! Fern cheers from the sidelines, throwing fertilizer spells to weaken the vine. Finally, with one mighty HEAVE-", + "choices": ["PULL!"], + "transitions": { + "PULL!": "weed_victory" + }, + "media": {} + }, + "weed_victory": { + "description": "RRRRRIIIIIPPP! The final Grudge Vine tears free, its angry face frozen in shock. 'This isn't... over...' it wheezes, before crumbling into purple dust. The Rainbow Lettuce immediately brightens, colors swirling back to full vibrancy!", + "choices": ["Check on the lettuce"], + "transitions": { + "Check on the lettuce": "lettuce_saved" + }, + "media": {} + }, + "lettuce_saved": { + "description": "The Rainbow Lettuce practically GLOWS with gratitude. Each leaf shimmers through the entire color spectrum - red, orange, yellow, green, blue, indigo, violet - before settling on a healthy, magical green. Fern zooms over, tears of joy in her eyes.", + "choices": ["Receive the lettuce"], + "transitions": { + "Receive the lettuce": "receive_lettuce" + }, + "media": {} + }, + "receive_lettuce": { + "description": "'You SAVED them!' Fern hugs your finger (it's all she can reach). 'Take the biggest, best, most rainbowy lettuce of the bunch! Actually, take THREE!' She harvests the most beautiful leaves you've ever seen - they seem to contain actual rainbows.", + "choices": ["Thank you, Fern!"], + "transitions": { + "Thank you, Fern!": "fern_farewell" + }, + "media": {} + }, + "fern_farewell": { + "description": "'No, thank YOU!' Fern grins. 'The garden is safe again! And hey - if you ever want to learn plant puns, I'm your fairy! You know what's a gardener's favorite Beatles song? Lettuce Be!' She giggles uncontrollably. 'GET IT? Get it?!'", + "choices": ["Return to village (laughing politely)"], + "transitions": { + "Return to village (laughing politely)": "village/return_hub" + }, + "media": {} + } + }, + + "sunny_farm": { + "farm_entrance": { + "description": "Golden sunlight bathes everything here. The Sunny Farm stretches before you - endless fields of vegetables basking under a sky that seems permanently sunny. A weathered wooden sign reads: 'Solana's Sun Farm - Where Every Day is Harvest Day.' In the distance, a figure in overalls battles a flock of... crows?", + "choices": ["Run to help with the crows", "Walk up the main path"], + "transitions": { + "Run to help with the crows": "crow_battle", + "Walk up the main path": "approach_farmer" + }, + "media": {} + }, + "approach_farmer": { + "description": "As you walk up the dusty path, the sounds of the crow battle intensify. You can hear a woman shouting: 'Get BACK, you feathered demons! These tomatoes aren't for YOU!' A crow cackles in response. The situation seems urgent.", + "choices": ["Run to help!"], + "transitions": { + "Run to help!": "crow_battle" + }, + "media": {} + }, + "crow_battle": { + "description": "Farmer Solana stands her ground against a dozen clever-looking crows. She's got a broom in one hand and a bucket in the other, but the crows are coordinated - some distract her while others dive for the magnificent tomatoes behind her. Sun-Ripened Tomatoes that glow like rubies.", + "choices": ["Join the fight!", "Shout to distract the crows"], + "transitions": { + "Join the fight!": "fight_crows", + "Shout to distract the crows": "distract_crows" + }, + "media": {} + }, + "fight_crows": { + "description": "You grab a spare broom and wade in! The crows weren't expecting reinforcements - their formation breaks! 'HA!' Solana shouts. 'That's right, fly away! And tell your friends - THESE TOMATOES ARE PROTECTED!'", + "choices": ["Watch the crows retreat"], + "transitions": { + "Watch the crows retreat": "meet_solana" + }, + "media": {} + }, + "distract_crows": { + "description": "You wave your arms and make the most alarming noise you can manage! The crows startle, breaking formation! Solana seizes the moment, swinging her broom with practiced efficiency. Within moments, the crows retreat, cawing in frustration.", + "choices": ["Catch your breath"], + "transitions": { + "Catch your breath": "meet_solana" + }, + "media": {} + }, + "meet_solana": { + "description": "Solana wipes sweat from her brow and turns to you. She's weathered but strong, with sun-darkened skin and eyes that have seen a thousand harvests. 'Well now! That's the first time someone's helped without askin' for payment first. You're either very kind or very stupid.' She grins. 'Name's Solana. What can I do you for?'", + "choices": ["I need Sun-Ripened Tomatoes", "What's with all the crows?"], + "transitions": { + "I need Sun-Ripened Tomatoes": "tomato_request", + "What's with all the crows?": "crow_explanation" + }, + "media": {} + }, + "tomato_request": { + "description": "'The Sun-Ripened Tomatoes!' Solana beams with pride. 'Best in the land! Sweetest, juiciest, most perfectly red-' She pauses. 'But I can't give 'em away while those crows keep coming back. They'll just steal whatever I have left. Unless...' She looks at you appraisingly.", + "choices": ["Unless what?", "I'll help however I can!"], + "transitions": { + "Unless what?": "scarecrow_idea", + "I'll help however I can!": "scarecrow_idea" + }, + "media": {} + }, + "crow_explanation": { + "description": "'These aren't ordinary crows,' Solana grumbles. 'They're SMART. Organized. Got a leader somewhere, probably. They figured out that my tomatoes are worth stealing - nothing else in the valley tastes as good.' She crosses her arms. 'Been fighting 'em off for weeks now.'", + "choices": ["How can I help?"], + "transitions": { + "How can I help?": "scarecrow_idea" + }, + "media": {} + }, + "scarecrow_idea": { + "description": "'You want tomatoes, right?' Solana says. 'Help me build a proper scarecrow - one that'll keep those crows away for GOOD. I've got all the materials, just never had the time to build it myself.' She gestures at a pile of supplies. 'What do you say?'", + "choices": ["Let's build a scarecrow!"], + "transitions": { + "Let's build a scarecrow!": "scarecrow_build_1" + }, + "media": {} + }, + "scarecrow_build_1": { + "description": "Solana dumps out the supplies: wooden posts, old clothes, straw, rope, and... 'Is that a disco ball?' you ask. 'Crows hate reflections,' Solana explains. 'Plus it'll look fabulous.' First things first - you need to build the frame.", + "choices": ["Start with a tall post for the body", "Use two posts crossed for arms"], + "transitions": { + "Start with a tall post for the body": "frame_body", + "Use two posts crossed for arms": "frame_arms" + }, + "media": {} + }, + "frame_body": { + "description": "You plant the main post firmly in the ground. It's sturdy and tall - about seven feet. 'Good thinking!' Solana nods. 'Needs to be imposing. Now let's add the arm piece.'", + "choices": ["Attach the crossbar"], + "transitions": { + "Attach the crossbar": "frame_complete" + }, + "media": {} + }, + "frame_arms": { + "description": "You cross two posts for the arms first, but realize they need something to stand on. 'Cart before the horse, friend,' Solana chuckles. 'Let's get the main post up first.'", + "choices": ["Set up the main post"], + "transitions": { + "Set up the main post": "frame_body" + }, + "media": {} + }, + "frame_complete": { + "description": "The T-shaped frame stands proud against the sunny sky. 'Now comes the fun part,' Solana grins, holding up the old clothes. 'Let's dress this fella!'", + "choices": ["Add the shirt and pants", "Stuff it with straw first"], + "transitions": { + "Add the shirt and pants": "dress_scarecrow", + "Stuff it with straw first": "stuff_scarecrow" + }, + "media": {} + }, + "stuff_scarecrow": { + "description": "You stuff handfuls of straw into the frame, creating a body shape. It's messy but effective. 'Perfect!' Solana says. 'Now let's get it dressed. I've got my grandfather's old overalls - he'd be honored.'", + "choices": ["Add the clothes"], + "transitions": { + "Add the clothes": "dress_scarecrow" + }, + "media": {} + }, + "dress_scarecrow": { + "description": "You wrestle the straw-stuffed frame into grandfather's overalls and a plaid shirt. It's starting to look like something! But it needs a head. Solana produces a burlap sack with a face drawn on it - lopsided but menacing.", + "choices": ["Attach the head"], + "transitions": { + "Attach the head": "add_head" + }, + "media": {} + }, + "add_head": { + "description": "The burlap head sits atop the scarecrow, staring out with wobbly button eyes. 'Just one more thing...' Solana holds up the disco ball. 'For MAXIMUM crow deterrence!' She positions it to spin and catch the sunlight.", + "choices": ["Add the disco ball"], + "transitions": { + "Add the disco ball": "scarecrow_complete" + }, + "media": {} + }, + "scarecrow_complete": { + "description": "The scarecrow stands complete: tall, dressed in farmer chic, with a disco ball heart that sends rainbows scattering across the field. It's ridiculous. It's beautiful. And as if on cue, a scout crow flies overhead, sees the creation, and IMMEDIATELY turns around.", + "choices": ["Watch the crows' reaction"], + "transitions": { + "Watch the crows' reaction": "crow_retreat" + }, + "media": {} + }, + "crow_retreat": { + "description": "Word spreads fast in crow society. Within minutes, you see the entire flock retreating over the hills, casting frightened glances back at the glittering guardian. 'Would you look at that!' Solana laughs. 'Haven't seen 'em fly that fast since the Great Pie Incident of '09.'", + "choices": ["Celebrate the victory!"], + "transitions": { + "Celebrate the victory!": "receive_tomatoes" + }, + "media": {} + }, + "receive_tomatoes": { + "description": "Solana leads you to the tomato patch - and what tomatoes they are! Each one glows with inner warmth, so ripe they're nearly translucent. She picks the three best. 'These here are Sun-Ripened Tomatoes. My grandmother's strain. Thank you for helping me protect 'em.'", + "choices": ["Thank you, Solana"], + "transitions": { + "Thank you, Solana": "solana_farewell" + }, + "media": {} + }, + "solana_farewell": { + "description": "'Ain't nothing,' Solana waves off your thanks. 'That's how it works round here - you help me, I help you. The land provides when you work together.' She tips her hat. 'Now git on with your sandwich quest. And if those tomatoes don't make you happy, I'll eat my hat. No I won't, it's my only one.'", + "choices": ["Return to village"], + "transitions": { + "Return to village": "village/return_hub" + }, + "media": {} + } + }, + + "hunters_lodge": { + "forest_path": { + "description": "The path winds through ancient forest. Shafts of light pierce the canopy, illuminating moss-covered stones and the occasional hunting trophy nailed to trees. A sign carved into oak reads: 'Hunter's Lodge - By Appointment Only. (Just kidding, come in, we're lonely.)'", + "choices": ["Continue to the lodge", "Examine the trophies"], + "transitions": { + "Continue to the lodge": "lodge_exterior", + "Examine the trophies": "examine_trophies" + }, + "media": {} + }, + "examine_trophies": { + "description": "The trees display various impressive kills - but all are labeled 'FAIR HUNT' with ethical hunting certification. One newer sign reads: 'The Golden Pig has NEVER been caught. Records: 0 hunters. Pig: 847.' Impressive.", + "choices": ["Continue to the lodge"], + "transitions": { + "Continue to the lodge": "lodge_exterior" + }, + "media": {} + }, + "lodge_exterior": { + "description": "The Hunter's Lodge is a magnificent log structure, adorned with antlers and crossed spears. Smoke rises from the chimney. Inside, you can hear boisterous laughter and the clinking of mugs. A sign by the door reads: 'Leave your disputes outside. Inside, we are all prey... to good times!'", + "choices": ["Enter the lodge"], + "transitions": { + "Enter the lodge": "lodge_interior" + }, + "media": {} + }, + "lodge_interior": { + "description": "The lodge is warm, filled with hunters sharing tales. At the head table sits a massive man with a wild beard and wilder eyes - Huntmaster Boar. He's carving what appears to be a tiny wooden pig while the other hunters listen to his story. '...and THAT'S the 847th time it escaped!' Everyone groans.", + "choices": ["Approach Huntmaster Boar", "Listen to the story first"], + "transitions": { + "Approach Huntmaster Boar": "meet_boar", + "Listen to the story first": "pig_story" + }, + "media": {} + }, + "pig_story": { + "description": "'The Golden Pig,' Boar continues, 'is the greatest quarry in all the land! Its ham is legendary - one taste grants you the strength of ten hunters! But no one has EVER caught it. The beast is too clever, too fast, too... mocking.' He stabs his carving in frustration.", + "choices": ["Approach and offer help"], + "transitions": { + "Approach and offer help": "meet_boar" + }, + "media": {} + }, + "meet_boar": { + "description": "Huntmaster Boar looks up as you approach. His eyes are sharp, assessing. 'A newcomer! Let me guess - you want Legendary Ham for some fancy recipe, eh?' He barks a laugh. 'Everyone does! Problem is, the ham comes from ONE pig - the Golden Pig. And that beast has outsmarted every hunter in history.'", + "choices": ["I'll help you catch it!", "Tell me more about this pig"], + "transitions": { + "I'll help you catch it!": "tracking_quest", + "Tell me more about this pig": "pig_lore" + }, + "media": {} + }, + "pig_lore": { + "description": "'The Golden Pig is magical,' Boar explains, almost reverently. 'Its hide shimmers like gold. Its oinks can disorient even the keenest tracker. And its intelligence?' He shakes his head. 'It sets TRAPS. For US. The hunters become the hunted!'", + "choices": ["I'll help you track it"], + "transitions": { + "I'll help you track it": "tracking_quest" + }, + "media": {} + }, + "tracking_quest": { + "description": "'You've got spirit!' Boar slaps your shoulder (it hurts). 'Very well! We track the Golden Pig together. But we do this with HONOR. No traps, no poison. We use our wits and our legs.' He grabs a hunting horn. 'The pig was last seen near the Crystal Stream. Let's go!'", + "choices": ["Follow Boar into the forest"], + "transitions": { + "Follow Boar into the forest": "tracking_start" + }, + "media": {} + }, + "tracking_start": { + "description": "You and Boar move silently through the forest. He points at various signs - disturbed leaves, unusual scratches on bark. 'The pig passed through here,' he whispers. 'Recently. See these hoof prints?' They're oddly geometrical, almost artistic. 'It's TAUNTING us.'", + "choices": ["Follow the tracks", "Look for other signs"], + "transitions": { + "Follow the tracks": "pig_sighting", + "Look for other signs": "find_clue" + }, + "media": {} + }, + "find_clue": { + "description": "You notice something Boar missed - a tuft of golden hair caught on a branch, still warm. The pig is VERY close. You point it out silently. Boar's eyes widen with respect. 'Good eye!' he mouths. 'You might just be the partner I've needed.'", + "choices": ["Continue tracking"], + "transitions": { + "Continue tracking": "pig_sighting" + }, + "media": {} + }, + "pig_sighting": { + "description": "There! Through the brush - a flash of gold! The pig stands in a sunlit clearing, seemingly waiting. It's magnificent - shimmering hide, proud stance, and an expression that can only be described as SMUG. It sees you watching. It winks.", + "choices": ["Charge!", "Try to circle around"], + "transitions": { + "Charge!": "chase_begins", + "Try to circle around": "tactical_approach" + }, + "media": {} + }, + "tactical_approach": { + "description": "You and Boar split up, attempting to flank the pig. It watches with amusement, letting you get ALMOST in position before - 'OINK!' It dashes off, but not before knocking over Boar with a casual shoulder bump.", + "choices": ["Chase after it!"], + "transitions": { + "Chase after it!": "chase_begins" + }, + "media": {} + }, + "chase_begins": { + "description": "THE CHASE IS ON! The Golden Pig rockets through the forest, and you follow! It's impossibly fast, but also impossibly cocky - it stops occasionally to look back and make sure you're still following. 'It's playing with us!' Boar gasps.", + "choices": ["Keep chasing", "Try to predict where it's going"], + "transitions": { + "Keep chasing": "chase_continues", + "Try to predict where it's going": "predict_path" + }, + "media": {} + }, + "predict_path": { + "description": "You notice the pig is circling back toward the lodge. Of course! The smell of food! You veer off to cut it off while Boar keeps pursuing from behind.", + "choices": ["Set up an ambush"], + "transitions": { + "Set up an ambush": "ambush_setup" + }, + "media": {} + }, + "chase_continues": { + "description": "The pig leads you through streams, over logs, under branches. You're exhausted. Boar is sweating waterfalls. But then you notice - the pig is circling back toward the lodge!", + "choices": ["Try to cut it off!"], + "transitions": { + "Try to cut it off!": "ambush_setup" + }, + "media": {} + }, + "ambush_setup": { + "description": "You reach the lodge's back entrance just as the pig comes trotting up, confident in its victory. Its eyes widen with genuine surprise - 'OINK?!' - when it sees you waiting.", + "choices": ["Got you now!"], + "transitions": { + "Got you now!": "pig_caught" + }, + "media": {} + }, + "pig_caught": { + "description": "You tackle the Golden Pig just as Boar arrives! It squirms, it oinks, it tries every trick - but you've got it. Boar approaches, breathing hard but grinning. 'Well done! The first capture in HISTORY!' The pig huffs and actually looks... impressed?", + "choices": ["What happens now?"], + "transitions": { + "What happens now?": "pig_negotiation" + }, + "media": {} + }, + "pig_negotiation": { + "description": "The pig stops struggling. To your shock, it SPEAKS. 'Alright, alright, fair catch. It's been a good 847 runs, but you two actually earned it.' It sighs. 'I'll give you some ham - magical ham regrows anyway. Just... promise to make it into something WORTHY?'", + "choices": ["I promise - it's for the Perfect Sandwich"], + "transitions": { + "I promise - it's for the Perfect Sandwich": "receive_ham" + }, + "media": {} + }, + "receive_ham": { + "description": "The pig closes its eyes, concentrates, and a golden shimmer produces... a perfect ham! 'There. Legendary Ham, as promised.' It stands up, brushes itself off. 'Same time next month for another chase?' Boar laughs. 'Wouldn't miss it, old friend.'", + "choices": ["Thank them both"], + "transitions": { + "Thank them both": "boar_farewell" + }, + "media": {} + }, + "boar_farewell": { + "description": "'You've earned your ham AND my respect,' Boar says, clasping your hand. 'True hunting isn't about the kill - it's about the challenge, the pursuit, the respect between hunter and quarry.' He winks. 'Also the ham is REALLY good. Trust me.'", + "choices": ["Return to village"], + "transitions": { + "Return to village": "village/return_hub" + }, + "media": {} + } + }, + + "hidden_restaurant": { + "search_begin": { + "description": "The Hidden Restaurant is, true to its name, hidden. You stand in a nondescript alley behind the village market. There are no signs, no doors - just walls. But the Recipe Card's instructions are clear: 'Look for what isn't there.'", + "choices": ["Search the walls carefully", "Look up", "Look down"], + "transitions": { + "Search the walls carefully": "wall_search", + "Look up": "look_up", + "Look down": "trapdoor" + }, + "media": {} + }, + "wall_search": { + "description": "You run your hands along the brick walls. Nothing unusual... wait. One brick feels different. Warmer. You push it, and somewhere below, you hear a CLICK.", + "choices": ["Look for what opened"], + "transitions": { + "Look for what opened": "trapdoor" + }, + "media": {} + }, + "look_up": { + "description": "You look up and see... a normal sky. A pigeon stares at you judgmentally. It's not up there.", + "choices": ["Look down instead"], + "transitions": { + "Look down instead": "trapdoor" + }, + "media": {} + }, + "trapdoor": { + "description": "A trapdoor! Hidden beneath a false cobblestone, now ajar. Warm air and the smell of incredible food wafts up. A rope ladder descends into darkness lit by candles. A small sign reads: 'Reservations not required. Secrecy mandatory.'", + "choices": ["Descend into the restaurant"], + "transitions": { + "Descend into the restaurant": "restaurant_entrance" + }, + "media": {} + }, + "restaurant_entrance": { + "description": "The Hidden Restaurant is BEAUTIFUL. An underground cavern transformed into an intimate dining space - crystal chandeliers, velvet seats, and the SMELLS. Oh, the smells! At the far end, a kitchen erupts with flame and flair. A figure in dramatic chef whites emerges through smoke.", + "choices": ["Approach the chef"], + "transitions": { + "Approach the chef": "meet_mysterio" + }, + "media": {} + }, + "meet_mysterio": { + "description": "Chef Mysterio strikes a pose. He wears dark sunglasses indoors, his mustache is waxed into perfect points, and he speaks in a theatrical whisper. 'Ah... a SEEKER. You wish to taste... the SECRET SAUCE.' He pauses dramatically. '...Don't you?'", + "choices": ["Yes, please!", "How did you know?"], + "transitions": { + "Yes, please!": "sauce_request", + "How did you know?": "mysterio_explanation" + }, + "media": {} + }, + "mysterio_explanation": { + "description": "'How did I know?' Mysterio gasps. 'I am MYSTERIO! I know ALL!' He leans closer. '...Also you're holding a recipe card that specifically lists my sauce. But mostly the first thing. DRAMATIC PAUSE.' He actually says the words 'dramatic pause.'", + "choices": ["Right. Can I have some Secret Sauce?"], + "transitions": { + "Right. Can I have some Secret Sauce?": "sauce_request" + }, + "media": {} + }, + "sauce_request": { + "description": "'The Secret Sauce...' Mysterio sighs tragically. 'It is IMPOSSIBLE to make right now. I am missing three crucial ingredients! THE SACRED SPICES!' He gestures at three empty jars. 'Without them, the sauce is merely... sauce. Not SECRET sauce. Just... regular sauce. MUNDANE sauce.'", + "choices": ["Where can I find the spices?", "I'll help you get them!"], + "transitions": { + "Where can I find the spices?": "spice_quest", + "I'll help you get them!": "spice_quest" + }, + "media": {} + }, + "spice_quest": { + "description": "'Three spices, three locations!' Mysterio counts on his fingers. 'The CRIMSON CAYENNE from the Spice Merchant's secret stash! The MIDNIGHT PEPPER from the edge of the Night Market! And the GOLDEN SAFFRON from... dramatic pause... my EX-WIFE!'", + "choices": ["Let's start with the Crimson Cayenne"], + "transitions": { + "Let's start with the Crimson Cayenne": "spice_1_start" + }, + "media": {} + }, + "spice_1_start": { + "description": "Mysterio provides directions to the Spice Merchant's stall. 'He guards the Crimson Cayenne jealously! You must... BARGAIN!' He makes it sound terrifying. In reality, it's a five-minute walk to a nice man named Gerald.", + "choices": ["Visit the Spice Merchant"], + "transitions": { + "Visit the Spice Merchant": "spice_merchant" + }, + "media": {} + }, + "spice_merchant": { + "description": "Gerald the Spice Merchant is perfectly pleasant. His stall is colorful and aromatic. 'Crimson Cayenne? Sure, I've got some!' He pulls out a jar. 'But it's expensive. Alternatively, you could tell me a really good joke. My day's been boring.'", + "choices": ["Pay for the spice", "Tell a joke"], + "transitions": { + "Pay for the spice": "get_cayenne_money", + "Tell a joke": "tell_joke" + }, + "media": {} + }, + "tell_joke": { + "description": "You rack your brain. 'Why did the pepper win the race?' Gerald leans in eagerly. 'Because it was JALAPEN-NO TIME!' He stares. Then EXPLODES with laughter. 'That's TERRIBLE! I LOVE IT!' He hands over the Crimson Cayenne, wiping tears.", + "choices": ["One down, two to go"], + "transitions": { + "One down, two to go": "spice_2_start" + }, + "media": {} + }, + "get_cayenne_money": { + "description": "You hand over the coins. Gerald nods professionally and bags the Crimson Cayenne. 'Pleasure doing business! That chef Mysterio still owes me for LAST time, by the way. Something about a dramatic pause going too long.'", + "choices": ["One down, two to go"], + "transitions": { + "One down, two to go": "spice_2_start" + }, + "media": {} + }, + "spice_2_start": { + "description": "The Night Market only operates at night (shocking). You wait until dusk, when the alley transforms into a maze of glowing stalls and strange merchants. The air smells of mystery and questionable meat skewers.", + "choices": ["Search for the Midnight Pepper vendor"], + "transitions": { + "Search for the Midnight Pepper vendor": "night_market" + }, + "media": {} + }, + "night_market": { + "description": "The Night Market is overwhelming! Vendors sell dreams (literal bottled dreams), secondhand shadows, and pre-owned luck. Finally, you spot a stall that seems to absorb light - the Midnight Pepper must be here.", + "choices": ["Approach the dark stall"], + "transitions": { + "Approach the dark stall": "pepper_vendor" + }, + "media": {} + }, + "pepper_vendor": { + "description": "A figure shrouded in darkness (really, they're just wearing a very big cloak) whispers: 'You seek the Midnight Pepper. I can tell. To prove your worth... answer me this: What is the secret ingredient in any meal?'", + "choices": ["Love?", "Time?", "Hunger?"], + "transitions": { + "Love?": "pepper_answer_love", + "Time?": "pepper_answer_time", + "Hunger?": "pepper_answer_hunger" + }, + "media": {} + }, + "pepper_answer_love": { + "description": "The vendor chuckles. 'Close! But love is an ADDITION. The secret ingredient is already THERE.' They wait expectantly.", + "choices": ["Is it hunger?"], + "transitions": { + "Is it hunger?": "pepper_answer_hunger" + }, + "media": {} + }, + "pepper_answer_time": { + "description": "The vendor snorts. 'Time makes things rot as often as it makes them better. Think more... primal.' They wait expectantly.", + "choices": ["Hunger?"], + "transitions": { + "Hunger?": "pepper_answer_hunger" + }, + "media": {} + }, + "pepper_answer_hunger": { + "description": "'CORRECT!' The vendor throws off their cloak - it's just a regular person who really likes drama. 'Everything tastes better when you're hungry! Here's your Midnight Pepper.' They hand over a jar of pepper so dark it seems to drink light.", + "choices": ["Two down, one to go"], + "transitions": { + "Two down, one to go": "spice_3_start" + }, + "media": {} + }, + "spice_3_start": { + "description": "The last spice requires visiting Mysterio's ex-wife. He provides an address and a warning: 'She is... PASSIONATE. About cooking AND holding grudges. Perhaps mention that I have grown? Changed? Become... less dramatic?' He pauses dramatically. 'Okay, maybe don't mention that.'", + "choices": ["Visit the ex-wife"], + "transitions": { + "Visit the ex-wife": "ex_wife_house" + }, + "media": {} + }, + "ex_wife_house": { + "description": "The house is lovely and smells incredible. A woman answers - Sophia, according to the mailbox. She sees you and sighs. 'Let me guess. Mysterio sent you for the saffron.' Her tone is weary but not unkind. 'What's his dramatic excuse THIS time?'", + "choices": ["He says he's changed", "He's making something special"], + "transitions": { + "He says he's changed": "sophia_skeptical", + "He's making something special": "sophia_interested" + }, + "media": {} + }, + "sophia_skeptical": { + "description": "'Changed?' Sophia laughs. 'Did he stop saying dramatic pause out loud?' You shake your head. 'Then he hasn't changed.' But her eyes soften. 'Still... that man can COOK. What's the saffron for?'", + "choices": ["The Secret Sauce - I'm making the Perfect Sandwich"], + "transitions": { + "The Secret Sauce - I'm making the Perfect Sandwich": "sophia_relents" + }, + "media": {} + }, + "sophia_interested": { + "description": "'Something special, huh?' Sophia raises an eyebrow. 'What kind of special?' Despite herself, she seems curious.", + "choices": ["The Perfect Sandwich - it's for my birthday"], + "transitions": { + "The Perfect Sandwich - it's for my birthday": "sophia_relents" + }, + "media": {} + }, + "sophia_relents": { + "description": "Sophia's face softens. 'A birthday sandwich. That's actually sweet.' She disappears inside and returns with a small jar of golden threads. 'Here. The Golden Saffron. Tell that dramatic fool...' She pauses. '...tell him the sauce better be amazing.'", + "choices": ["Thank you!"], + "transitions": { + "Thank you!": "return_to_mysterio" + }, + "media": {} + }, + "return_to_mysterio": { + "description": "You return to the Hidden Restaurant with all three spices. Mysterio receives them with reverence. 'The SACRED TRIO! Now... watch closely. Or don't. The sauce is SECRET for a reason!' He vanishes into the kitchen in a puff of unnecessary smoke.", + "choices": ["Wait for the sauce"], + "transitions": { + "Wait for the sauce": "sauce_creation" + }, + "media": {} + }, + "sauce_creation": { + "description": "Sounds emerge from the kitchen - sizzling, stirring, what might be a small explosion. After an eternal dramatic pause, Mysterio emerges holding a bottle of deep red sauce that seems to shimmer with hidden depths.", + "choices": ["Receive the Secret Sauce"], + "transitions": { + "Receive the Secret Sauce": "receive_sauce" + }, + "media": {} + }, + "receive_sauce": { + "description": "'The SECRET SAUCE!' Mysterio presents it like a trophy. 'Made with Crimson Cayenne for HEAT, Midnight Pepper for DEPTH, and Golden Saffron for... actually that one's just expensive. But it adds COLOR!' He winks behind his sunglasses. 'Use it wisely.'", + "choices": ["Thank you, Chef Mysterio"], + "transitions": { + "Thank you, Chef Mysterio": "mysterio_farewell" + }, + "media": {} + }, + "mysterio_farewell": { + "description": "'One more thing,' Mysterio says, suddenly genuine. 'Tell Sophia... tell her the sauce turned out amazing. And... dramatic pause... that I miss her cooking.' He shakes himself back into character. 'NOW GO! Your sandwich awaits! DRAMATIC EXIT!' He vanishes behind a curtain.", + "choices": ["Return to village"], + "transitions": { + "Return to village": "village/return_hub" + }, + "media": {} + } + }, + + "pickle_temple": { + "temple_approach": { + "description": "The Pickle Temple rises from the marshy ground - a strange structure of green-tinted stone and glass jars. The air smells of vinegar and ancient wisdom. A sign reads: 'Enter with an open mind. And an empty stomach. The sacred brine demands it.'", + "choices": ["Enter the temple", "Study the architecture"], + "transitions": { + "Enter the temple": "temple_entrance", + "Study the architecture": "temple_architecture" + }, + "media": {} + }, + "temple_architecture": { + "description": "The temple appears to be built FROM pickle jars - thousands of them, fused together with some kind of ancient resin. Each jar contains a different type of pickle, creating a dizzying mosaic of preserved vegetables. It's weird. It's wonderful.", + "choices": ["Enter the temple"], + "transitions": { + "Enter the temple": "temple_entrance" + }, + "media": {} + }, + "temple_entrance": { + "description": "The entrance hall echoes with the sound of bubbling brine. Pickle-themed tapestries line the walls, depicting the history of fermentation. At the far end, on a throne made of cucumber vines, sits a figure in green robes - the Pickle Wizard Brine.", + "choices": ["Approach the Pickle Wizard"], + "transitions": { + "Approach the Pickle Wizard": "meet_brine" + }, + "media": {} + }, + "meet_brine": { + "description": "Pickle Wizard Brine looks up. His eyes are the exact color of pickle juice. His beard is braided with tiny gherkins. 'Ah, a SEEKER!' His voice gurgles slightly. 'You seek the Ancient Pickles, do you not? But first - can you tell me - what IS a pickle?'", + "choices": ["A preserved cucumber?", "A state of mind?", "A riddle?"], + "transitions": { + "A preserved cucumber?": "brine_approves", + "A state of mind?": "brine_intrigued", + "A riddle?": "brine_delighted" + }, + "media": {} + }, + "brine_approves": { + "description": "'Correct! A pickle is transformation - vegetable transcendence through brine!' Brine nods vigorously. 'But simple answers won't unlock the Ancient Pickles. They are sealed by THREE RIDDLES, posed by previous Pickle Wizards. Solve them, and the jar is yours!'", + "choices": ["I accept the challenge!"], + "transitions": { + "I accept the challenge!": "riddle_1" + }, + "media": {} + }, + "brine_intrigued": { + "description": "'A state of mind...' Brine strokes his gherkin-beard. 'Interesting! Being in a pickle means trouble. But the pickle itself represents TRANSFORMATION. I like your thinking.' He grins. 'Let's see if you can solve my riddles!'", + "choices": ["I'm ready!"], + "transitions": { + "I'm ready!": "riddle_1" + }, + "media": {} + }, + "brine_delighted": { + "description": "'A RIDDLE!' Brine cackles with glee. 'Oh, you understand! Everything is a riddle if you think hard enough! The Ancient Pickles are sealed by three riddles - but perhaps YOU are the one to solve them!' He rubs his hands together.", + "choices": ["Let's begin!"], + "transitions": { + "Let's begin!": "riddle_1" + }, + "media": {} + }, + "riddle_1": { + "description": "Brine's eyes gleam. 'RIDDLE THE FIRST: I am born in water but live in vinegar. I was once soft but am now firm with purpose. What am I?'", + "choices": ["A pickle!", "A cucumber?", "A memory?"], + "transitions": { + "A pickle!": "riddle_1_correct", + "A cucumber?": "riddle_1_almost", + "A memory?": "riddle_1_creative" + }, + "media": {} + }, + "riddle_1_almost": { + "description": "'Almost! A cucumber is the BEFORE - the question asks about the AFTER. Think of transformation!' Brine waves encouragingly.", + "choices": ["A pickle!"], + "transitions": { + "A pickle!": "riddle_1_correct" + }, + "media": {} + }, + "riddle_1_creative": { + "description": "'Creative!' Brine chuckles. 'But no, memories don't live in vinegar. Well, usually. The answer is more... vegetable.' He waits.", + "choices": ["A pickle!"], + "transitions": { + "A pickle!": "riddle_1_correct" + }, + "media": {} + }, + "riddle_1_correct": { + "description": "'CORRECT!' Brine claps. 'A pickle indeed! Born as cucumber in water, transformed by vinegar into something NEW!' A clicking sound echoes - the first seal is broken. 'RIDDLE THE SECOND awaits!'", + "choices": ["I'm ready"], + "transitions": { + "I'm ready": "riddle_2" + }, + "media": {} + }, + "riddle_2": { + "description": "'RIDDLE THE SECOND: I give flavor but am not consumed. I hold without hands. I work best in darkness. What am I?'", + "choices": ["Time?", "A jar?", "Patience?"], + "transitions": { + "Time?": "riddle_2_close", + "A jar?": "riddle_2_correct", + "Patience?": "riddle_2_philosophical" + }, + "media": {} + }, + "riddle_2_close": { + "description": "'Time gives flavor, true! But it cannot HOLD things. Think more... container-y.' Brine hints heavily.", + "choices": ["A jar!"], + "transitions": { + "A jar!": "riddle_2_correct" + }, + "media": {} + }, + "riddle_2_philosophical": { + "description": "'Ooh, philosophical! But patience doesn't hold things physically. Try again - something you'd find in this very temple!' Brine gestures at the walls of jars.", + "choices": ["A jar!"], + "transitions": { + "A jar!": "riddle_2_correct" + }, + "media": {} + }, + "riddle_2_correct": { + "description": "'A JAR! Yes!' Brine twirls happily. 'The humble jar - giving flavor through fermentation, holding contents without hands, and working its magic in dark pantries!' Another CLICK. 'One riddle remains!'", + "choices": ["Final riddle - bring it on!"], + "transitions": { + "Final riddle - bring it on!": "riddle_3" + }, + "media": {} + }, + "riddle_3": { + "description": "'THE FINAL RIDDLE!' Brine's voice echoes dramatically. 'I have lived a hundred years, yet taste of yesterday. I am given freely yet cannot be bought. I bring tears to the eye but joy to the tongue. What am I?'", + "choices": ["The Ancient Pickle!", "A family recipe?", "Nostalgia?"], + "transitions": { + "The Ancient Pickle!": "riddle_3_correct", + "A family recipe?": "riddle_3_close", + "Nostalgia?": "riddle_3_deep" + }, + "media": {} + }, + "riddle_3_close": { + "description": "'Family recipe is CLOSE - recipes are given freely and bring joy! But there's something more specific hiding in this temple...' Brine winks conspicuously at the sealed jar.", + "choices": ["The Ancient Pickle!"], + "transitions": { + "The Ancient Pickle!": "riddle_3_correct" + }, + "media": {} + }, + "riddle_3_deep": { + "description": "'Nostalgia! Poetic! But nostalgia can't be tasted with the tongue. Think of what you came here FOR.' Brine taps his foot near a very old, very sealed jar.", + "choices": ["The Ancient Pickle!"], + "transitions": { + "The Ancient Pickle!": "riddle_3_correct" + }, + "media": {} + }, + "riddle_3_correct": { + "description": "'THE ANCIENT PICKLE!' Brine throws his arms wide. 'One hundred years aged, tasting of history, given freely to those who solve the riddles, bringing tears of flavor and joy to all!' The final CLICK echoes, and a secret panel opens, revealing THE JAR.", + "choices": ["Behold the Ancient Pickles!"], + "transitions": { + "Behold the Ancient Pickles!": "receive_pickles" + }, + "media": {} + }, + "receive_pickles": { + "description": "The jar glows with a soft green light. Inside, perfectly preserved pickles float in century-old brine. Brine lifts the jar reverently and places it in your hands. 'The Ancient Pickles. Treat them with respect. They've earned it.'", + "choices": ["I will. Thank you, Pickle Wizard"], + "transitions": { + "I will. Thank you, Pickle Wizard": "brine_farewell" + }, + "media": {} + }, + "brine_farewell": { + "description": "'One more thing,' Brine says. 'The Ancient Pickles have a property - they enhance whatever they're paired with. Your sandwich won't just be perfect. It will be TRANSCENDENT.' He bows. 'Now go! May your brine stay salty!'", + "choices": ["Return to village"], + "transitions": { + "Return to village": "village/return_hub" + }, + "media": {} + } + }, + + "crystal_caverns": { + "cavern_entrance": { + "description": "The Crystal Caverns glow from within, casting rainbow light across the valley. The entrance is grand - natural crystal formations frame it like a cathedral door. A sign etched in crystal warns: 'The Onion Oracle sees all futures. Especially the ones that make you cry.'", + "choices": ["Enter the caverns", "Prepare yourself mentally"], + "transitions": { + "Enter the caverns": "cavern_descent", + "Prepare yourself mentally": "mental_prep" + }, + "media": {} + }, + "mental_prep": { + "description": "You take a deep breath. An Onion Oracle sounds intense. Crystal Onions sound magical. Prophecies sound ominous. But you've faced ghosts, chased golden pigs, and solved pickle riddles. You can handle this.", + "choices": ["Enter the caverns"], + "transitions": { + "Enter the caverns": "cavern_descent" + }, + "media": {} + }, + "cavern_descent": { + "description": "The cavern descends in natural crystal steps. Each formation glows a different color - pink, blue, gold, green. The air grows warm and carries a sharp, pungent scent. Onion, but somehow... sacred.", + "choices": ["Continue deeper"], + "transitions": { + "Continue deeper": "oracle_chamber" + }, + "media": {} + }, + "oracle_chamber": { + "description": "The Oracle's Chamber takes your breath away. Crystals the size of houses jut from every surface. In the center, tending a garden of crystalline onions, is a figure wrapped in shimmering robes - the Onion Oracle Lachrym. Tears stream endlessly down her face.", + "choices": ["Approach respectfully", "Ask if she's okay"], + "transitions": { + "Approach respectfully": "meet_lachrym", + "Ask if she's okay": "lachrym_tears" + }, + "media": {} + }, + "lachrym_tears": { + "description": "'The tears?' Lachrym smiles serenely. 'I cry because I see ALL futures. Every joy, every sorrow, every perfectly made sandwich and every burnt toast.' She dabs her eyes with a crystal handkerchief. 'It's a lot. But I wouldn't trade it. Now, you seek the Crystal Onions?'", + "choices": ["Yes, Oracle. How did you know?"], + "transitions": { + "Yes, Oracle. How did you know?": "meet_lachrym" + }, + "media": {} + }, + "meet_lachrym": { + "description": "'I have seen your coming,' Lachrym says, her voice like wind chimes. 'The birthday seeker. The sandwich dreamer. You desire the Crystal Onions, and I shall grant them - but only if you fulfill the prophecy.' Her tears sparkle. 'Three tasks, three truths, three steps toward your perfect meal.'", + "choices": ["What is the prophecy?", "I'll do whatever it takes"], + "transitions": { + "What is the prophecy?": "prophecy_explained", + "I'll do whatever it takes": "prophecy_explained" + }, + "media": {} + }, + "prophecy_explained": { + "description": "'The prophecy speaks of one who seeks perfection,' Lachrym recites. 'They must: FIRST, find the Crystal of Clarity and see their true self. SECOND, plant a seed in barren ground. THIRD, share a memory they've never shared.' She gestures into the depths. 'Begin with the Crystal of Clarity.'", + "choices": ["I'll find the crystal"], + "transitions": { + "I'll find the crystal": "task_1_start" + }, + "media": {} + }, + "task_1_start": { + "description": "Lachrym points to a tunnel lined with mirrors - crystal mirrors that reflect not light but... something else. 'The Crystal of Clarity lies within. But beware - the mirrors show truth. Some find it beautiful. Some find it painful. All find it real.'", + "choices": ["Enter the mirror tunnel"], + "transitions": { + "Enter the mirror tunnel": "mirror_tunnel" + }, + "media": {} + }, + "mirror_tunnel": { + "description": "The mirrors show... you. But different versions. One mirror shows you as a child, wide-eyed with wonder. Another shows you tired, stressed, doubting. A third shows you as you could be - confident, fulfilled, holding a perfect sandwich. The Crystal of Clarity pulses at the end.", + "choices": ["Accept all versions of yourself", "Focus only on the hopeful version"], + "transitions": { + "Accept all versions of yourself": "clarity_complete", + "Focus only on the hopeful version": "clarity_lesson" + }, + "media": {} + }, + "clarity_lesson": { + "description": "You try to focus only on the hopeful mirror, but the others crowd in. The crystal dims. Lachrym's voice echoes: 'Clarity comes from accepting ALL of yourself - not just the parts you like. Try again.'", + "choices": ["Accept all versions"], + "transitions": { + "Accept all versions": "clarity_complete" + }, + "media": {} + }, + "clarity_complete": { + "description": "'Yes,' you whisper. 'All of this is me. The hopeful AND the doubtful. The child AND the adult. This is who I am.' The Crystal of Clarity BLAZES with light and floats to your hand. Task one complete.", + "choices": ["Return to Lachrym"], + "transitions": { + "Return to Lachrym": "task_2_start" + }, + "media": {} + }, + "task_2_start": { + "description": "'You have found clarity,' Lachrym nods. 'Now - the second task. Plant this seed.' She hands you a tiny, crystalline seed. 'But plant it there.' She points to a patch of dead, grey stone. 'Where nothing grows.'", + "choices": ["That seems impossible...", "I'll try anyway"], + "transitions": { + "That seems impossible...": "lachrym_wisdom", + "I'll try anyway": "plant_seed" + }, + "media": {} + }, + "lachrym_wisdom": { + "description": "'Impossible?' Lachrym smiles through her tears. 'I once saw a future where someone made a sandwich so perfect it ended a war. Impossible is just a word we use before we try.'", + "choices": ["I'll plant the seed"], + "transitions": { + "I'll plant the seed": "plant_seed" + }, + "media": {} + }, + "plant_seed": { + "description": "You kneel before the barren stone. It's cold, lifeless. You press the crystal seed into a tiny crack and... nothing happens. But then you remember the Crystal of Clarity. You accepted yourself. Maybe the stone needs acceptance too.", + "choices": ["Speak to the stone kindly"], + "transitions": { + "Speak to the stone kindly": "stone_grows" + }, + "media": {} + }, + "stone_grows": { + "description": "'You're not barren,' you say to the stone. 'You're just waiting. Like the bread dough waiting to rise. Like the cheese waiting to age. Like me, waiting for my birthday sandwich.' The stone CRACKS. A crystal sprout emerges, growing rapidly into a tiny crystal flower. Task two complete.", + "choices": ["Return to Lachrym"], + "transitions": { + "Return to Lachrym": "task_3_start" + }, + "media": {} + }, + "task_3_start": { + "description": "'Beautiful,' Lachrym whispers, fresh tears flowing. 'Now the final task - share a memory you've never shared. Something true. Something meaningful. Something that explains why THIS sandwich matters so much.'", + "choices": ["Share the memory"], + "transitions": { + "Share the memory": "share_memory" + }, + "media": {} + }, + "share_memory": { + "description": "You take a breath. 'When I was little, my grandmother made me a sandwich on my birthday. Nothing fancy - just bread, cheese, lettuce, tomato, meat. But she made it with such love that it tasted... perfect. She passed away before she could teach me the recipe. This sandwich...' Your voice catches. '...is how I remember her.'", + "choices": ["..."], + "transitions": { + "...": "prophecy_fulfilled" + }, + "media": {} + }, + "prophecy_fulfilled": { + "description": "The cavern RINGS with crystal harmonics. Lachrym's tears glow like diamonds. 'The prophecy is fulfilled,' she says softly. 'You sought the perfect sandwich not for perfection's sake, but for LOVE.' She bows. 'The Crystal Onions are yours.'", + "choices": ["Receive the Crystal Onions"], + "transitions": { + "Receive the Crystal Onions": "receive_onions" + }, + "media": {} + }, + "receive_onions": { + "description": "Lachrym leads you to her garden and harvests the most beautiful Crystal Onions - translucent, glowing, impossibly fragrant. 'These will never make you cry,' she says. 'Only happy tears. Your grandmother would be proud.'", + "choices": ["Thank you, Oracle"], + "transitions": { + "Thank you, Oracle": "lachrym_farewell" + }, + "media": {} + }, + "lachrym_farewell": { + "description": "'I have seen many futures,' Lachrym says. 'In most of them, you make the sandwich. In some, you share it. In the best ones...' She smiles. 'You realize the sandwich was never about the ingredients. It was about the journey.' She dabs her eyes. 'Now go. Your birthday awaits.'", + "choices": ["Return to village"], + "transitions": { + "Return to village": "village/return_hub" + }, + "media": {} + } + }, + + "kitchen": { + "return_home": { + "description": "You step through your front door, arms full of legendary ingredients. Golden Bread. Aged Cave Cheese. Rainbow Lettuce. Sun-Ripened Tomatoes. Legendary Ham. Secret Sauce. Ancient Pickles. Crystal Onions. The Recipe Card glows faintly in your pocket, as if excited.", + "choices": ["Go to the kitchen"], + "transitions": { + "Go to the kitchen": "kitchen_prep" + }, + "media": {} + }, + "kitchen_prep": { + "description": "Your kitchen has never looked more ready. The afternoon sun streams through the window. You lay out each ingredient with reverence - they're not just food anymore. They're memories. Lessons. Little pieces of every person who helped you along the way.", + "choices": ["Begin assembling the sandwich"], + "transitions": { + "Begin assembling the sandwich": "assembly_1" + }, + "media": {} + }, + "assembly_1": { + "description": "First, the Golden Bread. You slice it carefully, remembering Crispin's nervous hands, the ghost's passion, the art of patience. The bread is perfect - crusty outside, soft inside, still slightly warm as if the ghost's blessing kept it fresh.", + "choices": ["Add the cheese"], + "transitions": { + "Add the cheese": "assembly_2" + }, + "media": {} + }, + "assembly_2": { + "description": "Next, the Aged Cave Cheese. You slice thin layers, each one releasing a hundred years of flavor. Brother Gouda's voice echoes: 'The best things in life are worth waiting for.' The cheese is perfect - sharp but creamy, complex but comforting.", + "choices": ["Add the lettuce"], + "transitions": { + "Add the lettuce": "assembly_3" + }, + "media": {} + }, + "assembly_3": { + "description": "The Rainbow Lettuce sparkles as you layer it on. Fern's giggle echoes in your memory, her terrible puns, her fierce protection of her garden. The lettuce is perfect - crisp, fresh, with a hint of magic in every bite.", + "choices": ["Add the tomatoes"], + "transitions": { + "Add the tomatoes": "assembly_4" + }, + "media": {} + }, + "assembly_4": { + "description": "The Sun-Ripened Tomatoes shine like rubies as you slice them. Solana's practical wisdom, the scarecrow's disco ball, the crows' retreat - all condensed into these glowing slices. The tomatoes are perfect - sweet, juicy, impossibly fresh.", + "choices": ["Add the ham"], + "transitions": { + "Add the ham": "assembly_5" + }, + "media": {} + }, + "assembly_5": { + "description": "The Legendary Ham needs no cutting - the Golden Pig's gift came in perfect slices. Huntmaster Boar's lesson echoes: 'True hunting is about respect between hunter and quarry.' The ham is perfect - savory, tender, with a hint of gold in every bite.", + "choices": ["Add the sauce"], + "transitions": { + "Add the sauce": "assembly_6" + }, + "media": {} + }, + "assembly_6": { + "description": "Chef Mysterio's Secret Sauce drizzles over everything in a perfect spiral. His dramatic pauses, Sophia's quiet love, the three sacred spices - all blended into this mysterious elixir. The sauce is perfect - complex, deep, with secrets you'll never fully understand.", + "choices": ["Add the pickles"], + "transitions": { + "Add the pickles": "assembly_7" + }, + "media": {} + }, + "assembly_7": { + "description": "The Ancient Pickles snap with satisfying crunch as you lay them across the ham. Pickle Wizard Brine's riddles, his joy, his wisdom about transformation - preserved in every bite. The pickles are perfect - crisp, tangy, with a hundred years of character.", + "choices": ["Add the onions"], + "transitions": { + "Add the onions": "assembly_8" + }, + "media": {} + }, + "assembly_8": { + "description": "Finally, the Crystal Onions. You slice them thin, and instead of tears, you feel warmth. Lachrym's prophecy, her wisdom, your own memory of your grandmother - all crystallized in these impossible vegetables. The onions are perfect - sweet, mild, glowing faintly.", + "choices": ["Complete the sandwich"], + "transitions": { + "Complete the sandwich": "sandwich_complete" + }, + "media": {} + }, + "sandwich_complete": { + "description": "You place the top slice of Golden Bread, and THE SANDWICH IS COMPLETE. It sits before you, glowing faintly with combined magic - ghost-blessed bread, crystal-aged cheese, fairy-grown lettuce, sun-ripened tomatoes, legendary ham, secret sauce, ancient pickles, and prophetic onions. It's beautiful. It's perfect. It's yours.", + "choices": ["Take a bite"], + "transitions": { + "Take a bite": "first_bite" + }, + "media": {} + }, + "first_bite": { + "description": "You lift the sandwich. It's heavier than it looks - heavy with stories, heavy with adventures, heavy with meaning. You take a bite, and...\n\n...the world stops.\n\nEvery flavor hits at once, but not in chaos - in HARMONY. Crispin's dedication. Gouda's patience. Fern's joy. Solana's strength. Boar's honor. Mysterio's drama. Brine's wisdom. Lachrym's love.\n\nAnd underneath it all... your grandmother's warmth.", + "choices": ["..."], + "transitions": { + "...": "ending" + }, + "media": {} + }, + "ending": { + "description": "You sit at your kitchen table as the sun sets on your birthday, eating the most perfect sandwich ever made. And you realize Lachrym was right - it was never about the ingredients.\n\nIt was about the baker who found peace with his past.\nThe monk who taught you patience.\nThe fairy who taught you to fight for what you love.\nThe farmer who taught you to work together.\nThe hunter who taught you respect.\nThe chef who taught you passion.\nThe wizard who taught you to think.\nThe oracle who taught you to feel.\n\nIt was about the journey. The people. The stories.\n\nBut also... the sandwich is REALLY good.\n\n🥪 HAPPY BIRTHDAY! 🥪\n\n~ THE END ~", + "choices": ["Play again"], + "transitions": { + "Play again": "home/wake_up" + }, + "media": {} + } + } +} diff --git a/song_scenarios.py b/song_scenarios.py new file mode 100644 index 0000000000000000000000000000000000000000..cf6f6e0723fa1421ed93530405e855e7e20a51b0 --- /dev/null +++ b/song_scenarios.py @@ -0,0 +1,196 @@ +"""Song/Lyrics emotional journey data and sequence generation. + +This module provides: +- EMOTIONAL_STATES: Dictionary of emotional states for song-based narratives +- generate_song_sequence: Function to generate emotional journey sequences +""" + +import json +import random + +# Emotional states organized by mood category +EMOTIONAL_STATES = { + "joy": [ + ("Euphoria", "Pure, overwhelming happiness radiates through every moment", ["celebration", "peak"]), + ("Contentment", "A gentle satisfaction with the present moment", ["peace", "gratitude"]), + ("Excitement", "Anticipation of wonderful things bubbles up inside", ["energy", "possibility"]), + ("Love's Bloom", "The warmth of affection fills every corner of being", ["romance", "connection"]), + ("Triumph", "Victory brings a surge of proud accomplishment", ["achievement", "power"]), + ], + "sorrow": [ + ("Heartbreak", "The ache of loss echoes through an empty chest", ["pain", "separation"]), + ("Melancholy", "A bittersweet sadness colors everything in blue", ["reflection", "beauty"]), + ("Grief", "The weight of absence presses down relentlessly", ["mourning", "memory"]), + ("Longing", "An unreachable desire stretches toward the horizon", ["yearning", "distance"]), + ("Regret", "If only things had been different...", ["hindsight", "weight"]), + ], + "anger": [ + ("Rage", "White-hot fury demands expression and release", ["intensity", "destruction"]), + ("Frustration", "Barriers block every attempt at progress", ["struggle", "tension"]), + ("Betrayal", "Trust shattered leaves sharp edges everywhere", ["wound", "disbelief"]), + ("Defiance", "A refusal to accept what others demand", ["rebellion", "strength"]), + ("Righteous Fire", "Injustice ignites a burning need for action", ["purpose", "conviction"]), + ], + "fear": [ + ("Dread", "Something terrible approaches with inevitable footsteps", ["anticipation", "darkness"]), + ("Anxiety", "Uncertainty spirals into cascading worries", ["overwhelm", "paralysis"]), + ("Vulnerability", "Exposed and unprotected in a hostile world", ["fragility", "need"]), + ("Paranoia", "Every shadow hides a threat, every silence a danger", ["suspicion", "isolation"]), + ("Terror", "Primal fear strips away everything but survival", ["crisis", "instinct"]), + ], + "hope": [ + ("Dawn Breaking", "After darkness, the first light of possibility appears", ["renewal", "promise"]), + ("Second Chance", "An opportunity to make things right emerges", ["redemption", "gratitude"]), + ("Dreams Forming", "Visions of what could be take shape in the mind", ["imagination", "aspiration"]), + ("Faith Restored", "Belief in something greater returns", ["trust", "meaning"]), + ("Courage Rising", "Fear transforms into determination to face what comes", ["strength", "resolve"]), + ], + "nostalgia": [ + ("Golden Memories", "The past glows with a warm, amber light", ["comfort", "loss"]), + ("Innocence Lost", "Remembering before the world became complicated", ["youth", "purity"]), + ("Phantom Touch", "The sensation of someone no longer present", ["love", "absence"]), + ("Places Remembered", "Spaces that hold pieces of who you were", ["identity", "change"]), + ("Old Songs", "Melodies that transport you across time", ["emotion", "connection"]), + ], + "peace": [ + ("Stillness", "The noise fades and silence becomes sanctuary", ["calm", "presence"]), + ("Acceptance", "What is, simply is - no struggle remains", ["surrender", "wisdom"]), + ("Unity", "Separation dissolves into connection with all things", ["transcendence", "belonging"]), + ("Forgiveness", "The weight of grudges lifts away", ["release", "healing"]), + ("Home", "A sense of belonging settles into the bones", ["safety", "arrival"]), + ], +} + +# Song structure elements +SONG_STRUCTURES = { + "verse": "The story unfolds, details emerge", + "chorus": "The central truth repeats, driving home the message", + "bridge": "A shift in perspective, a new angle on the theme", + "outro": "The emotion settles, leaving its lasting impression", +} + + +def generate_song_sequence(verse_count, include_joy, include_sorrow, include_anger, + include_fear, include_hope, include_nostalgia, include_peace, + emotional_arc="journey"): + """Generate an emotional journey sequence based on song structure. + + Args: + verse_count: Number of emotional beats (3-10) + include_*: Include specific emotional categories + emotional_arc: 'journey' (varied), 'crescendo' (building), 'resolution' (tension to peace) + + Returns: + Tuple of (list_output, json_output, prompts_output) + """ + available = [] + if include_joy: + available.extend([(e[0], e[1], "joy", e[2]) for e in EMOTIONAL_STATES["joy"]]) + if include_sorrow: + available.extend([(e[0], e[1], "sorrow", e[2]) for e in EMOTIONAL_STATES["sorrow"]]) + if include_anger: + available.extend([(e[0], e[1], "anger", e[2]) for e in EMOTIONAL_STATES["anger"]]) + if include_fear: + available.extend([(e[0], e[1], "fear", e[2]) for e in EMOTIONAL_STATES["fear"]]) + if include_hope: + available.extend([(e[0], e[1], "hope", e[2]) for e in EMOTIONAL_STATES["hope"]]) + if include_nostalgia: + available.extend([(e[0], e[1], "nostalgia", e[2]) for e in EMOTIONAL_STATES["nostalgia"]]) + if include_peace: + available.extend([(e[0], e[1], "peace", e[2]) for e in EMOTIONAL_STATES["peace"]]) + + if not available: + return "Select at least one emotional category!", "{}", "" + + sequence = [] + random.shuffle(available) + + # Apply emotional arc logic + if emotional_arc == "crescendo": + # Build from calm to intense + intensity_order = ["peace", "nostalgia", "hope", "joy", "fear", "sorrow", "anger"] + available.sort(key=lambda x: intensity_order.index(x[2]) if x[2] in intensity_order else 3) + elif emotional_arc == "resolution": + # Start intense, end peaceful + intense = [e for e in available if e[2] in ["anger", "fear", "sorrow"]] + calm = [e for e in available if e[2] in ["peace", "hope", "joy"]] + random.shuffle(intense) + random.shuffle(calm) + available = intense + calm + + sequence = available[:verse_count] + + if not sequence: + return "No emotional states available!", "{}", "" + + # Assign song structure elements + structure_labels = [] + for i in range(len(sequence)): + if i == 0: + structure_labels.append("INTRO") + elif i == len(sequence) - 1: + structure_labels.append("OUTRO") + elif i == len(sequence) // 2: + structure_labels.append("BRIDGE") + elif i % 2 == 0: + structure_labels.append("CHORUS") + else: + structure_labels.append("VERSE") + + # Format as list + list_output = f"## Emotional Journey ({emotional_arc.title()} Arc)\n\n" + for i, ((name, desc, mood, themes), structure) in enumerate(zip(sequence, structure_labels), 1): + themes_str = ", ".join(themes) + list_output += f"**{i}. [{structure}] {name}** ({mood})\n{desc}\n*Themes: {themes_str}*\n\n" + + # Format as config JSON + config = {"emotional_journey": {}} + for i, ((name, desc, mood, themes), structure) in enumerate(zip(sequence, structure_labels), 1): + state_name = name.lower().replace(" ", "_").replace("'", "").replace("-", "_") + current_state_id = f"moment_{i}_{state_name}" + + themes_str = ", ".join(themes) + + # Determine next state + if i < len(sequence): + next_name = sequence[i][0] + next_state_name = next_name.lower().replace(" ", "_").replace("'", "").replace("-", "_") + next_state_id = f"moment_{i+1}_{next_state_name}" + else: + next_state_id = "journey_end" + + config["emotional_journey"][current_state_id] = { + "description": f"[{structure}] {desc}\n\n*Feeling: {mood} | {themes_str}*", + "media_prompt": f"Abstract emotional art: {name} - {mood} mood, {desc}, visual representation of {themes_str}", + "choices": ["Feel the Next Wave"], + "transitions": { + "Feel the Next Wave": next_state_id + } + } + + # Add ending state + first_moment = sequence[0][0].lower().replace(" ", "_").replace("'", "").replace("-", "_") + config["emotional_journey"]["journey_end"] = { + "description": "The emotional journey comes full circle. Every feeling has been felt, every moment lived.", + "choices": ["Experience Again"], + "transitions": { + "Experience Again": f"moment_1_{first_moment}" + } + } + + json_output = json.dumps(config, indent=2) + + # Format prompts + prompts_output = f"## Emotional Art Prompts ({emotional_arc.title()})\n\n" + for i, (name, desc, mood, themes) in enumerate(sequence, 1): + themes_str = ", ".join(themes) + prompts_output += f"Abstract emotional art: {name} - {mood}, {themes_str}\n" + + return list_output, json_output, prompts_output + + +if __name__ == "__main__": + list_out, json_out, prompts_out = generate_song_sequence( + 6, True, True, False, False, True, True, True, "resolution" + ) + print(list_out) diff --git a/state_prompt_fileman_UI_functions.py b/state_prompt_fileman_UI_functions.py index 46664d4e9b93fe230943351b35a199f34c8c5450..b84a387d0fc9ceb89be3f1c2dc08d0cfcaab8d73 100644 --- a/state_prompt_fileman_UI_functions.py +++ b/state_prompt_fileman_UI_functions.py @@ -12,7 +12,7 @@ import base64 # Global variables SPFManstate = { "last_file": 0, - "output_dir": "output", + "output_dir": "saved_media", # Same as SAVE_DIR in file_explorer_and_upload.py "errors": [], "skipped_items": [], "is_paid_api": False, @@ -87,15 +87,24 @@ def SPFMangenerate_image(prompt, retries=0): SPFManstate["errors"].append(error_msg) return error_msg -def SPFMangenerate_audio(prompt, retries=0): +def SPFMangenerate_audio(prompt, seconds_total=30, steps=100, cfg_scale=7, retries=0): + """Generate audio using Stable Audio API. + + Args: + prompt: Text description of the sound + seconds_total: Duration in seconds (default 30) + steps: Number of generation steps (default 100) + cfg_scale: Classifier-free guidance scale (default 7) + retries: Number of retry attempts + """ SPFManensure_output_directory() try: client = Client("artificialguybr/Stable-Audio-Open-Zero") result = client.predict( prompt=prompt, - seconds_total=30, - steps=100, - cfg_scale=7, + seconds_total=int(seconds_total), + steps=int(steps), + cfg_scale=float(cfg_scale), api_name="/predict" ) @@ -117,10 +126,291 @@ def SPFMangenerate_audio(prompt, retries=0): SPFManstate["errors"].append(error_msg) return error_msg + +def SPFMangenerate_3d_model(prompt, guidance_scale=15.0, num_steps=64, retries=0, use_local=True): + """ + Generate 3D model using Shap-E. + Tries local ZeroGPU first, falls back to API if local fails. + + Args: + prompt: Text description of the 3D object + guidance_scale: Classifier-free guidance scale (default 15.0) + num_steps: Number of inference steps (default 64) + retries: Number of retry attempts + use_local: Whether to try local generation first + """ + SPFManensure_output_directory() + + # Try local generation first (faster, no rate limits) + if use_local: + try: + from leveraging_machine_learning import generate_3d_local + status, filepath = generate_3d_local(prompt, guidance_scale=guidance_scale, num_steps=num_steps) + if filepath and os.path.exists(filepath): + return status + print(f"Local 3D generation failed: {status}, falling back to API...") + except Exception as local_error: + print(f"Local 3D error: {local_error}, falling back to API...") + + # Fall back to API + try: + client = Client("hysts/Shap-E") + result = client.predict( + prompt=prompt, + seed=0, + guidance_scale=float(guidance_scale), + num_inference_steps=int(num_steps), + api_name="/text-to-3d" + ) + + if isinstance(result, str) and os.path.exists(result): + safe_prompt = prompt[:40].replace(' ', '_').replace('/', '_').replace('\\', '_') + filename = f"{SPFManstate['output_dir']}/{safe_prompt}_3d_{SPFManstate['timestamp']}.glb" + shutil.move(result, filename) + return f"3D model saved as {filename}" + else: + return f"Error: Unexpected result format from Shap-E" + except Exception as e: + error_msg = f"Error generating 3D model: {str(e)}" + if "exceeded your GPU quota" in str(e) and retries < SPFManstate['max_retries']: + time.sleep(SPFManstate['retry_delay']) + return SPFMangenerate_3d_model(prompt, retries + 1, use_local=False) + SPFManstate["errors"].append(error_msg) + return error_msg + + +def SPFMangenerate_tts(text, model="kokoro", voice="af_heart", retries=0, use_local=True): + """ + Generate TTS audio using multiple TTS backends. + Tries local ZeroGPU first, falls back to API if local fails. + + Supported models: + - kokoro: Kokoro-82M (fast, natural voices) + - supertonic: Supertonic-2 (high-quality, expressive) + - glm-tts: GLM-TTS (multilingual, voice cloning) - API only + """ + SPFManensure_output_directory() + + # Try local generation first (faster, no rate limits) + # GLM-TTS doesn't support local generation + if use_local and model != "glm-tts": + try: + from leveraging_machine_learning import generate_tts_local + status, filepath = generate_tts_local(text, model=model, voice=voice) + if filepath and os.path.exists(filepath): + return status + print(f"Local TTS failed: {status}, falling back to API...") + except Exception as local_error: + print(f"Local TTS error: {local_error}, falling back to API...") + + # Fall back to API + try: + from leveraging_machine_learning import generate_tts_api + status, filepath = generate_tts_api(text, model=model, voice=voice) + + if filepath and os.path.exists(filepath): + return status + return f"Error: Could not process TTS result - {status}" + except Exception as e: + error_msg = f"Error generating TTS: {str(e)}" + if "exceeded your GPU quota" in str(e) and retries < SPFManstate['max_retries']: + time.sleep(SPFManstate['retry_delay']) + return SPFMangenerate_tts(text, model, voice, retries + 1, use_local=False) + SPFManstate["errors"].append(error_msg) + return error_msg + + +def SPFMangenerate_talking_head(image_path, audio_path=None, text=None, retries=0, use_local=False): + """ + Generate talking head video from portrait image + audio/text. + Tries local ZeroGPU first, then falls back to HuggingFace Spaces. + + Args: + image_path: Path to portrait image + audio_path: Path to audio file (optional if text provided) + text: Text to convert to speech first (optional if audio provided) + retries: Retry counter for rate limiting + use_local: Try local ZeroGPU generation first + + Returns: + Status message with path to generated video + + TODO (Future local ZeroGPU implementation): + - Wav2Lip is simpler than SadTalker (just lip sync, ~300MB checkpoint) + - Could implement using: https://huggingface.co/spaces/pragnakalp/Wav2lip-ZeroGPU + - Requires: face_detection, wav2lip model checkpoint, ffmpeg + - See: https://github.com/Rudrabha/Wav2Lip for model details + """ + from gradio_client import Client, handle_file + SPFManensure_output_directory() + + errors = [] + + try: + # If text provided but no audio, generate TTS first + if text and not audio_path: + tts_result = SPFMangenerate_tts(text) + if "saved as" in tts_result: + audio_path = tts_result.split("saved as ")[-1].strip() + else: + return f"Error: Failed to generate TTS audio: {tts_result}" + + if not audio_path or not os.path.exists(audio_path): + return "Error: No audio file provided or generated" + + if not image_path or not os.path.exists(image_path): + return "Error: No portrait image provided" + + # Try local generation first (ZeroGPU) + if use_local: + try: + print("[TalkingHead] Trying local ZeroGPU generation...") + from leveraging_machine_learning import generate_talking_head_local + status, filepath = generate_talking_head_local(image_path, audio_path) + if filepath and os.path.exists(filepath): + return status + print(f"[TalkingHead] Local generation failed: {status}") + errors.append(f"Local (ZeroGPU): {status}") + except Exception as local_error: + print(f"[TalkingHead] Local error: {local_error}") + errors.append(f"Local (ZeroGPU): {str(local_error)}") + + result = None + + # Fallback to API spaces + print("[TalkingHead] Falling back to API spaces...") + + # List of spaces to try with their configurations + # Updated 2026-01-25: Prioritize MoDA (runs on ZeroGPU, MIT license) + # SadTalker/Hallo often have RUNTIME_ERROR state + spaces_to_try = [ + { + "name": "MoDA-FastTalkingHead", + "space": "multimodalart/MoDA-fast-talking-head", + "api_name": "/generate_motion", + "params": lambda img, aud: { + "source_image_path": handle_file(img), + "driving_audio_path": handle_file(aud), + "emotion_name": "None", # Options: None, Happy, Angry, Surprise, Sad, Disgust, Fear + "cfg_scale": 1.2, + } + }, + { + "name": "SadTalker", + "space": "vinthony/SadTalker", + "api_name": "/inference", + "params": lambda img, aud: { + "source_image": handle_file(img), + "driven_audio": handle_file(aud), + "preprocess": "crop", + "still_mode": False, + "use_enhancer": False, + "batch_size": 2, + "size": 256, + "pose_style": 0, + "facerender": "facevid2vid", + "exp_weight": 1.0, + "use_ref": False, + "ref_video": None, + "ref_info": "pose", + "use_idle": False, + "length": 0, + } + }, + { + "name": "Hallo", + "space": "fudan-generative-ai/hallo", + "api_name": "/predict", + "params": lambda img, aud: { + "source_image": handle_file(img), + "driving_audio": handle_file(aud), + } + }, + { + "name": "Wav2Lip-ZeroGPU", + "space": "pragnakalp/Wav2lip-ZeroGPU", + "api_name": "/run_infrence", # Note: typo is in original space + "params": lambda img, aud: { + "input_image": handle_file(img), + "input_audio": handle_file(aud), + } + }, + ] + + for space_config in spaces_to_try: + try: + print(f"[TalkingHead] Trying {space_config['name']} ({space_config['space']})...") + client = Client(space_config["space"]) + + # Get API info for debugging + try: + api_info = client.view_api(return_format="dict") + named_endpoints = list(api_info.get('named_endpoints', {}).keys()) if api_info else [] + unnamed_endpoints = list(api_info.get('unnamed_endpoints', {}).keys()) if api_info else [] + print(f"[TalkingHead] Named endpoints: {named_endpoints}") + print(f"[TalkingHead] Unnamed endpoints: {unnamed_endpoints}") + print(f"[TalkingHead] Attempting to use: {space_config['api_name']}") + except Exception as api_err: + print(f"[TalkingHead] Could not get API info: {api_err}") + + params = space_config["params"](image_path, audio_path) + print(f"[TalkingHead] Sending request with image={image_path}, audio={audio_path}") + result = client.predict(**params, api_name=space_config["api_name"]) + print(f"[TalkingHead] SUCCESS with {space_config['name']}!") + print(f"[TalkingHead] Result type: {type(result)}, value: {str(result)[:200]}") + break # Success, exit loop + + except Exception as e: + error_msg = f"{space_config['name']}: {str(e)}" + errors.append(error_msg) + print(f"[TalkingHead] FAILED: {error_msg}") + import traceback + print(f"[TalkingHead] Traceback: {traceback.format_exc()}") + continue # Try next space + + if result is None: + return f"Error: All talking head spaces failed.\n" + "\n".join(errors) + + # Handle result - could be file path or tuple + if isinstance(result, str) and os.path.exists(result): + video_path = result + elif isinstance(result, tuple) and len(result) > 0: + # Find the first valid file path in the tuple + video_path = None + for item in result: + if isinstance(item, str) and os.path.exists(item): + video_path = item + break + elif isinstance(result, dict) and 'video' in result: + video_path = result['video'] + else: + video_path = None + + if video_path and os.path.exists(video_path): + # Move to output directory + safe_name = os.path.splitext(os.path.basename(image_path))[0][:30] + filename = f"{SPFManstate['output_dir']}/talking_head_{safe_name}_{SPFManstate['timestamp']}.mp4" + shutil.move(video_path, filename) + return f"Talking head video saved as {filename}" + else: + return f"Error: Could not process result from talking head generation" + + except Exception as e: + error_msg = f"Error generating talking head video: {str(e)}" + if "exceeded your GPU quota" in str(e) and retries < SPFManstate['max_retries']: + time.sleep(SPFManstate['retry_delay']) + return SPFMangenerate_talking_head(image_path, audio_path, text, retries + 1) + SPFManstate["errors"].append(error_msg) + return error_msg + + def SPFManprocess_prompts(prompt_list): router = { 'image': SPFMangenerate_image, 'audio': SPFMangenerate_audio, + '3d': SPFMangenerate_3d_model, + 'tts': SPFMangenerate_tts, + 'talking_head': SPFMangenerate_talking_head, } results = [] for prompt_type, prompt in prompt_list: @@ -259,7 +549,7 @@ def SPFManload_config_file(file): # Clear existing state and prompt list SPFManstate = { "last_file": 0, - "output_dir": "output", + "output_dir": "saved_media", # Same as SAVE_DIR in file_explorer_and_upload.py "errors": [], "skipped_items": [], "is_paid_api": False, @@ -321,4 +611,740 @@ def SPFManload_json_configuration(json_text): except json.JSONDecodeError as e: return f"Error parsing JSON: {str(e)}", gr.update(value=len(SPFManprompt_list)) except Exception as e: - return f"Unexpected error: {str(e)}", gr.update(value=len(SPFManprompt_list)) \ No newline at end of file + return f"Unexpected error: {str(e)}", gr.update(value=len(SPFManprompt_list)) + + +# ============================================================ +# UNIFIED GENERATION QUEUE SYSTEM +# ============================================================ + +# Queue state for unified generation +generation_queue = [] +generation_log = [] + +def add_to_generation_queue(prompt_type, prompt, + audio_duration=30, audio_steps=100, audio_cfg=7, + tts_model="kokoro", tts_voice="af_heart", + threeds_guidance=15, threeds_steps=64): + """Add a single prompt to the generation queue with type-specific parameters. + + Args: + prompt_type: Type of generation (text, image, audio, tts, 3d) + prompt: The prompt/text to generate from + audio_duration: Duration for Stable Audio (seconds) + audio_steps: Steps for Stable Audio + audio_cfg: CFG scale for Stable Audio + tts_model: TTS model (kokoro, supertonic, glm-tts) + tts_voice: Voice for TTS + threeds_guidance: Guidance scale for 3D (Shap-E) + threeds_steps: Steps for 3D generation + """ + global generation_queue + if not prompt or not prompt.strip(): + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", "Please enter a prompt" + + item = { + "id": len(generation_queue) + 1, + "type": prompt_type, + "prompt": prompt.strip(), + "status": "pending", + "params": {} + } + + # Store type-specific parameters + if prompt_type == "audio": + item["params"] = { + "duration": int(audio_duration), + "steps": int(audio_steps), + "cfg_scale": float(audio_cfg) + } + elif prompt_type == "tts": + item["params"] = { + "model": tts_model, + "voice": tts_voice + } + elif prompt_type == "3d": + item["params"] = { + "guidance_scale": float(threeds_guidance), + "steps": int(threeds_steps) + } + + generation_queue.append(item) + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", f"Added: {prompt_type} prompt" + +def get_queue_dataframe(): + """Return queue as dataframe for display""" + if not generation_queue: + return [] + return [[q["id"], q["type"], q["prompt"][:60] + ("..." if len(q["prompt"]) > 60 else ""), q["status"]] + for q in generation_queue] + +def clear_generation_queue(): + """Clear the entire queue""" + global generation_queue, generation_log + generation_queue = [] + generation_log = [] + return get_queue_dataframe(), "**Queue: 0 items**", get_log_dataframe(), "Queue cleared" + +def remove_completed_from_queue(): + """Remove completed and errored items from queue""" + global generation_queue + generation_queue = [q for q in generation_queue if q["status"] == "pending"] + # Re-index + for i, q in enumerate(generation_queue): + q["id"] = i + 1 + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**" + +def get_log_dataframe(): + """Return generation log as dataframe""" + if not generation_log: + return [] + return [[l["id"], l["prompt"][:40], l["status"], l["result"][:60] if l["result"] else ""] + for l in generation_log] + +def extract_prompts_from_config(config_file): + """Extract media prompts from a game config JSON file""" + global generation_queue + if config_file is None: + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", "No file uploaded" + + try: + with open(config_file.name, 'r') as f: + config = json.load(f) + + extracted_count = 0 + # Walk through config looking for media fields + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + for subloc_key, subloc_data in location_data.items(): + if isinstance(subloc_data, dict): + # Check for media field + if 'media' in subloc_data: + media_list = subloc_data['media'] + if isinstance(media_list, list): + for media_item in media_list: + # If it looks like a prompt (not a filename), add to queue + if isinstance(media_item, str) and not any(ext in media_item.lower() for ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.mp3', '.wav', '.ogg', '.mp4', '.avi', '.mov', '.webm', '.glb', '.gltf', '.obj']): + item = { + "id": len(generation_queue) + 1, + "type": "image", # Default to image + "prompt": media_item, + "status": "pending" + } + generation_queue.append(item) + extracted_count += 1 + # Check for description as potential prompt + if 'description' in subloc_data and isinstance(subloc_data['description'], str): + desc = subloc_data['description'] + if len(desc) > 20: # Only meaningful descriptions + item = { + "id": len(generation_queue) + 1, + "type": "image", + "prompt": f"Scene: {desc[:200]}", + "status": "pending" + } + generation_queue.append(item) + extracted_count += 1 + + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", f"Extracted {extracted_count} prompts from config" + except json.JSONDecodeError as e: + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", f"Invalid JSON: {str(e)}" + except Exception as e: + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", f"Error: {str(e)}" + +def process_queue_item_local(item, text_model, image_model, use_rag, use_streaming, max_tokens=512): + """Process a single queue item using local ZeroGPU generation""" + from leveraging_machine_learning import generate_response, generate_image + + if item["type"] == "text": + # generate_response now handles model loading/switching internally + # Pass model_name so it can switch if needed + result = None + try: + for r in generate_response(item["prompt"], use_rag, use_streaming, + max_tokens=int(max_tokens), model_name=text_model): + result = r + if result: + return result[0], None # Return the generated text, no image + return "No response generated", None + except Exception as e: + return f"Error: {str(e)}", None + + elif item["type"] == "image": + try: + status, ram, image = generate_image(item["prompt"], image_model) + if image: + return status, image # Return status and actual image + return f"Error: {status}", None + except Exception as e: + return f"Error: {str(e)}", None + + elif item["type"] == "audio": + # Use the existing SPFMan audio generation with parameters + params = item.get("params", {}) + result = SPFMangenerate_audio( + item["prompt"], + seconds_total=params.get("duration", 30), + steps=params.get("steps", 100), + cfg_scale=params.get("cfg_scale", 7) + ) + return result, None + + elif item["type"] == "3d": + # Use the SPFMan 3D model generation with parameters + params = item.get("params", {}) + result = SPFMangenerate_3d_model( + item["prompt"], + guidance_scale=params.get("guidance_scale", 15), + num_steps=params.get("steps", 64) + ) + return result, None + + elif item["type"] == "tts": + # Use the SPFMan TTS generation with parameters + params = item.get("params", {}) + result = SPFMangenerate_tts( + item["prompt"], + model=params.get("model", "kokoro"), + voice=params.get("voice", "af_heart") + ) + return result, None + + return f"Unknown type: {item['type']}", None + +def process_queue_item_api(item, api_source, hf_model_id, replicate_model): + """Process a single queue item using API""" + if api_source == "HF Inference": + # Use existing SPFMan functions which call HF APIs + if item["type"] == "image": + result = SPFMangenerate_image(item["prompt"]) + return result, None # No image object for API (saved to file) + elif item["type"] == "audio": + result = SPFMangenerate_audio(item["prompt"]) + return result, None + elif item["type"] == "3d": + result = SPFMangenerate_3d_model(item["prompt"]) + return result, None + elif item["type"] == "tts": + result = SPFMangenerate_tts(item["prompt"]) + return result, None + else: + return "Text generation via HF Inference not implemented yet", None + + elif api_source == "External (Replicate)": + # Placeholder for Replicate API + return f"Replicate API not implemented yet for {item['type']}", None + + return f"Unknown API source: {api_source}", None + + +def retry_failed_items(): + """Reset all error items back to pending so they can be retried""" + global generation_queue + retry_count = 0 + for item in generation_queue: + if item["status"] == "error": + item["status"] = "pending" + retry_count += 1 + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", f"Reset {retry_count} failed items to pending" + +def process_next_queue_item(mode, text_model, image_model, use_rag, use_streaming, max_tokens, api_source, hf_model_id, replicate_model): + """Process the next pending item in the queue. + + Returns: (text_out, image_out, audio_out, progress, current_item, queue_df, log_df) + """ + global generation_queue, generation_log + + pending = [q for q in generation_queue if q["status"] == "pending"] + if not pending: + completed = len([q for q in generation_queue if q["status"] in ["completed", "error"]]) + total = len(generation_queue) + return ( + "No pending items in queue", None, None, + f"**Progress: {completed}/{total}** - No pending items", + "Queue empty or all items processed", + get_queue_dataframe(), + get_log_dataframe() + ) + + item = pending[0] + item["status"] = "processing" + + result_text = None + result_image = None + result_audio = None + + try: + if mode == "Local (ZeroGPU)": + result_text, result_image = process_queue_item_local(item, text_model, image_model, use_rag, use_streaming, max_tokens) + else: + result_text, result_image = process_queue_item_api(item, api_source, hf_model_id, replicate_model) + + if result_text and "Error" in str(result_text): + item["status"] = "error" + generation_log.append({ + "id": item["id"], + "prompt": item["prompt"][:40], + "status": "Error", + "result": str(result_text) + }) + else: + item["status"] = "completed" + + # Extract generated filename from result text + # Multiple formats possible depending on generation method + # IMPORTANT: Store full path (e.g., "saved_media/file.webp") for media to load correctly + import re + result_str = str(result_text) if result_text else "" + + # Try "saved as" format first (used by SPFMan functions) + # Format: "Image saved as saved_media/filename.webp" + if "saved as" in result_str.lower(): + match = re.search(r'saved as\s+(.+?)(?:\s|$)', result_str, re.IGNORECASE) + if match: + filepath = match.group(1).strip() + # Store full path for media component to load correctly + # If it already has saved_media/, use as-is; otherwise prepend it + if filepath.startswith("saved_media/") or filepath.startswith("saved_media\\"): + item["generated_file"] = filepath + else: + item["generated_file"] = f"saved_media/{os.path.basename(filepath)}" + + # Try ": filename" format (used by some local generation) + # Format: "Image generated with model: filename.png" + elif ": " in result_str: + parts = result_str.split(": ") + if len(parts) >= 2: + filename = parts[-1].strip() + # Check for valid image/media extensions + valid_extensions = ('.png', '.jpg', '.jpeg', '.webp', '.wav', '.mp3', '.ogg', '.glb', '.gltf', '.obj') + if filename.endswith(valid_extensions): + # Store with saved_media/ prefix for consistency + if filename.startswith("saved_media/") or filename.startswith("saved_media\\"): + item["generated_file"] = filename + else: + item["generated_file"] = f"saved_media/{os.path.basename(filename)}" + + # Try to find any file path in the result + elif not item.get("generated_file"): + # Look for saved_media/ path pattern + match = re.search(r'(saved_media/[^\s]+)', result_str) + if match: + item["generated_file"] = match.group(1) # Keep full path + + generation_log.append({ + "id": item["id"], + "prompt": item["prompt"][:40], + "status": "Success", + "result": str(result_text)[:100] if result_text else "Done" + }) + + except Exception as e: + item["status"] = "error" + generation_log.append({ + "id": item["id"], + "prompt": item["prompt"][:40], + "status": "Error", + "result": str(e) + }) + result_text = f"Error: {str(e)}" + result_image = None + + completed = len([q for q in generation_queue if q["status"] in ["completed", "error"]]) + total = len(generation_queue) + progress = f"**Progress: {completed}/{total}**" + + # Return appropriate outputs based on type + text_out = result_text if item["type"] == "text" else (result_text if result_text else "") + image_out = result_image # Actual image object from generation + + # For audio/tts types, extract filepath for audio player + audio_out = None + if item["type"] in ["audio", "tts"] and item.get("generated_file"): + audio_out = item["generated_file"] + + return ( + text_out, image_out, audio_out, + progress, + f"Processed: {item['prompt'][:50]}...", + get_queue_dataframe(), + get_log_dataframe() + ) + + +# ============================================================ +# CONFIG-BASED PROMPT BUILDER +# ============================================================ + +# Store loaded config and parsed sections +loaded_config_for_prompts = None +config_sections = [] +section_prompts = {} # {section_key: [{"type": "image", "prompt": "..."}]} + + +def load_config_for_prompts(config_json): + """Parse config and extract all sections""" + global loaded_config_for_prompts, config_sections, section_prompts + + if not config_json or not config_json.strip(): + return gr.update(choices=[]), "Please paste a config JSON" + + config_sections = [] + section_prompts = {} + + try: + config = json.loads(config_json) if isinstance(config_json, str) else config_json + loaded_config_for_prompts = config + + for location, location_data in config.items(): + if isinstance(location_data, dict): + for state, state_data in location_data.items(): + if isinstance(state_data, dict): + section = { + "location": location, + "state": state, + "key": f"{location} → {state}", + "description": state_data.get("description", ""), + "media": state_data.get("media", []), + "choices": state_data.get("choices", []) + } + config_sections.append(section) + + choices = [s["key"] for s in config_sections] + return gr.update(choices=choices, value=choices[0] if choices else None), f"Loaded {len(config_sections)} sections" + + except json.JSONDecodeError as e: + return gr.update(choices=[]), f"Invalid JSON: {str(e)}" + except Exception as e: + return gr.update(choices=[]), f"Error: {str(e)}" + + +def get_section_details(section_key): + """Get details for a specific section by key""" + if not section_key: + return "", "", "" + + for section in config_sections: + if section["key"] == section_key: + desc = section["description"] or "(No description)" + media = "\n".join(section["media"]) if section["media"] else "(No media)" + choices = ", ".join(section["choices"]) if section["choices"] else "(No choices)" + return desc, media, choices + + return "", "", "" + + +def add_prompt_to_section(section_key, prompt_type, prompt_text): + """Add a prompt for a specific section""" + global section_prompts + + if not section_key: + return get_section_prompts_dataframe(), "Please select a section" + if not prompt_text or not prompt_text.strip(): + return get_section_prompts_dataframe(), "Please enter a prompt" + + if section_key not in section_prompts: + section_prompts[section_key] = [] + + section_prompts[section_key].append({ + "type": prompt_type, + "prompt": prompt_text.strip() + }) + + return get_section_prompts_dataframe(), f"Added {prompt_type} prompt to {section_key}" + + +def get_section_prompts_dataframe(): + """Return all section prompts as dataframe""" + rows = [] + for section_key, prompts in section_prompts.items(): + for p in prompts: + rows.append([section_key, p["type"], p["prompt"][:80] + ("..." if len(p["prompt"]) > 80 else "")]) + return rows if rows else [] + + +def auto_generate_section_prompts(): + """Auto-generate prompts from descriptions for all sections""" + global section_prompts + section_prompts = {} + count = 0 + + for section in config_sections: + key = section["key"] + desc = section["description"] + if desc and len(desc) > 20: + section_prompts[key] = [{ + "type": "image", + "prompt": f"Scene illustration: {desc[:200]}" + }] + count += 1 + + return get_section_prompts_dataframe(), f"Generated {count} prompts from descriptions" + + +def clear_section_prompts(): + """Clear all section prompts""" + global section_prompts + section_prompts = {} + return get_section_prompts_dataframe(), "Cleared all section prompts" + + +def add_all_sections_to_queue(): + """Add all section prompts to the generation queue""" + global generation_queue, section_prompts + + if not section_prompts: + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", "No prompts to add" + + added_count = 0 + for section_key, prompts in section_prompts.items(): + # Parse location and state from key (format: "location → state") + parts = section_key.split(" → ") + location = parts[0] if len(parts) > 0 else "unknown" + state = parts[1] if len(parts) > 1 else "unknown" + + for p in prompts: + item = { + "id": len(generation_queue) + 1, + "type": p["type"], + "prompt": p["prompt"], + "status": "pending", + "section": { + "location": location, + "state": state, + "source": "config_builder" + }, + "generated_file": None + } + generation_queue.append(item) + added_count += 1 + + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", f"Added {added_count} prompts to queue" + + +def update_config_with_generated_media(): + """Update loaded config's media arrays with generated files""" + global loaded_config_for_prompts, generation_queue + + if not loaded_config_for_prompts: + return "", "No config loaded. Load a config first using 'Load Config' button above." + + updated_count = 0 + for item in generation_queue: + if item["status"] == "completed" and item.get("generated_file"): + section = item.get("section") + if section: + loc = section["location"] + state = section["state"] + if loc in loaded_config_for_prompts and state in loaded_config_for_prompts[loc]: + if "media" not in loaded_config_for_prompts[loc][state]: + loaded_config_for_prompts[loc][state]["media"] = [] + loaded_config_for_prompts[loc][state]["media"].append(item["generated_file"]) + updated_count += 1 + + return json.dumps(loaded_config_for_prompts, indent=2), f"Updated {updated_count} media entries" + + +# ============================================================ +# ONE-CLICK TO GAMEPLAY WORKFLOW FUNCTIONS +# ============================================================ + +# State for one-click workflow +otg_workflow_state = { + "active": False, + "config_json": "", + "total_items": 0, + "completed_items": 0 +} + + +def otg_clear_and_prepare_queue(): + """Clear the queue and reset workflow state for a fresh one-click run.""" + global generation_queue, generation_log, otg_workflow_state + generation_queue = [] + generation_log = [] + otg_workflow_state = { + "active": True, + "config_json": "", + "total_items": 0, + "completed_items": 0 + } + return get_queue_dataframe(), "Queue cleared, ready for new workflow" + + +def otg_add_prompts_with_section_tracking(prompts_text, config_json, media_type="image"): + """Add prompts to queue with section tracking for later config update. + + Args: + prompts_text: Text containing prompts (one per line starting with 'Cinematic shot:') + config_json: The generated config JSON string to track sections + media_type: Type of media to generate ('image', 'audio', '3d', 'tts') + """ + global generation_queue, loaded_config_for_prompts, otg_workflow_state + + if not prompts_text or not prompts_text.strip(): + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", "No prompts to add" + + # Parse config to get state keys for section tracking + try: + config = json.loads(config_json) if config_json else {} + loaded_config_for_prompts = config # Store for later media update + otg_workflow_state["config_json"] = config_json + + # Get list of state IDs in order (excluding 'ending' which has no media prompt) + state_keys = [] + if "story_location" in config: + state_keys = [k for k in config["story_location"].keys() if k != "ending"] + except: + config = {} + state_keys = [] + + lines = prompts_text.strip().split("\n") + added = 0 + state_index = 0 + + for line in lines: + line = line.strip() + # Skip header lines and empty lines + if not line or line.startswith("##") or line.startswith("#"): + continue + # Accept lines starting with "Cinematic shot:" or any meaningful line + if line.startswith("Cinematic shot:") or (len(line) > 10 and "Video Prompts" not in line): + # Map to corresponding config state if available + section = None + if state_index < len(state_keys): + state_key = state_keys[state_index] + section = { + "location": "story_location", + "state": state_key, + "source": "one_click_gameplay" + } + + item = { + "id": len(generation_queue) + 1, + "type": media_type, + "prompt": line.strip(), + "status": "pending", + "section": section, + "generated_file": None + } + generation_queue.append(item) + added += 1 + state_index += 1 + + otg_workflow_state["total_items"] = len(generation_queue) + return get_queue_dataframe(), f"**Queue: {len(generation_queue)} items**", f"Added {added} {media_type} prompts to queue" + + +def otg_process_queue_generator(mode, text_model, image_model, use_rag=False, use_streaming=False, + max_tokens=512, api_source="HF Inference", hf_model_id="", replicate_model=""): + """Generator function that processes queue items one at a time, yielding progress. + + This is used with Gradio to show live progress during background processing. + Yields: (progress_text, current_item_text, queue_dataframe, is_complete, final_config_json, error_details) + """ + global generation_queue, otg_workflow_state + + if not otg_workflow_state.get("active", False): + yield ("**Workflow not active**", "", get_queue_dataframe(), True, "", "Workflow was not started") + return + + total = len(generation_queue) + if total == 0: + yield ("**No items to process**", "", get_queue_dataframe(), True, "", "Queue is empty") + return + + error_messages = [] + + while otg_workflow_state.get("active", False): + pending = [q for q in generation_queue if q["status"] == "pending"] + if not pending: + break + + # Process one item with error handling + try: + result = process_next_queue_item( + mode, text_model, image_model, use_rag, use_streaming, + max_tokens, api_source, hf_model_id, replicate_model + ) + + # Check if this item errored + current_item_text = result[3] if len(result) > 3 else "" + if "Error" in str(result[0]): + error_messages.append(f"Item {len(generation_queue) - len(pending) + 1}: {result[0]}") + + except Exception as e: + error_messages.append(f"Processing error: {str(e)}") + current_item_text = f"Error: {str(e)}" + + # Count completed items + completed = len([q for q in generation_queue if q["status"] in ["completed", "error"]]) + errors = len([q for q in generation_queue if q["status"] == "error"]) + otg_workflow_state["completed_items"] = completed + + progress_text = f"**Processing: {completed}/{total}**" + (f" ({errors} errors)" if errors > 0 else "") + + yield (progress_text, current_item_text, get_queue_dataframe(), False, "", "") + + # All done - update config with generated media paths + final_config, update_status = update_config_with_generated_media() + + completed = len([q for q in generation_queue if q["status"] == "completed"]) + errors = len([q for q in generation_queue if q["status"] == "error"]) + + # Build final status message + if completed == 0 and errors > 0: + final_progress = f"**Failed: All {errors} items had errors**" + elif errors > 0: + final_progress = f"**Complete with errors: {completed}/{total} succeeded, {errors} failed**" + else: + final_progress = f"**Complete! {completed}/{total} succeeded**" + + # Add update status to progress + if update_status and "Updated" in update_status: + final_progress += f" - {update_status}" + + otg_workflow_state["active"] = False + + # Build error details string + error_details = "\n".join(error_messages) if error_messages else "" + + yield (final_progress, update_status, get_queue_dataframe(), True, final_config, error_details) + + +def otg_stop_workflow(): + """Stop the one-click workflow processing and update config with partial results.""" + global otg_workflow_state + otg_workflow_state["active"] = False + + completed = len([q for q in generation_queue if q["status"] == "completed"]) + errors = len([q for q in generation_queue if q["status"] == "error"]) + total = len(generation_queue) + + # Update config with any completed items + partial_config, update_status = update_config_with_generated_media() + + status_msg = f"**Stopped** ({completed} succeeded, {errors} errors out of {total})" + if update_status and "Updated" in update_status: + status_msg += f" - {update_status}" + + return status_msg, get_queue_dataframe(), partial_config + + +def otg_get_workflow_status(): + """Get current workflow status.""" + global otg_workflow_state, generation_queue + + if not generation_queue: + return "Ready", 0, 0 + + pending = len([q for q in generation_queue if q["status"] == "pending"]) + completed = len([q for q in generation_queue if q["status"] in ["completed", "error"]]) + total = len(generation_queue) + + if otg_workflow_state.get("active"): + return "Processing", completed, total + elif pending == 0 and total > 0: + return "Complete", completed, total + else: + return "Ready", completed, total \ No newline at end of file diff --git a/story-graph-v3.html b/story-graph-v3.html new file mode 100644 index 0000000000000000000000000000000000000000..1b48ee385fb83d99036d742e68d9b2f9d132621f --- /dev/null +++ b/story-graph-v3.html @@ -0,0 +1,980 @@ + + + + + + Story Beat Graph - Rich Attributes + + + + + +
+ +
+ +
+ + + + +
+ +
+

Glue Types

+
Causal
+
Ideological
+
Question
+
Emotional
+
Object
+
Location
+
+ +
+ + + + + +
+
+ + + + diff --git a/story_elements.py b/story_elements.py new file mode 100644 index 0000000000000000000000000000000000000000..5419beb5766a922f5dc071b310b82fdeb8c61f43 --- /dev/null +++ b/story_elements.py @@ -0,0 +1,142 @@ +""" +Story elements, player engagement items, and narrative concepts +""" + +# List of player engagement UI elements +player_engagement_items = [ + "Health Bar", "Mana/Energy Bar", "Experience Bar", "Mini-Map", "Quest Tracker", + "Inventory Quick Access Slots", "Skill Cooldown Indicators", "Currency Display", + "Compass", "Timer/Clock", "Action Prompts", "Reticle/Crosshair", "Enemy Health Bars", + "Score Display", "Inventory Grid", "Item Tooltip", "Item Categories/Tabs", + "Equip/Unequip Buttons", "Item Sorting Options", "Character Stats Panel", + "Character Model Viewer", "Quick Equip Slots", "Dialogue Box", "Dialogue Choices", + "Character Portraits", "Character Name Display", "Subtitle Text", "World Map", + "Local Map", "Fast Travel Points", "Map Markers", "Map Legend", "Crafting Recipe List", + "Ingredient Slots", "Craft Button", "Crafting Progress Bar", "Skill Nodes", + "Skill Descriptions", "Skill Point Counter", "Unlock Button", "Skill Path Highlight", + "Quest List", "Quest Details Panel", "Quest Objectives", "Quest Rewards", + "Quest Status Indicators", "Chat Box", "Player List", "Voice Chat Indicators", + "Ping/Latency Indicator", "Party/Team UI", "Scoreboard", "Matchmaking Screen", + "Pop-up Notifications", "Achievement Unlocks", "System Messages", + "On-screen Key Prompts", "Environmental Interaction Icons", "Save/Auto-Save Indicators", + "Credits Screen" +] + +# List of character-based story events +story_events = [ + 'exploreLocation', 'discoverClue', 'meetCharacter', 'solveRiddle', 'findItem', + 'faceChallenge', 'makeDecision', 'engageBattle', 'unlockAbility', 'learnSecret', + 'completeMission', 'experienceSetback', 'earnReward', 'uncoverMystery', 'formAlliance', + 'faceBetrayal', 'confrontNemesis', 'makeDiscovery', 'overcomeLoss', 'achieveVictory' +] + +sterotypicalstoryendings = """Here are 30 stereotypical story endings: + +1. The hero rides off into the sunset with their loved one. +2. The villain gets their comeuppance and is punished for their crimes. +3. The protagonist finds a long-lost treasure or inheritance. +4. The underdog team wins the championship game. +5. The main character learns a valuable lesson about the importance of friendship and family. +6. The hero sacrifices themselves to save the world. +7. The couple gets married and lives happily ever after. +8. The villain is revealed to be a long-lost relative or friend. +9. The protagonist discovers a hidden talent or skill. +10. The world is saved from an alien invasion. +11. The hero and villain engage in an epic battle, with the hero emerging victorious. +12. The main character returns home after a long journey, wiser and more experienced. +13. The underdog achieves their dream despite the odds being against them. +14. The story ends with a cliffhanger, setting up a sequel. +15. The hero's secret identity is revealed to the public. +16. The couple reunites after a long separation. +17. The protagonist discovers a conspiracy that threatens the world. +18. The villain's backstory is revealed, explaining their motivations. +19. The hero must make a difficult choice between two desirable outcomes. +20. The story concludes with a sense of closure and new beginnings. +21. The protagonist's journey is revealed to be a dream or simulation. +22. The hero and villain team up to take down a greater threat. +23. The main character finds redemption for past mistakes. +24. The story ends with a twist, revealing a surprise traitor or unexpected outcome. +25. The hero's loved one is rescued from captivity. +26. The protagonist achieves a long-sought goal, but at great personal cost. +27. The world is rebuilt after a catastrophic event. +28. The hero must confront their own dark past or inner demons. +29. The story concludes with a sense of hope and renewal. +30. The protagonist discovers a hidden prophecy or destiny that they must fulfill. + +These are just a few examples of stereotypical story endings. Many stories subvert or play with these tropes, and there are countless other ways to conclude a narrative. +""" + +InterpersonalConflictIdeasBetraylacksympathyunfairexpectations = """Confide a deeply personal secret in a friend who immediately gossips about it. +Promise a neighbor to water their plants, then forget completely and deny responsibility. +Ignore a roommate's obvious distress when they lose their job. +Cancel a friend's invitation at the last minute with no explanation, expecting them to be fine with it. +Blame a colleague for missing a deadline without giving them the information they requested. +Borrow an expensive item from a friend, then return it damaged and refuse to pay for repairs. +Dismiss a sibling's request for emotional support, insisting "it's not a big deal." +Interrupt every time a friend tries to share good news, redirecting the conversation to your own achievements. +Overlook a loved one's birthday after they meticulously planned yours. +Ridicule a coworker's ideas in a meeting, yet count on their help afterward without apology. +Demand a partner sacrifice their free time for your projects but never offer to help with theirs. +Confront a friend about their small oversight but refuse to hear them out when they raise your mistakes. +Lose a close friend's trust by revealing a secret you promised to keep, then acting as though nothing happened. +Dismiss a teammate's contributions as trivial, only to claim credit if they succeed. +Invite a friend to a group event, then ignore them all evening. +Ask a coworker to cover for you repeatedly while you take extended breaks, offering no reciprocation. +Invalidate a family member's feelings by calling them "too sensitive" whenever they express sadness. +Exaggerate your role in a group project to get all the praise, overlooking everyone else's input. +Refuse to accommodate an employee's request for flexible hours but expect them to work late for your convenience. +Criticize a friend for not showing up to your party, even though they were caring for a sick relative. +Pressure a partner to spend money on your hobbies, then complain if they question the budget. +Downplay a child's accomplishment, making them feel insignificant, and then wonder why they're unenthusiastic later. +Repeat a rumor about a friend's personal life, shrugging it off as "harmless talk." +Persuade a colleague to take a risky approach, then blame them if it goes wrong. +Shut down someone's request for help with a project, saying "figure it out," while you rely on them for your tasks. +Lecture a friend about their choices without listening to their perspective, then wonder why they stop confiding in you. +Overstate the ease of a challenging task to a junior teammate, then criticize their performance when they struggle. +Downplay the effort a partner puts into daily chores, assuming they do "nothing all day." +Dodge responsibility for a shared error by pinning blame on a less influential teammate. +Arrange a high-stakes meeting and expect your subordinates to do all the prep without notice or support. +Reject a neighbor's plea for help fixing something, yet ask them to collect your mail every time you travel. +Neglect a friend's boundaries by repeatedly forcing them into uncomfortable social situations. +Publicize a colleague's personal struggles on social media for likes and sympathy points. +Interrupt a spouse's heartfelt apology to complain about your day, ignoring their sincere regret. +Ostracize a former friend for one minor mistake, never allowing them a chance to explain or make amends. +Devalue a friend's emotions by saying, "Just get over it," whenever they talk about something painful. +Push a team member to work overtime with no compensation, acting like it's a normal expectation. +Gossip about a friend who confided in you, then tell them you had no choice. +Force a coworker to share personal details at an office event, even though they repeatedly decline. +Accuse a loved one of being selfish simply because they took time for self-care. +Refuse to celebrate a friend's success because it "overshadows" your own achievements. +Demand that an introverted family member attend every social gathering, ignoring their comfort level. +Subtly bully a peer into helping with your tasks by saying you'll "remember this favor," implying future consequences. +Coerce a friend to defend you in an argument even when you know you're in the wrong. +Deny your child's request for basic emotional support, telling them they're "too dramatic." +Guilt-trip a sibling into giving up their personal time because "family obligations come first." +Promise a big reward to a classmate for doing extra group project work, then conveniently forget your end of the bargain. +Exploit a friend's kindness by constantly borrowing money without paying it back. +Discount a partner's illness as "not that serious," insisting they still handle household chores. +Walk away from someone in tears, believing they just "need to toughen up." +""" + +Nonlinearprogressionideas = """Llama 405B + +Non-linear storytelling involves presenting events out of chronological order. This technique can be used in literature, film, and other forms of narrative to create suspense, surprise, or to highlight certain themes or character developments. Here are some common examples of non-linear storytelling events: + +1. Flashbacks: These are scenes that take the story back in time, often used to explain a character's past or to provide context for the current situation. + +2. Flashforwards: Similar to flashbacks, but these scenes take the story forward in time, often used to create suspense or to show the consequences of current actions. + +3. Parallel storylines: This involves telling multiple stories that happen at the same time but are presented in a non-linear fashion, often intersecting or influencing each other at certain points. + +4. Reverse chronology: The story is presented in reverse order, starting with the conclusion and moving backward in time. + +5. Non-chronological chapters or scenes: The story is presented in a non-linear order, with each chapter or scene taking place at a different point in time. + +6. Time loops: A character relives the same period of time repeatedly, often with slight variations. + +7. Unreliable narrators: The narrator withholds or distorts information, leading to a non-linear presentation of events. + +8. Dream sequences: The story jumps into a character's dreams or imagination, which can be presented in a non-linear fashion. + +These are just a few examples of non-linear storytelling events. The possibilities are endless, and the technique can be adapted to fit the needs of the story and the creativity of the storyteller. +""" diff --git a/storygenattempt_hfport/HUGGINGFACE_INTEGRATION.md b/storygenattempt_hfport/HUGGINGFACE_INTEGRATION.md new file mode 100644 index 0000000000000000000000000000000000000000..c33988e07cc0ead47c34bb41c3323c1f8eee548a --- /dev/null +++ b/storygenattempt_hfport/HUGGINGFACE_INTEGRATION.md @@ -0,0 +1,577 @@ +# StoryArchitect - Hugging Face Gradio/Streamlit Integration Guide + +This guide explains how to integrate StoryArchitect's story planning features into an existing Hugging Face Gradio or Streamlit application. + +--- + +## Overview + +StoryArchitect provides: +- **Story Templates**: Pre-built beat structures for films (90-beat), TV episodes (29-beat), YouTube shorts (9-beat), and 30-second shorts (6-beat) +- **Story Foundations**: Core story elements (crux, struggle, emotional tone, character arcs) +- **Beat Management**: Create/edit individual story beats with metadata (verb, symbolism, visual flow, dialogue) +- **Modifier Libraries**: 100+ moral injury scenarios, sensory details, relationship loops, and more + +--- + +## Step 1: Install Dependencies + +```bash +pip install gradio # or streamlit +``` + +--- + +## Step 2: Convert Data Structures to Python + +Create a `story_data.py` file: + +```python +from dataclasses import dataclass, field +from typing import Optional, List +from enum import Enum + +# === Core Types === + +@dataclass +class Beat: + """A single story beat/moment""" + id: str + verb: str # The action (e.g., "Hook audience", "Reveal twist") + modifier: Optional[str] = None + adjective: Optional[str] = None # Emotional tone + reasoning: Optional[str] = None # Why this beat matters + symbolism: Optional[str] = None + visual_flow: Optional[str] = None # Camera/visual directions + script_text: Optional[str] = None # Dialogue/prose + characters_affected: Optional[str] = None + links: List[int] = field(default_factory=list) + +@dataclass +class StoryFoundation: + """Core story concept""" + crux: str = "" # Core concept + struggle: str = "" # Main conflicts + emotional_specifics: str = "" # Emotional tone + initial_state: str = "" # Hero's starting state + ending_state: str = "" # Hero's ending state + transition_type: str = "The Quest (Stasis to Change)" + characters: str = "" + +@dataclass +class SeriesEpisode: + """For TV series planning""" + id: str + number: int + title: str + logline: str + guest_characters: List[str] = field(default_factory=list) + episode_locations: List[str] = field(default_factory=list) + beats: List[Beat] = field(default_factory=list) + status: str = "outline" # outline | draft | complete + +# === Templates === + +TEMPLATES = { + "movie_90": [ + "Introduce world", "Introduce hero", "Show routine", "Hint flaw", "Tease conflict", + "Disrupt normal", "Question safety", "Escalate problem", "Force choice", "Resist change", + "Apply pressure", "Reveal stakes", "Complicate goal", "Fail attempt", "Increase cost", + "Meet ally", "Meet enemy", "Cross threshold", "Commit action", "Leave comfort", + "Test resolve", "Reinforce stakes", "Narrow options", "Approach turning point", "Decide journey", + "Enter new world", "Learn rules", "Try strategy", "Partially succeed", "Celebrate briefly", + "Expose weakness", "Counterattack hits", "Adjust plan", "Deepen relationship", "Discover clue", + "Misread truth", "Walk into trap", "Lose advantage", "Doubt self", "Argue allies", + "Raise stakes", "Time pressure", "Forced sacrifice", "Win small victory", "Reach midpoint", + "Reverse momentum", "Antagonist dominates", "Fail publicly", "Lose ally", "Lose resource", + "Chase solution", "Face consequence", "Reveal truth", "Reject truth", "Make mistake", + "Suffer fallout", "Isolate hero", "Reflect pain", "Recall lesson", "Attempt recovery", + "Interrupted hope", "Ultimate setback", "Everything lost", "Sit in defeat", "Choose growth", + "Form final plan", "Reunite allies", "Prepare showdown", "Enter final arena", "Trigger climax", + "Initiate plan", "Face antagonist", "Plan falters", "Improvise tactic", "Pay price", + "Use flaw positively", "Turn tables", "Gain upper hand", "Final confrontation", "Deliver decisive act", + "Antagonist falls", "Resolve danger", "Release tension", "Reunite characters", "Show changed hero", + "Restore order", "Echo opening", "Resolve subplots", "Offer final image", "End story" + ], + "tv_30": [ + "Hook audience", "Present problem", "Show stakes", "Introduce goal", "Complicate goal", + "Force decision", "Commit action", "Try solution", "Fail attempt", "Escalate conflict", + "Reveal twist", "React emotionally", "Adjust plan", "Face obstacle", "Lose advantage", + "Raise stakes", "Near success", "Major setback", "Reassess goal", "Make bold choice", + "Execute plan", "Reach climax", "Resolve conflict", "Pay consequence", "Show growth", + "Restore balance", "Tag subplot", "Echo theme", "Set future hook", "End beat" + ], + "youtube_9": [ + "Hook: Instant curiosity/Grab attention", + "Context: Why this matters now", + "Inciting Incident: The journey begins", + "First Attempt: The easy way fails", + "Rising Action: Increasing difficulty/stakes", + "The Turning Point: A new discovery/pivot", + "The Payoff/Climax: The big result/reveal", + "Key Takeaway: Value delivery", + "Resolution & CTA: What to do next" + ], + "short_30s": [ + "0-5s: Visual Hook / Headline", + "5-10s: The Problem/Setup", + "10-15s: The Build-up / Tension", + "15-20s: The Twist / Payoff Start", + "20-25s: The Reveal / Big Moment", + "25-30s: Satisfying Conclusion / Loop" + ] +} + +TRANSITION_TYPES = [ + "Tragedy (High to Low)", + "Comedy (Low to High)", + "Rebirth (Death to Life)", + "Rags to Riches", + "The Quest (Stasis to Change)", + "Overcoming the Monster" +] + +ADJECTIVES = [ + "Melancholic", "Claustrophobic", "Radiant", "Gritty", "Ethereal", "Ominous", + "Clinical", "Visceral", "Fragile", "Oppressive", "Vibrant", "Desolate", + "Sterile", "Chaotic", "Harmonious", "Jagged", "Velvety", "Caustic", + "Buoyant", "Stagnant", "Electric", "Somber", "Kinetic", "Luminous", + "Paradoxical", "Brutalist", "Serene", "Feverish", "Skeletal", "Lush" +] + +# Full modifier libraries available in data.json +# See: moral_injury (100 scenarios), sensory (30 items), relationship_loops (10), +# visual_grammar (10), dialogue_subtext (10), payoff_seeds (10), etc. +``` + +--- + +## Step 3: Gradio Integration + +Create `app_gradio.py`: + +```python +import gradio as gr +from story_data import TEMPLATES, TRANSITION_TYPES, ADJECTIVES, Beat, StoryFoundation +from typing import List +import json + +# State management +current_beats: List[Beat] = [] +current_foundation = StoryFoundation() + +def load_template(template_name: str) -> str: + """Load a story template and return beats as formatted text""" + global current_beats + template = TEMPLATES.get(template_name, []) + current_beats = [ + Beat(id=f"{template_name}-{i}", verb=verb) + for i, verb in enumerate(template) + ] + return format_beats_display(current_beats) + +def format_beats_display(beats: List[Beat]) -> str: + """Format beats for display""" + lines = [] + for i, beat in enumerate(beats): + line = f"{i+1}. {beat.verb}" + if beat.adjective: + line += f" [{beat.adjective}]" + if beat.script_text: + line += f"\n {beat.script_text[:100]}..." + lines.append(line) + return "\n".join(lines) + +def update_foundation(crux, struggle, emotional, initial, ending, transition, characters): + """Update story foundation""" + global current_foundation + current_foundation = StoryFoundation( + crux=crux, + struggle=struggle, + emotional_specifics=emotional, + initial_state=initial, + ending_state=ending, + transition_type=transition, + characters=characters + ) + return "Foundation updated!" + +def edit_beat(beat_index: int, verb: str, adjective: str, script_text: str, symbolism: str): + """Edit a specific beat""" + global current_beats + if 0 <= beat_index < len(current_beats): + current_beats[beat_index].verb = verb + current_beats[beat_index].adjective = adjective + current_beats[beat_index].script_text = script_text + current_beats[beat_index].symbolism = symbolism + return format_beats_display(current_beats) + +def export_story() -> str: + """Export story as JSON""" + return json.dumps({ + "foundation": { + "crux": current_foundation.crux, + "struggle": current_foundation.struggle, + "emotional_specifics": current_foundation.emotional_specifics, + "initial_state": current_foundation.initial_state, + "ending_state": current_foundation.ending_state, + "transition_type": current_foundation.transition_type, + "characters": current_foundation.characters + }, + "beats": [ + { + "id": b.id, + "verb": b.verb, + "adjective": b.adjective, + "script_text": b.script_text, + "symbolism": b.symbolism + } + for b in current_beats + ] + }, indent=2) + +# === Build Gradio Interface === + +with gr.Blocks(title="StoryArchitect") as demo: + gr.Markdown("# StoryArchitect - Story Planning Tool") + + with gr.Tab("Templates"): + template_dropdown = gr.Dropdown( + choices=list(TEMPLATES.keys()), + label="Select Template", + value="youtube_9" + ) + load_btn = gr.Button("Load Template") + beats_display = gr.Textbox( + label="Story Beats", + lines=15, + interactive=False + ) + load_btn.click(load_template, inputs=template_dropdown, outputs=beats_display) + + with gr.Tab("Foundation"): + crux_input = gr.Textbox(label="Story Crux (Core concept)") + struggle_input = gr.Textbox(label="Struggle (Main conflicts)") + emotional_input = gr.Textbox(label="Emotional Specifics") + + with gr.Row(): + initial_input = gr.Textbox(label="Initial State") + ending_input = gr.Textbox(label="Ending State") + + transition_dropdown = gr.Dropdown( + choices=TRANSITION_TYPES, + label="Transition Type", + value="The Quest (Stasis to Change)" + ) + characters_input = gr.Textbox(label="Characters") + + foundation_btn = gr.Button("Save Foundation") + foundation_status = gr.Textbox(label="Status", interactive=False) + + foundation_btn.click( + update_foundation, + inputs=[crux_input, struggle_input, emotional_input, + initial_input, ending_input, transition_dropdown, characters_input], + outputs=foundation_status + ) + + with gr.Tab("Edit Beats"): + beat_index = gr.Number(label="Beat Number (1-based)", value=1) + beat_verb = gr.Textbox(label="Verb/Action") + beat_adjective = gr.Dropdown(choices=ADJECTIVES, label="Emotional Tone") + beat_script = gr.Textbox(label="Script/Dialogue", lines=3) + beat_symbolism = gr.Textbox(label="Symbolism") + + edit_btn = gr.Button("Update Beat") + updated_display = gr.Textbox(label="Updated Beats", lines=15, interactive=False) + + edit_btn.click( + lambda idx, v, a, s, sym: edit_beat(int(idx)-1, v, a, s, sym), + inputs=[beat_index, beat_verb, beat_adjective, beat_script, beat_symbolism], + outputs=updated_display + ) + + with gr.Tab("Export"): + export_btn = gr.Button("Export Story JSON") + export_output = gr.Code(label="Story JSON", language="json") + export_btn.click(export_story, outputs=export_output) + +# To integrate with existing HF app, import and mount this block +# demo.launch() # Uncomment for standalone +``` + +--- + +## Step 4: Streamlit Integration + +Create `app_streamlit.py`: + +```python +import streamlit as st +from story_data import TEMPLATES, TRANSITION_TYPES, ADJECTIVES, Beat, StoryFoundation +import json + +st.set_page_config(page_title="StoryArchitect", layout="wide") + +# Initialize session state +if "beats" not in st.session_state: + st.session_state.beats = [] +if "foundation" not in st.session_state: + st.session_state.foundation = StoryFoundation() + +st.title("StoryArchitect - Story Planning Tool") + +# Sidebar for template selection +with st.sidebar: + st.header("Templates") + template = st.selectbox("Choose Template", list(TEMPLATES.keys())) + + if st.button("Load Template"): + st.session_state.beats = [ + Beat(id=f"{template}-{i}", verb=verb) + for i, verb in enumerate(TEMPLATES[template]) + ] + st.success(f"Loaded {len(st.session_state.beats)} beats") + +# Main content tabs +tab1, tab2, tab3, tab4 = st.tabs(["Foundation", "Beats", "Edit Beat", "Export"]) + +with tab1: + st.header("Story Foundation") + + col1, col2 = st.columns(2) + with col1: + st.session_state.foundation.crux = st.text_area( + "Crux (Core concept)", + value=st.session_state.foundation.crux + ) + st.session_state.foundation.struggle = st.text_area( + "Struggle", + value=st.session_state.foundation.struggle + ) + st.session_state.foundation.emotional_specifics = st.text_input( + "Emotional Tone", + value=st.session_state.foundation.emotional_specifics + ) + + with col2: + st.session_state.foundation.initial_state = st.text_input( + "Initial State", + value=st.session_state.foundation.initial_state + ) + st.session_state.foundation.ending_state = st.text_input( + "Ending State", + value=st.session_state.foundation.ending_state + ) + st.session_state.foundation.transition_type = st.selectbox( + "Transition Type", + TRANSITION_TYPES, + index=TRANSITION_TYPES.index(st.session_state.foundation.transition_type) + ) + st.session_state.foundation.characters = st.text_input( + "Characters", + value=st.session_state.foundation.characters + ) + +with tab2: + st.header("Story Beats") + + if st.session_state.beats: + for i, beat in enumerate(st.session_state.beats): + with st.expander(f"Beat {i+1}: {beat.verb}"): + if beat.adjective: + st.caption(f"Tone: {beat.adjective}") + if beat.script_text: + st.write(beat.script_text) + if beat.symbolism: + st.info(f"Symbolism: {beat.symbolism}") + else: + st.info("Load a template from the sidebar to begin") + +with tab3: + st.header("Edit Beat") + + if st.session_state.beats: + beat_idx = st.number_input( + "Beat Number", + min_value=1, + max_value=len(st.session_state.beats), + value=1 + ) - 1 + + current = st.session_state.beats[beat_idx] + + new_verb = st.text_input("Verb/Action", value=current.verb) + new_adjective = st.selectbox( + "Emotional Tone", + [""] + ADJECTIVES, + index=ADJECTIVES.index(current.adjective) + 1 if current.adjective in ADJECTIVES else 0 + ) + new_script = st.text_area("Script/Dialogue", value=current.script_text or "") + new_symbolism = st.text_input("Symbolism", value=current.symbolism or "") + + if st.button("Save Changes"): + st.session_state.beats[beat_idx].verb = new_verb + st.session_state.beats[beat_idx].adjective = new_adjective if new_adjective else None + st.session_state.beats[beat_idx].script_text = new_script if new_script else None + st.session_state.beats[beat_idx].symbolism = new_symbolism if new_symbolism else None + st.success("Beat updated!") + st.rerun() + +with tab4: + st.header("Export Story") + + story_data = { + "foundation": { + "crux": st.session_state.foundation.crux, + "struggle": st.session_state.foundation.struggle, + "emotional_specifics": st.session_state.foundation.emotional_specifics, + "initial_state": st.session_state.foundation.initial_state, + "ending_state": st.session_state.foundation.ending_state, + "transition_type": st.session_state.foundation.transition_type, + "characters": st.session_state.foundation.characters + }, + "beats": [ + { + "id": b.id, + "verb": b.verb, + "adjective": b.adjective, + "script_text": b.script_text, + "symbolism": b.symbolism + } + for b in st.session_state.beats + ] + } + + st.json(story_data) + + st.download_button( + "Download JSON", + data=json.dumps(story_data, indent=2), + file_name="story.json", + mime="application/json" + ) +``` + +--- + +## Step 5: Integration with Existing HF Project + +### Option A: Add as a Tab in Existing Gradio App + +```python +# In your existing app.py +import gradio as gr +from story_architect_gradio import demo as story_demo # Import the StoryArchitect blocks + +with gr.Blocks() as main_app: + with gr.Tab("Your Existing Feature"): + # ... your existing code + pass + + with gr.Tab("Story Planner"): + # Embed StoryArchitect + story_demo.render() + +main_app.launch() +``` + +### Option B: Add as a Page in Existing Streamlit App + +```python +# pages/story_architect.py (in your Streamlit multipage app) +# Simply copy the streamlit code above +``` + +### Option C: Import Components Selectively + +```python +# Import just what you need +from story_data import TEMPLATES, ADJECTIVES, Beat + +# Use in your existing UI +template_beats = TEMPLATES["youtube_9"] +``` + +--- + +## Step 6: AI Integration (Optional) + +Integrate with Hugging Face models for AI-assisted story generation: + +```python +from transformers import pipeline + +# Load a text generation model +generator = pipeline("text-generation", model="gpt2") # or any HF model + +def generate_beat_content(beat_verb: str, context: str) -> str: + """Generate script content for a beat using AI""" + prompt = f"Story beat: {beat_verb}\nContext: {context}\nScript:" + result = generator(prompt, max_length=150, num_return_sequences=1) + return result[0]["generated_text"] + +def expand_foundation(crux: str) -> dict: + """Expand a story crux into full foundation""" + prompt = f"Story concept: {crux}\nGenerate struggle, emotional tone, and character arc:" + result = generator(prompt, max_length=200) + # Parse and return structured data + return {"expanded": result[0]["generated_text"]} +``` + +--- + +## File Structure for HF Spaces + +``` +your-hf-space/ +├── app.py # Main Gradio/Streamlit app +├── story_data.py # Data models and templates +├── data/ +│ └── modifiers.json # Full modifier libraries from data.json +├── requirements.txt # gradio or streamlit + dependencies +└── README.md +``` + +### requirements.txt + +``` +gradio>=4.0.0 +# or +# streamlit>=1.28.0 +``` + +--- + +## Key Data Files to Port + +From the original `data.json`, you may want to include: + +| Key | Description | Count | +|-----|-------------|-------| +| `templates.movie_90` | 90-minute film beats | 93 | +| `templates.tv_30` | 30-minute TV beats | 29 | +| `templates.youtube_9` | 9-minute short beats | 9 | +| `templates.short_30s` | 30-second short beats | 6 | +| `modifiers.moral_injury` | Ethical dilemma scenarios | 100 | +| `modifiers.adjectives` | Emotional tone words | 100 | +| `modifiers.sensory` | Sensory detail prompts | 30 | +| `modifiers.relationship_loops` | Character relationship patterns | 10 | +| `modifiers.visual_grammar` | Camera/visual directions | 10 | +| `modifiers.dialogue_subtext` | Subtext examples | 10 | +| `modifiers.payoff_seeds` | Setup/payoff patterns | 10 | + +--- + +## Demo Stories Available + +The original app includes complete demo stories you can port: + +- **Heist** (9-min): Debt system rebellion +- **Coup** (9-min): Political betrayal cycle +- **Battery** (9-min): Survival sacrifice +- **Digital Shunning** (30-min): Tech dystopia +- **Radio Ghost** (30-min): Supernatural drama +- And more... + +These are fully fleshed out with all beat metadata (symbolism, visual flow, dialogue). diff --git a/storygenattempt_hfport/story_data.py b/storygenattempt_hfport/story_data.py new file mode 100644 index 0000000000000000000000000000000000000000..3bae4b25c30281a0a9760c0ee366362ec129b41c --- /dev/null +++ b/storygenattempt_hfport/story_data.py @@ -0,0 +1,398 @@ +""" +StoryArchitect Data Models and Templates for Python/Gradio/Streamlit + +This file contains the core data structures and templates from StoryArchitect, +ready to use in a Hugging Face Gradio or Streamlit application. +""" + +from dataclasses import dataclass, field +from typing import Optional, List +import json + +# === Core Data Models === + +@dataclass +class Beat: + """A single story beat/moment in the narrative""" + id: str + verb: str # The action (e.g., "Hook audience", "Reveal twist") + modifier: Optional[str] = None + adjective: Optional[str] = None # Emotional tone + reasoning: Optional[str] = None # Why this beat matters + symbolism: Optional[str] = None + visual_flow: Optional[str] = None # Camera/visual directions + script_text: Optional[str] = None # Dialogue/prose + characters_affected: Optional[str] = None + links: List[int] = field(default_factory=list) + + def to_dict(self) -> dict: + return { + "id": self.id, + "verb": self.verb, + "modifier": self.modifier, + "adjective": self.adjective, + "reasoning": self.reasoning, + "symbolism": self.symbolism, + "visual_flow": self.visual_flow, + "script_text": self.script_text, + "characters_affected": self.characters_affected, + "links": self.links + } + + @classmethod + def from_dict(cls, data: dict) -> "Beat": + return cls(**data) + + +@dataclass +class StoryFoundation: + """Core story concept and arc""" + crux: str = "" # Core concept + struggle: str = "" # Main conflicts + emotional_specifics: str = "" # Emotional tone + initial_state: str = "" # Hero's starting state + ending_state: str = "" # Hero's ending state + transition_type: str = "The Quest (Stasis to Change)" + characters: str = "" + + def to_dict(self) -> dict: + return { + "crux": self.crux, + "struggle": self.struggle, + "emotional_specifics": self.emotional_specifics, + "initial_state": self.initial_state, + "ending_state": self.ending_state, + "transition_type": self.transition_type, + "characters": self.characters + } + + +@dataclass +class SeriesFoundation: + """For TV series planning""" + title: str = "" + premise: str = "" + main_cast: List[str] = field(default_factory=list) + central_locations: List[str] = field(default_factory=list) + season_arc: str = "" + tone: str = "" + + +@dataclass +class SeriesEpisode: + """A single episode in a series""" + id: str + number: int + title: str + logline: str + guest_characters: List[str] = field(default_factory=list) + episode_locations: List[str] = field(default_factory=list) + beats: List[Beat] = field(default_factory=list) + status: str = "outline" # outline | draft | complete + + +# === Story Templates === + +TEMPLATES = { + "movie_90": [ + "Introduce world", "Introduce hero", "Show routine", "Hint flaw", "Tease conflict", + "Disrupt normal", "Question safety", "Escalate problem", "Force choice", "Resist change", + "Apply pressure", "Reveal stakes", "Complicate goal", "Fail attempt", "Increase cost", + "Meet ally", "Meet enemy", "Cross threshold", "Commit action", "Leave comfort", + "Test resolve", "Reinforce stakes", "Narrow options", "Approach turning point", "Decide journey", + "Enter new world", "Learn rules", "Try strategy", "Partially succeed", "Celebrate briefly", + "Expose weakness", "Counterattack hits", "Adjust plan", "Deepen relationship", "Discover clue", + "Misread truth", "Walk into trap", "Lose advantage", "Doubt self", "Argue allies", + "Raise stakes", "Time pressure", "Forced sacrifice", "Win small victory", "Reach midpoint", + "Reverse momentum", "Antagonist dominates", "Fail publicly", "Lose ally", "Lose resource", + "Chase solution", "Face consequence", "Reveal truth", "Reject truth", "Make mistake", + "Suffer fallout", "Isolate hero", "Reflect pain", "Recall lesson", "Attempt recovery", + "Interrupted hope", "Ultimate setback", "Everything lost", "Sit in defeat", "Choose growth", + "Form final plan", "Reunite allies", "Prepare showdown", "Enter final arena", "Trigger climax", + "Initiate plan", "Face antagonist", "Plan falters", "Improvise tactic", "Pay price", + "Use flaw positively", "Turn tables", "Gain upper hand", "Final confrontation", "Deliver decisive act", + "Antagonist falls", "Resolve danger", "Release tension", "Reunite characters", "Show changed hero", + "Restore order", "Echo opening", "Resolve subplots", "Offer final image", "End story" + ], + "tv_30": [ + "Hook audience", "Present problem", "Show stakes", "Introduce goal", "Complicate goal", + "Force decision", "Commit action", "Try solution", "Fail attempt", "Escalate conflict", + "Reveal twist", "React emotionally", "Adjust plan", "Face obstacle", "Lose advantage", + "Raise stakes", "Near success", "Major setback", "Reassess goal", "Make bold choice", + "Execute plan", "Reach climax", "Resolve conflict", "Pay consequence", "Show growth", + "Restore balance", "Tag subplot", "Echo theme", "Set future hook", "End beat" + ], + "youtube_9": [ + "Hook: Instant curiosity/Grab attention", + "Context: Why this matters now", + "Inciting Incident: The journey begins", + "First Attempt: The easy way fails", + "Rising Action: Increasing difficulty/stakes", + "The Turning Point: A new discovery/pivot", + "The Payoff/Climax: The big result/reveal", + "Key Takeaway: Value delivery", + "Resolution & CTA: What to do next" + ], + "short_30s": [ + "0-5s: Visual Hook / Headline", + "5-10s: The Problem/Setup", + "10-15s: The Build-up / Tension", + "15-20s: The Twist / Payoff Start", + "20-25s: The Reveal / Big Moment", + "25-30s: Satisfying Conclusion / Loop" + ] +} + +TEMPLATE_DESCRIPTIONS = { + "movie_90": "90-minute feature film (93 beats)", + "tv_30": "30-minute TV episode (29 beats)", + "youtube_9": "9-minute YouTube video (9 beats)", + "short_30s": "30-second short (6 beats)" +} + +# === Transition Types === + +TRANSITION_TYPES = [ + "Tragedy (High to Low)", + "Comedy (Low to High)", + "Rebirth (Death to Life)", + "Rags to Riches", + "The Quest (Stasis to Change)", + "Overcoming the Monster" +] + +# === Adjectives for Emotional Tone === + +ADJECTIVES = [ + "Melancholic", "Claustrophobic", "Radiant", "Gritty", "Ethereal", "Ominous", + "Clinical", "Visceral", "Fragile", "Oppressive", "Vibrant", "Desolate", + "Sterile", "Chaotic", "Harmonious", "Jagged", "Velvety", "Caustic", + "Buoyant", "Stagnant", "Electric", "Somber", "Kinetic", "Luminous", + "Paradoxical", "Brutalist", "Serene", "Feverish", "Skeletal", "Lush", + "Acrid", "Resonant", "Hollow", "Manic", "Stoic", "Volatile", + "Pristine", "Decadent", "Austere", "Mercurial", "Primal", "Byzantine", + "Petrified", "Labyrinthine", "Gilded", "Squalid", "Translucent", "Macabre", + "Pastoral", "Industrial", "Nocturnal", "Diaphanous", "Monolithic", "Pulsating", + "Tattered", "Opulent", "Frenetic", "Arcane", "Subterranean", "Crystalline", + "Oxidized", "Verdant", "Spectral", "Galvanized", "Obsidian", "Sepia", + "Neon", "Muddy", "Sharp", "Blurred", "Static", "Rhythmic", + "Jarring", "Fluid", "Frozen", "Burning", "Damp", "Dusty", + "Slick", "Coarse", "Weightless", "Crushing", "Expansive", "Intimate", + "Alien", "Ancestral", "Synthetic", "Organic", "Fractured", "Seamless", + "Grimy", "Polished", "Raw", "Refined", "Savage", "Civilized", + "Ancient", "Futuristic", "Timeless", "Mechanical" +] + +# === Sensory Details === + +SENSORY_DETAILS = [ + "Rhythmic thrum of failing HVAC", + "Metallic scrape of a heavy chair on marble", + "Distant, muffled pneumatic hiss", + "Wet slap of leather on wet pavement", + "High-frequency whine of an old monitor", + "The hollow 'tink' of cooling metal", + "Echoing click of government-issue boots", + "Static-filled rasp of a low-battery radio", + "Rhythmic dripping into a plastic bucket", + "The dull roar of a ventilation shaft", + "Ozone and burnt copper", + "Clinical pine and old blood", + "Wet cardboard and stale coffee", + "Searing electrical heat", + "Metallic tang of iron-rich water", + "Dusty paper and pressurized air", + "Sharp chemical cleaner", + "Sweet rot beneath a floorboard", + "Cold rain on hot asphalt", + "Gun oil and new leather", + "Gritty concrete dust", + "Slick, polished obsidian", + "Cold, vibrating metal", + "Tattered, oil-stained fabric", + "Sharp, jagged glass edges", + "Smooth, deceptive synthetic skin", + "Damp, heavy velvet", + "Corroded, flaking paint", + "Prickling static electricity", + "The numbing cold of dry ice" +] + +# === Relationship Friction Loops === + +RELATIONSHIP_LOOPS = [ + "Admire -> Emulate -> Envy -> Sabotage -> Regret", + "Trust -> Test -> Fail -> Resent -> Revenge", + "Protect -> Control -> Suffocate -> Rebel -> Sever", + "Need -> Use -> Devalue -> Discard -> Replace", + "Reveal -> Misinterpret -> Offend -> Defend -> Drift", + "Compete -> Respect -> Align -> Merge -> Lose Self", + "Observe -> Judge -> Condemn -> Punish -> Isolate", + "Doubt -> Question -> Interrogate -> Accuse -> Explode", + "Promise -> Delay -> Excuse -> Break -> Apologize", + "Give -> Expect -> Demand -> Resent -> Withdraw" +] + +# === Visual Grammar (Camera/Direction) === + +VISUAL_GRAMMAR = [ + "ECU (Extreme Close Up): Focus on a singular detail to signal overwhelming importance or internal pressure.", + "Low Angle: Character appears larger, signaling power, threat, or emerging confidence.", + "High Angle: Character appears small and vulnerable, signaling isolation or lack of control.", + "Dutch Tilt: Disorientation, psychological break, or a world out of balance.", + "Shallow Depth of Field: Isolating character from surroundings to signal focus or tunnel vision.", + "Rack Focus: Shifting importance from one subject to another within the same shot.", + "Tracking Shot: Following movement to signal momentum, inevitability, or being hunted.", + "Symmetrical Framing: Signaling order, rigid systems, or a 'stalemate' between characters.", + "Low-Key Lighting: Deep shadows to signal the 'unknown,' secrets, or moral ambiguity.", + "Handheld / Shaky Cam: Instability, panic, or raw, unedited reality." +] + +# === Character Flaws === + +CHARACTER_FLAWS = [ + "Sunk-cost Fallacy: Persistence in a failing path due to past investment.", + "Martyr Complex: Seeking out suffering to maintain a moral high ground.", + "Obsessive Symmetry: Need for perfect order that paralyzes action.", + "Selective Silence: Using the withholding of truth as a shield.", + "Inherited Debt: Belief that one must pay for the sins of the father.", + "Fragile Superiority: Ego that shatters at the first sign of criticism.", + "Hyper-vigilance: Inability to trust safety, leading to self-sabotage.", + "Paradoxical Apathy: Caring so much about the outcome that one stops trying.", + "Mimetic Desire: Wanting only what others have, regardless of value.", + "Systemic Blindness: Inability to see the rules of the cage one lives in.", + "Compulsive Altruism: Helping others to avoid dealing with one's own rot.", + "Emotional Hoarding: Collecting grievances to use as future leverage.", + "Fear of Stasis: Destroying peace because it feels like death.", + "The Anchor Complex: Dragging others down to one's own level of safety.", + "Chronic Foreshadowing: Living in fear of a future that hasn't happened." +] + +# === Dialogue Subtext Examples === + +DIALOGUE_SUBTEXT = [ + 'The False Compliment: "You\'ve always been so... consistent." (Meaning: You are predictable and boring).', + 'The Deflection: "The weather is changing." (Meaning: I don\'t want to talk about the dead body in the room).', + 'The Shielding Question: "Is that what you think?" (Meaning: I\'m terrified you\'ve found the truth).', + 'The Passive Aggressive Probe: "I wish I had your ability to just... let things go." (Meaning: You are irresponsible).', + 'The Conditional Mercy: "I\'ll see what I can do." (Meaning: I won\'t do anything unless you pay me).', + 'The Euphemistic Threat: "We want to ensure your continued... comfort." (Meaning: We will hurt you if you move).', + 'The Validation Trap: "You deserve so much more than this." (Meaning: I\'m trying to make you ungrateful so you\'ll help me steal).', + 'The Dismissive Care: "Don\'t worry about the details." (Meaning: The details will get you killed).', + 'The Reluctant Alliance: "We have the same enemies." (Meaning: I\'ll kill you as soon as they are gone).', + 'The Narrative Reframe: "It\'s not a theft, it\'s a redistribution." (Meaning: I\'m stealing this).' +] + +# === Payoff Seeds (Setup/Payoff Patterns) === + +PAYOFF_SEEDS = [ + "The Broken Key: Seen in Minute 1, fits a door in Minute 9.", + "The Misheard Name: Mentioned as a joke in Minute 2, is the password in Minute 8.", + "The Persistent Cough: Character sick in Minute 1, reveals a bio-weapon in Minute 7.", + "The Unpaid Bill: Tucked in a pocket in Minute 3, causes the arrest in Minute 9.", + "The Reflected Shadow: Seen in a mirror in Minute 1, reveals the hidden twin in Minute 8.", + "The Ticking Clock: Audio cue throughout, stops when the bomb is deactivated in Minute 9.", + "The Wrong Tool: Character uses a knife for a screwdriver in Minute 4, fails them in Minute 8.", + "The Childhood Toy: Symbolic comfort in Minute 2, holds the microchip in Minute 9.", + "The Red Glow: Visual motif starting in Minute 1, signifies the 'Redistribution' event in Minute 9.", + "The Missing Tooth: Character notices it in Minute 3, realizes the 'mentor' is a clone in Minute 7." +] + +# === Environmental Vibes === + +ENVIRONMENTAL_VIBES = [ + 'Brutalist Concrete: Cold, gray, imposing, monumental. Signals "The System."', + 'Overgrown Cyberpunk: Neon flickering through vines, rusted chrome, humidity. Signals "Nature reclaiming greed."', + 'High-Tech Sterile: White light, glass, silence, invisible screens. Signals "Dehumanized efficiency."', + 'Squalid Industrial: Burnt oil, steam, clanking metal, perpetual dusk. Signals "The grind of poverty."', + 'Neo-Baroque Dystopia: Gold leaf over rot, velvet curtains, candlelight, ornate torture devices. Signals "Decadent cruelty."', + 'Subterranean Bunker: Low ceilings, humming fans, recycled air, yellow light. Signals "Paranoia."', + 'Ancestral Dust: Ancient wood, cracked leather, smell of old paper, sepia tones. Signals "Lost history."', + 'Synthetic Pastoral: Plastic grass, hologram sky, odorless air. Signals "The lie of comfort."' +] + +# === Twist Mechanics === + +TWIST_MECHANICS = [ + "The False Victory: Hero gets what they wanted, only to realize it was exactly what the villain planned.", + "The Unreliable Mentor: The guide wasn't teaching the hero to win, but to be the perfect sacrifice.", + "The Hidden Inheritance: The 'junk' the hero carried is actually the key to the entire system.", + "The Moral Mirror: The hero realizes they have become exactly what they were fighting.", + "The Necessary Betrayal: The hero must betray their ally to save the world, or vice-versa.", + "The Late Realization: The 'ticking bomb' was already defused; the hero is fighting for a ghost.", + "The Reversal of Power: The oppressed group finally wins, but immediately starts using the same tools of oppression.", + "The Identity Leak: The hero realizes they are actually the 'Villain' they've been hunting (amnesia/clone).", + "The Insurance Clause: The 'victim' was in on the crime to collect a payout.", + "The Loop Revelation: The hero realizes this is the 100th time they've tried and failed this mission." +] + + +# === Helper Functions === + +def create_beats_from_template(template_key: str) -> List[Beat]: + """Create a list of Beat objects from a template""" + if template_key not in TEMPLATES: + raise ValueError(f"Unknown template: {template_key}") + + return [ + Beat(id=f"{template_key}-{i}", verb=verb) + for i, verb in enumerate(TEMPLATES[template_key]) + ] + + +def export_story_json(foundation: StoryFoundation, beats: List[Beat]) -> str: + """Export a story as JSON string""" + return json.dumps({ + "foundation": foundation.to_dict(), + "beats": [b.to_dict() for b in beats] + }, indent=2) + + +def import_story_json(json_str: str) -> tuple: + """Import a story from JSON string, returns (foundation, beats)""" + data = json.loads(json_str) + foundation = StoryFoundation(**data["foundation"]) + beats = [Beat.from_dict(b) for b in data["beats"]] + return foundation, beats + + +# === Demo Stories === + +DEMO_STORIES = { + "heist": { + "template": "youtube_9", + "foundation": { + "crux": "A woman smashes a system of state-mandated debt by turning her father's failure into a partner's freedom.", + "struggle": "Rules: Debt is life. Challenges: Old friends are guards.", + "emotional_specifics": "Suffocating, Ticking.", + "initial_state": "Isolated, indebted.", + "ending_state": "Connected, empowered.", + "transition_type": "Rebirth (Death to Life)", + "characters": "Elara, Sam" + } + }, + "coup": { + "template": "youtube_9", + "foundation": { + "crux": "A young aide leaks a truth that destroys a regime but realizes he just started the next one.", + "struggle": "Rules: Information is currency. Challenges: The Minister sees everything.", + "emotional_specifics": "Cold, Clinical, Sharp.", + "initial_state": "Loyal servant.", + "ending_state": "The new face of power.", + "transition_type": "Tragedy (High to Low)", + "characters": "Kael, The Minister" + } + }, + "battery": { + "template": "youtube_9", + "foundation": { + "crux": "Two scavengers decide who gets to see tomorrow.", + "struggle": "Rules: Energy is life. Challenges: One battery, two people.", + "emotional_specifics": "Heavy, Frozen, Metallic.", + "initial_state": "Dying scavengers.", + "ending_state": "One life saved.", + "transition_type": "Tragedy (High to Low)", + "characters": "Jax, Mina" + } + } +} diff --git a/timeline_and_UI_generation_functions.py b/timeline_and_UI_generation_functions.py index 5f47e657a4b1db456381e66d08baa0e86f495b0a..3726a0a504618d9a677154e88c655d924bde426c 100644 --- a/timeline_and_UI_generation_functions.py +++ b/timeline_and_UI_generation_functions.py @@ -70,7 +70,7 @@ def generate_story_and_timeline(no_story_timeline_points=10, no_ui_timeline_poin return formatted_timeline, story, json.dumps(game_structure_with_media, indent=2), suggestions, selected_list_names #no_media_formatted_timeline, json.dumps(game_structure_without_media, indent=2) #, game_structure_with_media -media_file_types = ["image", "video", "audio"] +media_file_types = ["image", "video", "audio", "3d", "tts"] def generate_media_file_list(n): return [random.choice(media_file_types) for _ in range(n)] @@ -104,32 +104,43 @@ def show_elements(text): elif content.lower() == 'image': # Placeholder for image element outputs.append(gr.Image(label=f"Image at {timestamp} in the order")) + elif content.lower() == '3d': + # Placeholder for 3D model element + outputs.append(gr.Model3D(label=f"3D Model at {timestamp} in the order")) + elif content.lower() == 'tts': + # Placeholder for TTS audio element + outputs.append(gr.Audio(label=f"TTS Audio at {timestamp} in the order")) return outputs def show_elements_json_input(json_input): - data = json.loads(json_input) + if not json_input: + return [] + try: + data = json.loads(json_input) + except json.JSONDecodeError: + return [] masterlocation1 = data['masterlocation1'] - + outputs = [] - + for location, details in masterlocation1.items(): if location == 'end': continue - + with gr.Accordion(f"Location: {location} - Previous description {details['description']}", open=False): description = gr.Textbox(label="Description", value=details['description'], interactive=True) outputs.append(description) - + events = gr.Textbox(label="Events", value=json.dumps(details['events']), interactive=True) outputs.append(events) - + choices = gr.Textbox(label="Choices", value=json.dumps(details['choices']), interactive=True) outputs.append(choices) - + transitions = gr.Textbox(label="Transitions", value=json.dumps(details['transitions']), interactive=True) outputs.append(transitions) - + # New media field media = gr.Textbox(label="Media", value=json.dumps(details['media']), interactive=True) outputs.append(media) @@ -140,7 +151,7 @@ def show_elements_json_input(json_input): #adding/removing a field means incrementing/decreasing the i+n to match the fields num_current_unique_fields = 6 - + def update_json(*current_values): updated_data = {"masterlocation1": {}} locations = [loc for loc in masterlocation1.keys() if loc != 'end'] @@ -155,17 +166,190 @@ def show_elements_json_input(json_input): } updated_data["masterlocation1"]["end"] = masterlocation1["end"] return json.dumps(updated_data, indent=2) #json.dumps(updated_data, default=lambda o: o.__dict__, indent=2) - + update_button = gr.Button("Update JSON - Still need to copy to correct textbox to load") json_output = gr.Textbox(label="Updated JSON - Still need to copy to correct textbox to load", lines=10) #json_output = gr.Code(label="Updated JSON", lines=10) #Locks whole UI so use textbox update_button.click(update_json, inputs=outputs, outputs=json_output) - + return outputs + [update_button, json_output] #, json_output_code] + +def show_elements_with_state_sync(json_input, config_state, media_paths_list): + """ + Stateful version of show_elements_json_input that syncs edits back via gr.State. + + Args: + json_input: The JSON config string + config_state: gr.State to store edited config (for syncing back) + media_paths_list: List of available media paths to choose from + + Returns components that can update the config_state when edited. + """ + if not json_input: + gr.Markdown("No config loaded. Enter JSON config and click Load.") + return + + try: + data = json.loads(json_input) + except json.JSONDecodeError as e: + gr.Markdown(f"**JSON Error:** {str(e)}") + return + + # Determine structure type (masterlocation1 or direct location keys) + if 'masterlocation1' in data: + locations_data = data['masterlocation1'] + wrapper_key = 'masterlocation1' + else: + locations_data = data + wrapper_key = None + + outputs = [] + location_keys = [] + + # Create media dropdown choices from available paths + media_choices = media_paths_list if media_paths_list else [] + + for location, details in locations_data.items(): + if location == 'end': + continue + + location_keys.append(location) + desc_text = details.get('description', '')[:50] + '...' if len(details.get('description', '')) > 50 else details.get('description', '') + + with gr.Accordion(f"📍 {location}: {desc_text}", open=False): + description = gr.Textbox( + label="Description", + value=details.get('description', ''), + interactive=True, + lines=2 + ) + outputs.append(description) + + events = gr.Textbox( + label="Events (JSON array)", + value=json.dumps(details.get('events', [])), + interactive=True + ) + outputs.append(events) + + choices = gr.Textbox( + label="Choices (JSON array)", + value=json.dumps(details.get('choices', [])), + interactive=True + ) + outputs.append(choices) + + transitions = gr.Textbox( + label="Transitions (JSON object)", + value=json.dumps(details.get('transitions', {})), + interactive=True + ) + outputs.append(transitions) + + # Media field with dropdown for available paths + current_media = details.get('media', []) + with gr.Row(): + media = gr.Textbox( + label="Media (JSON array)", + value=json.dumps(current_media), + interactive=True, + scale=3 + ) + if media_choices: + media_dropdown = gr.Dropdown( + choices=media_choices, + label="Add Media Path", + scale=1, + interactive=True + ) + outputs.append(media) + + developernotes = gr.Textbox( + label="Developer Notes", + value=json.dumps(details.get('developernotes', [])) if isinstance(details.get('developernotes'), list) else details.get('developernotes', ''), + interactive=True + ) + outputs.append(developernotes) + + # Add end state display (read-only) + if 'end' in locations_data: + with gr.Accordion("🏁 End State", open=False): + gr.JSON(value=locations_data['end'], label="End State Config") + + num_fields = 6 # description, events, choices, transitions, media, developernotes + + def build_updated_json(*current_values): + """Rebuild JSON from all field values""" + updated_data = {} + + for i, location in enumerate(location_keys): + try: + updated_data[location] = { + "description": current_values[i * num_fields], + "events": json.loads(current_values[i * num_fields + 1]) if current_values[i * num_fields + 1] else [], + "choices": json.loads(current_values[i * num_fields + 2]) if current_values[i * num_fields + 2] else [], + "transitions": json.loads(current_values[i * num_fields + 3]) if current_values[i * num_fields + 3] else {}, + "media": json.loads(current_values[i * num_fields + 4]) if current_values[i * num_fields + 4] else [], + "developernotes": json.loads(current_values[i * num_fields + 5]) if current_values[i * num_fields + 5].startswith('[') else current_values[i * num_fields + 5] + } + except json.JSONDecodeError as e: + # If JSON parsing fails, keep as string + updated_data[location] = { + "description": current_values[i * num_fields], + "events": [], + "choices": [], + "transitions": {}, + "media": [], + "developernotes": f"JSON Parse Error: {e}" + } + + # Add back end state + if 'end' in locations_data: + updated_data['end'] = locations_data['end'] + + # Wrap if original had wrapper + if wrapper_key: + final_data = {wrapper_key: updated_data} + else: + final_data = updated_data + + return json.dumps(final_data, indent=2) + + gr.Markdown("---") + with gr.Row(): + sync_btn = gr.Button("🔄 Sync Edits to Config", variant="primary") + preview_btn = gr.Button("👁 Preview Changes") + + preview_output = gr.Textbox(label="Preview of Updated Config", lines=8, visible=False) + + # Preview shows changes without syncing + preview_btn.click( + fn=build_updated_json, + inputs=outputs, + outputs=preview_output + ).then( + fn=lambda: gr.update(visible=True), + outputs=preview_output + ) + + # Sync button returns the updated JSON to be used by parent + sync_btn.click( + fn=build_updated_json, + inputs=outputs, + outputs=preview_output # We'll handle the actual sync in app.py + ) + + return outputs, sync_btn, preview_output, build_updated_json + def show_elements_json_input_play_and_edit_version(json_input): - data = json.loads(json_input) + if not json_input: + return [] + try: + data = json.loads(json_input) + except json.JSONDecodeError: + return [] outputs = [] for location_name, location_data in data.items(): @@ -346,7 +530,7 @@ def convert_timeline_to_game_structure(timeline): def timeline_get_random_suggestions(num_lists, items_per_list, include_existing_games, include_multiplayer): """ Generate random suggestions from a specified number of lists. - + :param num_lists: Number of lists to consider :param items_per_list: Number of items to select from each list :param include_existing_games: Whether to include existing game inspiration lists @@ -358,13 +542,443 @@ def timeline_get_random_suggestions(num_lists, items_per_list, include_existing_ available_lists = [lst for lst in available_lists if lst not in existing_game_inspirations] if not include_multiplayer: available_lists = [lst for lst in available_lists if lst != multiplayer_features] - + selected_lists = random.sample(available_lists, min(num_lists, len(available_lists))) suggestions = [] selected_list_names = [] - + for lst in selected_lists: suggestions.extend(random.sample(lst, min(items_per_list, len(lst)))) selected_list_names.append(list_names[all_idea_lists.index(lst)]) - - return suggestions, selected_list_names \ No newline at end of file + + return suggestions, selected_list_names + + +# ==================== NARRATIVE TEMPLATES FOR CONFIG GENERATION ==================== + +NARRATIVE_TEMPLATES = { + "heros_journey": { + "name": "Hero's Journey", + "description": "Classic monomyth structure: ordinary world -> call to adventure -> trials -> transformation -> return", + "stages": [ + {"id": "ordinary_world", "name": "Ordinary World", "description_template": "You find yourself in {setting}. Life is {mood}, but something feels {tension}."}, + {"id": "call_to_adventure", "name": "Call to Adventure", "description_template": "A {catalyst} disrupts your routine. You learn about {goal}."}, + {"id": "refusal", "name": "Refusal of the Call", "description_template": "Doubts creep in. {obstacle} makes you hesitate."}, + {"id": "meeting_mentor", "name": "Meeting the Mentor", "description_template": "You encounter {mentor_type}, who offers {aid}."}, + {"id": "crossing_threshold", "name": "Crossing the Threshold", "description_template": "You leave {old_world} behind and enter {new_world}."}, + {"id": "tests_allies", "name": "Tests, Allies, Enemies", "description_template": "You face {challenge} and meet {ally_or_enemy}."}, + {"id": "approach", "name": "Approach to Inmost Cave", "description_template": "You prepare for the greatest challenge: {final_obstacle}."}, + {"id": "ordeal", "name": "The Ordeal", "description_template": "You confront {main_conflict}. Everything is at stake."}, + {"id": "reward", "name": "Reward", "description_template": "You achieve {victory}. {reward} is yours."}, + {"id": "road_back", "name": "The Road Back", "description_template": "Returning is harder than expected. {complication} arises."}, + {"id": "resurrection", "name": "Resurrection", "description_template": "One final test: {ultimate_challenge}."}, + {"id": "return_elixir", "name": "Return with the Elixir", "description_template": "You return transformed, bringing {gift} to {beneficiary}."} + ], + "ending_count": 3, + "branch_points": ["refusal", "tests_allies", "ordeal"] + }, + "mystery": { + "name": "Mystery/Detective", + "description": "Investigation structure: discovery -> clues -> suspects -> revelation -> resolution", + "stages": [ + {"id": "discovery", "name": "The Discovery", "description_template": "You discover {mystery}. Something isn't right."}, + {"id": "first_clue", "name": "First Clue", "description_template": "Investigating, you find {clue}. It points to {direction}."}, + {"id": "suspect_1", "name": "First Suspect", "description_template": "You meet {suspect}. They seem {demeanor}, but {suspicion}."}, + {"id": "red_herring", "name": "Red Herring", "description_template": "{misleading_evidence} throws you off track."}, + {"id": "key_witness", "name": "Key Witness", "description_template": "{witness} reveals crucial information about {revelation}."}, + {"id": "suspect_2", "name": "Second Suspect", "description_template": "New evidence points to {suspect2}. The plot thickens."}, + {"id": "breakthrough", "name": "Breakthrough", "description_template": "You realize {key_insight}. Everything connects."}, + {"id": "confrontation", "name": "Confrontation", "description_template": "You confront {culprit}. {tension_moment}."}, + {"id": "resolution", "name": "Resolution", "description_template": "The truth is revealed: {truth}. Justice is {outcome}."} + ], + "ending_count": 4, + "branch_points": ["first_clue", "suspect_1", "confrontation"] + }, + "heist": { + "name": "Heist/Mission", + "description": "Planning and execution: target -> team -> plan -> complications -> execution -> aftermath", + "stages": [ + {"id": "the_target", "name": "The Target", "description_template": "You learn about {target}. It's worth {stakes}."}, + {"id": "assemble_team", "name": "Assemble the Team", "description_template": "You need {specialist}. They're the best at {skill}."}, + {"id": "recon", "name": "Reconnaissance", "description_template": "You scout {location}. You notice {vulnerability} and {danger}."}, + {"id": "the_plan", "name": "The Plan", "description_template": "The plan: {approach}. It's risky, but {justification}."}, + {"id": "complication", "name": "Complication", "description_template": "{unexpected_problem} threatens everything."}, + {"id": "point_of_no_return", "name": "Point of No Return", "description_template": "You're in. No turning back. {tension}."}, + {"id": "execution", "name": "Execution", "description_template": "The plan unfolds. {action_sequence}."}, + {"id": "twist", "name": "The Twist", "description_template": "{betrayal_or_surprise}. Nothing is as it seemed."}, + {"id": "escape", "name": "The Escape", "description_template": "{escape_method}. Every second counts."}, + {"id": "aftermath", "name": "Aftermath", "description_template": "When the dust settles: {consequences}."} + ], + "ending_count": 4, + "branch_points": ["assemble_team", "complication", "execution", "twist"] + }, + "survival": { + "name": "Survival", + "description": "Resource management and choices: crisis -> shelter -> resources -> threats -> rescue/adaptation", + "stages": [ + {"id": "disaster", "name": "The Disaster", "description_template": "{catastrophe} strikes. You're stranded in {hostile_environment}."}, + {"id": "immediate_needs", "name": "Immediate Needs", "description_template": "You need {urgent_need}. Time is critical."}, + {"id": "find_shelter", "name": "Find Shelter", "description_template": "You spot {shelter_option}. It's {pros}, but {cons}."}, + {"id": "first_threat", "name": "First Threat", "description_template": "{environmental_danger} threatens your survival."}, + {"id": "resource_decision", "name": "Resource Decision", "description_template": "You find {resource}. Do you {option1} or {option2}?"}, + {"id": "other_survivors", "name": "Other Survivors", "description_template": "You're not alone. {survivor} appears. They're {condition}."}, + {"id": "major_crisis", "name": "Major Crisis", "description_template": "{crisis} forces a desperate choice."}, + {"id": "hope_signal", "name": "Signal of Hope", "description_template": "You see {hope}. Rescue might be possible."}, + {"id": "final_challenge", "name": "Final Challenge", "description_template": "One last obstacle: {final_obstacle}."}, + {"id": "resolution", "name": "Resolution", "description_template": "{ending_scenario}. You survived, but at what cost?"} + ], + "ending_count": 5, + "branch_points": ["find_shelter", "resource_decision", "other_survivors", "major_crisis"] + }, + "memory_fragments": { + "name": "Memory Fragments (Non-linear)", + "description": "Non-linear exploration of memories: awakening -> explore memories in any order -> piece together truth -> confront reality", + "stages": [ + {"id": "awakening", "name": "Awakening", "description_template": "You awaken, disoriented. Memories float just out of reach. You sense {memory_count} distinct moments trying to surface..."}, + {"id": "memory_hub", "name": "Memory Hub", "description_template": "Fragments swirl in your mind. Each one pulls at you: {memory_hints}."}, + {"id": "memory_1", "name": "Memory: {memory_1_theme}", "description_template": "{memory_1_scene}. The details are vivid but the context is missing."}, + {"id": "memory_2", "name": "Memory: {memory_2_theme}", "description_template": "{memory_2_scene}. This connects to something important."}, + {"id": "memory_3", "name": "Memory: {memory_3_theme}", "description_template": "{memory_3_scene}. A piece of the puzzle falls into place."}, + {"id": "memory_4", "name": "Memory: {memory_4_theme}", "description_template": "{memory_4_scene}. Now you understand."}, + {"id": "convergence", "name": "Convergence", "description_template": "The memories align. You remember: {truth}. It was {key_person} all along."}, + {"id": "reality", "name": "Return to Reality", "description_template": "Armed with the truth, you face {present_situation}. What will you do?"} + ], + "ending_count": 4, + "branch_points": ["memory_hub", "convergence", "reality"], + "special": "non_linear_memories" + }, + "romance": { + "name": "Romance/Relationship", + "description": "Relationship development: meeting -> attraction -> obstacles -> deepening -> resolution", + "stages": [ + {"id": "first_meeting", "name": "First Meeting", "description_template": "You meet {love_interest} at {location}. They're {first_impression}."}, + {"id": "initial_attraction", "name": "Initial Attraction", "description_template": "Something about them {attraction_detail}. You want to know more."}, + {"id": "getting_to_know", "name": "Getting to Know", "description_template": "You spend time together. You learn they {character_detail}."}, + {"id": "first_obstacle", "name": "First Obstacle", "description_template": "{misunderstanding_or_conflict}. Things get complicated."}, + {"id": "vulnerability", "name": "Moment of Vulnerability", "description_template": "They share {personal_revelation}. You see the real them."}, + {"id": "growing_closer", "name": "Growing Closer", "description_template": "{bonding_moment}. Something shifts between you."}, + {"id": "major_conflict", "name": "Major Conflict", "description_template": "{relationship_crisis}. Everything hangs in the balance."}, + {"id": "resolution", "name": "Resolution", "description_template": "{resolution_scene}. Your relationship becomes {relationship_outcome}."} + ], + "ending_count": 4, + "branch_points": ["first_obstacle", "vulnerability", "major_conflict"] + } +} + + +def get_narrative_templates_list(): + """Return list of available narrative templates for dropdown.""" + return [(NARRATIVE_TEMPLATES[key]["name"], key) for key in NARRATIVE_TEMPLATES] + + +def generate_config_from_template(template_key, theme="fantasy", num_endings=3): + """ + Generate a game config based on a narrative template. + + Args: + template_key: Key from NARRATIVE_TEMPLATES + theme: Theme to apply (fantasy, scifi, modern, horror, etc.) + num_endings: Number of different endings to generate + + Returns: + JSON config string + """ + if template_key not in NARRATIVE_TEMPLATES: + return json.dumps({"error": f"Unknown template: {template_key}"}) + + template = NARRATIVE_TEMPLATES[template_key] + config = {} + + # Theme-specific word banks + theme_words = { + "fantasy": { + "setting": ["a quiet village", "a bustling kingdom", "an ancient forest"], + "mentor_type": ["a wise wizard", "an old warrior", "a mysterious sage"], + "new_world": ["the dark lands", "the enchanted realm", "the forbidden territory"], + "reward": ["the sacred artifact", "ancient knowledge", "magical powers"], + }, + "scifi": { + "setting": ["a space station", "a colony ship", "a research facility"], + "mentor_type": ["an AI companion", "a veteran pilot", "a scientist"], + "new_world": ["uncharted space", "the alien sector", "the forbidden zone"], + "reward": ["alien technology", "crucial data", "the truth about humanity"], + }, + "modern": { + "setting": ["a small town", "a big city apartment", "a suburban home"], + "mentor_type": ["a experienced colleague", "an unlikely friend", "a family member"], + "new_world": ["the unknown", "a new city", "unfamiliar territory"], + "reward": ["self-discovery", "justice", "closure"], + }, + "horror": { + "setting": ["an isolated cabin", "an old mansion", "a small town with secrets"], + "mentor_type": ["a skeptical investigator", "a local with knowledge", "a survivor"], + "new_world": ["the nightmare realm", "the haunted grounds", "the darkness"], + "reward": ["survival", "the terrible truth", "a chance to escape"], + } + } + + words = theme_words.get(theme, theme_words["fantasy"]) + + # Generate states from template stages + for i, stage in enumerate(template["stages"]): + state_id = stage["id"] + + # Generate choices based on position in story + if i == len(template["stages"]) - 1: + # Final state - ending choices + choices = [] + transitions = {} + elif stage["id"] in template.get("branch_points", []): + # Branch point - multiple meaningful choices + choices = ["take the safe path", "take the risky path", "find another way"] + next_stage = template["stages"][i + 1]["id"] + transitions = { + "take the safe path": f"main_{next_stage}", + "take the risky path": f"main_{next_stage}", + "find another way": f"main_{next_stage}" + } + else: + # Linear progression + choices = ["continue"] + next_stage = template["stages"][i + 1]["id"] + transitions = {"continue": f"main_{next_stage}"} + + # Simple description (template placeholders would be filled by LLM in production) + description = stage["description_template"] + for key, options in words.items(): + placeholder = "{" + key + "}" + if placeholder in description: + description = description.replace(placeholder, random.choice(options)) + + # Clean remaining placeholders with generic text + import re + description = re.sub(r'\{[^}]+\}', '[something important]', description) + + config[f"main_{state_id}"] = { + "description": description, + "choices": choices, + "transitions": transitions, + "media": [], + "developernotes": [f"Stage: {stage['name']}", f"Template: {template['name']}"] + } + + # Add endings + for i in range(min(num_endings, template.get("ending_count", 3))): + ending_types = ["triumphant", "bittersweet", "tragic", "mysterious", "open"] + ending_type = ending_types[i % len(ending_types)] + + config[f"ending_{ending_type}"] = { + "description": f"[{ending_type.upper()} ENDING] Your journey concludes. The choices you made led here.", + "choices": [], + "transitions": {}, + "media": [], + "developernotes": [f"Ending type: {ending_type}"] + } + + # Update final stage to point to endings + final_stage_id = f"main_{template['stages'][-1]['id']}" + if final_stage_id in config: + config[final_stage_id]["choices"] = [f"ending {i+1}" for i in range(min(num_endings, 3))] + ending_types = ["triumphant", "bittersweet", "tragic"] + config[final_stage_id]["transitions"] = { + f"ending {i+1}": f"story_ending_{ending_types[i]}" + for i in range(min(num_endings, 3)) + } + + # Wrap in the expected nested structure: {"location": {"state": {...}}} + # The game engine expects location_state format, so we use "story" as location + wrapped_config = {"story": config} + + # Update all transitions to use story_ prefix + for state_name, state_data in config.items(): + if "transitions" in state_data: + new_transitions = {} + for choice, target in state_data["transitions"].items(): + # Add story_ prefix if not already present + if not target.startswith("story_"): + new_transitions[choice] = f"story_{target}" + else: + new_transitions[choice] = target + state_data["transitions"] = new_transitions + + return json.dumps(wrapped_config, indent=2) + + +def generate_config_from_prompt(prompt, structure_type="branching"): + """ + Generate a config structure from a natural language prompt. + This creates the skeleton - actual content should be filled by LLM. + + Args: + prompt: Natural language description of the game + structure_type: "linear", "branching", or "hub" + + Returns: + JSON config string with placeholder content + """ + # Extract key elements from prompt (simple keyword extraction) + prompt_lower = prompt.lower() + + # Detect approximate number of scenes + scene_indicators = ["scene", "chapter", "part", "act", "stage", "location", "area"] + num_scenes = 8 # default + for indicator in scene_indicators: + if indicator in prompt_lower: + # Look for numbers near the indicator + import re + matches = re.findall(rf'(\d+)\s*{indicator}', prompt_lower) + if matches: + num_scenes = int(matches[0]) + break + + # Detect number of endings + ending_match = re.search(r'(\d+)\s*ending', prompt_lower) + num_endings = int(ending_match.group(1)) if ending_match else 3 + + # Detect if it mentions specific themes + themes_detected = [] + theme_keywords = { + "mystery": ["mystery", "detective", "investigate", "clue", "solve"], + "horror": ["horror", "scary", "haunted", "dark", "terror"], + "romance": ["romance", "love", "relationship", "dating"], + "adventure": ["adventure", "quest", "journey", "explore"], + "survival": ["survival", "survive", "stranded", "resource"], + } + for theme, keywords in theme_keywords.items(): + if any(kw in prompt_lower for kw in keywords): + themes_detected.append(theme) + + config = {} + + # Generate structure based on type + if structure_type == "linear": + for i in range(num_scenes): + state_id = f"scene_{i+1}" + next_state = f"scene_{i+2}" if i < num_scenes - 1 else "ending_main" + + config[state_id] = { + "description": f"[SCENE {i+1}] {prompt[:50]}... - Add description here", + "choices": ["continue"] if i < num_scenes - 1 else [], + "transitions": {"continue": next_state} if i < num_scenes - 1 else {}, + "media": [], + "developernotes": [f"Scene {i+1} of {num_scenes}", f"Themes: {themes_detected}"] + } + + config["ending_main"] = { + "description": "[ENDING] The story concludes.", + "choices": [], + "transitions": {}, + "media": [] + } + + elif structure_type == "branching": + # Create a tree structure + config["start"] = { + "description": f"[START] {prompt[:100]}... - The beginning of your story", + "choices": ["path A", "path B"], + "transitions": {"path A": "branch_a_1", "path B": "branch_b_1"}, + "media": [], + "developernotes": ["Starting point", f"Prompt: {prompt[:50]}"] + } + + # Branch A + for i in range(num_scenes // 2): + state_id = f"branch_a_{i+1}" + next_state = f"branch_a_{i+2}" if i < (num_scenes // 2) - 1 else "ending_a" + + config[state_id] = { + "description": f"[PATH A - Scene {i+1}] Following the first path...", + "choices": ["continue"] if i < (num_scenes // 2) - 1 else [], + "transitions": {"continue": next_state} if i < (num_scenes // 2) - 1 else {}, + "media": [] + } + + # Branch B + for i in range(num_scenes // 2): + state_id = f"branch_b_{i+1}" + next_state = f"branch_b_{i+2}" if i < (num_scenes // 2) - 1 else "ending_b" + + config[state_id] = { + "description": f"[PATH B - Scene {i+1}] Following the second path...", + "choices": ["continue"] if i < (num_scenes // 2) - 1 else [], + "transitions": {"continue": next_state} if i < (num_scenes // 2) - 1 else {}, + "media": [] + } + + # Endings + config["ending_a"] = { + "description": "[ENDING A] One possible conclusion.", + "choices": [], + "transitions": {}, + "media": [] + } + config["ending_b"] = { + "description": "[ENDING B] Another possible conclusion.", + "choices": [], + "transitions": {}, + "media": [] + } + + elif structure_type == "hub": + # Hub and spoke structure (like MemoryFragments) + config["hub"] = { + "description": f"[HUB] {prompt[:100]}... - You can explore in any direction", + "choices": [f"explore area {i+1}" for i in range(min(4, num_scenes))], + "transitions": {f"explore area {i+1}": f"area_{i+1}" for i in range(min(4, num_scenes))}, + "media": [], + "developernotes": ["Central hub - player can explore areas in any order"] + } + + # Create areas + for i in range(min(4, num_scenes)): + config[f"area_{i+1}"] = { + "description": f"[AREA {i+1}] An explorable area with its own story...", + "choices": ["investigate further", "return to hub"], + "transitions": { + "investigate further": f"area_{i+1}_deep", + "return to hub": "hub" + }, + "media": [] + } + config[f"area_{i+1}_deep"] = { + "description": f"[AREA {i+1} - DEEP] You discover something important here...", + "choices": ["return to hub", "go to finale"], + "transitions": { + "return to hub": "hub", + "go to finale": "finale" + }, + "media": [] + } + + config["finale"] = { + "description": "[FINALE] With everything discovered, the truth becomes clear...", + "choices": [f"ending {i+1}" for i in range(num_endings)], + "transitions": {f"ending {i+1}": f"ending_{i+1}" for i in range(num_endings)}, + "media": [] + } + + for i in range(num_endings): + config[f"ending_{i+1}"] = { + "description": f"[ENDING {i+1}] One of {num_endings} possible conclusions.", + "choices": [], + "transitions": {}, + "media": [] + } + + # Wrap in the expected nested structure: {"location": {"state": {...}}} + # The game engine expects location_state format, so we use "game" as location + wrapped_config = {"game": config} + + # Update all transitions to use game_ prefix + for state_name, state_data in config.items(): + if "transitions" in state_data: + new_transitions = {} + for choice, target in state_data["transitions"].items(): + # Add game_ prefix if not already present + if not target.startswith("game_"): + new_transitions[choice] = f"game_{target}" + else: + new_transitions[choice] = target + state_data["transitions"] = new_transitions + + return json.dumps(wrapped_config, indent=2) \ No newline at end of file diff --git a/timeline_and_features.py b/timeline_and_features.py new file mode 100644 index 0000000000000000000000000000000000000000..55afcf7bfbae1b5beb476b46cdb6c90ff7b1e155 --- /dev/null +++ b/timeline_and_features.py @@ -0,0 +1,543 @@ +""" +Timeline features, game mechanics inspirations, and mermaid story structures +""" + +timeline_components = [ + "Story progression tracker", + "Inventory management system", + "Character stat system", + "Quest log", + "Achievement system", + "NPC relationship tracker", + "Time and weather simulation", + "Economy and trading system", + "Crafting system", + "Skill tree", + "Dialogue manager", + "Combat system", + "Save/Load system", + "Random event generator", + "Faction reputation system" +] + +# Decisions (and context explanation) +timeline_decision_features = [ + "Moral alignment system (good/neutral/evil)", + "Consequence ripple effect (choices affect multiple future events)", + "Timed decisions (limited time to choose)", + "Hidden choices (unlocked by specific conditions)", + "Context-sensitive options (choices change based on character stats/items)", + "Dialogue trees with branching paths", + "Risk/reward choices (high risk, high reward options)", + "Character personality development through choices", + "Relationship-altering decisions", + "Story-altering key decisions", + "Multi-step decision processes", + "Decision journals (recap of major choices)", + "Faction-based decisions", + "Profession or class-specific choices", + "Ethical dilemmas with no clear 'right' answer" +] + +# Nested Sections +timeline_nested_section_ideas = [ + "Multilevel dungeons", + "City districts with sub-locations", + "Planetary system with multiple explorable planets", + "Dreamworld with nested dreams", + "Time periods (past, present, future) with sub-events", + "Parallel universes with alternate versions of locations", + "Miniature worlds inside objects", + "Body systems (for a medical game or body-exploration adventure)", + "Nested memories or flashbacks", + "Virtual reality levels within the game world", + "Dimensional rifts with sub-dimensions", + "Organizational hierarchies (e.g., crime family structure)", + "Ecosystem layers (e.g., forest canopy, understory, forest floor)", + "Nested puzzles (solving one unlocks another)", + "Matryoshka doll-like structures with hidden secrets" +] + +# Media (Especially to affect decisions) +timeline_media_integration = [ + "Dynamic background music changing with mood/tension", + "Character portrait changes reflecting emotional states", + "Environmental sound effects influencing player choices", + "Visual cues (color schemes, lighting) hinting at outcomes", + "Cinematic cutscenes revealing crucial information", + "Interactive maps updating with player discoveries", + "Voice acting with tone influencing trustworthiness", + "Symbolic imagery foreshadowing consequences", + "Augmented reality overlays providing decision hints", + "Character theme motifs indicating importance of NPCs", + "Weather effects reflecting story mood", + "Dream sequences visualizing potential outcomes", + "Time-lapse visuals showing long-term effects of choices", + "Split-screen effects for simultaneous events", + "Flashback scenes providing context for decisions" +] + +# Replayability (GTA and Tekken type mechanics in text form) +timeline_replayability_features = [ + "Multiple character origins with unique storylines", + "Randomized side quests and events", + "Unlockable skills and abilities for subsequent playthroughs", + "New Game+ mode with increased difficulty and rewards", + "Alternative routes to objectives", + "Hidden endings based on specific choices or achievements", + "Character customization affecting story and gameplay", + "Faction system with different allegiances each playthrough", + "Time-based events that change on each playthrough", + "Roguelike elements (permadeath, procedural generation)", + "Challenge modes (speed runs, minimalist runs)", + "Branching skill trees with mutually exclusive paths", + "Collectibles that unlock new content or lore", + "Dynamic world events that evolve differently each time", + "Secret characters or modes to unlock" +] + +# Theme integration (Modified Variables that affect UI or config order) +timeline_theme_integration = [ + "Dynamic UI color schemes based on location or story progress", + "Font styles changing with different eras or dimensions", + "Adaptive music system reflecting current theme", + "UI element shapes and designs matching the current environment", + "Loading screen tips and lore adapted to current theme", + "Cursor or pointer design changing with themes", + "Theme-specific animations for transitions between scenes", + "Variable text descriptors (e.g., 'cold' vs 'scorching' based on environment)", + "Themed icons for inventory and skills", + "Background textures or patterns reflecting current setting", + "Character portrait frames styled to match the theme", + "Theme-specific sound effects for UI interactions", + "Adaptive difficulty adjustments based on chosen theme", + "Mini-map or navigation elements styled to fit the theme", + "Theme-specific filters or overlays for the game view" +] + +# GTA Heists - Same Map with overlapping branching narratives +gta_heist_inspired = [ + "Multiple missions/quests available in the same location", + "Choices in one quest affect options in others", + "Time-sensitive missions that change the environment", + "Characters that appear in multiple storylines with different roles", + "Hidden connections between seemingly unrelated quests", + "Replay value through different approaches to the same heist", + "Dynamic difficulty based on previous mission outcomes", + "Unlockable areas within the same map as story progresses", + "Consequences of actions affecting the game world and future missions", + "Team assembly mechanics for different skill combinations", + "Planning phase before execution of major quests", + "Risk/reward choices for mission approach (stealth vs. aggressive)", + "Easter eggs and secrets hidden within the familiar environment", + "Alternate endings based on cumulative choices across multiple quests", + "NPC relationships affected by actions in different storylines" +] + +# Battlefront - Elites amongst Commoners +battlefront_inspired = [ + "Player can switch between 'grunt' and 'hero' modes", + "Special abilities or items only available to 'elite' characters", + "Earning 'elite' status through exemplary performance", + "Time-limited elite transformations", + "Missions where player commands both elites and regular units", + "Elites with unique storylines and dialogue options", + "Risk of losing elite status through poor performance", + "Elite characters influencing morale of common units", + "Unlockable elite characters with diverse abilities", + "Missions requiring coordination between elites and common units", + "Elite vs Elite showdowns as climactic events", + "Common units gaining experience to become elites over time", + "Elites having a larger impact on the story and world events", + "Special elite-only areas or quests", + "Dynamic difficulty adjustment when playing as elite characters" +] + +# Tekken Casino (one mistake = 1/2 or 1/3 of your Resources) +tekken_casino_inspired = [ + "High-stakes dialogue options with major resource loss for wrong choices", + "Timed decision-making with harsh penalties for indecision", + "Mini-games where a single mistake can cost significant resources", + "Opportunities to 'double or nothing' on quest rewards", + "Character skills affecting the odds of success in risky situations", + "Resource management system with high-risk, high-reward options", + "Combo system for dialogue or actions, breaking combo loses resources", + "Unlockable 'retry' options at the cost of resources", + "Progressive difficulty increase, with higher stakes for each level", + "Special 'all-in' options for desperate situations", + "Random events that can dramatically increase or decrease resources", + "Skill-based challenges with variable resource rewards/penalties", + "NPC interactions where trust is a resource that can be rapidly lost", + "Timed exploration with resource drain for inefficiency", + "Boss encounters with escalating stakes for each phase" +] + +# Turn-based: 'Tactics' type nintendo games +tactics_inspired = [ + "Grid-based movement and action system", + "Unit positioning affecting attack effectiveness and vulnerability", + "Diverse character classes with unique abilities and weaknesses", + "Terrain effects on movement and combat", + "Action points system for balancing multiple actions per turn", + "Status effects persisting across multiple turns", + "Synergy bonuses for complementary unit combinations", + "Permadeath or severe consequences for unit loss", + "Weapon triangle or elemental weakness system", + "Experience and leveling system for units", + "Limited resource management (e.g., mana, ammo) across battles", + "Multi-stage battles with changing objectives", + "Recruitment system for expanding team roster", + "Weather or time-of-day effects on battlefield conditions", + "Morale system affecting unit performance" +] + +# Chess (and any other tile-based game) +chess_inspired = [ + "Different 'pieces' with unique movement and action patterns", + "Objective-based victory conditions (e.g., capturing the 'king')", + "Limited 'board' space creating positioning challenges", + "Sacrificial strategies for long-term advantage", + "Opening, mid-game, and end-game phases with different strategies", + "Promotion system for 'pieces' reaching certain positions", + "Check and checkmate concepts for imminent defeat conditions", + "En passant-like special moves under specific circumstances", + "Castling-inspired defensive or positioning special actions", + "Pawn-like expendable units with potential for growth", + "Stalemate possibilities leading to draw outcomes", + "Time control mechanics for decision-making", + "Algebraic notation for recording and replaying game states", + "Handicap system for balancing player skill differences", + "Puzzle-like scenarios with optimal solution paths" +] + +# Cyberpunk RED (adapting tabletop RPG rules for text-based gameplay) +cyberpunk_red_inspired = [ + "Skill-based character creation with points allocation", + "Netrunning mechanics for hacking and virtual world exploration", + "Cybernetic enhancements with both benefits and drawbacks", + "Social status and reputation systems affecting interactions", + "Complex crafting system for weapons and gadgets", + "Trauma Team-like emergency services as game mechanic", + "Corporation-controlled zones with varying rule sets", + "Lifepath system affecting character background and abilities", + "Street slang glossary influencing dialogue options", + "Critical injury system with lasting effects", + "Improvised weapon creation from environment", + "Data mining and information brokering as key gameplay elements", + "Cyberpsychosis risk for excessive augmentation", + "Night City-inspired district exploration with distinct subcultures", + "Fixer-based job acquisition and reputation building" +] + +# Community playthrough - Tally of players' choices +community_playthrough = [ + "Global choice statistics visible to all players", + "Weekly community challenges based on least-chosen options", + "Adaptive storytelling influenced by majority choices", + "Community-unlocked content when certain choice thresholds are met", + "Leaderboards for most unique choice combinations", + "Time-limited voting on major story decisions", + "Character popularity contests affecting NPC prominence", + "Community-driven side quest creation", + "Collaborative puzzle solving with choice aggregation", + "Dynamic difficulty adjustment based on community performance", + "Seasonal events with community-wide consequences", + "Faction wars where player choices determine outcomes", + "Community achievements unlocking special game modes", + "Player choice heat maps for game areas", + "Periodic resets allowing community to reshape the game world" +] + +# Random item placed in a random location - first person to get it wins +random_item_hunt = [ + "Daily randomized legendary item hunts", + "Clue system hinting at item location based on player actions", + "Item location narrowing over time to increase tension", + "Multiple item tiers with varying rarity and win conditions", + "Limited-time windows for item discovery", + "Collaborative and competitive modes for item hunting", + "Randomized item powers affecting gameplay upon discovery", + "Cursed items with negative effects to add risk", + "Community-created hiding spots for items", + "Item hunt leaderboards with speed and efficiency metrics", + "Seasonal themed items with unique properties", + "Hint sharing system among players", + "Progressive clue unlocks for players lagging behind", + "Randomized environmental hazards during item hunts", + "Special game modes unlocked by finding rare items" +] + +# Survival by location or characters met +survival_mechanics = [ + "Location-based resource scarcity affecting survival strategies", + "Character relationships influencing survival odds", + "Dynamic weather system impacting survival difficulty", + "Randomized disease outbreaks requiring specific remedies", + "Crafting system adapted to location-specific materials", + "NPC trust system affecting trade and assistance availability", + "Time-based challenges for reaching safe locations", + "Unique survival skills unlocked by meeting specific characters", + "Randomized events testing different survival aspects", + "Permadeath mode with character legacy system", + "Base building mechanics adapted to different environments", + "Seasonal changes affecting survival strategies", + "Character-specific weaknesses requiring team-based survival", + "Morale system influenced by character interactions and environment", + "Scavenging missions with risk/reward balancing" +] + +# Additional community and randomization features +bonus_features = [ + "Player-created content voting and integration system", + "Randomized daily quests with community-wide rewards", + "Collaborative world-building events", + "Dynamic economy based on player actions and random events", + "Community-driven storyline branching", + "Randomized character backstories affecting gameplay", + "Player-run factions with territory control mechanics", + "Periodic world resets with randomized layouts", + "Community challenges requiring diverse skill combinations", + "Randomized NPC behavior and dialogue options", + "Player-created lore integration system", + "Dynamic difficulty scaling based on community skill level", + "Randomized ethical dilemmas with community-wide consequences", + "Collaborative raid events against procedurally generated dungeons", + "Community-driven development roadmap voting system" +] + +multiplayer_features = [ + "Real-time cooperative puzzle solving", + "Competitive leaderboards for speed runs or high scores", + "Shared world events affecting all players simultaneously", + "Player-vs-player mini-games or challenges", + "Guild or clan system for group activities", + "Trading system for in-game items or resources", + "Multiplayer-exclusive quests or storylines", + "Dynamic difficulty scaling based on group size and composition", + "Shared base-building or territory control mechanics", + "Cross-player skill or ability synergies" +] + +# Categorized lists +existing_game_inspirations = [ + gta_heist_inspired, + battlefront_inspired, + tekken_casino_inspired, + tactics_inspired, + chess_inspired, + cyberpunk_red_inspired +] + +feature_lists = [ + community_playthrough, + random_item_hunt, + survival_mechanics, + bonus_features +] + +# All lists combined +all_idea_lists = existing_game_inspirations + feature_lists + [multiplayer_features] + +# Create a list of all the idea lists for easy import and use +all_idea_lists = [ + gta_heist_inspired, + battlefront_inspired, + tekken_casino_inspired, + tactics_inspired, + chess_inspired, + cyberpunk_red_inspired, + community_playthrough, + random_item_hunt, + survival_mechanics, + bonus_features +] + +# List names for reference +list_names = [ + "GTA Heists observations", "Battlefront observations", "Tekken 'Casino' observations", "Tactics Games observations", "Chess observations", + "Cyberpunk RED observations", "Community Playthrough observations", "Random Item Hunt observations", + "Survival Mechanics observations", "Bonus Features observations", "Multiplayer Features observations" +] + +#-------------------------#-------------------------#-------------------------#------------------------- + +mermaidstorystructures = { + "Hidden Progression 1": + """graph TD + subgraph "Actual Timeline" + A1[Location 1 - First Visit] + A2[Location 2 - First Visit] + A3[Location 3 - First Visit] + A4[Location 4 - First Visit] + A1 --> A2 --> A3 --> A4 + end + subgraph "Narrative Timeline" + N1[Location 2 - Story Event 1] + N2[Location 4 - Story Event 2] + N3[Location 1 - Story Event 3] + N4[Location 3 - Story Event 4] + N1 --> N2 --> N3 --> N4 + end + subgraph "Hidden Timeline" + H1[Hidden Event 1] + H2[Hidden Event 2] + H3[Hidden Event 3] + H4[Hidden Event 4] + end + A1 -.-> N3 + A2 -.-> N1 + A3 -.-> N4 + A4 -.-> N2 + H1 -.- A1 + H1 -.- A2 + H1 -.- A3 + H1 -.- A4 + H2 -.- A1 + H2 -.- A2 + H2 -.- A3 + H2 -.- A4 + H3 -.- A1 + H3 -.- A2 + H3 -.- A3 + H3 -.- A4 + H4 -.- A1 + H4 -.- A2 + H4 -.- A3 + H4 -.- A4""", + + "Hidden Progression 2": + """graph LR + subgraph "Actual Timeline" + A1[Location 1 - First Visit] + A2[Location 2 - First Visit] + A3[Location 3 - First Visit] + A4[Location 4 - First Visit] + A1 --> A2 --> A3 --> A4 + end + subgraph "Narrative Timeline" + N1[Location 2 - Story Event 1] + N2[Location 4 - Story Event 2] + N3[Location 1 - Story Event 3] + N4[Location 3 - Story Event 4] + N1 --> N2 --> N3 --> N4 + end + subgraph "Hidden Timeline" + H1[Hidden Event 1] + H2[Hidden Event 2] + H3[Hidden Event 3] + H4[Hidden Event 4] + H1 --> H2 --> H3 --> H4 + end + A1 -.-> N3 + A2 -.-> N1 + A3 -.-> N4 + A4 -.-> N2 + H1 -.- N1 + H2 -.- N2 + H3 -.- N3 + H4 -.- N4""", + + "Hidden Progression 3": + """graph LR + subgraph "Actual Timeline" + A1[Location 1 - First Visit] + A2[Location 2 - First Visit] + A3[Location 3 - First Visit] + A4[Location 4 - First Visit] + A1 --> A2 --> A3 --> A4 + end + subgraph "Narrative Timeline" + N1[Location 2 - Story Event 1] + N2[Location 4 - Story Event 2] + N3[Location 1 - Story Event 3] + N4[Location 3 - Story Event 4] + N1 --> N2 --> N3 --> N4 + end + A1 -.-> N3 + A2 -.-> N1 + A3 -.-> N4 + A4 -.-> N2""", + + "Parallel Storylines Progression": + """graph TD + A[Start] --> B[Character 1 Introduction] + A --> C[Character 2 Introduction] + B --> D[Character 1 Event 1] + C --> E[Character 2 Event 1] + D --> F[Character 1 Event 2] + E --> G[Character 2 Event 2] + F --> H[Characters Meet] + G --> H + H --> I[Climax] + I --> J[Resolution] + """, + } + +examplemermaidconceptblendingstrutures = { + "Star Wars Bakery Example": + """flowchart TD + + A[Bakery] + B[Star Wars] + C[Adjectives] + D[Combinatorics] + E[Seed Ideas] + F[LLM Processing] + + A -- has attributes --> C + B -- has attributes --> C + C -- generates --> D + D -- creates --> E + E -- inputs to --> F + + subgraph Bakery Attributes + direction TB + A1[Sweet] + A2[Warm] + A3[Comforting] + A4[Artisanal] + A -- includes --> A1 + A -- includes --> A2 + A -- includes --> A3 + A -- includes --> A4 + end + + subgraph Star Wars Attributes + direction TB + B1[Galactic] + B2[Epic] + B3[Heroic] + B4[Dramatic] + B -- includes --> B1 + B -- includes --> B2 + B -- includes --> B3 + B -- includes --> B4 + end + + subgraph Combinatorics + direction TB + C1[Sweet Galactic] + C2[Warm Epic] + C3[Comforting Heroic] + C4[Artisanal Dramatic] + D -- generates --> C1 + D -- generates --> C2 + D -- generates --> C3 + D -- generates --> C4 + end + + subgraph Seed Ideas + direction TB + E1[Galactic Pastries] + E2[Epic Bread Battles] + E3[Heroic Baker's Quest] + E4[Dramatic Cake Decor] + E -- includes --> E1 + E -- includes --> E2 + E -- includes --> E3 + E -- includes --> E4 + end""" +} + +#-------------------------#-------------------------#-------------------------#------------------------- + diff --git a/timeline_narrative_construction_tools.py b/timeline_narrative_construction_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..e83315163fd93b526ca46432c593c8ded7986585 --- /dev/null +++ b/timeline_narrative_construction_tools.py @@ -0,0 +1,261 @@ +import gradio as gr +import json +from collections import defaultdict +import random + +######################################## +# ORIGINAL TIMELINE LOGIC (from before) +######################################## + +def tnct_plan_timeline( + endings, + modifications, + locations, + characters, + interpersonal_events, + third_party_roles, + num_steps=5, + pair_size=2 +): + relationships = defaultdict(int) # frozenset({char1, char2}) -> score + timeline = [] # list of step dictionaries + + chosen_modifications = [random.choice(modifications) for _ in range(num_steps)] + + for step_index in range(num_steps): + location = random.choice(locations) + chosen_chars = random.sample(characters, min(pair_size, len(characters))) + event_description, rel_change = random.choice(interpersonal_events) + witness_role = random.choice(third_party_roles) + + if len(chosen_chars) > 1: + for i in range(len(chosen_chars)): + for j in range(i + 1, len(chosen_chars)): + pair_key = frozenset({chosen_chars[i], chosen_chars[j]}) + relationships[pair_key] += rel_change + + narrative = tnct_build_narrative( + step_number=step_index + 1, + location=location, + characters=chosen_chars, + event_description=event_description, + modification=chosen_modifications[step_index], + witness=witness_role + ) + + step_info = { + "step_number": step_index + 1, + "location": location, + "characters": chosen_chars, + "interpersonal_event": event_description, + "relationship_change": rel_change, + "modification_to_move_forward": chosen_modifications[step_index], + "third_party_role": witness_role, + "narrative_text": narrative + } + timeline.append(step_info) + + chosen_ending = tnct_pick_ending(endings, relationships) + + relationships_str_keys = { + "|".join(sorted(list(pair_key))): rel_value + for pair_key, rel_value in relationships.items() + } + + final_output = { + "steps": timeline, + "relationships": relationships_str_keys, + "chosen_ending": chosen_ending + } + return final_output + +def tnct_build_narrative(step_number, location, characters, event_description, modification, witness): + if len(characters) == 1: + char_text = characters[0] + else: + char_text = ", ".join(characters[:-1]) + " and " + characters[-1] + + return ( + f"Step {step_number}: In {location}, {char_text} experience the following: " + f"{event_description}. Observing from the sidelines: {witness}. " + f"They then proceed to: {modification}." + ) + +def tnct_pick_ending(endings, relationships): + total_relationship = sum(relationships.values()) + sorted_endings = sorted(endings.items(), key=lambda x: x[1], reverse=True) + + for ending_text, threshold in sorted_endings: + if total_relationship >= threshold: + return ending_text + return "No suitable ending found." + +def tnct_build_character_timelines(steps, all_characters): + character_timelines = {c: [] for c in all_characters} + for step in steps: + for c in step["characters"]: + if c in character_timelines: + character_timelines[c].append(step) + return character_timelines + +######################################## +# DATA DEFINITIONS (expanded) +######################################## + +tnct_endings = { + "A Heroic Triumph Over Impossible Odds": 6, + "Everyone Rides Off into the Sunset": 4, + "A New Era Dawns with Cautious Hope": 2, + "A Tense but Hopeful Departure": 1, + "They Part Ways, Uncertain of the Future": -1, + "A Shocking Betrayal Divides the Group": -3, + "Tragic Sacrifice Concludes the Tale": -5, + "The Darkness Claims All": -10 +} + +tnct_modifications = [ + "Acquire a powerful ancient artifact", + "Earn the trust of a local guide", + "Seek aid from a hidden community", + "Resolve a longstanding family feud", + "Escape a dangerous situation unscathed", + "Secure safe passage across treacherous terrain", + "Recover from a crippling setback", + "Forge a secret pact with a dubious ally" +] + +tnct_locations = [ + "A Remote Mountain Village", + "The Bustling Market of an Opulent City", + "The Deserted Ruins Overgrown with Vines", + "The Secret Hideout Beneath a Ruined Fort", + "The Royal Palace Throne Room", + "A Haunted Marsh Shrouded in Fog", + "A Hidden Cove Where Pirates Gather", + "The Fiery Caverns Beneath a Sleeping Volcano" +] + +tnct_interpersonal_events = [ + ("A heated argument that nearly comes to blows", -2), + ("A surprising confession that changes everything", +1), + ("They reconcile after discovering a shared purpose", +2), + ("A betrayal is uncovered, sowing seeds of doubt", -3), + ("An unexpected romance begins, altering loyalties", +2), + ("They forge a temporary alliance for mutual gain", +1), + ("They part on bitter terms, trust erodes further", -2), + ("A triumphant moment of camaraderie unites them", +3) +] + +tnct_third_party_roles = [ + "A wandering bard with a keen eye for drama", + "An inquisitive guard, always seeking the truth", + "A local gossip eager to spread rumors", + "A mysterious traveler who speaks in riddles", + "A curious child fascinated by adventurers", + "A wandering merchant hawking rare wares", + "A beggar listening quietly in the shadows", + "A masked stranger who remains silent" +] + +# Character lists +tnct_short_characters = ["Nyx", "Orin", "Tali", "Voss"] +tnct_medium_characters = ["Alice", "Bob", "Carol", "Dave", "Eve", "Mallory", "Trudy", "Peggy"] +tnct_long_characters = [ + "Aria", "Bran", "Cassian", "Diora", "Eamon", + "Fiora", "Galen", "Helena", "Iris", "Jarek", + "Kora", "Lysander", "Mirella", "Niko", "Ophelia", + "Percival", "Quincy", "Rowan", "Soraya", "Tavian" +] + +######################################## +# GRADIO INTERFACE +######################################## + +# 1) We’ll create a function that Gradio will call +def tnct_generate_story(character_list_choice, num_steps, pair_size): + """ + Generates the story timeline (JSON) and also returns the per-character + sub-timelines for display. + """ + + # Decide which character list to use + if character_list_choice == "Short (4 chars)": + chosen_characters = tnct_short_characters + elif character_list_choice == "Medium (8 chars)": + chosen_characters = tnct_medium_characters + else: # "Long (20 chars)" + chosen_characters = tnct_long_characters + + # Create the timeline + result = tnct_plan_timeline( + endings=tnct_endings, + modifications=tnct_modifications, + locations=tnct_locations, + characters=chosen_characters, + interpersonal_events=tnct_interpersonal_events, + third_party_roles=tnct_third_party_roles, + num_steps=num_steps, + pair_size=pair_size + ) + + # Convert entire timeline to JSON (for display in a text box) + timeline_json = json.dumps(result, indent=2) + + # Build per-character timelines + per_char = tnct_build_character_timelines(result["steps"], chosen_characters) + + # Format the per-character timelines into a string for display + # (Alternatively, we could return a dict or separate tabs in Gradio.) + character_timeline_text = [] + for char_name, steps_for_char in per_char.items(): + character_timeline_text.append(f"--- {char_name} ---") + for s in steps_for_char: + character_timeline_text.append(f"Step {s['step_number']}: {s['narrative_text']}") + character_timeline_text.append("") # Blank line + + per_character_output = "\n".join(character_timeline_text) + + return timeline_json, per_character_output + + +# 2) Build Gradio Blocks (or Interface) +def tnct_ui(): + with gr.Tab("Timeline Generator") as tnct_demo: + gr.Markdown("# Dynamic Story Timeline Generator") + gr.Markdown( + "Select a character list size, choose how many steps, " + "and how many characters to include in each step, then click **Generate**." + ) + + with gr.Row(): + character_list_input = gr.Dropdown( + label="Character List Size", + choices=["Short (4 chars)", "Medium (8 chars)", "Long (20 chars)"], + value="Short (4 chars)" + ) + num_steps_input = gr.Slider( + label="Number of Steps", minimum=1, maximum=10, step=1, value=5 + ) + pair_size_input = gr.Slider( + label="Pair Size", minimum=1, maximum=5, step=1, value=2 + ) + + generate_button = gr.Button("Generate") + + # Outputs + timeline_json_output = gr.Textbox( + label="Full Timeline JSON", + lines=15 + ) + per_character_output = gr.Textbox( + label="Per-Character Timelines", + lines=15 + ) + + generate_button.click( + fn=tnct_generate_story, + inputs=[character_list_input, num_steps_input, pair_size_input], + outputs=[timeline_json_output, per_character_output] + ) + + return tnct_demo \ No newline at end of file diff --git a/timeline_scenarios.py b/timeline_scenarios.py new file mode 100644 index 0000000000000000000000000000000000000000..f294ac6622bae229469f402490bed540ea6b3c6d --- /dev/null +++ b/timeline_scenarios.py @@ -0,0 +1,180 @@ +"""Timeline/Chronological scenario data and sequence generation. + +This module provides: +- TIMELINE_EVENTS: Dictionary of event templates by era/period +- generate_timeline_sequence: Function to generate chronological sequences +""" + +import json +import random + +# Event templates organized by time period/era +TIMELINE_EVENTS = { + "morning": [ + ("Dawn Awakening", "The first light of day brings new possibilities", ["hope", "beginning"]), + ("Morning Routine", "Familiar rituals ground you before the day's challenges", ["comfort", "preparation"]), + ("Breakfast Meeting", "Important conversations happen over the morning meal", ["social", "planning"]), + ("Early Discovery", "Something unexpected reveals itself in the quiet hours", ["mystery", "opportunity"]), + ("Departure", "The journey begins as the sun climbs higher", ["transition", "adventure"]), + ], + "midday": [ + ("Peak Activity", "The day reaches its most intense and busy period", ["action", "pressure"]), + ("Crucial Decision", "A choice must be made before time runs out", ["tension", "choice"]), + ("Unexpected Encounter", "Someone or something appears without warning", ["surprise", "challenge"]), + ("Revelation", "Truth comes to light in the brightness of noon", ["discovery", "clarity"]), + ("Conflict Erupts", "Tensions that have been building finally explode", ["confrontation", "drama"]), + ], + "afternoon": [ + ("Waning Energy", "The initial rush fades as reality sets in", ["reflection", "doubt"]), + ("Second Attempt", "Failure leads to a new approach", ["persistence", "growth"]), + ("Alliance Forms", "Shared goals bring unlikely partners together", ["cooperation", "trust"]), + ("Pursuit", "The chase continues with time slipping away", ["urgency", "determination"]), + ("Hidden Truth", "Shadows grow longer, concealing secrets", ["mystery", "danger"]), + ], + "evening": [ + ("Sunset Reckoning", "As day ends, accounts must be settled", ["consequence", "judgment"]), + ("Gathering Storm", "Signs of trouble appear on the horizon", ["foreboding", "preparation"]), + ("Final Preparation", "Last chance to ready yourself for what comes", ["anticipation", "strategy"]), + ("Emotional Peak", "Feelings long suppressed demand expression", ["catharsis", "vulnerability"]), + ("Quiet Before Storm", "An uneasy calm precedes the main event", ["tension", "atmosphere"]), + ], + "night": [ + ("Darkness Falls", "The world transforms under cover of night", ["mystery", "danger"]), + ("Midnight Confrontation", "The darkest hour brings the greatest challenge", ["climax", "courage"]), + ("Secret Meeting", "Hidden dealings occur when others sleep", ["intrigue", "risk"]), + ("Dream Sequence", "Reality blurs as the subconscious takes over", ["surreal", "insight"]), + ("Night Watch", "Vigilance through the long dark hours", ["endurance", "loyalty"]), + ], + "past": [ + ("Childhood Memory", "A formative moment from long ago resurfaces", ["nostalgia", "origin"]), + ("The Mistake", "A past error continues to haunt the present", ["regret", "consequence"]), + ("Lost Love", "Remembrance of someone no longer present", ["emotion", "motivation"]), + ("Glory Days", "When everything seemed possible and victories came easy", ["contrast", "longing"]), + ("The Promise", "A vow made long ago that still binds", ["obligation", "honor"]), + ], + "future": [ + ("Premonition", "A glimpse of what may come to pass", ["warning", "possibility"]), + ("Hope Rekindled", "A vision of better times ahead", ["motivation", "determination"]), + ("Inevitable End", "The destination that cannot be avoided", ["acceptance", "preparation"]), + ("Fork in the Road", "Multiple futures branch from this moment", ["choice", "consequence"]), + ("Legacy", "What will remain when you are gone", ["meaning", "purpose"]), + ], +} + +# Time transitions +TIME_TRANSITIONS = { + "morning": ["As morning progresses...", "The sun climbs higher as..."], + "midday": ["At the height of day...", "When the sun stands highest..."], + "afternoon": ["As shadows lengthen...", "The day begins to fade as..."], + "evening": ["As twilight approaches...", "The day draws to a close when..."], + "night": ["Under cover of darkness...", "When stars emerge..."], + "past": ["Memory surfaces of...", "Long ago, there was..."], + "future": ["What lies ahead is...", "The future holds..."], +} + + +def generate_timeline_sequence(event_count, include_morning, include_midday, include_afternoon, + include_evening, include_night, include_past, include_future, + chronological_order=True): + """Generate a timeline/chronological sequence. + + Args: + event_count: Number of events (3-12) + include_*: Include specific time periods + chronological_order: Arrange events in time order + + Returns: + Tuple of (list_output, json_output, prompts_output) + """ + available = [] + period_order = ["past", "morning", "midday", "afternoon", "evening", "night", "future"] + + if include_morning: + available.extend([(e[0], e[1], "morning", e[2]) for e in TIMELINE_EVENTS["morning"]]) + if include_midday: + available.extend([(e[0], e[1], "midday", e[2]) for e in TIMELINE_EVENTS["midday"]]) + if include_afternoon: + available.extend([(e[0], e[1], "afternoon", e[2]) for e in TIMELINE_EVENTS["afternoon"]]) + if include_evening: + available.extend([(e[0], e[1], "evening", e[2]) for e in TIMELINE_EVENTS["evening"]]) + if include_night: + available.extend([(e[0], e[1], "night", e[2]) for e in TIMELINE_EVENTS["night"]]) + if include_past: + available.extend([(e[0], e[1], "past", e[2]) for e in TIMELINE_EVENTS["past"]]) + if include_future: + available.extend([(e[0], e[1], "future", e[2]) for e in TIMELINE_EVENTS["future"]]) + + if not available: + return "Select at least one time period!", "{}", "" + + # Select events + random.shuffle(available) + sequence = available[:event_count] + + # Sort chronologically if requested + if chronological_order: + sequence.sort(key=lambda x: period_order.index(x[2]) if x[2] in period_order else 3) + + if not sequence: + return "No events available!", "{}", "" + + # Format as list + list_output = "## Timeline of Events\n\n" + for i, (name, desc, period, themes) in enumerate(sequence, 1): + themes_str = ", ".join(themes) + list_output += f"**{i}. [{period.upper()}] {name}**\n{desc}\n*Themes: {themes_str}*\n\n" + + # Format as config JSON + config = {"timeline": {}} + for i, (name, desc, period, themes) in enumerate(sequence, 1): + state_name = name.lower().replace(" ", "_").replace("'", "").replace("-", "_") + current_state_id = f"event_{i}_{state_name}" + + themes_str = ", ".join(themes) + + # Determine next state + if i < len(sequence): + next_name = sequence[i][0] + next_period = sequence[i][2] + next_state_name = next_name.lower().replace(" ", "_").replace("'", "").replace("-", "_") + next_state_id = f"event_{i+1}_{next_state_name}" + transition_text = random.choice(TIME_TRANSITIONS.get(next_period, ["Time passes..."])) + else: + next_state_id = "timeline_end" + transition_text = "The story concludes..." + + config["timeline"][current_state_id] = { + "description": f"[{period.upper()}] {desc}\n\n*{themes_str}*", + "media_prompt": f"Atmospheric scene: {name} during {period} - {desc}, mood: {themes_str}", + "choices": ["Continue"], + "transitions": { + "Continue": next_state_id + } + } + + # Add ending state + first_event = sequence[0][0].lower().replace(" ", "_").replace("'", "").replace("-", "_") + config["timeline"]["timeline_end"] = { + "description": "The timeline reaches its conclusion. Every moment has led to this.", + "choices": ["Relive the Timeline"], + "transitions": { + "Relive the Timeline": f"event_1_{first_event}" + } + } + + json_output = json.dumps(config, indent=2) + + # Format prompts + prompts_output = "## Timeline Scene Prompts\n\n" + for i, (name, desc, period, themes) in enumerate(sequence, 1): + themes_str = ", ".join(themes) + prompts_output += f"Atmospheric {period} scene: {name} - {desc}\n" + + return list_output, json_output, prompts_output + + +if __name__ == "__main__": + list_out, json_out, prompts_out = generate_timeline_sequence( + 6, True, True, True, True, True, False, False, True + ) + print(list_out) diff --git a/ui_gr_media_management.py b/ui_gr_media_management.py index 3d4a7bb330b25847419c59ca4172bcfd582a0afc..cc432d09eaa7ef2ee797f4b7b500e9861a7654d1 100644 --- a/ui_gr_media_management.py +++ b/ui_gr_media_management.py @@ -1,7 +1,26 @@ import gradio as gr -from file_explorer_and_upload import * +from file_explorer_and_upload import ( + SAVE_DIR, save_file, view_file, refresh_file_explorer, delete_file, + get_all_media_files, delete_file_and_refresh, save_file_and_refresh, + import_config_with_media, upload_file_or_bundle, export_config_with_media_wrapper, + SUPPORTED_EXTENSIONS +) from state_prompt_fileman_UI_functions import * +from state_prompt_fileman_UI_functions import ( + add_to_generation_queue, get_queue_dataframe, clear_generation_queue, + remove_completed_from_queue, get_log_dataframe, extract_prompts_from_config, + process_next_queue_item, retry_failed_items, + load_config_for_prompts, get_section_details, add_prompt_to_section, + auto_generate_section_prompts, clear_section_prompts, add_all_sections_to_queue, + update_config_with_generated_media +) from my_text_game_engine_attempt import display_website +from leveraging_machine_learning import ( + modelnames, modelname, imagemodelnames, + generate_response, generate_image, model_change_handler, + clear_all_models, loaded_model_list, format_output_dict, get_ram_usage, + seq2seq_modelnames, load_seq2seq_model, generate_seq2seq_response +) def TestGradioClientQwen270b(text): # client = Client("Qwen/Qwen2-72B-Instruct") @@ -11,7 +30,7 @@ def TestGradioClientQwen270b(text): # system="You are a helpful assistant.", # api_name="/model_chat" # ) - + client = Client("huggingface-projects/gemma-2-9b-it") result = client.predict( message=text, #"Hello!!", @@ -62,375 +81,1085 @@ def SPFManJLupdate_part_choices(config_json, location): return gr.update(choices=parts, value=parts[0] if parts else "") except json.JSONDecodeError: return gr.update(choices=[], value="") - + def SPFManJLPromptSuggestionGradClient(config, additionalnotes): prompt = f"Here is a config for a game and our job is to poulate the media field to fit the context: \n{config} \nadditional information from the user: \n{additionalnotes}" FinalOutput = TestGradioClientQwen270b(text=prompt) - + return FinalOutput SPFManload_state() -def ui_gr_media_management_acc(): - with gr.Accordion("Incomplete Media Management Assist - click to open", open=False) as filemanager: - gr.HTML("Make Files and Text ideas for the field and paste
When Space is restarted it will clear - zip export and import will be added later") - #with gr.Tab("Media Management Assistance"): #, open=False): - with gr.Accordion("Some Idea / Inspiration Sources / Demos", open=False): - with gr.Row(): - gr.HTML("Licenses for the spaces still to be evaluated - June 2024
Users to follow with cool spaces -
https://huggingface.co/osanseviero - https://huggingface.co/spaces/osanseviero/TheMLGame
https://huggingface.co/jbilcke-hf
https://huggingface.co/dylanebert
https://huggingface.co/fffiloni
https://huggingface.co/artificialguybr
https://huggingface.co/radames
https://huggingface.co/multimodalart, ") - gr.HTML("Some Youtube Channels to keep up with updates

https://www.youtube.com/@lev-selector
https://www.youtube.com/@fahdmirza/videos") - gr.HTML("Social media that shows possiblities

https://www.reddit.com/r/aivideo/ https://www.reddit.com/r/StableDiffusion/ https://www.reddit.com/r/midjourney/ https://x.com/blizaine https://www.reddit.com/r/singularity/comments/1ead7vp/not_sure_if_this_is_the_right_place_to_post_but_i/ https://www.reddit.com/r/singularity/comments/1ebpra6/the_big_reveal_book_trailer_made_with_runway_gen3/ https://www.reddit.com/r/LocalLLaMA/comments/1e3aboz/folks_with_one_24gb_gpu_you_can_use_an_llm_sdxl/
https://www.reddit.com/r/singularity/comments/1f81ira/utilizing_ai_in_solo_game_development_my/
https://www.reddit.com/r/singularity/comments/1fhe402/another_runawaymls_vid_to_vid_testing_for_first/
https://www.reddit.com/r/singularity/comments/1fh08nz/runawaymls_new_videotovideo_feature_is_crazy/
https://www.reddit.com/r/singularity/comments/1fe4rkv/6_chinese_ai_video_generator_comparisons_hailuo/") - with gr.Accordion("Upload Files for config", open=False): - gr.Markdown("# Media Saver and Explorer (refresh file list to be resolved - for now upload all files and reload the space - they persist as long as the space creator doesnt reset/update the space - will add manual clear options later)") - with gr.Tab("Upload Files"): - file_input = gr.File(label="Choose File to Upload") - save_output = gr.Textbox(label="Upload Status") - - with gr.Tab("File Explorer"): - - file_explorer = gr.FileExplorer( - root_dir=SAVE_DIR, - glob="*.*", - file_count="single", - height=300, - label="Select a file to view" +def ui_gr_media_management_acc(generated_media_paths=None): + """ + Media Studio - Unified media generation and management interface. + + Args: + generated_media_paths: gr.State for tracking generated media paths + """ + with gr.Accordion("Media Studio - click to open", open=False) as filemanager: + gr.HTML("Generate and manage media files for your game config.
When Space is restarted it will clear - zip export and import will be added later") + + # ============================================================ + # TAB 1: GENERATE + # ============================================================ + with gr.Tab("Generate"): + gr.Markdown("## Generate Media Assets") + + # ---------------------------------------------------------- + # Generation Studio (Unified Queue + Engine + Audio) + # ---------------------------------------------------------- + with gr.Tab("Generation Studio"): + gr.Markdown("### Unified Generation Studio") + gr.Markdown("Add prompts to queue, configure generation settings, and process. Supports text, image, audio, TTS, 3D, and video.") + + # ==================== ADD TO QUEUE ==================== + with gr.Accordion("➕ Add to Queue", open=True): + with gr.Row(): + gen_prompt_type = gr.Dropdown( + choices=[ + ("Text (LLM generation)", "text"), + ("Image (Stable Diffusion)", "image"), + ("Audio (Stable Audio SFX)", "audio"), + ("TTS (Text-to-Speech)", "tts"), + ("3D Model (Shap-E)", "3d"), + ], + value="image", + label="Generation Type", + scale=1 + ) + gen_prompt_input = gr.Textbox( + label="Prompt / Text", + placeholder="Enter your prompt here...", + lines=2, + scale=3 + ) + + # Type-specific parameters (dynamic visibility) + # Audio parameters + with gr.Group(visible=False) as audio_params_group: + gr.Markdown("**Audio Settings (Stable Audio)**") + with gr.Row(): + gen_audio_duration = gr.Slider(minimum=5, maximum=60, value=30, step=5, label="Duration (seconds)") + gen_audio_steps = gr.Slider(minimum=50, maximum=200, value=100, step=10, label="Steps") + gen_audio_cfg = gr.Slider(minimum=1, maximum=15, value=7, step=0.5, label="CFG Scale") + + # TTS parameters + with gr.Group(visible=False) as tts_params_group: + gr.Markdown("**TTS Settings**") + with gr.Row(): + gen_tts_model_queue = gr.Dropdown( + choices=[ + ("Kokoro-82M (Fast, natural)", "kokoro"), + ("Supertonic-2 (High-quality)", "supertonic"), + ("GLM-TTS (Multilingual)", "glm-tts") + ], + value="kokoro", + label="TTS Model", + scale=1 + ) + gen_tts_voice_queue = gr.Dropdown( + choices=[ + ("Heart (Female, warm)", "af_heart"), + ("Bella (Female, clear)", "af_bella"), + ("Nicole (Female, professional)", "af_nicole"), + ("Sarah (Female, friendly)", "af_sarah"), + ("Adam (Male, neutral)", "am_adam"), + ("Michael (Male, deep)", "am_michael"), + ("Emma (British Female)", "bf_emma"), + ("George (British Male)", "bm_george"), + ], + value="af_heart", + label="Voice", + scale=1 + ) + + # 3D parameters + with gr.Group(visible=False) as threeds_params_group: + gr.Markdown("**3D Settings (Shap-E)**") + with gr.Row(): + gen_3d_guidance = gr.Slider(minimum=5, maximum=30, value=15, step=1, label="Guidance Scale") + gen_3d_steps = gr.Slider(minimum=32, maximum=128, value=64, step=8, label="Steps") + + with gr.Row(): + gen_add_btn = gr.Button("Add to Queue", variant="primary", scale=1) + gen_add_status = gr.Textbox(label="Status", interactive=False, scale=2) + + # Toggle visibility based on type + def toggle_type_params(gen_type): + return ( + gr.update(visible=(gen_type == "audio")), + gr.update(visible=(gen_type == "tts")), + gr.update(visible=(gen_type == "3d")), + ) + + gen_prompt_type.change( + toggle_type_params, + inputs=[gen_prompt_type], + outputs=[audio_params_group, tts_params_group, threeds_params_group] + ) + + # Batch import (simple) + with gr.Accordion("Quick Import from Config File", open=False): + gr.Markdown("Extract prompts from a game config JSON file. Descriptions and media fields will be added to the queue.") + gen_config_file = gr.File(label="Config JSON") + gen_extract_btn = gr.Button("Extract Prompts from Config") + gen_extract_status = gr.Textbox(label="Extract Status", interactive=False) + + # Config-Based Prompt Builder (detailed) + with gr.Accordion("Config-Based Prompt Builder", open=False): + gr.Markdown("### Build prompts section-by-section from your game config") + gr.Markdown("Load your config, browse each scene, and create tailored prompts for each section.") + + # Load config + cfg_config_input = gr.Textbox(label="Paste Config JSON", lines=5, placeholder='{"location": {"state": {"description": "..."}}}') + with gr.Row(): + cfg_load_btn = gr.Button("Load Config", variant="primary") + cfg_load_status = gr.Textbox(label="Status", interactive=False, scale=2) + + # Section navigator + cfg_section_dropdown = gr.Dropdown(label="Select Section (location → state)", choices=[], interactive=True) + + # Section details display + with gr.Row(): + cfg_section_desc = gr.Textbox(label="Description", lines=3, interactive=False) + with gr.Column(): + cfg_section_media = gr.Textbox(label="Existing Media", lines=2, interactive=False) + cfg_section_choices = gr.Textbox(label="Choices", lines=1, interactive=False) + + # Prompt editor for section + gr.Markdown("**Add Prompt for This Section:**") + with gr.Row(): + cfg_prompt_type = gr.Dropdown(["image", "audio", "3d", "tts", "text"], value="image", label="Type", scale=1) + cfg_prompt_text = gr.Textbox(label="Prompt", placeholder="Describe what to generate for this scene...", lines=2, scale=4) + cfg_add_prompt_btn = gr.Button("Add", scale=1) + cfg_prompt_status = gr.Textbox(label="", interactive=False, show_label=False) + + # Bulk actions + gr.Markdown("**Bulk Actions:**") + with gr.Row(): + cfg_auto_gen_btn = gr.Button("Auto-Generate from Descriptions") + cfg_clear_prompts_btn = gr.Button("Clear All Prompts") + cfg_add_all_btn = gr.Button("Add All to Queue", variant="primary") + + # Preview all section prompts + cfg_prompts_display = gr.Dataframe( + headers=["Section", "Type", "Prompt"], + label="Prompts by Section", + interactive=False, + wrap=True + ) + + # Export updated config (after generation) + gr.Markdown("---") + gr.Markdown("**After Generation: Get Updated Config**") + gr.Markdown("Once assets are generated, update your config with the new media paths:") + with gr.Row(): + cfg_update_config_btn = gr.Button("Update Config with Generated Media", variant="secondary") + cfg_update_status = gr.Textbox(label="", interactive=False, show_label=False, scale=2) + cfg_updated_config_output = gr.Textbox( + label="Updated Config JSON (copy this to Playtest & Edit → Load Config)", + lines=8, + interactive=True + ) + + # Wire up config builder events + cfg_load_btn.click( + load_config_for_prompts, + inputs=[cfg_config_input], + outputs=[cfg_section_dropdown, cfg_load_status] + ) + + cfg_section_dropdown.change( + get_section_details, + inputs=[cfg_section_dropdown], + outputs=[cfg_section_desc, cfg_section_media, cfg_section_choices] + ) + + cfg_add_prompt_btn.click( + add_prompt_to_section, + inputs=[cfg_section_dropdown, cfg_prompt_type, cfg_prompt_text], + outputs=[cfg_prompts_display, cfg_prompt_status] + ) + + cfg_auto_gen_btn.click( + auto_generate_section_prompts, + outputs=[cfg_prompts_display, cfg_prompt_status] + ) + + cfg_clear_prompts_btn.click( + clear_section_prompts, + outputs=[cfg_prompts_display, cfg_prompt_status] + ) + + cfg_update_config_btn.click( + update_config_with_generated_media, + outputs=[cfg_updated_config_output, cfg_update_status] + ) + + # NOTE: cfg_add_all_btn.click is wired after gen_queue_display is defined + + # Queue display + gen_queue_count = gr.Markdown("**Queue: 0 items**") + gen_queue_display = gr.Dataframe( + headers=["#", "Type", "Prompt", "Status"], + label="Current Queue", + interactive=False, + wrap=True + ) + + with gr.Row(): + gen_clear_queue_btn = gr.Button("Clear Queue") + gen_remove_completed_btn = gr.Button("Remove Completed") + gen_retry_failed_btn = gr.Button("Retry Failed") + + # Wire up queue events (with type-specific parameters) + gen_add_btn.click( + add_to_generation_queue, + inputs=[gen_prompt_type, gen_prompt_input, + gen_audio_duration, gen_audio_steps, gen_audio_cfg, + gen_tts_model_queue, gen_tts_voice_queue, + gen_3d_guidance, gen_3d_steps], + outputs=[gen_queue_display, gen_queue_count, gen_add_status] + ) + + gen_extract_btn.click( + extract_prompts_from_config, + inputs=[gen_config_file], + outputs=[gen_queue_display, gen_queue_count, gen_extract_status] + ) + + gen_clear_queue_btn.click( + clear_generation_queue, + outputs=[gen_queue_display, gen_queue_count, gr.Dataframe(), gen_add_status] + ) + + gen_remove_completed_btn.click( + remove_completed_from_queue, + outputs=[gen_queue_display, gen_queue_count] + ) + + gen_retry_failed_btn.click( + retry_failed_items, + outputs=[gen_queue_display, gen_queue_count, gen_add_status] + ) + + # Wire up config builder "Add All to Queue" (needs to be here because it references gen_queue_display/count) + cfg_add_all_btn.click( + add_all_sections_to_queue, + outputs=[gen_queue_display, gen_queue_count, cfg_prompt_status] + ) + + # ==================== GENERATION ENGINE ==================== + with gr.Accordion("⚙️ Generation Engine", open=True): + gr.Markdown("Process queue items using Local (ZeroGPU) or API generation.") + + # Toggle between Local and API + gen_mode = gr.Radio( + ["Local (ZeroGPU)", "API"], + value="Local (ZeroGPU)", + label="Generation Mode" + ) + + # Local Options + with gr.Group(visible=True) as local_options_group: + gr.Markdown("**Local Generation Settings (ZeroGPU)**") + with gr.Accordion("Model Notes", open=False): + gr.HTML("https://huggingface.co/posts/cbensimon/747180194960645
https://github.com/NexaAI/Awesome-LLMs-on-device/") + gr.Markdown("Models requiring over 50GB to load will likely not work. 57b MOE takes 6min to load.") + gr.Markdown("**ZeroGPU Specs:** H200 with 70GB VRAM, default 60s timeout (extended for video)") + with gr.Row(): + # Default to smaller 1B model for faster loading + default_text_model = "unsloth/Llama-3.2-1B-Instruct" if "unsloth/Llama-3.2-1B-Instruct" in modelnames else modelnames[1] if len(modelnames) > 1 else modelname + gen_text_model = gr.Dropdown(choices=modelnames, value=default_text_model, label="Text Model") + gen_image_model = gr.Dropdown(choices=imagemodelnames, value=imagemodelnames[0], label="Image Model") + with gr.Row(): + gen_use_rag = gr.Checkbox(label="Enable RAG (text)", value=False) + gen_use_streaming = gr.Checkbox(label="Enable Streaming (text)", value=False) + + # Max tokens slider - outside group so it's always accessible + with gr.Row(): + gen_max_tokens = gr.Slider( + minimum=64, maximum=4096, value=512, step=64, + label="Max Tokens (text generation)", + info="Maximum number of tokens to generate" + ) + + # API Options + with gr.Group(visible=False) as api_options_group: + gr.Markdown("**API Generation Settings**") + gen_api_source = gr.Radio( + ["HF Inference", "External (Replicate)"], + value="HF Inference", + label="API Source" + ) + + with gr.Group() as hf_inference_group: + gr.Markdown("Using HuggingFace Serverless Inference API") + gr.HTML("HF Pro = 25 min/day for ZeroGPU, 20000 req/day for serverless API") + gen_hf_model_id = gr.Textbox(label="HF Model ID", value="black-forest-labs/FLUX.1-dev") + + with gr.Group(visible=False) as external_api_group: + gr.Markdown("External API (Replicate, etc.)") + gr.HTML("https://replicate.com/collections/text-to-image") + gen_replicate_model = gr.Textbox(label="Replicate Model ID", placeholder="e.g., stability-ai/sdxl") + + # Generation controls + gr.Markdown("---") + with gr.Row(): + gen_next_btn = gr.Button("Generate Next", variant="primary") + gen_all_btn = gr.Button("Generate All (coming soon)", variant="secondary", interactive=False) + + # Toggle visibility handlers + def toggle_gen_mode(mode): + if mode == "Local (ZeroGPU)": + return gr.update(visible=True), gr.update(visible=False) + else: + return gr.update(visible=False), gr.update(visible=True) + + def toggle_api_source(source): + if source == "HF Inference": + return gr.update(visible=True), gr.update(visible=False) + else: + return gr.update(visible=False), gr.update(visible=True) + + gen_mode.change( + toggle_gen_mode, + inputs=[gen_mode], + outputs=[local_options_group, api_options_group] + ) + + gen_api_source.change( + toggle_api_source, + inputs=[gen_api_source], + outputs=[hf_inference_group, external_api_group] + ) + + # ==================== PROGRESS & OUTPUT ==================== + with gr.Accordion("📊 Progress & Output", open=True): + gen_progress = gr.Markdown("**Progress: 0/0**") + gen_current_item = gr.Textbox(label="Current Item", interactive=False) + gen_log_display = gr.Dataframe( + headers=["#", "Type", "Prompt", "Status", "Result"], + label="Generation Log", + interactive=False, + wrap=True + ) + + # Output display - includes all media types + gr.Markdown("**Generated Output:**") + with gr.Row(): + gen_text_output = gr.Textbox(label="Text Output", lines=4) + gen_image_output = gr.Image(label="Image Output") + with gr.Row(): + gen_audio_output = gr.Audio(label="Audio Output", type="filepath") + + # Wire up generation + gen_next_btn.click( + process_next_queue_item, + inputs=[gen_mode, gen_text_model, gen_image_model, gen_use_rag, gen_use_streaming, + gen_max_tokens, gen_api_source, gen_hf_model_id, gen_replicate_model], + outputs=[gen_text_output, gen_image_output, gen_audio_output, gen_progress, gen_current_item, + gen_queue_display, gen_log_display] + ) + + # ---------------------------------------------------------- + # Quick Audio Generation (Direct, outside queue) + # ---------------------------------------------------------- + with gr.Tab("Quick Audio"): + gr.Markdown("### Quick Audio Generation") + gr.Markdown("Generate audio directly without using the queue. Files saved to `saved_media/`.") + + with gr.Tab("Sound Effects"): + gr.Markdown("**Stable Audio - Sound Effects & Ambient**") + with gr.Row(): + quick_audio_prompt = gr.Textbox( + label="Describe the sound", + placeholder="Forest ambience with birds chirping, footsteps on gravel, sword clash...", + lines=2, + scale=4 + ) + quick_audio_btn = gr.Button("Generate Sound", variant="primary", scale=1) + quick_audio_status = gr.Textbox(label="Status", interactive=False) + quick_audio_output = gr.Audio(label="Generated Audio", type="filepath") + + with gr.Tab("Text-to-Speech"): + gr.Markdown("### Text-to-Speech Generation") + gr.Markdown("Generate speech from text using multiple TTS models.") + gen_tts_text = gr.Textbox( + label="Text to speak", + placeholder="Welcome to the village, traveler. What brings you here?", + lines=3 + ) + with gr.Row(): + gen_tts_model = gr.Dropdown( + choices=[ + ("Kokoro-82M (Fast, natural voices)", "kokoro"), + ("Supertonic-2 (High-quality, expressive)", "supertonic"), + ("GLM-TTS (Multilingual, voice cloning)", "glm-tts") + ], + value="kokoro", + label="TTS Model", + scale=2 + ) + gen_tts_voice = gr.Dropdown( + choices=[ + ("Heart (Female, warm)", "af_heart"), + ("Bella (Female, clear)", "af_bella"), + ("Nicole (Female, professional)", "af_nicole"), + ("Sarah (Female, friendly)", "af_sarah"), + ("Sky (Female, bright)", "af_sky"), + ("Adam (Male, neutral)", "am_adam"), + ("Michael (Male, deep)", "am_michael"), + ("Emma (British Female)", "bf_emma"), + ("Isabella (British Female)", "bf_isabella"), + ("George (British Male)", "bm_george"), + ("Lewis (British Male)", "bm_lewis") + ], + value="af_heart", + label="Voice (Kokoro)", + scale=2 + ) + gen_tts_btn = gr.Button("Generate Speech", variant="primary", scale=1) + gen_tts_status = gr.Textbox(label="Status", interactive=False) + gen_tts_output = gr.Audio(label="Generated Speech", type="filepath") + + # ---------------------------------------------------------- + # 3D Generation (Direct API) + # ---------------------------------------------------------- + with gr.Tab("3D Generation"): + gr.Markdown("## 3D Model Generation") + gr.Markdown("Generate 3D models from text. Files are saved to `saved_media/` and appear in Library → Browse.") + + gr.Markdown("### Text-to-3D via Shap-E") + gr.Markdown("Generate 3D models from text descriptions.") + with gr.Row(): + gen_3d_prompt = gr.Textbox( + label="Describe your 3D model", + placeholder="A cute robot character, a medieval sword, a wooden treasure chest...", + lines=2, + scale=4 + ) + gen_3d_btn = gr.Button("Generate 3D Model", variant="primary", scale=1) + gen_3d_status = gr.Textbox(label="Status", interactive=False) + gen_3d_output = gr.Model3D(label="Generated 3D Model") + + gr.Markdown("---") + gr.Markdown("**Tip:** After generating, refresh the Library → Browse tab to see your new 3D model.") + + # ---------------------------------------------------------- + # Video Generation + # ---------------------------------------------------------- + with gr.Tab("Video Generation"): + gr.Markdown("## Video Generation") + + with gr.Tab("Text-to-Video"): + gr.Markdown("### Text-to-Video Generation") + gr.Markdown("Generate videos from text prompts using state-of-the-art models.") + + with gr.Row(): + t2v_model = gr.Dropdown( + choices=[ + ("LTX-Video Distilled (Fast, 7-8 steps)", "Lightricks/LTX-Video-0.9.7-distilled"), + ("Wan2.2 TI2V-5B (T2V+I2V, 720P)", "Wan-AI/Wan2.2-TI2V-5B-Diffusers"), + ("Wan2.2 T2V-14B (MoE, High Quality)", "Wan-AI/Wan2.2-T2V-A14B-Diffusers"), + ("HunyuanVideo 1.5 (8.3B, Consumer GPU)", "tencent/HunyuanVideo-1.5"), + ], + value="Lightricks/LTX-Video-0.9.7-distilled", + label="Video Model" + ) + t2v_prompt = gr.Textbox( + label="Video Prompt", + placeholder="A cat walking through a garden, cinematic lighting, 4K quality...", + lines=3 + ) + with gr.Row(): + t2v_steps = gr.Slider(minimum=2, maximum=20, value=4, step=1, label="Inference Steps (lower=faster)") + t2v_duration = gr.Slider(minimum=1, maximum=5, value=2, step=1, label="Duration (seconds)") + with gr.Row(): + t2v_width = gr.Dropdown(choices=[320, 480, 512, 720], value=512, label="Width") + t2v_height = gr.Dropdown(choices=[320, 480, 512, 720], value=320, label="Height") + t2v_btn = gr.Button("Generate Video", variant="primary") + t2v_status = gr.Textbox(label="Status", interactive=False) + t2v_output = gr.Video(label="Generated Video") + + gr.Markdown("---") + gr.Markdown("**ZeroGPU Limits:** H200 with 70GB VRAM, 3 min timeout. Use low settings (2s duration, 4 steps, 512x320) for best results. LTX-Distilled is fastest.") + + with gr.Tab("Image-to-Video"): + gr.Markdown("### Image-to-Video Generation") + gr.Markdown("Animate a static image into a video.") + + with gr.Row(): + i2v_model = gr.Dropdown( + choices=[ + ("Wan2.2 TI2V-5B (720P, Consumer GPU)", "Wan-AI/Wan2.2-TI2V-5B-Diffusers"), + ("Wan2.2 I2V-14B (MoE, High Quality)", "Wan-AI/Wan2.2-I2V-A14B-Diffusers"), + ], + value="Wan-AI/Wan2.2-TI2V-5B-Diffusers", + label="Video Model" + ) + i2v_image = gr.Image(label="Input Image", type="filepath") + i2v_prompt = gr.Textbox( + label="Motion Prompt (optional)", + placeholder="Camera slowly zooms in, gentle movement...", + lines=2 + ) + with gr.Row(): + i2v_steps = gr.Slider(minimum=2, maximum=20, value=8, step=1, label="Inference Steps (lower=faster)") + i2v_duration = gr.Slider(minimum=1, maximum=5, value=2, step=1, label="Duration (seconds)") + i2v_btn = gr.Button("Generate Video from Image", variant="primary") + i2v_status = gr.Textbox(label="Status", interactive=False) + i2v_output = gr.Video(label="Generated Video") + + with gr.Tab("Talking Head"): + gr.Markdown("### Talking Head Video") + gr.Markdown("Create animated talking head videos from a portrait image + text/audio. Uses SadTalker API.") + + with gr.Accordion("How It Works", open=False): + gr.Markdown(""" +1. **Upload a portrait image** - A clear front-facing photo works best +2. **Provide speech** - Either upload audio OR enter text (text will be converted to speech first) +3. **Generate** - The AI animates the portrait to speak the audio + +**Best Results:** +- Portrait should be front-facing, well-lit +- Audio should be clear speech +- Keep audio under 30 seconds for faster processing + """) + + with gr.Row(): + with gr.Column(scale=1): + gen_video_image = gr.Image(label="Portrait Image", type="filepath") + with gr.Column(scale=1): + gen_video_audio = gr.Audio(label="Audio (optional if using text)", type="filepath") + + gen_video_text = gr.Textbox( + label="Or enter text (will generate TTS first)", + placeholder="Hello! Welcome to our game. I'm your guide through this adventure...", + lines=3 + ) + gen_video_btn = gr.Button("Generate Talking Head Video", variant="primary") + gen_video_status = gr.Textbox(label="Status", interactive=False) + gen_video_output = gr.Video(label="Generated Video") + + gr.Markdown("---") + gr.Markdown("**Note:** Video generation can take 1-5 minutes depending on audio length and API availability.") + + # ---------------------------------------------------------- + # T5Gemma2 / Encoder-Decoder + # ---------------------------------------------------------- + with gr.Tab("Encoder-Decoder (T5Gemma2)"): + gr.Markdown("""## T5Gemma2 - Multimodal Encoder-Decoder Model + +T5Gemma2 is an encoder-decoder model (different from typical causal LMs). +It can process both text and images for generation tasks. + """) + + # Random prompts for T5Gemma2 + T5_RANDOM_PROMPTS = [ + "Describe a fantasy tavern with interesting characters", + "What items would you find in a wizard's workshop?", + "Create a short dialogue between a knight and a dragon", + "Describe an alien marketplace on a distant planet", + "What would a steampunk city look like at sunset?", + "Describe a mystical forest clearing at midnight", + "What treasures might a pirate ship carry?", + "Describe a post-apocalyptic settlement and its inhabitants", + ] + + def get_random_t5_prompt(): + """Get a random prompt for T5Gemma2.""" + import random + return random.choice(T5_RANDOM_PROMPTS) + + with gr.Row(): + t5_model_selector = gr.Dropdown( + choices=seq2seq_modelnames, + value=seq2seq_modelnames[0] if seq2seq_modelnames else None, + label="T5Gemma2 Model" + ) + t5_load_btn = gr.Button("Load Model") + t5_load_status = gr.Textbox(label="Load Status", interactive=False) + + t5_load_btn.click( + fn=load_seq2seq_model, + inputs=[t5_model_selector], + outputs=[t5_load_status] + ) + + gr.Markdown("### Generate with T5Gemma2") + t5_prompt = gr.Textbox( + label="Prompt", + lines=3, + placeholder="Describe what you want to generate..." + ) + t5_image_url = gr.Textbox( + label="Image URL (optional - for multimodal)", + placeholder="https://example.com/image.jpg" + ) + with gr.Row(): + t5_random_btn = gr.Button("🎲 Random Prompt", variant="secondary") + t5_generate_btn = gr.Button("Generate", variant="primary") + t5_output = gr.Textbox(label="Generated Output", lines=10) + + t5_random_btn.click( + fn=get_random_t5_prompt, + outputs=[t5_prompt] + ) + + t5_generate_btn.click( + fn=generate_seq2seq_response, + inputs=[t5_prompt, t5_image_url], + outputs=[t5_output] ) + + with gr.Accordion("About T5Gemma2", open=False): + gr.Markdown(""" +**What is T5Gemma2?** +- Encoder-decoder architecture (like T5, BART) built on Gemma 3 +- Supports multimodal inputs (text + images) +- Available in 270M-270M, 1B-1B, and 4B-4B parameter variants + +**Use Cases:** +- Image captioning / description +- Visual question answering +- Multimodal content generation +- Summarization tasks + +**Note:** This uses `AutoModelForSeq2SeqLM` instead of `AutoModelForCausalLM`. + """) + + # ---------------------------------------------------------- + # HF Spaces (Embedded) + # ---------------------------------------------------------- + with gr.Tab("HF Spaces"): + gr.HTML("Generate files using embedded HuggingFace Spaces, then save to PC and re-upload to use in config") + gr.HTML("Whole game engine in a space? - https://huggingface.co/spaces/thomwolf/test_godot_editor

https://huggingface.co/godot-demo https://huggingface.co/spaces/thomwolf/test_godot") + + with gr.Tab("Text/LLM"): + gr.HTML("Some Benchmark Leaderboards - https://huggingface.co/spaces/allenai/ZebraLogic | https://huggingface.co/spaces/allenai/WildBench https://scale.com/leaderboard https://livebench.ai") + with gr.Accordion("LLM HF Spaces/Sites (Click Here to Open) - Ask for a story and suggestions based on the autoconfig", open=False): + with gr.Row(): + linktochat = gr.Dropdown(choices=[ "--Long Output--", "https://thudm-longwriter.hf.space", + "--Function Calling--", "https://groq-demo-groq-tool-use.hf.space", + "--Multiple Models--", "https://huggingface.co/spaces/nvidia/minitron", "https://lmsys-gpt-4o-mini-battles.hf.space", "https://labs.perplexity.ai/", "https://chat.lmsys.org", "https://sdk.vercel.ai/docs", "https://cyzgab-catch-me-if-you-can.hf.space", + "--11B and above--", "https://llamameta-llama3-1-405b.static.hf.space", "https://qwen-qwen-max-0428.hf.space", "https://cohereforai-c4ai-command-r-plus.hf.space", "https://qwen-qwen1-5-110b-chat-demo.hf.space", + "--70B and above--", "https://cognitivecomputations-chat.hf.space", "https://snowflake-snowflake-arctic-st-demo.hf.space", "https://databricks-dbrx-instruct.hf.space", "https://qwen-qwen1-5-72b-chat.hf.space", + "--20B and above--", "https://grin-moe-demo-grin-moe.hf.space", "https://gokaygokay-gemma-2-llamacpp.hf.space", "https://01-ai-yi-34b-chat.hf.space", "https://cohereforai-c4ai-command-r-v01.hf.space", "https://ehristoforu-mixtral-46-7b-chat.hf.space", "https://mosaicml-mpt-30b-chat.hf.space", + "--7B and above--", "https://vilarin-mistral-nemo.hf.space", "https://arcee-ai-arcee-scribe.hf.space", "https://vilarin-llama-3-1-8b-instruct.hf.space", "https://ysharma-chat-with-meta-llama3-8b.hf.space", "https://qwen-qwen1-5-moe-a2-7b-chat-demo.hf.space", "https://deepseek-ai-deepseek-coder-7b-instruct.hf.space", "https://osanseviero-mistral-super-fast.hf.space", "https://artificialguybr-qwen-14b-chat-demo.hf.space", "https://huggingface-projects-llama-2-7b-chat.hf.space", + "--1B and above--", "https://huggingface.co/spaces/eswardivi/Phi-3-mini-128k-instruct", "https://eswardivi-phi-3-mini-4k-instruct.hf.space", "https://stabilityai-stablelm-2-1-6b-zephyr.hf.space", + "--under 1B--", + "--unorganised--", "https://ysharma-zephyr-playground.hf.space", "https://huggingfaceh4-zephyr-chat.hf.space", "https://ysharma-explore-llamav2-with-tgi.hf.space", "https://huggingfaceh4-falcon-chat.hf.space", "https://uwnlp-guanaco-playground-tgi.hf.space", "https://stabilityai-stablelm-tuned-alpha-chat.hf.space", "https://mosaicml-mpt-7b-storywriter.hf.space", "https://huggingfaceh4-starchat-playground.hf.space", "https://bigcode-bigcode-playground.hf.space", "https://mosaicml-mpt-7b-chat.hf.space", "https://huggingchat-chat-ui.hf.space", "https://togethercomputer-openchatkit.hf.space"], label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) + chatspacebtn = gr.Button("Use the chosen URL to load interface with a chat model. For sdk.vercel click the chat button on the top left. For lymsys / chat arena copy the link and use a new tab") + chatspace = gr.HTML("Chat Space Chosen will load here") + chatspacebtn.click(display_website, inputs=linktochat, outputs=chatspace) + + with gr.Tab("Media Understanding"): + gr.HTML("Can make random images and ask for narrative options - https://huggingface.co/spaces/Qwen/QVQ-72B-preview") + gr.HTML("NPC Response Engines? Camera, Shopkeeper, Companion, Enemies, etc.") + with gr.Accordion("Media understanding model Spaces/Sites (Click Here to Open)", open=False): + with gr.Row(): + linktomediaunderstandingspace = gr.Dropdown(choices=[ "--Weak Audio Understanding = Audio to text, Weak Video Understanding = Video to Image to Image Understanding", "https://skalskip-florence-2-video.hf.space", "https://kingnish-opengpt-4o.hf.space", + "--Audio Understanding--", "https://jan-hq-llama3-1-s-v0-2.hf.space", + "--Video Understanding--", "https://ivgsz-flash-vstream-demo.hf.space", + "--Image Understanding--", "https://qwen-qvq-72b-preview.hf.space", "https://gokaygokay-flux-prompt-generator.hf.space", "https://gokaygokay-florence-2.hf.space", "https://vilarin-vl-chatbox.hf.space", "https://qnguyen3-nanollava.hf.space", "https://skalskip-better-florence-2.hf.space", "https://merve-llava-interleave.hf.space", + "--Img-to-img Understanding--", "https://merve-draw-to-search-art.hf.space", + "--Image Understanding without conversation--", "https://epfl-vilab-4m.hf.space", "https://epfl-vilab-multimae.hf.space", "https://gokaygokay-sd3-long-captioner.hf.space" ], + label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) + mediaunderstandingspacebtn = gr.Button("Use the chosen URL to load interface with a media understanding space") + mediaunderstandingspace = gr.HTML("Media Understanding Space Chosen will load here") + mediaunderstandingspacebtn.click(display_website, inputs=linktomediaunderstandingspace, outputs=mediaunderstandingspace) + gr.HTML("Image Caption = https://huggingface.co/spaces/microsoft/Promptist (Prompt Lengthen) ") + + + with gr.Tab("Images"): + with gr.Accordion("Image Gen or Animation HF Spaces/Sites (Click Here to Open) - Have to download and upload at the the top", open=False): + # with gr.Tabs("General"): + with gr.Row(): + linktoimagegen = gr.Dropdown(choices=["--Text-Interleaved--", "https://ethanchern-anole.hf.space", + "--Hidden Image--", "https://ap123-illusiondiffusion.hf.space", + "--Panorama--", "https://gokaygokay-360panoimage.hf.space", + "--General--", "https://takarajordan-cinediffusion.hf.space", "https://aifeifei798-darkidol-flux-feifei.hf.space", "https://diyism-datou1111-shou-xin.hf.space", "https://pixart-alpha-pixart-sigma.hf.space", "https://stabilityai-stable-diffusion-3-medium.hf.space", "https://prodia-sdxl-stable-diffusion-xl.hf.space", "https://prodia-fast-stable-diffusion.hf.space", "https://bytedance-hyper-sdxl-1step-t2i.hf.space", "https://multimodalart-cosxl.hf.space", "https://cagliostrolab-animagine-xl-3-1.hf.space", "https://stabilityai-stable-diffusion.hf.space", + "--Speed--", "https://radames-real-time-text-to-image-sdxl-lightning.hf.space", "https://ap123-sdxl-lightning.hf.space", + "--LORA Support--", "https://multimodalart-flux-lora-the-explorer.hf.space", "https://artificialguybr-artificialguybr-demo-lora.hf.space", "https://artificialguybr-studio-ghibli-lora-sdxl.hf.space", "https://artificialguybr-pixel-art-generator.hf.space", "https://fffiloni-sdxl-control-loras.hf.space", "https://ehristoforu-dalle-3-xl-lora-v2.hf.space", + "--Image to Image--", "https://gokaygokay-kolorsplusplus.hf.space", "https://lllyasviel-ic-light.hf.space", "https://gparmar-img2img-turbo-sketch.hf.space", + "--Upscaler--", "https://gokaygokay-tile-upscaler.hf.space", + "--Control of Pose--", "https://instantx-instantid.hf.space", "https://modelscope-transferanything.hf.space", "https://okaris-omni-zero.hf.space" + "--Control of Shapes--", "https://linoyts-scribble-sdxl-flash.hf.space", + "--Control of Text--", "", + "--Clothing Try on demos--", "https://kwai-kolors-kolors-virtual-try-on.hf.space", + "--Foreign Language Input--", "https://gokaygokay-kolors.hf.space"], label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) + imagegenspacebtn = gr.Button("Use the chosen URL to load interface with a image generation model") + + imagegenspace = gr.HTML("Image Space Chosen will load here") + imagegenspacebtn.click(display_website, inputs=linktoimagegen, outputs=imagegenspace) + + linkstobecollectednoembed = "https://artgan-diffusion-api.hf.space", "https://multimodalart-stable-cascade.hf.space", "https://google-sdxl.hf.space", "https://visionmaze-magic-me.hf.space", "https://segmind-segmind-stable-diffusion.hf.space", "https://simianluo-latent-consistency-model.hf.space", + gr.HTML("Concept Art, UI elements, Static/3D Characters, Environments and Objects - https://huggingface.co/Datou1111/shou_xin (Hand drawn style)") + gr.HTML("Images Generation General (3rd Party) = https://www.craiyon.com/") + gr.HTML("Images Generation Posters with text - https://huggingface.co/spaces/GlyphByT5/Glyph-SDXL-v2") + gr.HTML("SVG Generation = Coding models / SOTA LLM ") + gr.HTML("Images Generation - Upscaling - https://huggingface.co/spaces/gokaygokay/Tile-Upscaler") + gr.HTML("Vision Models for descriptions
https://huggingface.co/spaces/gokaygokay/Florence-2
https://huggingface.co/spaces/vilarin/VL-Chatbox - glm 4v 9b
") + gr.HTML("Upscalers (save data transfer costs? highly detailed characters?) - https://huggingface.co/spaces/gokaygokay/AuraSR") + gr.HTML("Placeholder for huggingface spaces that can assist ") + gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist") + + with gr.Tab("Video"): + with gr.Accordion("Video Spaces/Sites (Click Here to Open)", open=False): + with gr.Row(): + linktovideogenspace = gr.Dropdown(choices=["--Texttovid--", "https://kingnish-instant-video.hf.space", + "--General--", "https://zheyangqin-vader.hf.space", "https://kadirnar-open-sora.hf.space", + "--Talking Portrait--", "https://fffiloni-tts-hallo-talking-portrait.hf.space", + "--Gif / ImgtoImg based video--", "https://wangfuyun-animatelcm-svd.hf.space", "https://bytedance-animatediff-lightning.hf.space", "https://wangfuyun-animatelcm.hf.space", "https://guoyww-animatediff.hf.space",], + label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) + videogenspacebtn = gr.Button("Use the chosen URL to load interface with video generation") + videogenspace = gr.HTML("Video Space Chosen will load here") + videogenspacebtn.click(display_website, inputs=linktovideogenspace, outputs=videogenspace) + + gr.HTML("Cutscenes, Tutorials, Trailers") + gr.HTML("Portrait Video eg. Solo Taking NPC - https://huggingface.co/spaces/fffiloni/tts-hallo-talking-portrait (Image + Audio and combination) https://huggingface.co/spaces/KwaiVGI/LivePortrait (Non verbal communication eg. in a library, when running from a pursuer)") + gr.HTML("Placeholder for huggingface spaces that can assist - https://huggingface.co/spaces/KingNish/Instant-Video, https://huggingface.co/spaces/multimodalart/stable-video-diffusion, https://huggingface.co/spaces/multimodalart/stable-video-diffusion") + gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist") + gr.HTML("3rd Party / Closed Source - https://runwayml.com/
") + with gr.Tab("Animations (for lower resource use)"): + gr.HTML("Characters, Environments, Objects") + gr.HTML("Placeholder for huggingface spaces that can assist - image as 3d object in video https://huggingface.co/spaces/ashawkey/LGM") + gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist") + + with gr.Tab("Audio"): + gr.Markdown("*For direct audio generation, use the **Audio Generation** tab above.*") + gr.HTML("Music - Background, Interactive, Cutscene, Menu
Sound Effects - Environment, character, action (environmental triggered by user eg. gun), UI
Speech - Dialogue, narration, voiceover") + + with gr.Accordion("Embed Audio Spaces (iframe)", open=True): + with gr.Row(): + linktoaudiogenspace = gr.Dropdown(choices=["General", "https://artificialguybr-stable-audio-open-zero.hf.space", "", + "--Talking Portrait--","https://fffiloni-tts-hallo-talking-portrait.hf.space"], + label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) + audiogenspacebtn = gr.Button("Embed Space") + audiogenspace = gr.HTML("Audio Space Chosen will load here") + audiogenspacebtn.click(display_website, inputs=linktoaudiogenspace, outputs=audiogenspace) + + with gr.Accordion("Audio Resources & Links", open=False): + gr.HTML("Audio Sound Effects - https://huggingface.co/spaces/artificialguybr/Stable-Audio-Open-Zero") + gr.HTML("Voices - Voice clone eg. actors part of your project - https://huggingface.co/spaces/tonyassi/voice-clone") + gr.HTML("3rd Party / Closed Source - https://suno.com/
https://www.udio.com/") + + with gr.Tab("3D"): + gr.Markdown("*For direct 3D generation, use the **3D Generation** tab above.*") + gr.HTML("Characters, Environments, Objects") + + with gr.Accordion("HuggingFace 3D Spaces (opens in new tab)", open=True): + gr.HTML(""" + 3DTopia-XL | + InstantMesh | + 3D Arena | + LGM Mini | + TripoSR | + Shap-E | + Splat to Mesh + """) + + with gr.Accordion("3D Editors & Libraries (opens in new tab)", open=True): + gr.HTML(""" + Editors (Free/Free Tier):
+ SculptGL - Sculpting Editor, No Login
+ Tinkercad - Editor
+ Vectary - Editor
+ Clara.io - Editor + Huge Models Library
+ Figuro - Editor + Tutorials
+ Spline - Editor + Presentation
+ SelfCAD - Editor

+ Model Libraries:
+ Sketchfab - Model Library + """) + + with gr.Accordion("Embed HF Space (iframe - cannot navigate back)", open=False): + gr.HTML("Note: Embedded spaces load in iframe. Use links above to open in new tab instead.") + with gr.Row(): + linktoThreedModel = gr.Dropdown(choices=["--Image prompt--", "https://vast-ai-charactergen.hf.space", + "--Video prompt--", "https://facebook-vggsfm.hf.space", + "--Text prompt--", "https://wuvin-unique3d.hf.space", "https://stabilityai-triposr.hf.space", "https://hysts-shap-e.hf.space", "https://tencentarc-instantmesh.hf.space", "https://ashawkey-lgm.hf.space", "https://dylanebert-lgm-mini.hf.space", "https://dylanebert-splat-to-mesh.hf.space", "https://dylanebert-multi-view-diffusion.hf.space"], label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) + ThreedModelspacebtn = gr.Button("Embed in iframe below") + ThreedModelspace = gr.HTML("3D Space Chosen will load here") + ThreedModelspacebtn.click(display_website, inputs=linktoThreedModel, outputs=ThreedModelspace) + + gr.HTML("Closed Source - Meshy.ai") + + with gr.Tab("Fonts"): + gr.HTML("Style of whole game, or locations, or characters") + gr.HTML("Placeholder for huggingface spaces that can assist - there was a space that could make letter into pictures based on the prompt but I cant find it now") + gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist") + + with gr.Tab("Shaders/Effects"): + with gr.Accordion("'Special Effects' Spaces/Sites (Click Here to Open)", open=False): + with gr.Row(): + linktospecialeffectsgenspace = gr.Dropdown(choices=["--Distorted Image--", "https://epfl-vilab-4m.hf.space", "https://epfl-vilab-multimae.hf.space"], + label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) + specialeffectsgenspacebtn = gr.Button("Use the chosen URL to load interface with special effects generation") + specialeffectsgenspace = gr.HTML("Special Effects Space Chosen will load here") + specialeffectsgenspacebtn.click(display_website, inputs=linktospecialeffectsgenspace, outputs=specialeffectsgenspace) + gr.HTML("Any output that is not understood by the common person can be used as special effects eg. depth map filters on images etc.") + gr.HTML("Post-processing Effects, material effects, Particle systems, visual feedback") + gr.HTML("Visual Effects - eg. explosion can turn all items white for a moment, losing conciousness blurs whole screen") + gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist") + + # ---------------------------------------------------------- + # External Tools (3rd Party) + # ---------------------------------------------------------- + with gr.Tab("External Tools"): + gr.Markdown("## 3rd Party Generation Tools") + gr.HTML("General - https://artificialanalysis.ai/models/llama-3-1-instruct-405b/providers") + gr.HTML("Chat - https://t3.chat/") + gr.HTML("Image - https://playground.com/login | https://www.midjourney.com/showcase") + gr.HTML("Audio - https://www.udio.com/home https://www.udio.com/pricing | https://suno.com/ ") + gr.HTML("Video - https://lumalabs.ai/dream-machine https://lumalabs.ai/dream-machine/pricing (free 30 gens a month) | https://klingai.com/ - https://www.reddit.com/r/singularity/comments/1fk4tgp/kling_ai_showcasing_the_use_of_the_motion_brush/ | https://www.hedra.com/") + gr.HTML("3D - https://www.csm.ai/ | https://www.tripo3d.ai/ | https://zoo.dev/text-to-cad") + gr.HTML("Multiple - https://fal.ai/pricing | ") + + # ============================================================ + # TAB 2: LIBRARY + # ============================================================ + with gr.Tab("Library"): + gr.Markdown("## Media Library") + + with gr.Tab("Upload / Import"): + gr.Markdown("### Upload Media Files or Import Config Bundle") + gr.Markdown("Upload individual media files (images, audio, video) or import a zip bundle containing config.json + media files.") + + with gr.Row(): + with gr.Column(scale=2): + file_input = gr.File(label="Choose File to Upload (media or .zip bundle)") + save_output = gr.Textbox(label="Upload Status") + with gr.Column(scale=1): + gr.Markdown("**Supported formats:**") + gr.Markdown("• Images: jpg, png, gif, bmp, webp") + gr.Markdown("• Audio: mp3, wav, ogg") + gr.Markdown("• Video: mp4, avi, mov, webm") + gr.Markdown("• Bundle: .zip with config.json") + + with gr.Accordion("Import Results (for .zip bundles)", open=False): + import_config_output = gr.Code(label="Imported Config JSON", language="json") + import_files_output = gr.Textbox(label="Extracted Files") + + with gr.Tab("Browse"): + gr.Markdown("### Files in saved_media/") + with gr.Row(): + file_dropdown = gr.Dropdown( + choices=get_all_media_files(), + label="Select a file to view", + interactive=True, + scale=3 + ) + refresh_button = gr.Button("Refresh List", scale=1) with gr.Row(): - refresh_button = gr.Button("Refresh") view_button = gr.Button("View File") - delete_button = gr.Button("Delete") + delete_button = gr.Button("Delete File") image_output = gr.Image(label="Image Output", type="pil") audio_output = gr.Audio(label="Audio Output") video_output = gr.Video(label="Video Output") - error_output = gr.Textbox(label="Error") + model3d_output = gr.Model3D(label="3D Model Output") + error_output = gr.Textbox(label="Status") + + with gr.Tab("Generated"): + gr.Markdown("## Generated Media Paths") + gr.Markdown("*Media generated via Local (ZeroGPU) appears here. For Audio/3D generation, use the Browse tab to view saved files.*") + if generated_media_paths is not None: + generated_display = gr.Textbox(label="Generated Files", lines=5, interactive=False) + refresh_generated_btn = gr.Button("Refresh Generated List") + + def show_generated(paths): + if paths: + return "\n".join(paths) + return "No media generated yet. Use Generate → Local (ZeroGPU) → Image Generation to create images." + + refresh_generated_btn.click( + show_generated, + inputs=[generated_media_paths], + outputs=[generated_display] + ) + else: + gr.Markdown("*Generated media tracking not available - state not passed*") + + with gr.Tab("Export"): + gr.Markdown("### Export Config + Media Bundle") + gr.Markdown("Export your game config along with all referenced media files as a .zip bundle.") + export_config_input = gr.Textbox( + label="Config JSON to Export", + placeholder="Paste your game config JSON here...", + lines=8 + ) + export_btn = gr.Button("Export Config with Media") + export_output = gr.File(label="Download Bundle (.zip)") + export_status = gr.Textbox(label="Export Status") + + gr.HTML("Placeholder for clearing uploaded assets (public space and temporary persistence = sharing and space problems)") + # Wire up file management events file_input.upload( - save_file, + upload_file_or_bundle, inputs=file_input, - outputs=[save_output, file_explorer, file_input] + outputs=[save_output, file_dropdown, file_input, import_config_output, import_files_output] + ) + + # Wire up export + export_btn.click( + export_config_with_media_wrapper, + inputs=export_config_input, + outputs=[export_output, export_status] ) view_button.click( view_file, - inputs=file_explorer, - outputs=[image_output, audio_output, video_output, error_output] + inputs=file_dropdown, + outputs=[image_output, audio_output, video_output, error_output, model3d_output] ) refresh_button.click( - refresh_file_explorer, - outputs=file_explorer + fn=lambda: gr.update(choices=get_all_media_files()), + outputs=file_dropdown ) delete_button.click( - delete_file, - inputs=file_explorer, - outputs=[file_explorer, error_output] + delete_file_and_refresh, + inputs=file_dropdown, + outputs=[file_dropdown, error_output] ) - with gr.Tab("Batch add files to config"): - gr.HTML("Placeholder for Config parser to allow dropdowns for the media parts of the config inserted to make assigning media quick") - gr.HTML("Placeholder for Config parser to allow for current zerospace creation and placement into the config (LLM can give list of media but still have to figure out workflow from there)") - gr.Interface(import_config_with_media, inputs=["file"], outputs=["code", "text"], description="Uploads the files needed for your config and present config for loading somewhere else") + # Wire up 3D generation (also refreshes file dropdown) + def generate_3d_ui(prompt): + if not prompt or not prompt.strip(): + return "Please enter a prompt", None, gr.update() + result = SPFMangenerate_3d_model(prompt) + if "saved as" in result: + filepath = result.split("saved as ")[-1] + return result, filepath, gr.update(choices=get_all_media_files()) + return result, None, gr.update() - gr.HTML("Placeholder for clearing uploaded assets (public space and temporary persistence = sharing and space problems)") - - with gr.Tab("Using Embedded HF spaces / Premade files in external sources"): - gr.HTML("Generate the files and then sort and manage them in their appropriate field in the config") - gr.HTML("Currently - need to create then save to pc then reupload to use here in test tab") - gr.HTML("Whole game engine in a space? - https://huggingface.co/spaces/thomwolf/test_godot_editor

https://huggingface.co/godot-demo https://huggingface.co/spaces/thomwolf/test_godot") - - with gr.Accordion("Media Suggestions based on config"): - #gr.Interface( , description="Load a config and get some prompt ideas") - gr.HTML("Either duplicate the interface on the api side or move it out so that both sides can use it") - - with gr.Tab("Text-based"): - gr.HTML("Some Benchmark Leaderboards - https://huggingface.co/spaces/allenai/ZebraLogic | https://huggingface.co/spaces/allenai/WildBench https://scale.com/leaderboard https://livebench.ai") - with gr.Accordion("LLM HF Spaces/Sites (Click Here to Open) - Ask for a story and suggestions based on the autoconfig", open=False): - with gr.Row(): - linktochat = gr.Dropdown(choices=[ "--Long Output--", "https://thudm-longwriter.hf.space", - "--Function Calling--", "https://groq-demo-groq-tool-use.hf.space", - "--Multiple Models--", "https://huggingface.co/spaces/nvidia/minitron", "https://lmsys-gpt-4o-mini-battles.hf.space", "https://labs.perplexity.ai/", "https://chat.lmsys.org", "https://sdk.vercel.ai/docs", "https://cyzgab-catch-me-if-you-can.hf.space", - "--11B and above--", "https://llamameta-llama3-1-405b.static.hf.space", "https://qwen-qwen-max-0428.hf.space", "https://cohereforai-c4ai-command-r-plus.hf.space", "https://qwen-qwen1-5-110b-chat-demo.hf.space", - "--70B and above--", "https://cognitivecomputations-chat.hf.space", "https://snowflake-snowflake-arctic-st-demo.hf.space", "https://databricks-dbrx-instruct.hf.space", "https://qwen-qwen1-5-72b-chat.hf.space", - "--20B and above--", "https://grin-moe-demo-grin-moe.hf.space", "https://gokaygokay-gemma-2-llamacpp.hf.space", "https://01-ai-yi-34b-chat.hf.space", "https://cohereforai-c4ai-command-r-v01.hf.space", "https://ehristoforu-mixtral-46-7b-chat.hf.space", "https://mosaicml-mpt-30b-chat.hf.space", - "--7B and above--", "https://vilarin-mistral-nemo.hf.space", "https://arcee-ai-arcee-scribe.hf.space", "https://vilarin-llama-3-1-8b-instruct.hf.space", "https://ysharma-chat-with-meta-llama3-8b.hf.space", "https://qwen-qwen1-5-moe-a2-7b-chat-demo.hf.space", "https://deepseek-ai-deepseek-coder-7b-instruct.hf.space", "https://osanseviero-mistral-super-fast.hf.space", "https://artificialguybr-qwen-14b-chat-demo.hf.space", "https://huggingface-projects-llama-2-7b-chat.hf.space", - "--1B and above--", "https://huggingface.co/spaces/eswardivi/Phi-3-mini-128k-instruct", "https://eswardivi-phi-3-mini-4k-instruct.hf.space", "https://stabilityai-stablelm-2-1-6b-zephyr.hf.space", - "--under 1B--", - "--unorganised--", "https://ysharma-zephyr-playground.hf.space", "https://huggingfaceh4-zephyr-chat.hf.space", "https://ysharma-explore-llamav2-with-tgi.hf.space", "https://huggingfaceh4-falcon-chat.hf.space", "https://uwnlp-guanaco-playground-tgi.hf.space", "https://stabilityai-stablelm-tuned-alpha-chat.hf.space", "https://mosaicml-mpt-7b-storywriter.hf.space", "https://huggingfaceh4-starchat-playground.hf.space", "https://bigcode-bigcode-playground.hf.space", "https://mosaicml-mpt-7b-chat.hf.space", "https://huggingchat-chat-ui.hf.space", "https://togethercomputer-openchatkit.hf.space"], label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) - chatspacebtn = gr.Button("Use the chosen URL to load interface with a chat model. For sdk.vercel click the chat button on the top left. For lymsys / chat arena copy the link and use a new tab") - chatspace = gr.HTML("Chat Space Chosen will load here") - chatspacebtn.click(display_website, inputs=linktochat, outputs=chatspace) - - with gr.Tab("Media Understanding"): - gr.HTML("NPC Response Engines? Camera, Shopkeeper, Companion, Enemies, etc.") - with gr.Accordion("Media understanding model Spaces/Sites (Click Here to Open)", open=False): - with gr.Row(): - linktomediaunderstandingspace = gr.Dropdown(choices=[ "--Weak Audio Understanding = Audio to text, Weak Video Understanding = Video to Image to Image Understanding", "https://skalskip-florence-2-video.hf.space", "https://kingnish-opengpt-4o.hf.space", - "--Audio Understanding--", "https://jan-hq-llama3-1-s-v0-2.hf.space", - "--Video Understanding--", "https://ivgsz-flash-vstream-demo.hf.space", - "--Image Understanding--", "https://gokaygokay-flux-prompt-generator.hf.space", "https://gokaygokay-florence-2.hf.space", "https://vilarin-vl-chatbox.hf.space", "https://qnguyen3-nanollava.hf.space", "https://skalskip-better-florence-2.hf.space", "https://merve-llava-interleave.hf.space", - "--Img-to-img Understanding--", "https://merve-draw-to-search-art.hf.space", - "--Image Understanding without conversation--", "https://epfl-vilab-4m.hf.space", "https://epfl-vilab-multimae.hf.space", "https://gokaygokay-sd3-long-captioner.hf.space" ], - label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) - mediaunderstandingspacebtn = gr.Button("Use the chosen URL to load interface with a media understanding space") - mediaunderstandingspace = gr.HTML("Mdeia Understanding Space Chosen will load here") - mediaunderstandingspacebtn.click(display_website, inputs=linktomediaunderstandingspace, outputs=mediaunderstandingspace) - gr.HTML("Image Caption = https://huggingface.co/spaces/microsoft/Promptist (Prompt Lengthen) ") - - - with gr.Tab("Images"): - with gr.Accordion("Image Gen or Animation HF Spaces/Sites (Click Here to Open) - Have to download and upload at the the top", open=False): - # with gr.Tabs("General"): - with gr.Row(): - linktoimagegen = gr.Dropdown(choices=["--Text-Interleaved--", "https://ethanchern-anole.hf.space", - "--Hidden Image--", "https://ap123-illusiondiffusion.hf.space", - "--Panorama--", "https://gokaygokay-360panoimage.hf.space", - "--General--", "https://pixart-alpha-pixart-sigma.hf.space", "https://stabilityai-stable-diffusion-3-medium.hf.space", "https://prodia-sdxl-stable-diffusion-xl.hf.space", "https://prodia-fast-stable-diffusion.hf.space", "https://bytedance-hyper-sdxl-1step-t2i.hf.space", "https://multimodalart-cosxl.hf.space", "https://cagliostrolab-animagine-xl-3-1.hf.space", "https://stabilityai-stable-diffusion.hf.space", - "--Speed--", "https://radames-real-time-text-to-image-sdxl-lightning.hf.space", "https://ap123-sdxl-lightning.hf.space", - "--LORA Support--", "https://multimodalart-flux-lora-the-explorer.hf.space", "https://artificialguybr-artificialguybr-demo-lora.hf.space", "https://artificialguybr-studio-ghibli-lora-sdxl.hf.space", "https://artificialguybr-pixel-art-generator.hf.space", "https://fffiloni-sdxl-control-loras.hf.space", "https://ehristoforu-dalle-3-xl-lora-v2.hf.space", - "--Image to Image--", "https://gokaygokay-kolorsplusplus.hf.space", "https://lllyasviel-ic-light.hf.space", "https://gparmar-img2img-turbo-sketch.hf.space", - "--Upscaler--", "https://gokaygokay-tile-upscaler.hf.space", - "--Control of Pose--", "https://instantx-instantid.hf.space", "https://modelscope-transferanything.hf.space", "https://okaris-omni-zero.hf.space" - "--Control of Shapes--", "https://linoyts-scribble-sdxl-flash.hf.space", - "--Control of Text--", "", - "--Clothing Try on demos--", "https://kwai-kolors-kolors-virtual-try-on.hf.space", - "--Foreign Language Input--", "https://gokaygokay-kolors.hf.space"], label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) - imagegenspacebtn = gr.Button("Use the chosen URL to load interface with a image generation model") - - imagegenspace = gr.HTML("Image Space Chosen will load here") - imagegenspacebtn.click(display_website, inputs=linktoimagegen, outputs=imagegenspace) - - linkstobecollectednoembed = "https://artgan-diffusion-api.hf.space", "https://multimodalart-stable-cascade.hf.space", "https://google-sdxl.hf.space", "https://visionmaze-magic-me.hf.space", "https://segmind-segmind-stable-diffusion.hf.space", "https://simianluo-latent-consistency-model.hf.space", - gr.HTML("Concept Art, UI elements, Static/3D Characters, Environments and Objects") - gr.HTML("Images Generation General (3rd Party) = https://www.craiyon.com/") - gr.HTML("Images Generation Posters with text - https://huggingface.co/spaces/GlyphByT5/Glyph-SDXL-v2") - gr.HTML("SVG Generation = Coding models / SOTA LLM ") - gr.HTML("Images Generation - Upscaling - https://huggingface.co/spaces/gokaygokay/Tile-Upscaler") - gr.HTML("Vision Models for descriptions
https://huggingface.co/spaces/gokaygokay/Florence-2
https://huggingface.co/spaces/vilarin/VL-Chatbox - glm 4v 9b
") - gr.HTML("Upscalers (save data transfer costs? highly detailed characters?) - https://huggingface.co/spaces/gokaygokay/AuraSR") - gr.HTML("Placeholder for huggingface spaces that can assist ") - gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist") - - with gr.Tab("Video"): - with gr.Accordion("Video Spaces/Sites (Click Here to Open)", open=False): - with gr.Row(): - linktovideogenspace = gr.Dropdown(choices=["--Texttovid--", "https://kingnish-instant-video.hf.space", - "--General--", "https://zheyangqin-vader.hf.space", "https://kadirnar-open-sora.hf.space", - "--Talking Portrait--", "https://fffiloni-tts-hallo-talking-portrait.hf.space", - "--Gif / ImgtoImg based video--", "https://wangfuyun-animatelcm-svd.hf.space", "https://bytedance-animatediff-lightning.hf.space", "https://wangfuyun-animatelcm.hf.space", "https://guoyww-animatediff.hf.space",], - label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) - videogenspacebtn = gr.Button("Use the chosen URL to load interface with video generation") - videogenspace = gr.HTML("Video Space Chosen will load here") - videogenspacebtn.click(display_website, inputs=linktovideogenspace, outputs=videogenspace) - - gr.HTML("Cutscenes, Tutorials, Trailers") - gr.HTML("Portrait Video eg. Solo Taking NPC - https://huggingface.co/spaces/fffiloni/tts-hallo-talking-portrait (Image + Audio and combination) https://huggingface.co/spaces/KwaiVGI/LivePortrait (Non verbal communication eg. in a library, when running from a pursuer)") - gr.HTML("Placeholder for huggingface spaces that can assist - https://huggingface.co/spaces/KingNish/Instant-Video, https://huggingface.co/spaces/multimodalart/stable-video-diffusion, https://huggingface.co/spaces/multimodalart/stable-video-diffusion") - gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist") - gr.HTML("3rd Party / Closed Source - https://runwayml.com/
") - with gr.Tab("Animations (for lower resource use)"): - gr.HTML("Characters, Environments, Objects") - gr.HTML("Placeholder for huggingface spaces that can assist - image as 3d object in video https://huggingface.co/spaces/ashawkey/LGM") - gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist") + gen_3d_btn.click( + generate_3d_ui, + inputs=[gen_3d_prompt], + outputs=[gen_3d_status, gen_3d_output, file_dropdown] + ) - with gr.Tab("Audio"): - with gr.Accordion("Audio Spaces/Sites (Click Here to Open)", open=False): - with gr.Row(): - linktoaudiogenspace = gr.Dropdown(choices=["General", "https://artificialguybr-stable-audio-open-zero.hf.space", "", - "--Talking Portrait--","https://fffiloni-tts-hallo-talking-portrait.hf.space"], - label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) - audiogenspacebtn = gr.Button("Use the chosen URL to load interface with audio generation") - audiogenspace = gr.HTML("Audio Space Chosen will load here") - audiogenspacebtn.click(display_website, inputs=linktoaudiogenspace, outputs=audiogenspace) - gr.HTML("Music - Background, Interactive, Cutscene, Menu
Sound Effects - Environment, character, action (environmental triggered by user eg. gun), UI
Speech - Dialouge, narration, voiceover
The new render function means the Config can be made and iframe/api functions can be ordered as neccessary based on the part of the config that needs it to streamline workflows based on current state of config ") - gr.HTML("Placeholder for huggingface spaces that can assist") - gr.HTML("Audio Sound Effects - https://huggingface.co/spaces/artificialguybr/Stable-Audio-Open-Zero") - gr.HTML("Voices - Voice clone eg. actors part of your project - https://huggingface.co/spaces/tonyassi/voice-clone") - gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist") - gr.HTML("3rd Party / Closed Source - https://suno.com/
https://www.udio.com/") - - with gr.Tab("3D"): - with gr.Accordion("3D Model Spaces/Sites (Click Here to Open)", open=False): - with gr.Row(): - linktoThreedModel = gr.Dropdown(choices=["--Image prompt--", "https://vast-ai-charactergen.hf.space" - "--Video prompt--", "https://facebook-vggsfm.hf.space", - "--Text prompt--", "https://wuvin-unique3d.hf.space", "https://stabilityai-triposr.hf.space", "https://hysts-shap-e.hf.space", "https://tencentarc-instantmesh.hf.space", "https://ashawkey-lgm.hf.space", "https://dylanebert-lgm-mini.hf.space", "https://dylanebert-splat-to-mesh.hf.space", "https://dylanebert-multi-view-diffusion.hf.space"], label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) - ThreedModelspacebtn = gr.Button("Use the chosen URL to load interface with a 3D model") - ThreedModelspace = gr.HTML("3D Space Chosen will load here") - ThreedModelspacebtn.click(display_website, inputs=linktoThreedModel, outputs=ThreedModelspace) - gr.HTML("Characters, Environments, Objects") - gr.HTML("Placeholder for huggingface spaces that can assist - https://huggingface.co/spaces/dylanebert/3d-arena") - gr.HTML("Closed Source - https://www.meshy.ai/") - - with gr.Tab("Fonts"): - gr.HTML("Style of whole game, or locations, or characters") - gr.HTML("Placeholder for huggingface spaces that can assist - there was a space that could make letter into pictures based on the prompt but I cant find it now") - gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist") - - with gr.Tab("Shaders and related"): - with gr.Accordion("'Special Effects' Spaces/Sites (Click Here to Open)", open=False): - with gr.Row(): - linktospecialeffectsgenspace = gr.Dropdown(choices=["--Distorted Image--", "https://epfl-vilab-4m.hf.space", "https://epfl-vilab-multimae.hf.space"], - label="Choose/Cancel type any .hf.space link here (can also type a link)'", allow_custom_value=True) - specialeffectsgenspacebtn = gr.Button("Use the chosen URL to load interface with special effects generation") - specialeffectsgenspace = gr.HTML("Special Effects Space Chosen will load here") - specialeffectsgenspacebtn.click(display_website, inputs=linktospecialeffectsgenspace, outputs=specialeffectsgenspace) - gr.HTML("Any output that is not understood by the common person can be used as special effects eg. depth map filters on images etc.") - gr.HTML("Post-processing Effects, material effects, Particle systems, visual feedback") - gr.HTML("Visual Effects - eg. explosion can turn all items white for a moment, losing conciousness blurs whole screen") - gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist") - - with gr.Tab("NPCS"): - gr.HTML("For ideas on NPCS check: https://lifearchitect.ai/leta/, ") - with gr.Tab("Save files"): - gr.HTML("For Dynamic events overnight or when player is not active what can LLMS edit?

eg. Waiting for a letter from a random npc can be decided by the llm
eg. Improved Stats on certain days (eg. bitrthday)
Privacy
User Directed DLC eg. Rockstar Editor with local llm guide") - gr.HTML("Some ideas - In game websites eg. GTA esp stock markets, news; ") - gr.HTML("Placeholder for huggingface spaces that can assist - https://huggingface.co/nvidia/Nemotron-4-340B-Instruct (Purpose is supposed to be synthetic data generation), https://huggingface.co/spaces/gokaygokay/Gemma-2-llamacpp ") - gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist (9b and under)
initial floor for testing can be https://huggingface.co/spaces/Qwen/Qwen2-0.5B-Instruct, https://huggingface.co/spaces/Qwen/Qwen2-1.5b-instruct-demo, https://huggingface.co/spaces/stabilityai/stablelm-2-1_6b-zephyr, https://huggingface.co/spaces/IndexTeam/Index-1.9B, https://huggingface.co/microsoft/Phi-3-mini-4k-instruct") - with gr.Tab("Diagrams"): - gr.HTML("Claude 3.5 sonnet is very good with mermaid graphs - can used for maps, situational explanations") - with gr.Tab("Maths"): - gr.HTML("https://huggingface.co/spaces/AI-MO/math-olympiad-solver") - - with gr.Tab("Using gradio client / External Source as API provider"): - gr.HTML("State file managed and one item at time creation using gradio client") - with gr.Tab("Bulk Generate Assets"): - with gr.Tab("Load Configuration"): - with gr.Row(): - SPFManconfig_file = gr.File(label="Load Configuration File") - SPFManload_config_button = gr.Button("Load Configuration") - SPFManload_result = gr.Textbox(label="Load Result") + # Wire up Audio generation (also refreshes file dropdown) + def generate_audio_ui(prompt): + if not prompt or not prompt.strip(): + return "Please enter a prompt", None, gr.update() + result = SPFMangenerate_audio(prompt) + if "saved as" in result: + filepath = result.split("saved as ")[-1] + return result, filepath, gr.update(choices=get_all_media_files()) + return result, None, gr.update() - with gr.Tab("Manage Prompts"): - with gr.Accordion("Load the current game state that you want to add media to", open=False): - gr.Markdown("# Load and View Game Configuration") + quick_audio_btn.click( + generate_audio_ui, + inputs=[quick_audio_prompt], + outputs=[quick_audio_status, quick_audio_output, file_dropdown] + ) - with gr.Accordion("Click to hide config for space"): - SPFManJLconfig_input = gr.Textbox(label="Enter Game Configuration JSON", placeholder="Paste JSON config here", lines=4) - SPFManJLlocation_choice = gr.Dropdown(choices=[], label="Choose location") - SPFManJLpart_choice = gr.Dropdown(choices=[], label="Choose part") - SPFManJLconfig_view = gr.JSON(label="Configuration Details") + # Wire up TTS generation (also refreshes file dropdown) + def generate_tts_ui(text, model, voice): + if not text or not text.strip(): + return "Please enter text to speak", None, gr.update() + result = SPFMangenerate_tts(text, model=model, voice=voice) + if "saved as" in result: + filepath = result.split("saved as ")[-1] + return result, filepath, gr.update(choices=get_all_media_files()) + return result, None, gr.update() - SPFManJLconfig_input.change(fn=SPFManJLupdate_location_choices, inputs=SPFManJLconfig_input, outputs=[SPFManJLlocation_choice, SPFManJLpart_choice]) - SPFManJLlocation_choice.change(fn=SPFManJLupdate_part_choices, inputs=[SPFManJLconfig_input, SPFManJLlocation_choice], outputs=SPFManJLpart_choice) + gen_tts_btn.click( + generate_tts_ui, + inputs=[gen_tts_text, gen_tts_model, gen_tts_voice], + outputs=[gen_tts_status, gen_tts_output, file_dropdown] + ) - def SPFManJLupdate_config_view(config_json, location, part): - return SPFManJLget_config_part(config_json, location, part) + # Wire up Text-to-Video generation + def generate_t2v_ui(prompt, model, steps, duration, width, height): + if not prompt or not prompt.strip(): + return "Please enter a video prompt", None, gr.update() + from leveraging_machine_learning import generate_video_t2v + status, filepath = generate_video_t2v( + prompt, model_name=model, + num_steps=int(steps), duration_seconds=int(duration), + width=int(width), height=int(height) + ) + if filepath: + return status, filepath, gr.update(choices=get_all_media_files()) + return status, None, gr.update() - SPFManJLpart_choice.change(fn=SPFManJLupdate_config_view, inputs=[SPFManJLconfig_input, SPFManJLlocation_choice, SPFManJLpart_choice], outputs=SPFManJLconfig_view) + t2v_btn.click( + generate_t2v_ui, + inputs=[t2v_prompt, t2v_model, t2v_steps, t2v_duration, t2v_width, t2v_height], + outputs=[t2v_status, t2v_output, file_dropdown] + ) - gr.Interface(SPFManJLPromptSuggestionGradClient, inputs=[SPFManJLconfig_view, "text"], outputs=["text"], description="Ask a random LLM for suggestions eg. bring life to the game by focusing more on the description") + # Wire up Image-to-Video generation + def generate_i2v_ui(image, prompt, model, steps, duration): + if not image: + return "Please upload an image", None, gr.update() + from leveraging_machine_learning import generate_video_i2v + status, filepath = generate_video_i2v( + image, prompt=prompt or "", model_name=model, + num_steps=int(steps), duration_seconds=int(duration) + ) + if filepath: + return status, filepath, gr.update(choices=get_all_media_files()) + return status, None, gr.update() - with gr.Row(): - SPFManprompt_type = gr.Dropdown(["image", "audio"], label="Prompt Type") - SPFManprompt = gr.Textbox(label="Prompt") - with gr.Row(): - SPFManadd_button = gr.Button("Add Prompt") - SPFManclear_button = gr.Button("Clear Prompts") - SPFManauto_generate_button = gr.Button("Auto-Generate Prompt") - with gr.Row(): - SPFManprompt_output = gr.Textbox(label="Added Prompts") - SPFManprompt_count = gr.Number(value=0, label="Number of Prompts") - SPFManview_prompts_button = gr.Button("View All Prompts") - SPFManall_prompts_output = gr.Textbox(label="All Prompts", lines=10) - #SPFManconfig_view = gr.Textbox(label="Current Configuration", lines=10) + i2v_btn.click( + generate_i2v_ui, + inputs=[i2v_image, i2v_prompt, i2v_model, i2v_steps, i2v_duration], + outputs=[i2v_status, i2v_output, file_dropdown] + ) - # New JSON input for loading configurations - #SPFManjson_input = gr.Textbox(label="JSON Configuration", lines=10) - #SPFManload_json_button = gr.Button("Load JSON Configuration") + # Wire up Video/Talking Head generation (also refreshes file dropdown) + def generate_video_ui(image_path, audio_path, text): + if not image_path: + return "Please upload a portrait image", None, gr.update() + if not audio_path and (not text or not text.strip()): + return "Please provide audio OR enter text for TTS", None, gr.update() + # Use audio if provided, otherwise use text + audio_to_use = audio_path if audio_path else None + text_to_use = text if text and text.strip() else None + result = SPFMangenerate_talking_head(image_path, audio_to_use, text_to_use) + if "saved as" in result: + filepath = result.split("saved as ")[-1] + return result, filepath, gr.update(choices=get_all_media_files()) + return result, None, gr.update() - # Link the button to the function - #SPFManload_json_button.click(SPFManload_json_configuration, inputs=[SPFManjson_input], outputs=[load_result, prompt_count]) + gen_video_btn.click( + generate_video_ui, + inputs=[gen_video_image, gen_video_audio, gen_video_text], + outputs=[gen_video_status, gen_video_output, file_dropdown] + ) - with gr.Tab("API Settings"): - with gr.Row(): - SPFManapi_provider = gr.Textbox(label="API Provider", value=lambda: SPFManstate["api_provider"]) - SPFMancost_per_item = gr.Number(label="Cost per Item", value=lambda: SPFManstate["cost_per_item"]) - SPFManupdate_api_button = gr.Button("Update API Details") - SPFManapi_update_result = gr.Textbox(label="API Update Result") + # ============================================================ + # TAB 3: USE IN CONFIG + # ============================================================ + with gr.Tab("Use in Config"): + gr.Markdown("## Bridge Media to Game Config") - with gr.Tab("Generate Files and Media"): - with gr.Row(): - SPFManoutput_dir = gr.Textbox(label="Output Directory", value=lambda: SPFManstate["output_dir"]) - SPFManresume = gr.Checkbox(label="Resume from last run", value=True) - with gr.Row(): - SPFManpaid_api_checkbox = gr.Checkbox(label="Use Paid API", value=lambda: SPFManstate["is_paid_api"]) - SPFMancost_estimate = gr.Textbox(label="Estimated Cost") - SPFMangenerate_button = gr.Button("Generate Files and Media") - SPFManskip_button = gr.Button("Skip Current Item") - SPFManresults = gr.Textbox(label="Results", lines=10) - SPFManerror_output = gr.Textbox(label="Errors", lines=5) - SPFManzip_button = gr.Button("Zip Output Files") - SPFManzip_result = gr.Textbox(label="Zip Result") - - SPFManload_config_button.click(SPFManload_config_file, inputs=[SPFManconfig_file], outputs=[SPFManload_result, SPFManprompt_count]) - SPFManadd_button.click(SPFManadd_prompt, inputs=[SPFManprompt_type, SPFManprompt], outputs=[SPFManprompt_output, SPFManprompt_count]) - SPFManclear_button.click(SPFManclear_prompts, outputs=[SPFManprompt_output, SPFManprompt_count]) - SPFManview_prompts_button.click(SPFManview_all_prompts, outputs=SPFManall_prompts_output) - SPFManauto_generate_button.click(SPFManauto_generate_prompt, inputs=[SPFManprompt_type], outputs=[SPFManprompt]) - SPFManupdate_api_button.click(SPFManupdate_api_details, inputs=[SPFManapi_provider, SPFMancost_per_item], outputs=[SPFManapi_update_result]) - SPFManpaid_api_checkbox.change(SPFMantoggle_paid_api, inputs=[SPFManpaid_api_checkbox], outputs=[SPFMancost_estimate]) - SPFMangenerate_button.click(SPFMancreate_files_with_generation, inputs=[SPFManresume], outputs=SPFManresults) - SPFManskip_button.click(SPFManskip_item, outputs=SPFManresults) - SPFManzip_button.click(SPFManzip_files, outputs=SPFManzip_result) - gr.Textbox(label="Configuration", value=SPFManview_config, every=1) - - with gr.Tab("Replicate - API Definitions crucial models"): - gr.HTML("Many custom models set-up and the ability to set up the neccesary ones using cog
https://replicate.com/collections/text-to-image | https://replicate.com/collections/text-to-video | https://replicate.com/collections/audio-generation https://replicate.com/collections/text-to-speech | https://replicate.com/collections/3d-models ") - gr.Code("""Install Replicate’s Python client library -pip install replicate - -Copy -Set the REPLICATE_API_TOKEN environment variable -export REPLICATE_API_TOKEN= - -Visibility - -Copy -Find your API token in your account settings. - -Import the client -import replicate - -Copy -Run meta/musicgen using Replicate’s API. Check out the model's schema for an overview of inputs and outputs. - -output = replicate.run( - "meta/musicgen:671ac645ce5e552cc63a54a2bbff63fcf798043055d2dac5fc9e36a837eedcfb", - input={ - "top_k": 250, - "top_p": 0, - "prompt": "Edo25 major g melodies that sound triumphant and cinematic. Leading up to a crescendo that resolves in a 9th harmonic", - "duration": 8, - "temperature": 1, - "continuation": False, - "model_version": "stereo-large", - "output_format": "mp3", - "continuation_start": 0, - "multi_band_diffusion": False, - "normalization_strategy": "peak", - "classifier_free_guidance": 3 - } -) -print(output)""", language="python") - - with gr.Tab("Cloud Computing"): - gr.HTML("Storage, AI api access, VMs, ") + with gr.Accordion("Media Suggestions based on config", open=False): + gr.HTML("Either duplicate the interface on the api side or move it out so that both sides can use it") + with gr.Tab("NPCs"): + gr.HTML("For ideas on NPCS check: https://lifearchitect.ai/leta/, ") - with gr.Tab("Using 3rd Party Interface - free/paid"): - gr.HTML("General - https://artificialanalysis.ai/models/llama-3-1-instruct-405b/providers") - gr.HTML("Image - https://playground.com/login | https://www.midjourney.com/showcase") - gr.HTML("Audio - https://www.udio.com/home https://www.udio.com/pricing | https://suno.com/ ") - gr.HTML("Video - https://lumalabs.ai/dream-machine https://lumalabs.ai/dream-machine/pricing (free 30 gens a month) | https://klingai.com/ - https://www.reddit.com/r/singularity/comments/1fk4tgp/kling_ai_showcasing_the_use_of_the_motion_brush/ | https://www.hedra.com/") - gr.HTML("3D - https://www.csm.ai/ | https://www.tripo3d.ai/ | https://zoo.dev/text-to-cad") - gr.HTML("Multiple - https://fal.ai/pricing | ") + with gr.Tab("Save Files"): + gr.HTML("For Dynamic events overnight or when player is not active what can LLMS edit?

eg. Waiting for a letter from a random npc can be decided by the llm
eg. Improved Stats on certain days (eg. bitrthday)
Privacy
User Directed DLC eg. Rockstar Editor with local llm guide") + gr.HTML("Some ideas - In game websites eg. GTA esp stock markets, news; ") + gr.HTML("Placeholder for huggingface spaces that can assist - https://huggingface.co/nvidia/Nemotron-4-340B-Instruct (Purpose is supposed to be synthetic data generation), https://huggingface.co/spaces/gokaygokay/Gemma-2-llamacpp ") + gr.HTML("Placeholder for models small enough to run on cpu here in this space that can assist (9b and under)
initial floor for testing can be https://huggingface.co/spaces/Qwen/Qwen2-0.5B-Instruct, https://huggingface.co/spaces/Qwen/Qwen2-1.5b-instruct-demo, https://huggingface.co/spaces/stabilityai/stablelm-2-1_6b-zephyr, https://huggingface.co/spaces/IndexTeam/Index-1.9B, https://huggingface.co/microsoft/Phi-3-mini-4k-instruct") + + with gr.Tab("Diagrams"): + gr.HTML("Claude 3.5 sonnet is very good with mermaid graphs - can used for maps, situational explanations") - with gr.Tab("Examples of Generated Media on Reddit"): - gr.HTML("https://www.reddit.com/r/singularity/comments/1fknejj/facecamai_lets_anyone_convert_a_single_image_into/
https://www.reddit.com/r/singularity/comments/1fjylow/tripo_v20_is_out_now_you_can_create_stunning_3d/") + with gr.Tab("Maths"): + gr.HTML("https://huggingface.co/spaces/AI-MO/math-olympiad-solver") - with gr.Tab("Licensing"): - gr.HTML("To be continued.... Need to find the press release to see license eg. https://blackforestlabs.ai/announcing-black-forest-labs/") + # NOTE: Resources tab moved to Mini-Tutorial & Resources accordion in app.py - return filemanager \ No newline at end of file + return filemanager diff --git a/ui_tabs/__init__.py b/ui_tabs/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..77d70e435bd133b4976e2551e1dd61faf8a03938 --- /dev/null +++ b/ui_tabs/__init__.py @@ -0,0 +1,29 @@ +"""UI Tabs Package for Game Config IDE. + +This package contains modular UI tab components that can be +imported and used within the main Gradio application. +""" + +from .story_graph_tab import create_story_graph_tab +from .study_3d_tab import create_3d_study_tab +from .resources_hub_tab import create_resources_hub_tab +from .mechanic_translation_tab import create_mechanic_translation_tab +from .llm_playtest_tab import create_llm_playtest_tab +from .big_rpg_scale_tab import create_big_rpg_scale_tab +from .dnd_gm_tab import create_dnd_gm_tab +from .story_architect_tab import create_story_architect_tab +from .narrative_engine_tab import create_narrative_engine_tab +from .config_analysis_tab import create_config_analysis_tab + +__all__ = [ + 'create_story_graph_tab', + 'create_3d_study_tab', + 'create_resources_hub_tab', + 'create_mechanic_translation_tab', + 'create_llm_playtest_tab', + 'create_big_rpg_scale_tab', + 'create_dnd_gm_tab', + 'create_story_architect_tab', + 'create_narrative_engine_tab', + 'create_config_analysis_tab', +] diff --git a/ui_tabs/big_rpg_scale_tab.py b/ui_tabs/big_rpg_scale_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..0768e86dd1df9d35d2aa024be33fe4de1866f2f6 --- /dev/null +++ b/ui_tabs/big_rpg_scale_tab.py @@ -0,0 +1,4474 @@ +"""Big RPG Scale Tab - Large-Scale RPG Planning Tools. + +This module provides tools for planning RPGs at Skyrim-level complexity: +- Entity Database: Define NPCs, items, locations, factions +- Quest Designer: Create quests with stages, objectives, radiant templates +- World Builder: Place locations with coordinates, define regions +- NPC Scheduler: Daily routines, AI packages, schedule templates +""" + +import json +import math +import statistics +import random +import gradio as gr +import spaces +from dataclasses import dataclass, field, asdict +from typing import List, Dict, Optional, Any, Tuple +from datetime import datetime + + +# ==================== LAZY IMPORTS FOR GPU ==================== +_torch = None +_transformers = None + +def _ensure_torch(): + """Lazy import torch.""" + global _torch + if _torch is None: + import torch + _torch = torch + return _torch + +def _ensure_transformers(): + """Lazy import transformers.""" + global _transformers + if _transformers is None: + import transformers + _transformers = transformers + return _transformers + + +# ==================== LLM MODEL MANAGEMENT ==================== +_rpg_model = None +_rpg_tokenizer = None +_rpg_model_name = None + +def _ensure_rpg_model(model_name="Qwen/Qwen3-4B"): + """Lazy load the RPG generation model, reloading if model changes.""" + global _rpg_model, _rpg_tokenizer, _rpg_model_name + + if _rpg_model is None or _rpg_model_name != model_name: + tf = _ensure_transformers() + print(f"Loading RPG generation model: {model_name}") + + if _rpg_model is not None: + del _rpg_model + del _rpg_tokenizer + _torch = _ensure_torch() + if _torch.cuda.is_available(): + _torch.cuda.empty_cache() + + _rpg_tokenizer = tf.AutoTokenizer.from_pretrained(model_name) + _rpg_model = tf.AutoModelForCausalLM.from_pretrained( + model_name, + torch_dtype="auto", + device_map="auto" + ) + _rpg_model_name = model_name + print(f"RPG model loaded successfully: {model_name}") + + return _rpg_model, _rpg_tokenizer + + +# ==================== DATA MODELS ==================== + +@dataclass +class Entity: + """Base entity class for all game objects.""" + id: str + type: str + name: str + properties: Dict[str, Any] = field(default_factory=dict) + relationships: Dict[str, Any] = field(default_factory=dict) + +@dataclass +class Location: + """Location entity with spatial data.""" + id: str + name: str + region: str = "" + coordinates: Dict[str, float] = field(default_factory=lambda: {"x": 0, "y": 0}) + location_type: str = "generic" # city, town, dungeon, camp, landmark + population: int = 0 + connected_to: List[str] = field(default_factory=list) + interior_cells: List[str] = field(default_factory=list) + +@dataclass +class NPC: + """NPC entity with schedule and relationships.""" + id: str + name: str + race: str = "human" + role: str = "commoner" + level: int = 1 + essential: bool = False + home_location: str = "" + faction_memberships: List[Dict] = field(default_factory=list) + schedule: List[Dict] = field(default_factory=list) + dialogue_topics: List[str] = field(default_factory=list) + +@dataclass +class QuestStage: + """Single stage in a quest.""" + stage: int + journal_entry: str + objectives: List[Dict] = field(default_factory=list) + on_complete: Dict = field(default_factory=dict) + +@dataclass +class Quest: + """Quest entity with stages and rewards.""" + id: str + name: str + quest_type: str = "side" # main, faction, side, radiant + stages: List[QuestStage] = field(default_factory=list) + rewards: Dict = field(default_factory=dict) + prerequisites: List[str] = field(default_factory=list) + +@dataclass +class Item: + """Item entity with properties.""" + id: str + name: str + item_type: str = "misc" # weapon, armor, consumable, ingredient, misc + weight: float = 0.0 + value: int = 0 + properties: Dict = field(default_factory=dict) + +@dataclass +class Faction: + """Faction entity with ranks and relationships.""" + id: str + name: str + faction_type: str = "guild" # guild, government, religious, criminal, military + ranks: List[Dict] = field(default_factory=list) # [{name, level, requirements}] + relationships: Dict[str, int] = field(default_factory=dict) # faction_id -> disposition (-100 to 100) + join_requirements: Dict = field(default_factory=dict) + headquarters: str = "" + +@dataclass +class DialogueTopic: + """Dialogue topic with conditions and responses.""" + id: str + name: str + speaker_id: str = "" + priority: int = 50 # Higher = shown first + conditions: List[Dict] = field(default_factory=list) # [{type, value}] + response_text: str = "" + effects: List[Dict] = field(default_factory=list) # [{type, value}] + next_topics: List[str] = field(default_factory=list) + +@dataclass +class Skill: + """Skill definition with XP rules.""" + id: str + name: str + category: str = "combat" # combat, magic, stealth, crafting + description: str = "" + governing_attribute: str = "" + xp_actions: List[Dict] = field(default_factory=list) # [{action, xp_amount}] + perks: List[str] = field(default_factory=list) + +@dataclass +class Perk: + """Perk in a skill tree.""" + id: str + name: str + skill_id: str = "" + skill_requirement: int = 0 + description: str = "" + effects: List[Dict] = field(default_factory=list) + prerequisites: List[str] = field(default_factory=list) # Other perk IDs + cost: int = 1 + +@dataclass +class Recipe: + """Crafting recipe.""" + id: str + name: str + recipe_type: str = "smithing" # smithing, alchemy, enchanting, cooking + ingredients: List[Dict] = field(default_factory=list) # [{item_id, count}] + result: Dict = field(default_factory=dict) # {item_id, count} + skill_requirement: Dict = field(default_factory=dict) # {skill_id, level} + workstation: str = "" + + +# ==================== ENTITY DATABASE ==================== + +class EntityDatabase: + """In-memory entity database for the session.""" + + def __init__(self): + self.locations: Dict[str, Location] = {} + self.npcs: Dict[str, NPC] = {} + self.quests: Dict[str, Quest] = {} + self.items: Dict[str, Item] = {} + self.factions: Dict[str, Faction] = {} + self.dialogues: Dict[str, DialogueTopic] = {} + self.skills: Dict[str, Skill] = {} + self.perks: Dict[str, Perk] = {} + self.recipes: Dict[str, Recipe] = {} + + def add_location(self, loc: Location): + self.locations[loc.id] = loc + + def add_npc(self, npc: NPC): + self.npcs[npc.id] = npc + + def add_quest(self, quest: Quest): + self.quests[quest.id] = quest + + def add_item(self, item: Item): + self.items[item.id] = item + + def add_faction(self, faction: Faction): + self.factions[faction.id] = faction + + def add_dialogue(self, dialogue: DialogueTopic): + self.dialogues[dialogue.id] = dialogue + + def add_skill(self, skill: Skill): + self.skills[skill.id] = skill + + def add_perk(self, perk: Perk): + self.perks[perk.id] = perk + + def add_recipe(self, recipe: Recipe): + self.recipes[recipe.id] = recipe + + def to_json(self) -> str: + return json.dumps({ + "locations": {k: asdict(v) for k, v in self.locations.items()}, + "npcs": {k: asdict(v) for k, v in self.npcs.items()}, + "quests": {k: asdict(v) for k, v in self.quests.items()}, + "items": {k: asdict(v) for k, v in self.items.items()}, + "factions": {k: asdict(v) for k, v in self.factions.items()}, + "dialogues": {k: asdict(v) for k, v in self.dialogues.items()}, + "skills": {k: asdict(v) for k, v in self.skills.items()}, + "perks": {k: asdict(v) for k, v in self.perks.items()}, + "recipes": {k: asdict(v) for k, v in self.recipes.items()}, + }, indent=2) + + def from_json(self, json_str: str): + """Import database from JSON string.""" + try: + data = json.loads(json_str) + # Parse locations + if "locations" in data: + for loc_id, loc_data in data["locations"].items(): + self.locations[loc_id] = Location(**loc_data) + # Parse NPCs + if "npcs" in data: + for npc_id, npc_data in data["npcs"].items(): + self.npcs[npc_id] = NPC(**npc_data) + # Parse quests + if "quests" in data: + for quest_id, quest_data in data["quests"].items(): + stages = quest_data.pop("stages", []) + quest_stages = [QuestStage(**s) for s in stages] + self.quests[quest_id] = Quest(**quest_data, stages=quest_stages) + # Parse items + if "items" in data: + for item_id, item_data in data["items"].items(): + self.items[item_id] = Item(**item_data) + # Parse factions + if "factions" in data: + for fac_id, fac_data in data["factions"].items(): + self.factions[fac_id] = Faction(**fac_data) + # Parse dialogues + if "dialogues" in data: + for dlg_id, dlg_data in data["dialogues"].items(): + self.dialogues[dlg_id] = DialogueTopic(**dlg_data) + # Parse skills + if "skills" in data: + for skill_id, skill_data in data["skills"].items(): + self.skills[skill_id] = Skill(**skill_data) + # Parse perks + if "perks" in data: + for perk_id, perk_data in data["perks"].items(): + self.perks[perk_id] = Perk(**perk_data) + # Parse recipes + if "recipes" in data: + for recipe_id, recipe_data in data["recipes"].items(): + self.recipes[recipe_id] = Recipe(**recipe_data) + return True, "Database imported successfully" + except Exception as e: + return False, f"Import error: {str(e)}" + + def get_stats(self) -> Dict: + return { + "locations": len(self.locations), + "npcs": len(self.npcs), + "quests": len(self.quests), + "items": len(self.items), + "factions": len(self.factions), + "dialogues": len(self.dialogues), + "skills": len(self.skills), + "perks": len(self.perks), + "recipes": len(self.recipes), + } + + def clear(self): + """Clear all entities from the database.""" + self.locations.clear() + self.npcs.clear() + self.quests.clear() + self.items.clear() + self.factions.clear() + self.dialogues.clear() + self.skills.clear() + self.perks.clear() + self.recipes.clear() + + def delete_location(self, loc_id: str) -> bool: + if loc_id in self.locations: + del self.locations[loc_id] + return True + return False + + def delete_npc(self, npc_id: str) -> bool: + if npc_id in self.npcs: + del self.npcs[npc_id] + return True + return False + + def delete_quest(self, quest_id: str) -> bool: + if quest_id in self.quests: + del self.quests[quest_id] + return True + return False + + def delete_item(self, item_id: str) -> bool: + if item_id in self.items: + del self.items[item_id] + return True + return False + + def delete_faction(self, faction_id: str) -> bool: + if faction_id in self.factions: + del self.factions[faction_id] + return True + return False + + def delete_dialogue(self, dlg_id: str) -> bool: + if dlg_id in self.dialogues: + del self.dialogues[dlg_id] + return True + return False + + def delete_skill(self, skill_id: str) -> bool: + if skill_id in self.skills: + del self.skills[skill_id] + return True + return False + + def validate(self) -> Dict[str, List[str]]: + """Validate cross-entity references. Returns dict of issues by category.""" + issues = { + "errors": [], + "warnings": [], + "info": [] + } + + # Check NPC home locations + for npc_id, npc in self.npcs.items(): + if npc.home_location and npc.home_location not in self.locations: + issues["errors"].append(f"NPC '{npc.name}' references non-existent home location '{npc.home_location}'") + + # Check NPC faction memberships + for npc_id, npc in self.npcs.items(): + for membership in npc.faction_memberships: + faction_id = membership.get("faction", "") + if faction_id and faction_id not in self.factions: + issues["warnings"].append(f"NPC '{npc.name}' belongs to undefined faction '{faction_id}'") + + # Check faction headquarters + for fac_id, faction in self.factions.items(): + if faction.headquarters and faction.headquarters not in self.locations: + issues["errors"].append(f"Faction '{faction.name}' references non-existent HQ '{faction.headquarters}'") + + # Check faction relationships reference valid factions + for fac_id, faction in self.factions.items(): + for related_fac in faction.relationships.keys(): + if related_fac not in self.factions: + issues["warnings"].append(f"Faction '{faction.name}' has relationship with undefined faction '{related_fac}'") + + # Check dialogue speakers + for dlg_id, dialogue in self.dialogues.items(): + if dialogue.speaker_id and dialogue.speaker_id not in self.npcs: + issues["errors"].append(f"Dialogue '{dialogue.name}' references non-existent speaker '{dialogue.speaker_id}'") + + # Check dialogue next_topics + for dlg_id, dialogue in self.dialogues.items(): + for next_topic in dialogue.next_topics: + if next_topic not in self.dialogues: + issues["warnings"].append(f"Dialogue '{dialogue.name}' links to undefined topic '{next_topic}'") + + # Check perk skill references + for perk_id, perk in self.perks.items(): + if perk.skill_id and perk.skill_id not in self.skills: + issues["errors"].append(f"Perk '{perk.name}' references non-existent skill '{perk.skill_id}'") + + # Check perk prerequisites + for perk_id, perk in self.perks.items(): + for prereq in perk.prerequisites: + if prereq not in self.perks: + issues["warnings"].append(f"Perk '{perk.name}' requires undefined perk '{prereq}'") + + # Check quest prerequisites + for quest_id, quest in self.quests.items(): + for prereq in quest.prerequisites: + if prereq not in self.quests: + issues["warnings"].append(f"Quest '{quest.name}' requires undefined quest '{prereq}'") + + # Check location connections + for loc_id, loc in self.locations.items(): + for connected in loc.connected_to: + if connected not in self.locations: + issues["warnings"].append(f"Location '{loc.name}' connected to undefined location '{connected}'") + + # Check recipe ingredients + for recipe_id, recipe in self.recipes.items(): + for ingredient in recipe.ingredients: + item_id = ingredient.get("item_id", "") + if item_id and item_id not in self.items: + issues["warnings"].append(f"Recipe '{recipe.name}' uses undefined ingredient '{item_id}'") + result_id = recipe.result.get("item_id", "") + if result_id and result_id not in self.items: + issues["warnings"].append(f"Recipe '{recipe.name}' produces undefined item '{result_id}'") + + # Summary info + stats = self.get_stats() + total_entities = sum(stats.values()) + issues["info"].append(f"Total entities: {total_entities}") + issues["info"].append(f"Locations: {stats['locations']}, NPCs: {stats['npcs']}, Quests: {stats['quests']}") + issues["info"].append(f"Items: {stats['items']}, Factions: {stats['factions']}, Skills: {stats['skills']}") + + return issues + + def analyze_derivative_changes(self, entity_type: str, entity_id: str, change_type: str = "modify") -> Dict: + """ + Analyze what would be affected if you change an entity. + + Returns a dict with: + - direct_impacts: entities that directly reference this one + - semantic_suggestions: property-based change suggestions + - improvement_focus: which improvement focus (env/char/choice/tension) applies + """ + result = { + "entity_type": entity_type, + "entity_id": entity_id, + "entity_name": "", + "change_type": change_type, + "direct_impacts": [], + "semantic_suggestions": [], + "improvement_focus": [], + "cascade_warning": "" + } + + # Get the entity being changed + entity = None + if entity_type == "location" and entity_id in self.locations: + entity = self.locations[entity_id] + result["entity_name"] = entity.name + elif entity_type == "npc" and entity_id in self.npcs: + entity = self.npcs[entity_id] + result["entity_name"] = entity.name + elif entity_type == "faction" and entity_id in self.factions: + entity = self.factions[entity_id] + result["entity_name"] = entity.name + elif entity_type == "quest" and entity_id in self.quests: + entity = self.quests[entity_id] + result["entity_name"] = entity.name + elif entity_type == "item" and entity_id in self.items: + entity = self.items[entity_id] + result["entity_name"] = entity.name + elif entity_type == "dialogue" and entity_id in self.dialogues: + entity = self.dialogues[entity_id] + result["entity_name"] = entity.name + + if not entity: + result["cascade_warning"] = f"Entity {entity_type}/{entity_id} not found" + return result + + # Find direct references based on entity type + if entity_type == "location": + # Who lives here? + for npc_id, npc in self.npcs.items(): + if npc.home_location == entity_id: + result["direct_impacts"].append({ + "type": "npc", "id": npc_id, "name": npc.name, + "relationship": "lives here", + "action": "Update home_location or NPC's dialogue about home" + }) + # Which factions HQ here? + for fac_id, fac in self.factions.items(): + if fac.headquarters == entity_id: + result["direct_impacts"].append({ + "type": "faction", "id": fac_id, "name": fac.name, + "relationship": "headquarters here", + "action": "Update faction HQ or faction description" + }) + # What locations connect here? + for loc_id, loc in self.locations.items(): + if entity_id in loc.connected_to: + result["direct_impacts"].append({ + "type": "location", "id": loc_id, "name": loc.name, + "relationship": "connected to this", + "action": "Update travel connections" + }) + # Improvement focus + result["improvement_focus"] = ["environment", "tension"] + result["semantic_suggestions"] = [ + "If changing region: Update regional description themes for all NPCs here", + "If changing type (city→dungeon): Update NPC schedules, remove merchants", + "If changing name: Update dialogue that mentions this location by name" + ] + + elif entity_type == "npc": + # What dialogue belongs to this NPC? + for dlg_id, dlg in self.dialogues.items(): + if dlg.speaker_id == entity_id: + result["direct_impacts"].append({ + "type": "dialogue", "id": dlg_id, "name": dlg.name, + "relationship": "spoken by this NPC", + "action": "Update dialogue tone/content to match NPC changes" + }) + # What quests involve this NPC? (check objectives) + for quest_id, quest in self.quests.items(): + for stage in quest.stages: + for obj in stage.objectives: + if obj.get("target") == entity_id or obj.get("npc") == entity_id: + result["direct_impacts"].append({ + "type": "quest", "id": quest_id, "name": quest.name, + "relationship": f"objective involves this NPC", + "action": "Update quest objective or journal text" + }) + break + # What other NPCs mention this one in dialogue? + for dlg_id, dlg in self.dialogues.items(): + if entity.name.lower() in dlg.response_text.lower() and dlg.speaker_id != entity_id: + result["direct_impacts"].append({ + "type": "dialogue", "id": dlg_id, "name": dlg.name, + "relationship": "mentions this NPC", + "action": "Update dialogue text if NPC name/role changed" + }) + # Improvement focus + result["improvement_focus"] = ["characters", "choices"] + result["semantic_suggestions"] = [ + f"If changing role ({entity.role}→X): Update all dialogue topics for role-appropriate content", + "If changing faction membership: Update dialogue about faction, faction ranks", + "If changing home_location: Update schedule, travel patterns, regional dialogue", + "If making essential: Consider adding death-substitute dialogue" + ] + + elif entity_type == "faction": + # Who belongs to this faction? + for npc_id, npc in self.npcs.items(): + for membership in npc.faction_memberships: + if membership.get("faction") == entity_id: + result["direct_impacts"].append({ + "type": "npc", "id": npc_id, "name": npc.name, + "relationship": f"member (rank: {membership.get('rank', '?')})", + "action": "Update NPC dialogue about faction" + }) + # What factions have relationships with this one? + for fac_id, fac in self.factions.items(): + if entity_id in fac.relationships: + disposition = fac.relationships[entity_id] + result["direct_impacts"].append({ + "type": "faction", "id": fac_id, "name": fac.name, + "relationship": f"disposition {disposition}", + "action": "Update inter-faction dialogue and quest availability" + }) + # Faction quests + for quest_id, quest in self.quests.items(): + if quest.quest_type == "faction": + # Check if quest mentions faction in name or rewards + if entity.name.lower() in quest.name.lower(): + result["direct_impacts"].append({ + "type": "quest", "id": quest_id, "name": quest.name, + "relationship": "faction quest", + "action": "Update quest narrative and rewards" + }) + # Improvement focus + result["improvement_focus"] = ["characters", "tension"] + result["semantic_suggestions"] = [ + "If changing faction_type: Update member NPC roles to match", + "If changing relationships: Update cross-faction dialogue (allies vs enemies)", + "If changing ranks: Update NPC rank titles in dialogue", + "If changing HQ: Update faction-specific location descriptions" + ] + + elif entity_type == "quest": + # What quests require this as prerequisite? + for quest_id, quest in self.quests.items(): + if entity_id in quest.prerequisites: + result["direct_impacts"].append({ + "type": "quest", "id": quest_id, "name": quest.name, + "relationship": "requires this quest", + "action": "Update quest chain logic" + }) + # Improvement focus + result["improvement_focus"] = ["choices", "tension"] + result["semantic_suggestions"] = [ + "If changing stages: Update journal entries for narrative flow", + "If changing rewards: Balance against prerequisite quest difficulty", + "If changing objectives: Update NPC dialogue hints about objectives" + ] + + elif entity_type == "item": + # What recipes use this item? + for recipe_id, recipe in self.recipes.items(): + for ing in recipe.ingredients: + if ing.get("item_id") == entity_id: + result["direct_impacts"].append({ + "type": "recipe", "id": recipe_id, "name": recipe.name, + "relationship": "uses as ingredient", + "action": "Update recipe or substitute ingredient" + }) + if recipe.result.get("item_id") == entity_id: + result["direct_impacts"].append({ + "type": "recipe", "id": recipe_id, "name": recipe.name, + "relationship": "produces this item", + "action": "Update recipe result" + }) + # What quests involve this item? + for quest_id, quest in self.quests.items(): + for stage in quest.stages: + for obj in stage.objectives: + if obj.get("item") == entity_id: + result["direct_impacts"].append({ + "type": "quest", "id": quest_id, "name": quest.name, + "relationship": "objective requires this item", + "action": "Update quest objective" + }) + # Improvement focus + result["improvement_focus"] = ["environment", "choices"] + result["semantic_suggestions"] = [ + "If changing value: Update merchant dialogue about item worth", + "If changing weight: Consider inventory management impact", + "If changing item_type: Update which merchants sell it" + ] + + # Generate cascade warning + if len(result["direct_impacts"]) > 5: + result["cascade_warning"] = f"⚠️ High impact: {len(result['direct_impacts'])} entities affected. Consider incremental changes." + elif len(result["direct_impacts"]) > 0: + result["cascade_warning"] = f"ℹ️ {len(result['direct_impacts'])} entities reference this. Review each before finalizing." + else: + result["cascade_warning"] = "✓ No direct references found. Safe to modify." + + return result + + def format_derivative_report(self, analysis: Dict) -> str: + """Format derivative analysis as readable markdown.""" + lines = [] + lines.append(f"## Derivative Change Analysis") + lines.append(f"**Entity:** {analysis['entity_type'].title()} / {analysis['entity_name']} (`{analysis['entity_id']}`)") + lines.append(f"**Change Type:** {analysis['change_type']}") + lines.append("") + lines.append(f"### {analysis['cascade_warning']}") + lines.append("") + + if analysis["improvement_focus"]: + focuses = ", ".join(f"**{f.title()}**" for f in analysis["improvement_focus"]) + lines.append(f"**Improvement Focus Areas:** {focuses}") + lines.append("") + + if analysis["direct_impacts"]: + lines.append("### Direct Impacts") + lines.append("| Type | Name | Relationship | Suggested Action |") + lines.append("|------|------|--------------|------------------|") + for impact in analysis["direct_impacts"]: + lines.append(f"| {impact['type']} | {impact['name']} | {impact['relationship']} | {impact['action']} |") + lines.append("") + + if analysis["semantic_suggestions"]: + lines.append("### Semantic Suggestions") + for suggestion in analysis["semantic_suggestions"]: + lines.append(f"- {suggestion}") + lines.append("") + + return "\n".join(lines) + + def load_demo_data(self): + """Load demonstration data for a Skyrim-like fantasy RPG.""" + self.clear() + + # === LOCATIONS === + demo_locations = [ + Location("whiterun", "Whiterun", "Whiterun Hold", {"x": 100, "y": 100}, "city", 200, ["riverwood", "rorikstead"]), + Location("riverwood", "Riverwood", "Whiterun Hold", {"x": 80, "y": 150}, "village", 50, ["whiterun", "bleak_falls"]), + Location("bleak_falls", "Bleak Falls Barrow", "Whiterun Hold", {"x": 60, "y": 170}, "dungeon", 0, ["riverwood"]), + Location("dragonsreach", "Dragonsreach", "Whiterun Hold", {"x": 105, "y": 95}, "landmark", 20, ["whiterun"]), + Location("jorrvaskr", "Jorrvaskr", "Whiterun Hold", {"x": 110, "y": 100}, "landmark", 15, ["whiterun"]), + Location("rorikstead", "Rorikstead", "Whiterun Hold", {"x": 40, "y": 80}, "village", 30, ["whiterun"]), + Location("solitude", "Solitude", "Haafingar", {"x": 20, "y": 20}, "city", 300, ["dragon_bridge"]), + Location("dragon_bridge", "Dragon Bridge", "Haafingar", {"x": 40, "y": 40}, "village", 40, ["solitude", "rorikstead"]), + Location("riften", "Riften", "The Rift", {"x": 180, "y": 180}, "city", 150, ["ivarstead"]), + Location("ivarstead", "Ivarstead", "The Rift", {"x": 150, "y": 160}, "village", 25, ["riften", "high_hrothgar"]), + Location("high_hrothgar", "High Hrothgar", "The Throat of the World", {"x": 140, "y": 140}, "landmark", 5, ["ivarstead"]), + ] + for loc in demo_locations: + self.add_location(loc) + + # === FACTIONS === + demo_factions = [ + Faction("companions", "The Companions", "guild", [ + {"name": "Whelp", "level": 1}, + {"name": "Companion", "level": 2}, + {"name": "Circle Member", "level": 3}, + {"name": "Harbinger", "level": 4} + ], {"silver_hand": -100, "whiterun_guard": 50}, {}, "jorrvaskr"), + Faction("thieves_guild", "Thieves Guild", "criminal", [ + {"name": "Footpad", "level": 1}, + {"name": "Bandit", "level": 2}, + {"name": "Prowler", "level": 3}, + {"name": "Guild Master", "level": 4} + ], {"riften_guard": -25}, {}, "riften"), + Faction("whiterun_guard", "Whiterun Guard", "military", [ + {"name": "Guard", "level": 1}, + {"name": "Captain", "level": 2} + ], {"companions": 50}, {}, "dragonsreach"), + Faction("greybeards", "The Greybeards", "religious", [ + {"name": "Initiate", "level": 1}, + {"name": "Master", "level": 2} + ], {}, {}, "high_hrothgar"), + ] + for faction in demo_factions: + self.add_faction(faction) + + # === NPCs === + demo_npcs = [ + NPC("balgruuf", "Jarl Balgruuf", "nord", "noble", 20, True, "dragonsreach", + [{"faction": "whiterun_guard", "rank": "leader"}], + [{"hours": "0-8", "action": "sleep", "location": "dragonsreach"}, + {"hours": "8-20", "action": "sit_throne", "location": "dragonsreach"}, + {"hours": "20-24", "action": "wander", "location": "dragonsreach"}]), + NPC("kodlak", "Kodlak Whitemane", "nord", "warrior", 30, True, "jorrvaskr", + [{"faction": "companions", "rank": "Harbinger"}], + [{"hours": "0-6", "action": "sleep", "location": "jorrvaskr"}, + {"hours": "6-20", "action": "train", "location": "jorrvaskr"}, + {"hours": "20-24", "action": "eat", "location": "jorrvaskr"}]), + NPC("vilkas", "Vilkas", "nord", "warrior", 25, False, "jorrvaskr", + [{"faction": "companions", "rank": "Circle Member"}], + [{"hours": "0-6", "action": "sleep", "location": "jorrvaskr"}, + {"hours": "6-18", "action": "train", "location": "jorrvaskr"}, + {"hours": "18-24", "action": "patrol", "location": "whiterun"}]), + NPC("belethor", "Belethor", "breton", "merchant", 10, False, "whiterun", + [], + [{"hours": "0-6", "action": "sleep", "location": "whiterun"}, + {"hours": "6-20", "action": "work", "location": "whiterun"}, + {"hours": "20-24", "action": "relax", "location": "whiterun"}]), + NPC("alvor", "Alvor", "nord", "blacksmith", 12, False, "riverwood", + [], + [{"hours": "0-6", "action": "sleep", "location": "riverwood"}, + {"hours": "6-20", "action": "work", "location": "riverwood"}, + {"hours": "20-22", "action": "eat", "location": "riverwood"}]), + NPC("arngeir", "Arngeir", "nord", "priest", 50, True, "high_hrothgar", + [{"faction": "greybeards", "rank": "Master"}], + [{"hours": "0-6", "action": "meditate", "location": "high_hrothgar"}, + {"hours": "6-24", "action": "teach", "location": "high_hrothgar"}]), + ] + for npc in demo_npcs: + self.add_npc(npc) + + # === ITEMS === + demo_items = [ + Item("iron_sword", "Iron Sword", "weapon", 9.0, 25, {"damage": 7, "speed": 1.0, "material": "iron"}), + Item("steel_sword", "Steel Sword", "weapon", 10.0, 45, {"damage": 9, "speed": 1.0, "material": "steel"}), + Item("iron_ingot", "Iron Ingot", "misc", 1.0, 7, {"material": "iron"}), + Item("steel_ingot", "Steel Ingot", "misc", 1.0, 20, {"material": "steel"}), + Item("leather_strips", "Leather Strips", "misc", 0.1, 3, {}), + Item("health_potion", "Potion of Minor Healing", "consumable", 0.5, 25, {"restore_health": 25}), + Item("golden_claw", "Golden Claw", "key", 0.5, 100, {"quest_item": True}), + Item("dragonstone", "Dragonstone", "misc", 25.0, 0, {"quest_item": True}), + Item("iron_armor", "Iron Armor", "armor", 30.0, 125, {"armor_rating": 25, "slot": "chest", "material": "iron"}), + ] + for item in demo_items: + self.add_item(item) + + # === QUESTS === + demo_quests = [ + Quest("mq101", "Unbound", "main", + [QuestStage(10, "Escape the dragon attack", [{"type": "go_to", "target": "riverwood"}]), + QuestStage(20, "Speak with Alvor in Riverwood", [{"type": "talk_to", "target": "alvor"}])], + {"gold": 0}, []), + Quest("mq102", "Before the Storm", "main", + [QuestStage(10, "Speak to the Jarl of Whiterun", [{"type": "talk_to", "target": "balgruuf"}]), + QuestStage(20, "Speak to Farengar", [{"type": "talk_to", "target": "farengar"}])], + {"gold": 0}, ["mq101"]), + Quest("mq103", "Bleak Falls Barrow", "main", + [QuestStage(10, "Retrieve the Dragonstone", [{"type": "go_to", "target": "bleak_falls"}, {"type": "collect", "item": "dragonstone"}]), + QuestStage(20, "Return the Dragonstone to Farengar", [{"type": "deliver", "item": "dragonstone", "to": "farengar"}])], + {"gold": 500, "items": ["steel_sword"]}, ["mq102"]), + Quest("comp01", "Take Up Arms", "faction", + [QuestStage(10, "Speak to Kodlak Whitemane", [{"type": "talk_to", "target": "kodlak"}]), + QuestStage(20, "Train with Vilkas", [{"type": "talk_to", "target": "vilkas"}]), + QuestStage(30, "Give Vilkas's sword to Eorlund", [{"type": "deliver", "item": "vilkas_sword", "to": "eorlund"}])], + {"faction_rep": {"companions": 25}}, []), + ] + for quest in demo_quests: + self.add_quest(quest) + + # === SKILLS === + demo_skills = [ + Skill("one_handed", "One-Handed", "combat", "The art of fighting with one-handed weapons.", "strength", + [{"action": "hit_with_1h", "xp": 5}, {"action": "kill_with_1h", "xp": 20}]), + Skill("two_handed", "Two-Handed", "combat", "The art of fighting with two-handed weapons.", "strength", + [{"action": "hit_with_2h", "xp": 6}, {"action": "kill_with_2h", "xp": 25}]), + Skill("smithing", "Smithing", "crafting", "The art of creating and improving weapons and armor.", "strength", + [{"action": "forge_item", "xp": 25}, {"action": "temper_item", "xp": 15}]), + Skill("speech", "Speech", "stealth", "The art of persuasion and bartering.", "charisma", + [{"action": "successful_persuade", "xp": 15}, {"action": "sell_item", "xp": 1}]), + Skill("destruction", "Destruction", "magic", "The art of fire, frost, and shock magic.", "intelligence", + [{"action": "cast_destruction", "xp": 8}, {"action": "kill_with_magic", "xp": 30}]), + ] + for skill in demo_skills: + self.add_skill(skill) + + # === PERKS === + demo_perks = [ + Perk("armsman_1", "Armsman (1/5)", "one_handed", 0, "One-handed weapons do 20% more damage.", [{"type": "damage_mult", "value": 1.2}], [], 1), + Perk("armsman_2", "Armsman (2/5)", "one_handed", 20, "One-handed weapons do 40% more damage.", [{"type": "damage_mult", "value": 1.4}], ["armsman_1"], 1), + Perk("bladesman_1", "Bladesman (1/3)", "one_handed", 30, "10% chance of critical hit with swords.", [{"type": "crit_chance", "value": 0.1}], ["armsman_1"], 1), + Perk("steel_smithing", "Steel Smithing", "smithing", 0, "Can create Steel armor and weapons.", [{"type": "unlock_recipe", "category": "steel"}], [], 1), + Perk("haggling", "Haggling (1/5)", "speech", 0, "Buying and selling prices are 10% better.", [{"type": "price_mult", "value": 0.9}], [], 1), + ] + for perk in demo_perks: + self.add_perk(perk) + + # === RECIPES === + demo_recipes = [ + Recipe("forge_iron_sword", "Forge Iron Sword", "smithing", + [{"item_id": "iron_ingot", "count": 2}, {"item_id": "leather_strips", "count": 1}], + {"item_id": "iron_sword", "count": 1}, {"skill_id": "smithing", "level": 0}, "forge"), + Recipe("forge_steel_sword", "Forge Steel Sword", "smithing", + [{"item_id": "steel_ingot", "count": 2}, {"item_id": "leather_strips", "count": 1}], + {"item_id": "steel_sword", "count": 1}, {"skill_id": "smithing", "level": 20}, "forge"), + ] + for recipe in demo_recipes: + self.add_recipe(recipe) + + # === DIALOGUES === + demo_dialogues = [ + DialogueTopic("balgruuf_greeting", "Greeting", "balgruuf", 100, [], "What brings you to Dragonsreach?", [], ["balgruuf_dragons", "balgruuf_war"]), + DialogueTopic("balgruuf_dragons", "About the Dragons", "balgruuf", 50, [{"type": "quest_stage", "quest": "mq102", "min": 10}], + "Dragons? Nonsense. Although... there have been reports from the watchtowers.", [{"type": "advance_quest", "quest": "mq102", "stage": 15}], []), + DialogueTopic("balgruuf_war", "About the Civil War", "balgruuf", 50, [], + "I've tried to stay neutral in this conflict, but Ulfric's actions make that difficult.", [], []), + DialogueTopic("kodlak_greeting", "Greeting", "kodlak", 100, [], "A stranger comes to our hall. What do you seek?", [], ["kodlak_join"]), + DialogueTopic("kodlak_join", "I want to join the Companions", "kodlak", 75, [], + "Hmm. Yes, I think you might be what we're looking for. Speak to Vilkas.", [{"type": "start_quest", "quest": "comp01"}], []), + ] + for dlg in demo_dialogues: + self.add_dialogue(dlg) + + return self.get_stats() + + def _generate_observations(self, loc) -> List[str]: + """Generate varied 'look around' descriptions based on population. + + More population = more variety in observations. + """ + observations = [] + pop = loc.population + + # Base observations by location type + type_observations = { + "city": [ + "Merchants hawk their wares from crowded stalls.", + "Guards patrol the streets, watching for trouble.", + "Children run between the legs of busy adults.", + "A bard plays a tune outside a tavern.", + "Two nobles argue about taxes near the town square.", + "A blacksmith hammers away at red-hot steel.", + "Beggars hold out hands near the temple steps.", + "A courier rushes past with urgent messages.", + ], + "town": [ + "Farmers lead carts of produce to market.", + "The local innkeeper sweeps the front steps.", + "Dogs bark at passing strangers.", + "Smoke rises from cottage chimneys.", + "An elder sits on a bench, watching the world go by.", + ], + "village": [ + "A chicken pecks at the dirt road.", + "A woman hangs laundry to dry.", + "The sound of an axe splitting wood echoes nearby.", + "Children play with wooden swords.", + ], + "landmark": [ + "The structure towers above you, awe-inspiring.", + "Pilgrims kneel in reverent silence.", + "Ancient inscriptions cover the weathered stone.", + ], + } + + base_obs = type_observations.get(loc.location_type, [ + "The area is quiet.", + "Nothing unusual catches your eye.", + ]) + + # Scale observations by population + # pop 1-10: 2 obs, 11-50: 3 obs, 51-100: 4 obs, 100+: 5 obs + if pop <= 10: + count = min(2, len(base_obs)) + elif pop <= 50: + count = min(3, len(base_obs)) + elif pop <= 100: + count = min(4, len(base_obs)) + else: + count = min(5, len(base_obs)) + + # Add NPCs at this location to observations + npcs_here = [npc for npc in self.npcs.values() if npc.home_location == loc.id] + for npc in npcs_here[:2]: # Max 2 NPC observations + observations.append(f"You notice {npc.name} the {npc.role} nearby.") + + # Add base observations + observations.extend(base_obs[:count]) + + # Add faction presence + factions_here = set() + for npc in npcs_here: + for mem in npc.faction_memberships: + fac_id = mem.get("faction", "") + if fac_id in self.factions: + factions_here.add(self.factions[fac_id].name) + + if factions_here: + observations.append(f"You see signs of {', '.join(factions_here)} influence here.") + + return observations if observations else ["The area seems unremarkable."] + + def _get_npc_title(self, npc) -> str: + """Generate a proper title for an NPC based on role and faction.""" + role_titles = { + "noble": "Lord" if npc.race in ["nord", "imperial"] else "Lady", + "warrior": "the Warrior", + "merchant": "the Merchant", + "blacksmith": "the Smith", + "innkeeper": "the Innkeeper", + "priest": "the Elder", + "mage": "the Mage", + "guard": "Guard", + "thief": "the Rogue", + "farmer": "the Farmer", + "beggar": "the Beggar", + "commoner": "", + } + + # Check for faction rank titles + for membership in npc.faction_memberships: + fac_id = membership.get("faction", "") + rank_name = membership.get("rank", "") + if fac_id in self.factions: + faction = self.factions[fac_id] + # Special titles for leaders + if rank_name.lower() in ["leader", "harbinger", "guild master", "jarl"]: + if npc.role == "noble": + return "Jarl" + return rank_name + + return role_titles.get(npc.role, "") + + def _build_npc_dialogue_states(self, states: dict, loc_id: str, npc, quest_objectives: dict): + """Build comprehensive dialogue states for an NPC.""" + npc_dialogues = [d for d in self.dialogues.values() if d.speaker_id == npc.id] + title = self._get_npc_title(npc) + full_name = f"{title} {npc.name}".strip() + + # Check for quest objectives involving this NPC + npc_quests = quest_objectives.get(npc.id, []) + + if npc_dialogues: + # Use highest priority dialogue as greeting + greeting = max(npc_dialogues, key=lambda d: d.priority) + + # Build greeting state + npc_desc = f"**{full_name}** ({npc.race.title()} {npc.role.title()})\n\n" + npc_desc += f'"{greeting.response_text}"' + + # Add quest context if this NPC is a quest target + if npc_quests: + quest, stage, obj = npc_quests[0] + npc_desc += f"\n\n*This person is involved in the quest: {quest.name}*" + + npc_choices = [] + npc_transitions = {} + + # All dialogue topics (not just next_topics from greeting) + for dlg in npc_dialogues: + if dlg.id != greeting.id: + npc_choices.append(f'"{dlg.name}"') + npc_transitions[f'"{dlg.name}"'] = f"{loc_id}/dlg_{dlg.id}" + + # Add quest-specific dialogue option + if npc_quests: + quest, stage, obj = npc_quests[0] + obj_type = obj.get("type", "") + if obj_type == "talk_to": + choice = f"About {quest.name}..." + npc_choices.insert(0, choice) + npc_transitions[choice] = f"{loc_id}/quest_{npc.id}_{quest.id}" + + # Faction-specific option + if npc.faction_memberships: + fac_id = npc.faction_memberships[0].get("faction", "") + if fac_id in self.factions: + faction = self.factions[fac_id] + choice = f"Tell me about {faction.name}" + npc_choices.append(choice) + npc_transitions[choice] = f"{loc_id}/faction_{npc.id}_{fac_id}" + + npc_choices.append("Farewell") + npc_transitions["Farewell"] = f"{loc_id}/arrive" + + states[f"talk_{npc.id}"] = { + "description": npc_desc, + "choices": npc_choices, + "transitions": npc_transitions + } + + # Create states for each dialogue topic + for dlg in npc_dialogues: + if dlg.id != greeting.id: + dlg_desc = f"**{full_name}**\n\n\"{dlg.response_text}\"" + + # Show effects narratively + for effect in dlg.effects: + eff_type = effect.get("type", "") + if eff_type == "advance_quest": + dlg_desc += f"\n\n*Quest updated: {effect.get('quest', 'Unknown')}*" + elif eff_type == "start_quest": + dlg_desc += f"\n\n*New quest started!*" + + dlg_choices = ["Back to conversation", "Farewell"] + dlg_transitions = { + "Back to conversation": f"{loc_id}/talk_{npc.id}", + "Farewell": f"{loc_id}/arrive" + } + + states[f"dlg_{dlg.id}"] = { + "description": dlg_desc, + "choices": dlg_choices, + "transitions": dlg_transitions + } + + # Quest interaction state + if npc_quests: + quest, stage, obj = npc_quests[0] + quest_desc = f"**{full_name}**\n\n" + quest_desc += f"*Quest: {quest.name}*\n" + quest_desc += f"*Objective: {stage.journal_entry}*\n\n" + quest_desc += f'"Yes, I can help you with that matter..."' + + states[f"quest_{npc.id}_{quest.id}"] = { + "description": quest_desc, + "choices": ["Continue conversation", "Farewell"], + "transitions": { + "Continue conversation": f"{loc_id}/talk_{npc.id}", + "Farewell": f"{loc_id}/arrive" + } + } + + # Faction info state + for membership in npc.faction_memberships: + fac_id = membership.get("faction", "") + if fac_id in self.factions: + faction = self.factions[fac_id] + fac_desc = f"**{full_name}** speaks of **{faction.name}**:\n\n" + fac_desc += f'"{faction.name} is a {faction.faction_type} organization.' + + if faction.ranks: + rank_names = [r.get("name", "Unknown") for r in faction.ranks] + fac_desc += f' Our ranks are: {", ".join(rank_names)}.' + + fac_desc += '"' + + states[f"faction_{npc.id}_{fac_id}"] = { + "description": fac_desc, + "choices": ["Back to conversation", "Farewell"], + "transitions": { + "Back to conversation": f"{loc_id}/talk_{npc.id}", + "Farewell": f"{loc_id}/arrive" + } + } + + else: + # Generic NPC without specific dialogues + factions_str = "" + if npc.faction_memberships: + fac_names = [] + for m in npc.faction_memberships: + fac_id = m.get("faction", "") + if fac_id in self.factions: + fac_names.append(self.factions[fac_id].name) + if fac_names: + factions_str = f"\n\nThey are associated with: {', '.join(fac_names)}." + + states[f"talk_{npc.id}"] = { + "description": f"**{full_name}** ({npc.race.title()} {npc.role.title()})\n\n\"Greetings, traveler.\"{factions_str}", + "choices": ["Ask about this place", "Farewell"], + "transitions": { + "Ask about this place": f"{loc_id}/arrive", + "Farewell": f"{loc_id}/arrive" + } + } + + def _build_shop_states(self, states: dict, loc_id: str, merchant): + """Build shop interface for merchant NPCs.""" + title = self._get_npc_title(merchant) + full_name = f"{title} {merchant.name}".strip() + + # Get items this merchant might sell (based on role) + available_items = [] + for item in self.items.values(): + if merchant.role == "blacksmith" and item.item_type in ["weapon", "armor"]: + available_items.append(item) + elif merchant.role == "merchant": + available_items.append(item) + elif merchant.role == "innkeeper" and item.item_type == "consumable": + available_items.append(item) + + shop_desc = f"**{full_name}'s Shop**\n\n" + shop_desc += '"Take a look at my wares..."\n\n' + + if available_items: + shop_desc += "**Available:**\n" + for item in available_items[:8]: # Limit to 8 items + shop_desc += f"- {item.name} ({item.item_type}) - {item.value} gold\n" + else: + shop_desc += "*The shelves are bare...*" + + choices = [] + transitions = {} + + for item in available_items[:4]: # Max 4 buyable items as choices + choice = f"Buy {item.name} ({item.value}g)" + choices.append(choice) + transitions[choice] = f"{loc_id}/buy_{merchant.id}_{item.id}" + + choices.append("Leave shop") + transitions["Leave shop"] = f"{loc_id}/arrive" + + states[f"shop_{merchant.id}"] = { + "description": shop_desc, + "choices": choices, + "transitions": transitions + } + + # Buy confirmation states + for item in available_items[:4]: + states[f"buy_{merchant.id}_{item.id}"] = { + "description": f"You purchase **{item.name}** for {item.value} gold.\n\n*{item.name} added to inventory!*", + "choices": ["Continue shopping", "Leave shop"], + "transitions": { + "Continue shopping": f"{loc_id}/shop_{merchant.id}", + "Leave shop": f"{loc_id}/arrive" + } + } + + def _build_dungeon_states(self, states: dict, loc_id: str, loc): + """Build dungeon exploration states with proper loot.""" + # Find items that might be here (quest items or dungeon loot) + loot_items = [] + + # Quest items at this location + for quest in self.quests.values(): + for stage in quest.stages: + for obj in stage.objectives: + target = obj.get("target", "") + item_id = obj.get("item", "") + if target == loc_id and item_id in self.items: + loot_items.append(self.items[item_id]) + elif obj.get("type") == "go_to" and target == loc_id: + # Check if there's an associated collect objective + if item_id and item_id in self.items: + loot_items.append(self.items[item_id]) + + # Add some generic loot based on location type + generic_loot = [i for i in self.items.values() + if i.item_type in ["weapon", "armor", "consumable"] and i.value < 100][:2] + + explore_desc = f"**Exploring {loc.name}**\n\n" + explore_desc += "You venture deeper into the darkness...\n\n" + + if loot_items: + explore_desc += "**You discover something important:**\n" + for item in loot_items: + explore_desc += f"- **{item.name}** (Quest Item)\n" + explore_desc += "\n*This looks like what you were searching for!*" + elif generic_loot: + explore_desc += "**You find:**\n" + for item in generic_loot: + explore_desc += f"- {item.name}\n" + else: + explore_desc += "The area has been picked clean. Nothing of value remains." + + all_loot = loot_items + generic_loot + choices = [] + transitions = {} + + if all_loot: + choices.append("Take everything") + transitions["Take everything"] = f"{loc_id}/loot_all" + + for item in all_loot[:3]: + choice = f"Take {item.name}" + choices.append(choice) + transitions[choice] = f"{loc_id}/loot_{item.id}" + + choices.append("Search more") + choices.append("Return to entrance") + transitions["Search more"] = f"{loc_id}/explore_deep" + transitions["Return to entrance"] = f"{loc_id}/arrive" + + states["explore"] = { + "description": explore_desc, + "choices": choices, + "transitions": transitions + } + + # Loot states + if all_loot: + loot_desc = "**Collected:**\n" + for item in all_loot: + loot_desc += f"- {item.name}\n" + loot_desc += "\n*Items added to inventory!*" + + states["loot_all"] = { + "description": loot_desc, + "choices": ["Continue exploring", "Return to entrance"], + "transitions": { + "Continue exploring": f"{loc_id}/explore_deep", + "Return to entrance": f"{loc_id}/arrive" + } + } + + for item in all_loot[:3]: + states[f"loot_{item.id}"] = { + "description": f"You take the **{item.name}**.\n\n*{item.name} added to inventory!*", + "choices": ["Continue exploring", "Return to entrance"], + "transitions": { + "Continue exploring": f"{loc_id}/explore", + "Return to entrance": f"{loc_id}/arrive" + } + } + + # Deeper exploration + states["explore_deep"] = { + "description": f"You push further into {loc.name}...\n\nThe passages grow narrower. Ancient carvings cover the walls.", + "choices": ["Keep going", "Turn back"], + "transitions": { + "Keep going": f"{loc_id}/explore", + "Turn back": f"{loc_id}/arrive" + } + } + + def _build_inventory_states(self) -> dict: + """Build inventory system states.""" + first_loc = list(self.locations.keys())[0] if self.locations else "start" + + inv_desc = "**Your Inventory**\n\n" + inv_desc += "**Gold:** Check your purse below\n\n" + + # Group items by type + weapons = [i for i in self.items.values() if i.item_type == "weapon"] + armor = [i for i in self.items.values() if i.item_type == "armor"] + consumables = [i for i in self.items.values() if i.item_type == "consumable"] + misc = [i for i in self.items.values() if i.item_type not in ["weapon", "armor", "consumable"]] + + inv_desc += "**Items you can find in this world:**\n\n" + + if weapons: + inv_desc += "**Weapons:**\n" + for w in weapons[:3]: + props = w.properties + dmg = props.get("damage", "?") + inv_desc += f"- {w.name} (Damage: {dmg})\n" + + if armor: + inv_desc += "\n**Armor:**\n" + for a in armor[:3]: + props = a.properties + rating = props.get("armor_rating", "?") + inv_desc += f"- {a.name} (Rating: {rating})\n" + + if consumables: + inv_desc += "\n**Consumables:**\n" + for c in consumables[:3]: + inv_desc += f"- {c.name}\n" + + if misc: + inv_desc += "\n**Other:**\n" + for m in misc[:5]: + inv_desc += f"- {m.name}\n" + + if not any([weapons, armor, consumables, misc]): + inv_desc += "*Your pack is empty.*" + + # Skills section + if self.skills: + inv_desc += "\n\n**Skills:**\n" + for skill in self.skills.values(): + inv_desc += f"- {skill.name} ({skill.category})\n" + + choices = ["Check purse"] + transitions = {"Check purse": "inventory/gold"} + + # Use consumable options + for c in consumables[:2]: + choice = f"Use {c.name}" + choices.append(choice) + transitions[choice] = "inventory/use_item" + + choices.append("Close inventory") + transitions["Close inventory"] = f"{first_loc}/arrive" + + return { + "check": { + "description": inv_desc, + "choices": choices, + "transitions": transitions + }, + "gold": { + "description": "**Your Purse**\n\nYou started with **100 gold**.\n\nGold is spent at shops and earned from quests and loot.\n\n*Note: Your actual gold balance is tracked by the game engine.*", + "choices": ["Back to inventory", "Close inventory"], + "transitions": { + "Back to inventory": "inventory/check", + "Close inventory": f"{first_loc}/arrive" + } + }, + "use_item": { + "description": "*You use the item...*\n\nYou feel refreshed!", + "choices": ["Back to inventory"], + "transitions": {"Back to inventory": "inventory/check"} + } + } + + def _build_quest_journal_states(self) -> dict: + """Build comprehensive quest journal with per-quest detail states.""" + first_loc = list(self.locations.keys())[0] if self.locations else "start" + states = {} + + # Main journal page - list all quests as clickable + journal_desc = "**Quest Journal**\n\nSelect a quest to view details and track progress.\n\n" + + choices = [] + transitions = {} + choice_config = {} + + if self.quests: + # Group by quest type + main_quests = [q for q in self.quests.values() if q.quest_type == "main"] + faction_quests = [q for q in self.quests.values() if q.quest_type == "faction"] + side_quests = [q for q in self.quests.values() if q.quest_type in ["side", "misc"]] + + if main_quests: + journal_desc += "**Main Quests:**\n" + for q in main_quests: + journal_desc += f"- {q.name}\n" + choice = f"View: {q.name}" + choices.append(choice) + transitions[choice] = f"quest_journal/quest_{q.id}" + + if faction_quests: + journal_desc += "\n**Faction Quests:**\n" + for q in faction_quests: + journal_desc += f"- {q.name}\n" + choice = f"View: {q.name}" + choices.append(choice) + transitions[choice] = f"quest_journal/quest_{q.id}" + + if side_quests: + journal_desc += "\n**Side Quests:**\n" + for q in side_quests: + journal_desc += f"- {q.name}\n" + choice = f"View: {q.name}" + choices.append(choice) + transitions[choice] = f"quest_journal/quest_{q.id}" + else: + journal_desc += "*No quests available.*" + + choices.append("Close journal") + transitions["Close journal"] = f"{first_loc}/arrive" + + states["journal"] = { + "description": journal_desc, + "choices": choices, + "transitions": transitions + } + + # Create detail state for each quest + for quest in self.quests.values(): + quest_desc = f"**{quest.name}**\n" + quest_desc += f"*Type: {quest.quest_type.title()}*\n\n" + + if quest.prerequisites: + prereq_names = [self.quests[p].name for p in quest.prerequisites if p in self.quests] + if prereq_names: + quest_desc += f"Requires: {', '.join(prereq_names)}\n\n" + + quest_desc += "**Objectives** (select to check status):\n" + + quest_choices = [] + quest_transitions = {} + quest_choice_config = {} + + # Build objectives with conditional completion display + obj_num = 0 + for stage in quest.stages: + quest_desc += f"\n**Stage {stage.stage}:** {stage.journal_entry}\n" + + for obj in stage.objectives: + obj_num += 1 + obj_type = obj.get("type", "unknown") + target = obj.get("target", "") + + if obj_type == "go_to" and target in self.locations: + loc = self.locations[target] + flag_name = f"reached_{target}" + + # Create a choice that shows status based on flag + status_choice = f"Objective {obj_num}: Go to {loc.name}" + quest_choices.append(status_choice) + quest_transitions[status_choice] = f"{target}/arrive" + + # Use choice_config to show completion status + quest_choice_config[status_choice] = { + "condition": {"flag": flag_name}, + "visible_text": f"[COMPLETE] Go to {loc.name}", + "hidden_text": f"[ ] Go to {loc.name} - Travel there to complete" + } + + elif obj_type == "talk_to" and target in self.npcs: + npc = self.npcs[target] + flag_name = f"talked_{target}" + + status_choice = f"Objective {obj_num}: Talk to {npc.name}" + + # If NPC has home location, link there + if npc.home_location and npc.home_location in self.locations: + quest_choices.append(status_choice) + quest_transitions[status_choice] = f"{npc.home_location}/arrive" + quest_choice_config[status_choice] = { + "condition": {"flag": flag_name}, + "visible_text": f"[COMPLETE] Talk to {npc.name}", + "hidden_text": f"[ ] Talk to {npc.name} - Find them to complete" + } + + elif obj_type == "collect" or obj_type == "find": + flag_name = f"found_{target}" + status_choice = f"Objective {obj_num}: Find {target}" + quest_choices.append(status_choice) + quest_transitions[status_choice] = "quest_journal/journal" + quest_choice_config[status_choice] = { + "condition": {"has_item": target}, + "visible_text": f"[COMPLETE] Found {target}", + "hidden_text": f"[ ] Find {target} - Search for it" + } + + else: + quest_desc += f" - {obj_type}: {target}\n" + + # Add rewards section + if quest.rewards: + quest_desc += "\n**Rewards on completion:**\n" + if "gold" in quest.rewards: + quest_desc += f" - {quest.rewards['gold']} gold\n" + if "items" in quest.rewards: + for item in quest.rewards["items"]: + item_name = self.items[item].name if item in self.items else item + quest_desc += f" - {item_name}\n" + if "faction_rep" in quest.rewards: + for fac, rep in quest.rewards["faction_rep"].items(): + fac_name = self.factions[fac].name if fac in self.factions else fac + quest_desc += f" - {rep:+d} reputation with {fac_name}\n" + + quest_choices.append("Back to journal") + quest_transitions["Back to journal"] = "quest_journal/journal" + + states[f"quest_{quest.id}"] = { + "description": quest_desc, + "choices": quest_choices, + "transitions": quest_transitions, + "choice_config": quest_choice_config + } + + return states + + +# ==================== RADIANT QUEST TEMPLATE SYSTEM ==================== + +# Pre-built radiant quest templates +RADIANT_TEMPLATES = { + "tg_burglary": { + "id": "tg_burglary", + "display_name": "The {TARGET_ITEM} Job", + "description": "Thieves Guild burglary mission - steal a valuable item", + "variable_slots": { + "TARGET_NPC": {"pool": "npcs", "filters": [("role", "in", ["merchant", "noble"])]}, + "TARGET_LOCATION": {"derived_from": "TARGET_NPC.home_location"}, + "TARGET_ITEM": {"pool": "items", "filters": [("value", ">", 50)]} + }, + "stages": [ + {"stage": 10, "journal_entry": "Steal {TARGET_ITEM} from {TARGET_NPC}'s residence"}, + {"stage": 20, "journal_entry": "Return to the Thieves Guild"} + ], + "rewards": {"gold": 200, "faction_rep": {"thieves_guild": 15}}, + "quest_type": "radiant" + }, + "comp_exterminate": { + "id": "comp_exterminate", + "display_name": "Trouble at {TARGET_LOCATION}", + "description": "Companions extermination mission - clear out threats", + "variable_slots": { + "TARGET_LOCATION": {"pool": "locations", "filters": [("location_type", "in", ["dungeon", "cave", "ruin"])]}, + "ENEMY_TYPE": {"random_choice": ["wolves", "bandits", "draugr", "spiders"]} + }, + "stages": [ + {"stage": 10, "journal_entry": "Clear {TARGET_LOCATION} of {ENEMY_TYPE}"}, + {"stage": 20, "journal_entry": "Return to Jorrvaskr"} + ], + "rewards": {"gold": 300, "faction_rep": {"companions": 20}}, + "quest_type": "radiant" + }, + "misc_delivery": { + "id": "misc_delivery", + "display_name": "Delivery to {TARGET_NPC}", + "description": "Simple delivery quest between NPCs", + "variable_slots": { + "QUEST_GIVER": {"pool": "npcs", "filters": [("role", "in", ["merchant", "innkeeper"])]}, + "TARGET_NPC": {"pool": "npcs", "filters": [("role", "!=", "QUEST_GIVER.role")]}, + "DELIVERY_ITEM": {"pool": "items", "filters": [("weight", "<", 5), ("item_type", "==", "misc")]} + }, + "stages": [ + {"stage": 10, "journal_entry": "Deliver {DELIVERY_ITEM} to {TARGET_NPC}"}, + {"stage": 20, "journal_entry": "Return to {QUEST_GIVER} for payment"} + ], + "rewards": {"gold": 75}, + "quest_type": "radiant" + }, + "bounty_hunt": { + "id": "bounty_hunt", + "display_name": "Bounty: {TARGET_NPC}", + "description": "Hunt down a dangerous target", + "variable_slots": { + "TARGET_NPC": {"pool": "npcs", "filters": [("role", "in", ["thief", "warrior"]), ("essential", "==", False)]}, + "TARGET_LOCATION": {"derived_from": "TARGET_NPC.home_location"}, + "BOUNTY_AMOUNT": {"random_range": [100, 500]} + }, + "stages": [ + {"stage": 10, "journal_entry": "Find and eliminate {TARGET_NPC}"}, + {"stage": 20, "journal_entry": "Collect the {BOUNTY_AMOUNT}g bounty"} + ], + "rewards": {"gold": "{BOUNTY_AMOUNT}"}, + "quest_type": "radiant" + } +} + + +def generate_radiant_quest_instance(template_id: str, db) -> Tuple[Optional[Quest], str]: + """Generate a quest instance from a radiant template. + + Args: + template_id: ID of the template to use + db: EntityDatabase instance to pull entities from + + Returns: + Tuple of (Quest object or None, status message) + """ + if template_id not in RADIANT_TEMPLATES: + return None, f"Template '{template_id}' not found" + + template = RADIANT_TEMPLATES[template_id] + resolved_vars = {} + + # Resolve variable slots + for var_name, var_config in template.get("variable_slots", {}).items(): + # Handle derived values + if "derived_from" in var_config: + source_path = var_config["derived_from"] + parts = source_path.split(".") + source_var = parts[0] + source_attr = parts[1] if len(parts) > 1 else "" + + if source_var in resolved_vars and source_attr: + source_entity = resolved_vars[source_var] + resolved_vars[var_name] = getattr(source_entity, source_attr, "unknown") + continue + + # Handle random choice from list + if "random_choice" in var_config: + choices = var_config["random_choice"] + resolved_vars[var_name] = random.choice(choices) + continue + + # Handle random range + if "random_range" in var_config: + min_val, max_val = var_config["random_range"] + resolved_vars[var_name] = random.randint(min_val, max_val) + continue + + # Handle pool selection + pool_name = var_config.get("pool", "") + filters = var_config.get("filters", []) + + candidates = _get_pool_candidates(db, pool_name, filters, resolved_vars) + + if not candidates: + return None, f"No candidates found for variable '{var_name}' in pool '{pool_name}'" + + resolved_vars[var_name] = random.choice(candidates) + + # Substitute variables in display name + quest_name = template["display_name"] + for var_name, var_value in resolved_vars.items(): + placeholder = "{" + var_name + "}" + if hasattr(var_value, 'name'): + quest_name = quest_name.replace(placeholder, var_value.name) + else: + quest_name = quest_name.replace(placeholder, str(var_value)) + + # Build stages with variable substitution + stages = [] + for stage_template in template.get("stages", []): + journal = stage_template["journal_entry"] + for var_name, var_value in resolved_vars.items(): + placeholder = "{" + var_name + "}" + if hasattr(var_value, 'name'): + journal = journal.replace(placeholder, var_value.name) + else: + journal = journal.replace(placeholder, str(var_value)) + + stages.append(QuestStage( + stage=stage_template["stage"], + journal_entry=journal, + objectives=[] + )) + + # Process rewards + rewards = {} + for reward_key, reward_value in template.get("rewards", {}).items(): + if isinstance(reward_value, str) and "{" in reward_value: + # Variable substitution in rewards + for var_name, var_value in resolved_vars.items(): + placeholder = "{" + var_name + "}" + if placeholder in reward_value: + if isinstance(var_value, (int, float)): + reward_value = str(var_value) + elif hasattr(var_value, 'value'): + reward_value = str(var_value.value) + try: + rewards[reward_key] = int(reward_value) + except: + rewards[reward_key] = reward_value + else: + rewards[reward_key] = reward_value + + # Generate unique ID + import time + quest_id = f"{template['id']}_{int(time.time() * 1000) % 100000}" + + quest = Quest( + id=quest_id, + name=quest_name, + quest_type=template.get("quest_type", "radiant"), + stages=stages, + rewards=rewards + ) + + # Build info about resolved variables + var_info = [] + for var_name, var_value in resolved_vars.items(): + if hasattr(var_value, 'name'): + var_info.append(f"{var_name}: {var_value.name}") + else: + var_info.append(f"{var_name}: {var_value}") + + return quest, f"Generated '{quest_name}' | Variables: {', '.join(var_info)}" + + +def _get_pool_candidates(db, pool_name: str, filters: List, resolved_vars: Dict) -> List: + """Get candidates from an entity pool with filters applied. + + Args: + db: EntityDatabase instance + pool_name: Name of the pool ('npcs', 'locations', 'items') + filters: List of filter tuples like (attr, op, value) + resolved_vars: Already resolved variables for cross-references + + Returns: + List of matching entities + """ + # Get base pool + if pool_name == "npcs": + candidates = list(db.npcs.values()) + elif pool_name == "locations": + candidates = list(db.locations.values()) + elif pool_name == "items": + candidates = list(db.items.values()) + elif pool_name == "factions": + candidates = list(db.factions.values()) + else: + return [] + + # Apply filters + for filter_tuple in filters: + if len(filter_tuple) != 3: + continue + + attr, op, value = filter_tuple + + # Resolve value if it references another variable + if isinstance(value, str) and value.startswith("{") and value.endswith("}"): + var_ref = value[1:-1] + if "." in var_ref: + ref_var, ref_attr = var_ref.split(".") + if ref_var in resolved_vars: + value = getattr(resolved_vars[ref_var], ref_attr, None) + elif var_ref in resolved_vars: + value = resolved_vars[var_ref] + + filtered = [] + for c in candidates: + attr_value = getattr(c, attr, None) + + if op == "==" and attr_value == value: + filtered.append(c) + elif op == "!=" and attr_value != value: + filtered.append(c) + elif op == ">" and attr_value is not None and attr_value > value: + filtered.append(c) + elif op == "<" and attr_value is not None and attr_value < value: + filtered.append(c) + elif op == "in" and attr_value in value: + filtered.append(c) + + candidates = filtered + + return candidates + + +# ==================== LLM GENERATION FUNCTIONS ==================== + +@spaces.GPU +def generate_npc_from_prompt(prompt: str, db, model_name: str = "Qwen/Qwen3-4B", enable_thinking: bool = True) -> Tuple[str, str]: + """Generate an NPC using LLM based on user prompt. + + Args: + prompt: User description of the NPC to create + db: EntityDatabase for context (existing locations, factions) + model_name: Model to use for generation + enable_thinking: Enable Qwen3 thinking mode + + Returns: + Tuple of (JSON preview string, status message) + """ + try: + model, tokenizer = _ensure_rpg_model(model_name) + + # Build context from database + locations = list(db.locations.keys())[:20] + factions = list(db.factions.keys())[:10] + + system_prompt = f"""You are an RPG game designer creating NPCs. Generate a complete NPC based on the user's description. + +Available locations: {', '.join(locations) if locations else 'none yet'} +Available factions: {', '.join(factions) if factions else 'none yet'} + +Output ONLY valid JSON in this exact format: +{{ + "id": "unique_snake_case_id", + "name": "Display Name", + "race": "human/elf/dwarf/orc/etc", + "role": "merchant/warrior/mage/thief/innkeeper/noble/guard/etc", + "level": 1-50, + "essential": true/false, + "home_location": "location_id or empty", + "factions": ["faction_id"], + "schedule": {{ + "6": {{"action": "wake_up", "location": "home"}}, + "8": {{"action": "work", "location": "shop"}}, + "20": {{"action": "eat", "location": "tavern"}}, + "22": {{"action": "sleep", "location": "home"}} + }} +}}""" + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt} + ] + + text = tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True, + enable_thinking=enable_thinking + ) + + inputs = tokenizer([text], return_tensors="pt").to(model.device) + + outputs = model.generate( + **inputs, + max_new_tokens=1024, + temperature=0.7, + do_sample=True, + pad_token_id=tokenizer.eos_token_id + ) + + response = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True) + + # Extract JSON from response + json_start = response.find('{') + json_end = response.rfind('}') + 1 + if json_start >= 0 and json_end > json_start: + json_str = response[json_start:json_end] + # Validate JSON + npc_data = json.loads(json_str) + formatted = json.dumps(npc_data, indent=2) + return formatted, f"**Generated NPC:** {npc_data.get('name', 'Unknown')}" + + return "", f"**Error:** Could not parse JSON from response" + + except Exception as e: + return "", f"**Error:** {str(e)}" + + +@spaces.GPU +def generate_quest_from_prompt(prompt: str, db, model_name: str = "Qwen/Qwen3-4B") -> Tuple[str, str]: + """Generate a quest using LLM based on user prompt. + + Args: + prompt: User description of the quest to create + db: EntityDatabase for context + model_name: Model to use + + Returns: + Tuple of (JSON preview string, status message) + """ + try: + model, tokenizer = _ensure_rpg_model(model_name) + + # Build context + npcs = [f"{n.name} ({n.id})" for n in list(db.npcs.values())[:15]] + locations = [f"{l.name} ({l.id})" for l in list(db.locations.values())[:15]] + items = [f"{i.name} ({i.id})" for i in list(db.items.values())[:15]] + + system_prompt = f"""You are an RPG quest designer. Create a quest based on the user's description. + +Available NPCs: {', '.join(npcs) if npcs else 'none yet'} +Available locations: {', '.join(locations) if locations else 'none yet'} +Available items: {', '.join(items) if items else 'none yet'} + +Output ONLY valid JSON in this exact format: +{{ + "id": "unique_snake_case_id", + "name": "Quest Display Name", + "quest_type": "main/side/radiant/faction", + "stages": [ + {{ + "stage": 10, + "journal_entry": "Description of this stage", + "objectives": [ + {{"type": "talk_to", "target": "npc_id"}}, + {{"type": "go_to", "target": "location_id"}}, + {{"type": "collect", "item": "item_id", "count": 1}} + ] + }}, + {{ + "stage": 20, + "journal_entry": "Next stage description", + "objectives": [] + }} + ], + "rewards": {{ + "gold": 100, + "items": ["item_id"], + "experience": 500 + }}, + "prerequisites": [] +}}""" + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt} + ] + + text = tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True + ) + + inputs = tokenizer([text], return_tensors="pt").to(model.device) + + outputs = model.generate( + **inputs, + max_new_tokens=1500, + temperature=0.7, + do_sample=True, + pad_token_id=tokenizer.eos_token_id + ) + + response = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True) + + json_start = response.find('{') + json_end = response.rfind('}') + 1 + if json_start >= 0 and json_end > json_start: + json_str = response[json_start:json_end] + quest_data = json.loads(json_str) + formatted = json.dumps(quest_data, indent=2) + return formatted, f"**Generated Quest:** {quest_data.get('name', 'Unknown')}" + + return "", f"**Error:** Could not parse JSON from response" + + except Exception as e: + return "", f"**Error:** {str(e)}" + + +@spaces.GPU +def generate_dialogue_from_prompt(npc_id: str, topic: str, db, model_name: str = "Qwen/Qwen3-4B") -> Tuple[str, str]: + """Generate dialogue for an NPC using LLM. + + Args: + npc_id: ID of the NPC to generate dialogue for + topic: Topic/description of the dialogue + db: EntityDatabase for context + model_name: Model to use + + Returns: + Tuple of (JSON preview string, status message) + """ + try: + if npc_id not in db.npcs: + return "", f"**Error:** NPC '{npc_id}' not found" + + npc = db.npcs[npc_id] + model, tokenizer = _ensure_rpg_model(model_name) + + # Get other NPCs for reference + other_npcs = [n.name for n in db.npcs.values() if n.id != npc_id][:10] + + system_prompt = f"""You are an RPG dialogue writer. Create dialogue for this NPC: + +NPC: {npc.name} +Race: {npc.race} +Role: {npc.role} +Factions: {', '.join(npc.factions) if npc.factions else 'none'} + +Other NPCs the player might ask about: {', '.join(other_npcs) if other_npcs else 'none'} + +Output ONLY valid JSON in this exact format: +{{ + "npc_id": "{npc_id}", + "topic": "{topic}", + "initial_line": "What the NPC says when this topic starts", + "branches": [ + {{ + "player_choice": "What the player can say", + "npc_response": "How the NPC responds", + "next_branch": null + }}, + {{ + "player_choice": "Another option", + "npc_response": "Different response", + "conditions": {{"quest_stage": "quest_id >= 10"}}, + "next_branch": null + }} + ], + "conditions": {{}} +}}""" + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": f"Create dialogue about: {topic}"} + ] + + text = tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True + ) + + inputs = tokenizer([text], return_tensors="pt").to(model.device) + + outputs = model.generate( + **inputs, + max_new_tokens=1200, + temperature=0.8, + do_sample=True, + pad_token_id=tokenizer.eos_token_id + ) + + response = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True) + + json_start = response.find('{') + json_end = response.rfind('}') + 1 + if json_start >= 0 and json_end > json_start: + json_str = response[json_start:json_end] + dialogue_data = json.loads(json_str) + formatted = json.dumps(dialogue_data, indent=2) + return formatted, f"**Generated dialogue for {npc.name}:** {topic}" + + return "", f"**Error:** Could not parse JSON from response" + + except Exception as e: + return "", f"**Error:** {str(e)}" + + +# ==================== UI FUNCTIONS ==================== +# (Radiant functions above are module-level, EntityDatabase class method follows differently) + +# Monkey-patch to add export method to EntityDatabase +def _db_export_to_game_config(self) -> str: + """Export entities to a playable 1D text game config JSON. + + Creates a comprehensive state machine config with: + - All locations with bidirectional travel + - NPCs with titles, roles, and full dialogue trees + - Faction context at headquarters + - Merchant shops for trader NPCs + - Quest objectives as interactive content + - Items as lootable/purchasable + - Skill-flavored choices + - Game start with gold and quest initialization + """ + config = {} + first_location = list(self.locations.keys())[0] if self.locations else "start" + + # Create game_start location with initialization + main_quests = [q.id for q in self.quests.values() if q.quest_type == "main"] + start_effects = { + "set_money": 100, # Starting gold + } + if main_quests: + start_effects["start_mission"] = main_quests + + config["game_start"] = { + "intro": { + "description": "**Welcome, Adventurer!**\n\nYour journey begins here. You have been given 100 gold to start your adventure.\n\n*Your active quests have been recorded in your journal.*", + "choices": ["Begin adventure"], + "transitions": {"Begin adventure": f"{first_location}/arrive"}, + "on_enter": start_effects + } + } + + # Pre-compute bidirectional connections (if A connects to B, B can reach A) + all_connections = {} + for loc_id in self.locations: + all_connections[loc_id] = set(self.locations[loc_id].connected_to) + for loc_id, loc in self.locations.items(): + for conn_id in loc.connected_to: + if conn_id in all_connections: + all_connections[conn_id].add(loc_id) + + # Pre-compute faction HQs + faction_hqs = {} + for fac_id, fac in self.factions.items(): + if fac.headquarters: + faction_hqs[fac.headquarters] = fac + + # Pre-compute quest objectives by location/NPC + quest_objectives_by_location = {} + quest_objectives_by_npc = {} + for quest in self.quests.values(): + for stage in quest.stages: + for obj in stage.objectives: + target = obj.get("target", "") + if target in self.locations: + if target not in quest_objectives_by_location: + quest_objectives_by_location[target] = [] + quest_objectives_by_location[target].append((quest, stage, obj)) + if target in self.npcs: + if target not in quest_objectives_by_npc: + quest_objectives_by_npc[target] = [] + quest_objectives_by_npc[target].append((quest, stage, obj)) + + # Create states for each location + for loc_id, loc in self.locations.items(): + location_states = {} + + # Gather NPCs at this location + npcs_here = [npc for npc in self.npcs.values() if npc.home_location == loc_id] + + # Check if this is a faction HQ + faction_here = faction_hqs.get(loc_id) + + # Build rich description + desc = f"**{loc.name}**" + if loc.region: + desc += f" ({loc.region})" + desc += "\n\n" + + # Location type descriptions + type_descs = { + "city": f"A bustling city of {loc.population} souls. The streets are alive with activity.", + "town": f"A modest town with {loc.population} residents going about their daily lives.", + "village": f"A quiet village of {loc.population} people. Life here is simple but honest.", + "dungeon": "Ancient stone corridors stretch into darkness. Danger lurks in every shadow.", + "landmark": "An awe-inspiring sight stands before you, testament to ages past.", + "cave": "The cave mouth yawns before you, promising secrets within.", + "ruin": "Crumbling walls and fallen columns speak of forgotten glory.", + "fort": "Sturdy walls and watchful guards protect this military outpost.", + "camp": "Tents and campfires dot the area. Someone has made this place home.", + } + desc += type_descs.get(loc.location_type, "You take in your surroundings.") + + # Faction HQ context + if faction_here: + desc += f"\n\nThis is the headquarters of **{faction_here.name}**." + if faction_here.ranks: + top_rank = faction_here.ranks[-1].get("name", "Leader") if faction_here.ranks else "Leader" + desc += f" The {top_rank} holds court here." + + # NPCs present with titles + if npcs_here: + npc_intros = [] + for npc in npcs_here: + title = self._get_npc_title(npc) + npc_intros.append(f"{title} {npc.name}") + desc += f"\n\nYou see: {', '.join(npc_intros)}." + + # Quest objectives here + if loc_id in quest_objectives_by_location: + desc += "\n\n*There is something important to do here...*" + + # Build choices + choices = [] + transitions = {} + + # NPC interactions with proper titles + for npc in npcs_here: + title = self._get_npc_title(npc) + choice = f"Speak with {title} {npc.name}" + choices.append(choice) + transitions[choice] = f"{loc_id}/talk_{npc.id}" + + # Merchant NPCs get shop option + merchants = [n for n in npcs_here if n.role in ["merchant", "blacksmith", "innkeeper"]] + for merchant in merchants: + choice = f"Browse {merchant.name}'s wares" + choices.append(choice) + transitions[choice] = f"{loc_id}/shop_{merchant.id}" + + # Bidirectional travel to connected locations + for conn_id in sorted(all_connections.get(loc_id, [])): + if conn_id in self.locations: + conn_loc = self.locations[conn_id] + # Vary the travel verb based on location type + if conn_loc.location_type in ["dungeon", "cave", "ruin"]: + verb = "Enter" + elif conn_loc.location_type == "landmark": + verb = "Approach" + elif loc.location_type in ["city", "town"] and conn_loc.location_type in ["city", "town", "village"]: + verb = "Travel to" + else: + verb = "Go to" + choice = f"{verb} {conn_loc.name}" + choices.append(choice) + transitions[choice] = f"{conn_id}/arrive" + + # Dungeon exploration + if loc.location_type in ["dungeon", "cave", "ruin"]: + choices.append("Explore deeper") + transitions["Explore deeper"] = f"{loc_id}/explore" + + # Look around (population-based variety) + if loc.population > 0: + choices.append("Look around") + transitions["Look around"] = f"{loc_id}/observe" + + # Quest journal + if self.quests: + choices.append("Check quest journal") + transitions["Check quest journal"] = "quest_journal/journal" + + # Inventory check + if self.items: + choices.append("Check inventory") + transitions["Check inventory"] = "inventory/check" + + # Build arrive state with quest completion effects + arrive_state = { + "description": desc, + "choices": choices, + "transitions": transitions + } + + # Add on_enter effects for quest objectives at this location + if loc_id in quest_objectives_by_location: + on_enter_effects = {} + quest_completions = [] + for quest, stage, obj in quest_objectives_by_location[loc_id]: + if obj.get("type") == "go_to": + # Arriving completes this objective - mark quest progress + quest_completions.append(quest.id) + if quest_completions: + # Use update_mission to advance stage (or just set a flag for now) + on_enter_effects["set_flag"] = [f"reached_{loc_id}"] + on_enter_effects["set_knowledge"] = { + "last_quest_location": loc_id + } + arrive_state["on_enter"] = on_enter_effects + # Add quest completion notice to description + arrive_state["description"] += "\n\n*Quest objective completed!*" + + location_states["arrive"] = arrive_state + + # Population-based observations + if loc.population > 0: + observations = self._generate_observations(loc) + for i, obs in enumerate(observations): + state_name = "observe" if i == 0 else f"observe_{i}" + next_obs = f"observe_{i+1}" if i < len(observations) - 1 else "observe" + location_states[state_name] = { + "description": obs, + "choices": ["Keep looking", "Done looking"], + "transitions": { + "Keep looking": f"{loc_id}/{next_obs}", + "Done looking": f"{loc_id}/arrive" + } + } + + # NPC conversation states with full dialogue trees + for npc in npcs_here: + self._build_npc_dialogue_states(location_states, loc_id, npc, quest_objectives_by_npc) + + # Merchant shop states + for merchant in merchants: + self._build_shop_states(location_states, loc_id, merchant) + + # Dungeon exploration with proper item loot + if loc.location_type in ["dungeon", "cave", "ruin"]: + self._build_dungeon_states(location_states, loc_id, loc) + + config[loc_id] = location_states + + # Inventory system + config["inventory"] = self._build_inventory_states() + + # Quest journal with full details + config["quest_journal"] = self._build_quest_journal_states() + + return json.dumps(config, indent=2) + +def _db_export_to_movingdotspace_config(self) -> str: + """Export to MovingDotSpace-specific config with coordinates. + + Similar to game config but includes position data for 2D rendering. + """ + # Use the base game config + base_config = json.loads(self.export_to_game_config()) + + # Build items list for MovingDotSpace inventory + items_list = [] + for item_id, item in self.items.items(): + items_list.append({ + "type": item.item_type, + "name": item.name, + "description": f"{item.name} - Value: {item.value} gold, Weight: {item.weight}" + }) + + # Build skills list for MovingDotSpace + skills_list = [] + for skill_id, skill in self.skills.items(): + skills_list.append({ + "branch": skill.category.title(), + "name": skill.name, + "learned": False + }) + + # Build objectives from quests + objectives_list = [] + for quest_id, quest in self.quests.items(): + objectives_list.append({ + "id": quest_id, + "name": quest.name, + "complete": False + }) + + # Add metadata for 2D positioning (MovingDotSpace will use this) + metadata = { + "_movingdotspace_meta": { + "locations": {}, + "spawn": None, + "inventory": items_list, + "skills": skills_list, + "objectives": objectives_list, + "startingMoney": 100 + } + } + + for loc_id, loc in self.locations.items(): + metadata["_movingdotspace_meta"]["locations"][loc_id] = { + "x": loc.coordinates["x"] * 5, # Scale up for 2D space + "y": loc.coordinates["y"] * 5, + "type": loc.location_type, + "name": loc.name + } + if metadata["_movingdotspace_meta"]["spawn"] is None: + metadata["_movingdotspace_meta"]["spawn"] = loc_id + + # Merge metadata into config + base_config.update(metadata) + + return json.dumps(base_config, indent=2) + + +# Attach export methods to EntityDatabase class +EntityDatabase.export_to_game_config = _db_export_to_game_config +EntityDatabase.export_to_movingdotspace_config = _db_export_to_movingdotspace_config + + +# ==================== UI FUNCTIONS ==================== + +def parse_location_form(id_val, name, region, x, y, loc_type, population, connections): + """Parse location form inputs into Location object.""" + loc = Location( + id=id_val or name.lower().replace(" ", "_"), + name=name, + region=region, + coordinates={"x": float(x or 0), "y": float(y or 0)}, + location_type=loc_type, + population=int(population or 0), + connected_to=[c.strip() for c in connections.split(",") if c.strip()] + ) + return loc + +def parse_npc_form(id_val, name, race, role, level, essential, home, factions, schedule_json): + """Parse NPC form inputs into NPC object.""" + npc = NPC( + id=id_val or name.lower().replace(" ", "_"), + name=name, + race=race, + role=role, + level=int(level or 1), + essential=essential, + home_location=home, + faction_memberships=[{"faction": f.strip(), "rank": "member"} for f in factions.split(",") if f.strip()], + schedule=json.loads(schedule_json) if schedule_json else [] + ) + return npc + +def parse_quest_form(id_val, name, quest_type, stages_json, rewards_json, prereqs): + """Parse quest form inputs into Quest object.""" + quest = Quest( + id=id_val or name.lower().replace(" ", "_"), + name=name, + quest_type=quest_type, + stages=[QuestStage(**s) for s in json.loads(stages_json)] if stages_json else [], + rewards=json.loads(rewards_json) if rewards_json else {}, + prerequisites=[p.strip() for p in prereqs.split(",") if p.strip()] + ) + return quest + + +def create_big_rpg_scale_tab(): + """Create the Big RPG Scale tab with all 4 modules.""" + + # Session database (note: in Gradio, state should be managed via gr.State for persistence) + # For simplicity, using a module-level database that persists during the session + db = EntityDatabase() + + with gr.Tab("Big RPG Scale"): + gr.Markdown("## Big RPG Scale - Large-Scale RPG Planning") + gr.Markdown("Design games at Skyrim-level complexity. **Start with the Demo & Guide tab** to see what's possible.") + + # Database stats display + with gr.Row(): + stats_display = gr.Markdown("**Database:** 0 locations | 0 NPCs | 0 quests | 0 items | 0 factions | 0 skills") + refresh_stats_btn = gr.Button("Refresh Stats", size="sm") + + with gr.Tabs(): + # ==================== TAB 0: DEMO & GUIDE ==================== + with gr.Tab("Demo & Guide"): + gr.Markdown("""### Welcome to Big RPG Scale + +This tool helps you plan complex RPGs at the scale of games like Skyrim. Instead of just writing +a story, you can define interconnected systems: **locations**, **NPCs**, **factions**, **quests**, +**items**, **skills**, and **dialogue** - all with validation to catch broken references. + +--- + +#### Quick Start: Load the Demo +Click the button below to load a sample Skyrim-inspired dataset with 11 locations, 6 NPCs, +4 factions, 4 quests, 9 items, 5 skills, and 5 dialogue topics - all properly connected. +""") + with gr.Row(): + load_demo_btn = gr.Button("Load Demo Data", variant="primary", size="lg") + demo_status = gr.Markdown("") + + gr.Markdown("""--- + +#### Guided Walkthrough + +After loading the demo, explore these tabs in order: + +| Step | Tab | What You'll See | +|------|-----|-----------------| +| 1 | **World Builder** | Visual map of 11 locations with travel connections | +| 2 | **NPC Scheduler** | 6 characters with daily schedules and faction ties | +| 3 | **Factions** | The Companions, Thieves Guild with ranks | +| 4 | **Quest Designer** | Main quest chain with stages and rewards | +| 5 | **Dialogue** | NPC conversations with branching topics | +| 6 | **Validation** | Check for broken references | +| 7 | **Entity Database** | **EXPORT to playable config!** | + +--- + +#### The Payoff: Export and Play + +The whole point of this tab is to generate a **playable game config**: + +1. Load the demo (or create your own data) +2. Go to **Entity Database** tab +3. Click **"Export Game Config (1D/2D)"** +4. Copy the generated JSON +5. Go to **Game Preview** tab (in main navigation) +6. Paste and play your game! + +Your entities become a real, playable text adventure with: +- Travel between locations +- NPC conversations with dialogue +- Dungeon exploration +- Quest tracking + +--- + +#### Key Concepts + +**Entities reference each other:** +- NPCs have a `home_location` → must match a Location ID +- Factions have a `headquarters` → must match a Location ID +- Dialogue has a `speaker_id` → must match an NPC ID +- Perks have a `skill_id` → must match a Skill ID +- Quests have `prerequisites` → must match other Quest IDs + +**The Validation tab catches broken references** so you can fix them before exporting. + +--- + +#### Workflow Tips + +1. **Start with Locations** - Define your world geography first +2. **Add Factions** - Groups that NPCs belong to +3. **Create NPCs** - Assign them to locations and factions +4. **Design Quests** - Reference NPCs and locations in objectives +5. **Add Items** - Weapons, armor, quest items +6. **Write Dialogue** - Assign to NPCs, link to quests +7. **Define Skills** - Create progression systems +8. **Validate** - Check for errors before export +9. **Export** - Get JSON for your game engine + +--- + +#### What This Tool Does NOT Do (Yet) + +- No actual game engine export (Godot, Unity, etc.) +- No visual perk tree editor +- No procedural quest generation +- No LLM-assisted content creation + +These features are planned for future versions. +""") + + # ==================== TAB 0.5: VALIDATION ==================== + with gr.Tab("Validation"): + gr.Markdown("### Database Validation") + gr.Markdown("Check for broken references between entities. Run this before exporting.") + + validate_btn = gr.Button("Run Validation", variant="primary") + + with gr.Row(): + with gr.Column(): + gr.Markdown("#### Errors (Must Fix)") + validation_errors = gr.Markdown("*Click 'Run Validation' to check*") + with gr.Column(): + gr.Markdown("#### Warnings (Should Review)") + validation_warnings = gr.Markdown("") + + gr.Markdown("#### Summary") + validation_info = gr.Markdown("") + + gr.Markdown("---") + gr.Markdown("### Derivative Change Analyzer") + gr.Markdown("**Before modifying an entity**, see what else would need to change.") + + with gr.Row(): + derivative_entity_type = gr.Dropdown( + choices=["location", "npc", "faction", "quest", "item", "dialogue"], + value="npc", + label="Entity Type", + scale=1 + ) + derivative_entity_id = gr.Textbox( + label="Entity ID", + placeholder="e.g., 'whiterun' or 'alvor'", + scale=2 + ) + analyze_derivative_btn = gr.Button("Analyze Impact", variant="secondary", scale=1) + + derivative_report = gr.Markdown("*Select an entity and click 'Analyze Impact' to see derivative changes*") + + # ==================== TAB 1: ENTITY DATABASE ==================== + with gr.Tab("Entity Database"): + gr.Markdown("### Central Entity Repository") + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("#### Quick Add Entity") + entity_type_select = gr.Dropdown( + choices=["Location", "NPC", "Quest", "Item", "Faction"], + value="Location", + label="Entity Type" + ) + entity_json_input = gr.Textbox( + label="Entity JSON", + lines=10, + placeholder='{"id": "whiterun", "name": "Whiterun", "region": "whiterun_hold", "location_type": "city", "population": 100}' + ) + add_entity_btn = gr.Button("Add Entity", variant="primary") + add_status = gr.Markdown("") + + with gr.Column(scale=1): + gr.Markdown("#### Database Export") + export_btn = gr.Button("Export All Entities") + db_export_output = gr.Textbox( + label="Full Database JSON", + lines=15, + interactive=False + ) + + with gr.Accordion("Import Database", open=False): + import_json_input = gr.Textbox( + label="Paste Database JSON", + lines=10, + placeholder='{"locations": {...}, "npcs": {...}, "quests": {...}, "items": {...}}' + ) + with gr.Row(): + import_btn = gr.Button("Import Database") + clear_db_btn = gr.Button("Clear Database", variant="stop") + import_status = gr.Markdown("") + + gr.Markdown("---") + gr.Markdown("### Export to Playable Game Config") + gr.Markdown("Generate a config that works with the **Game Preview** tab and **MovingDotSpace**.") + + with gr.Row(): + export_game_config_btn = gr.Button("Export Game Config (1D/2D)", variant="primary", size="lg") + copy_to_preview_info = gr.Markdown("*Click to generate config, then copy to Game Preview tab*") + + game_config_output = gr.Code( + label="Playable Game Config JSON - Copy this to Game Preview or MovingDotSpace", + language="json", + lines=20 + ) + export_config_status = gr.Markdown("") + + gr.Markdown(""" +**How to use your exported config:** + +1. **Click "Export Game Config"** above to generate the config +2. **Copy the JSON** from the code box +3. Go to the **Game Preview** tab or **MovingDotSpace** section +4. **Paste the JSON** and click Generate/Load +5. **Play your game!** + +The config includes: +- All locations as explorable areas +- NPCs with their dialogue +- Travel connections between locations +- Dungeon exploration states +- Quest journal +""") + + # ==================== TAB 2: WORLD BUILDER ==================== + with gr.Tab("World Builder"): + gr.Markdown("### Region & Location Design") + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("#### Add/Edit Location") + loc_id = gr.Textbox(label="ID (auto-generated if blank)") + loc_name = gr.Textbox(label="Name", placeholder="Whiterun") + loc_region = gr.Textbox(label="Region", placeholder="Whiterun Hold") + with gr.Row(): + loc_x = gr.Number(label="X Coordinate", value=0) + loc_y = gr.Number(label="Y Coordinate", value=0) + loc_type = gr.Dropdown( + choices=["city", "town", "village", "dungeon", "camp", "landmark", "cave", "ruin", "fort"], + value="town", + label="Location Type" + ) + loc_population = gr.Number(label="Population", value=0) + loc_connections = gr.Textbox( + label="Connected Locations (comma-separated)", + placeholder="riverwood, rorikstead" + ) + with gr.Row(): + add_location_btn = gr.Button("Add/Update Location", variant="primary") + delete_location_btn = gr.Button("Delete Selected", variant="stop") + loc_status = gr.Markdown("") + + with gr.Column(scale=1): + gr.Markdown("#### Location List") + gr.Markdown("*Click a row to load it for editing*") + location_list_output = gr.Dataframe( + headers=["ID", "Name", "Type", "Region", "X", "Y", "Population"], + datatype=["str", "str", "str", "str", "number", "number", "number"], + interactive=False + ) + with gr.Row(): + refresh_locations_btn = gr.Button("Refresh List") + refresh_map_btn = gr.Button("Refresh Map") + + with gr.Accordion("World Map Preview", open=True): + gr.Markdown("*Visual map - locations plotted by X,Y coordinates. Lines show connections.*") + map_preview = gr.HTML("
Load demo data or add locations to see the map
") + + # ==================== TAB 3: NPC SCHEDULER ==================== + with gr.Tab("NPC Scheduler"): + gr.Markdown("### NPC Definition & Daily Schedules") + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("#### Add/Edit NPC") + npc_id = gr.Textbox(label="ID (auto-generated if blank)") + npc_name = gr.Textbox(label="Name", placeholder="Balgruuf") + npc_race = gr.Dropdown( + choices=["human", "elf", "dwarf", "orc", "halfling", "nord", "imperial", "breton", "redguard", "khajiit", "argonian"], + value="human", + label="Race" + ) + npc_role = gr.Dropdown( + choices=["commoner", "merchant", "guard", "noble", "blacksmith", "innkeeper", "priest", "mage", "warrior", "thief", "beggar", "farmer"], + value="commoner", + label="Role" + ) + npc_level = gr.Number(label="Level", value=1) + npc_essential = gr.Checkbox(label="Essential (Cannot Die)") + npc_home = gr.Textbox(label="Home Location ID", placeholder="whiterun") + npc_factions = gr.Textbox( + label="Faction Memberships (comma-separated)", + placeholder="whiterun_guard, companions" + ) + + gr.Markdown("#### Schedule (JSON)") + npc_schedule = gr.Textbox( + label="Daily Schedule", + lines=5, + placeholder='[{"hours": "0-8", "action": "sleep", "location": "home"}, {"hours": "8-20", "action": "work", "location": "shop"}]' + ) + with gr.Row(): + add_npc_btn = gr.Button("Add/Update NPC", variant="primary") + delete_npc_btn = gr.Button("Delete Selected", variant="stop") + npc_status = gr.Markdown("") + + with gr.Column(scale=1): + gr.Markdown("#### NPC List") + npc_list_output = gr.Dataframe( + headers=["ID", "Name", "Race", "Role", "Level", "Home", "Essential"], + datatype=["str", "str", "str", "str", "number", "str", "bool"], + interactive=False + ) + refresh_npcs_btn = gr.Button("Refresh List") + + with gr.Accordion("Schedule Templates", open=False): + gr.Markdown(""" +**Common Schedule Templates:** + +**Shopkeeper:** +```json +[ + {"hours": "0-6", "action": "sleep", "location": "home_bedroom"}, + {"hours": "6-8", "action": "eat", "location": "home_dining"}, + {"hours": "8-20", "action": "work", "location": "shop_counter"}, + {"hours": "20-22", "action": "eat", "location": "tavern"}, + {"hours": "22-24", "action": "wander", "location": "home"} +] +``` + +**Guard:** +```json +[ + {"hours": "0-6", "action": "sleep", "location": "barracks"}, + {"hours": "6-18", "action": "patrol", "location": "city_walls"}, + {"hours": "18-20", "action": "eat", "location": "barracks_mess"}, + {"hours": "20-24", "action": "guard", "location": "gate"} +] +``` + +**Noble:** +```json +[ + {"hours": "0-8", "action": "sleep", "location": "palace_bedroom"}, + {"hours": "8-10", "action": "eat", "location": "palace_dining"}, + {"hours": "10-18", "action": "sit_throne", "location": "throne_room"}, + {"hours": "18-20", "action": "eat", "location": "palace_dining"}, + {"hours": "20-24", "action": "wander", "location": "palace"} +] +``` + +**Farmer:** +```json +[ + {"hours": "0-5", "action": "sleep", "location": "farmhouse_bedroom"}, + {"hours": "5-6", "action": "eat", "location": "farmhouse_kitchen"}, + {"hours": "6-12", "action": "work", "location": "farm_fields"}, + {"hours": "12-13", "action": "eat", "location": "farmhouse_kitchen"}, + {"hours": "13-19", "action": "work", "location": "farm_fields"}, + {"hours": "19-21", "action": "relax", "location": "tavern"}, + {"hours": "21-24", "action": "sleep", "location": "farmhouse_bedroom"} +] +``` + """) + + with gr.Accordion("Schedule Timeline Visualization", open=True): + gr.Markdown("*Visual 24-hour timeline of NPC daily routines*") + timeline_npc_select = gr.Dropdown( + label="Select NPC", + choices=[], + interactive=True + ) + timeline_preview = gr.HTML("
Select an NPC to view their schedule
") + refresh_timeline_btn = gr.Button("Refresh Timeline", size="sm") + + with gr.Accordion("AI-Assisted NPC Generation", open=False): + gr.Markdown("*Generate complete NPCs using AI based on your description*") + npc_gen_prompt = gr.Textbox( + label="Describe the NPC", + placeholder="A gruff dwarven blacksmith who runs a shop in Whiterun, secretly sympathizes with the Stormcloaks", + lines=2 + ) + with gr.Row(): + npc_gen_model = gr.Dropdown( + label="Model", + choices=[ + ("Qwen3 4B (Recommended)", "Qwen/Qwen3-4B"), + ("Llama 3.2 1B (Fast)", "meta-llama/Llama-3.2-1B-Instruct") + ], + value="Qwen/Qwen3-4B" + ) + npc_gen_thinking = gr.Checkbox(label="Enable Thinking", value=True, info="Qwen3 only") + generate_npc_btn = gr.Button("Generate NPC", variant="primary") + npc_gen_preview = gr.Code(label="Generated NPC Preview", language="json", interactive=False) + npc_gen_status = gr.Markdown("") + add_generated_npc_btn = gr.Button("Add to Database", variant="secondary") + + # ==================== TAB 4: QUEST DESIGNER ==================== + with gr.Tab("Quest Designer"): + gr.Markdown("### Quest Creation & Radiant Templates") + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("#### Add Quest") + quest_id = gr.Textbox(label="ID (auto-generated if blank)") + quest_name = gr.Textbox(label="Quest Name", placeholder="Before the Storm") + quest_type = gr.Dropdown( + choices=["main", "faction", "side", "misc", "radiant"], + value="side", + label="Quest Type" + ) + quest_prereqs = gr.Textbox( + label="Prerequisite Quests (comma-separated)", + placeholder="mq101, mq102" + ) + + gr.Markdown("#### Stages (JSON)") + quest_stages = gr.Textbox( + label="Quest Stages", + lines=8, + placeholder='''[ + {"stage": 10, "journal_entry": "Talk to the Jarl", "objectives": [{"type": "talk_to", "target": "balgruuf"}]}, + {"stage": 20, "journal_entry": "Investigate the barrow", "objectives": [{"type": "go_to", "target": "bleak_falls"}]} +]''' + ) + + gr.Markdown("#### Rewards (JSON)") + quest_rewards = gr.Textbox( + label="Quest Rewards", + lines=3, + placeholder='{"gold": 500, "items": ["steel_sword"], "faction_rep": {"companions": 25}}' + ) + with gr.Row(): + add_quest_btn = gr.Button("Add/Update Quest", variant="primary") + delete_quest_btn = gr.Button("Delete Selected", variant="stop") + quest_status = gr.Markdown("") + + with gr.Column(scale=1): + gr.Markdown("#### Quest List") + quest_list_output = gr.Dataframe( + headers=["ID", "Name", "Type", "Stages", "Prerequisites"], + datatype=["str", "str", "str", "number", "str"], + interactive=False + ) + refresh_quests_btn = gr.Button("Refresh List") + + with gr.Accordion("Radiant Quest Templates", open=False): + gr.Markdown(""" +**Radiant Quest Template System** - Generate procedural quests from templates that fill variables from your entity pools. + """) + + with gr.Row(): + radiant_template_dropdown = gr.Dropdown( + label="Select Template", + choices=[ + ("Thieves Guild Burglary (tg_burglary)", "tg_burglary"), + ("Companions Extermination (comp_exterminate)", "comp_exterminate"), + ("Delivery Quest (misc_delivery)", "misc_delivery"), + ("Bounty Hunt (bounty_hunt)", "bounty_hunt") + ], + value="tg_burglary" + ) + generate_radiant_btn = gr.Button("Generate Quest", variant="primary") + + radiant_template_info = gr.Markdown(""" +**tg_burglary**: Steal {TARGET_ITEM} from {TARGET_NPC}'s residence +- Requires: NPCs with merchant/noble roles, items with value > 50 +- Rewards: 200 gold, Thieves Guild reputation + """) + + radiant_preview_json = gr.Code( + label="Generated Quest Preview", + language="json", + value="", + interactive=False + ) + + radiant_status = gr.Markdown("") + + add_radiant_quest_btn = gr.Button("Add to Database", variant="secondary") + + with gr.Accordion("Template Documentation", open=False): + gr.Markdown(""" +**Available Templates:** + +| Template ID | Description | Required Entities | +|-------------|-------------|-------------------| +| `tg_burglary` | Steal a valuable item | NPCs (merchant/noble), Items (value > 50) | +| `comp_exterminate` | Clear a location of enemies | Locations (dungeon/cave/ruin) | +| `misc_delivery` | Deliver an item between NPCs | NPCs (merchant/innkeeper), Items (misc, weight < 5) | +| `bounty_hunt` | Hunt a dangerous target | NPCs (thief/warrior, non-essential) | + +**Variable Types:** +- `pool`: Select from entity pool (locations, npcs, items) +- `derived_from`: Get attribute from another variable (e.g., TARGET_NPC.home_location) +- `random_choice`: Pick randomly from a list +- `random_range`: Generate random number in range + """) + + with gr.Accordion("Quest Objective Types Reference", open=False): + gr.Markdown(""" +**Standard Objective Types:** + +| Type | Parameters | Description | +|------|------------|-------------| +| `talk_to` | `target` (npc_id) | Speak with an NPC | +| `go_to` | `target` (location_id) | Travel to a location | +| `kill` | `target` (npc_id or type), `count` | Defeat enemies | +| `collect` | `item` (item_id), `count` | Gather items | +| `escort` | `target` (npc_id), `destination` | Protect and guide NPC | +| `use_object` | `target` (object_id) | Interact with world object | +| `steal` | `target` (item_id), `from` (npc/location) | Take without detection | +| `deliver` | `item`, `to` (npc_id) | Give item to NPC | +| `wait` | `hours`, `location` | Pass time at location | +| `craft` | `item`, `at` (station) | Create item at workstation | + +**Example Stage with Multiple Objectives:** +```json +{ + "stage": 30, + "journal_entry": "Retrieve the artifact and escape", + "objectives": [ + {"type": "go_to", "target": "ancient_ruins_inner"}, + {"type": "collect", "item": "ancient_artifact", "count": 1}, + {"type": "kill", "target": "draugr", "count": 5}, + {"type": "go_to", "target": "ancient_ruins_exit"} + ], + "on_complete": {"advance_to": 40, "add_item": "ancient_artifact"} +} +``` + """) + + with gr.Accordion("AI-Assisted Quest Generation", open=False): + gr.Markdown("*Generate complete quests using AI based on your description*") + quest_gen_prompt = gr.Textbox( + label="Describe the Quest", + placeholder="A main quest where the player must retrieve an ancient artifact from a dragon's lair, with help from a mysterious mage", + lines=2 + ) + quest_gen_model = gr.Dropdown( + label="Model", + choices=[ + ("Qwen3 4B (Recommended)", "Qwen/Qwen3-4B"), + ("Llama 3.2 1B (Fast)", "meta-llama/Llama-3.2-1B-Instruct") + ], + value="Qwen/Qwen3-4B" + ) + generate_quest_btn = gr.Button("Generate Quest", variant="primary") + quest_gen_preview = gr.Code(label="Generated Quest Preview", language="json", interactive=False) + quest_gen_status = gr.Markdown("") + add_generated_quest_btn = gr.Button("Add to Database", variant="secondary") + + # ==================== TAB 5: ITEM & CRAFTING SYSTEM ==================== + with gr.Tab("Items & Crafting"): + gr.Markdown("### Item Database & Crafting Recipes") + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("#### Add Item") + item_id = gr.Textbox(label="ID (auto-generated if blank)") + item_name = gr.Textbox(label="Name", placeholder="Iron Sword") + item_type_select = gr.Dropdown( + choices=["weapon", "armor", "consumable", "ingredient", "misc", "key", "book", "soul_gem"], + value="misc", + label="Item Type" + ) + with gr.Row(): + item_weight = gr.Number(label="Weight", value=0) + item_value = gr.Number(label="Value (Gold)", value=0) + item_properties = gr.Textbox( + label="Properties (JSON)", + lines=3, + placeholder='{"damage": 10, "speed": 1.0, "material": "iron"}' + ) + with gr.Row(): + add_item_btn = gr.Button("Add/Update Item", variant="primary") + delete_item_btn = gr.Button("Delete Selected", variant="stop") + item_status = gr.Markdown("") + + with gr.Column(scale=1): + gr.Markdown("#### Item List") + item_list_output = gr.Dataframe( + headers=["ID", "Name", "Type", "Weight", "Value"], + datatype=["str", "str", "str", "number", "number"], + interactive=False + ) + refresh_items_btn = gr.Button("Refresh List") + + with gr.Accordion("Add Crafting Recipe", open=False): + gr.Markdown("#### Crafting Recipe") + recipe_id = gr.Textbox(label="Recipe ID") + recipe_name = gr.Textbox(label="Recipe Name", placeholder="Forge Iron Sword") + recipe_type = gr.Dropdown( + choices=["smithing", "alchemy", "enchanting", "cooking", "tanning", "smelting"], + value="smithing", + label="Recipe Type" + ) + recipe_ingredients = gr.Textbox( + label="Ingredients (JSON)", + lines=2, + placeholder='[{"item_id": "iron_ingot", "count": 2}, {"item_id": "leather_strips", "count": 1}]' + ) + recipe_result = gr.Textbox( + label="Result (JSON)", + placeholder='{"item_id": "iron_sword", "count": 1}' + ) + recipe_workstation = gr.Textbox(label="Workstation", placeholder="forge") + add_recipe_btn = gr.Button("Add Recipe", variant="primary") + recipe_status = gr.Markdown("") + + with gr.Accordion("Item Type Templates", open=False): + gr.Markdown(""" +**Weapon Properties:** +```json +{"damage": 10, "speed": 1.0, "reach": 1.0, "material": "iron", "skill": "one_handed"} +``` + +**Armor Properties:** +```json +{"armor_rating": 25, "slot": "chest", "material": "steel", "skill": "heavy_armor"} +``` + +**Consumable Properties:** +```json +{"effects": [{"type": "restore_health", "magnitude": 50}], "duration": 0} +``` + +**Ingredient Properties (Alchemy):** +```json +{"effects": ["restore_health", "damage_stamina", "fortify_smithing", "weakness_to_fire"]} +``` + +**Soul Gem Properties:** +```json +{"capacity": "grand", "filled": false, "soul_size": null} +``` + """) + + with gr.Accordion("Economy Balance Analysis", open=False): + gr.Markdown("*Analyze item values and detect balance issues*") + analyze_economy_btn = gr.Button("Analyze Economy", variant="secondary") + with gr.Row(): + economy_report = gr.Markdown("*Click 'Analyze Economy' to see statistics*") + economy_chart = gr.HTML("
Chart will appear here
") + + # ==================== TAB 6: FACTION SYSTEM ==================== + with gr.Tab("Factions"): + gr.Markdown("### Faction Management & Relationships") + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("#### Add Faction") + faction_id = gr.Textbox(label="ID (auto-generated if blank)") + faction_name = gr.Textbox(label="Faction Name", placeholder="Companions") + faction_type = gr.Dropdown( + choices=["guild", "government", "religious", "criminal", "military", "merchant", "noble_house"], + value="guild", + label="Faction Type" + ) + faction_hq = gr.Textbox(label="Headquarters Location ID", placeholder="jorrvaskr") + faction_ranks = gr.Textbox( + label="Ranks (JSON)", + lines=4, + placeholder='''[ + {"name": "Initiate", "level": 1}, + {"name": "Member", "level": 2}, + {"name": "Senior", "level": 3}, + {"name": "Leader", "level": 4} +]''' + ) + faction_relationships = gr.Textbox( + label="Relationships (JSON: faction_id -> disposition)", + lines=2, + placeholder='{"imperial_legion": -50, "silver_hand": -100}' + ) + with gr.Row(): + add_faction_btn = gr.Button("Add/Update Faction", variant="primary") + delete_faction_btn = gr.Button("Delete Selected", variant="stop") + faction_status = gr.Markdown("") + + with gr.Column(scale=1): + gr.Markdown("#### Faction List") + faction_list_output = gr.Dataframe( + headers=["ID", "Name", "Type", "HQ", "Ranks"], + datatype=["str", "str", "str", "str", "number"], + interactive=False + ) + refresh_factions_btn = gr.Button("Refresh List") + + with gr.Accordion("Faction Relationship Matrix Guide", open=False): + gr.Markdown(""" +**Disposition Values:** +- **100**: Allied (will fight for each other) +- **50**: Friendly (positive interactions) +- **0**: Neutral (no special treatment) +- **-50**: Unfriendly (hostile dialogue, refused services) +- **-100**: Enemy (attack on sight) + +**Example Faction Relationships:** +```json +{ + "companions": { + "silver_hand": -100, + "whiterun_guard": 50, + "college_of_winterhold": 0 + }, + "thieves_guild": { + "riften_guard": -25, + "maven_black_briar": 75, + "dark_brotherhood": 25 + } +} +``` + +**Crime System Integration:** +Factions can track bounties: +```json +{ + "faction_id": "whiterun_hold", + "crime_gold": 1000, + "crimes": ["murder", "theft", "assault"] +} +``` + """) + + with gr.Accordion("Faction Relationship Graph", open=True): + gr.Markdown("*Visual graph showing alliances and rivalries between factions*") + faction_graph_preview = gr.HTML("
Load demo data or add factions to see the graph
") + refresh_faction_graph_btn = gr.Button("Refresh Graph", size="sm") + + # ==================== TAB 7: DIALOGUE SYSTEM ==================== + with gr.Tab("Dialogue"): + gr.Markdown("### Dialogue Topics & Conversation Trees") + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("#### Add Dialogue Topic") + dlg_id = gr.Textbox(label="Topic ID") + dlg_name = gr.Textbox(label="Topic Name", placeholder="About the war...") + dlg_speaker = gr.Textbox(label="Speaker NPC ID", placeholder="balgruuf") + dlg_priority = gr.Number(label="Priority (higher = shown first)", value=50) + dlg_conditions = gr.Textbox( + label="Conditions (JSON)", + lines=3, + placeholder='[{"type": "quest_stage", "quest": "mq102", "min": 10}, {"type": "faction_rank", "faction": "companions", "min": 2}]' + ) + dlg_response = gr.Textbox( + label="Response Text", + lines=3, + placeholder="The war has been hard on all of us..." + ) + dlg_effects = gr.Textbox( + label="Effects (JSON)", + lines=2, + placeholder='[{"type": "advance_quest", "quest": "mq103", "stage": 10}]' + ) + dlg_next = gr.Textbox( + label="Follow-up Topics (comma-separated IDs)", + placeholder="topic_war_details, topic_war_end" + ) + with gr.Row(): + add_dialogue_btn = gr.Button("Add/Update Dialogue", variant="primary") + delete_dialogue_btn = gr.Button("Delete Selected", variant="stop") + dlg_status = gr.Markdown("") + + with gr.Column(scale=1): + gr.Markdown("#### Dialogue List") + dialogue_list_output = gr.Dataframe( + headers=["ID", "Name", "Speaker", "Priority", "Has Conditions"], + datatype=["str", "str", "str", "number", "bool"], + interactive=False + ) + refresh_dialogue_btn = gr.Button("Refresh List") + + with gr.Accordion("Dialogue Condition & Effect Types", open=False): + gr.Markdown(""" +**Condition Types:** +| Type | Parameters | Description | +|------|------------|-------------| +| `quest_stage` | `quest`, `min`, `max` | Quest at specific stage | +| `faction_rank` | `faction`, `min` | Member of faction at rank | +| `has_item` | `item_id`, `count` | Possesses item(s) | +| `skill_level` | `skill`, `min` | Skill meets threshold | +| `flag` | `flag_name`, `value` | Game flag is set | +| `disposition` | `npc_id`, `min` | NPC likes player enough | +| `time_of_day` | `min_hour`, `max_hour` | Current game time | + +**Effect Types:** +| Type | Parameters | Description | +|------|------------|-------------| +| `advance_quest` | `quest`, `stage` | Progress quest | +| `add_item` | `item_id`, `count` | Give item to player | +| `remove_item` | `item_id`, `count` | Take item from player | +| `add_gold` | `amount` | Give gold | +| `set_disposition` | `npc_id`, `value` | Change NPC opinion | +| `set_flag` | `flag_name`, `value` | Set game flag | +| `start_quest` | `quest_id` | Begin new quest | +| `unlock_topic` | `topic_id` | Make topic available | + +**Example Dialogue Tree:** +``` +[Greeting] ─┬─> [About the War] ─┬─> [Support Imperials] + │ └─> [Support Stormcloaks] + ├─> [About Dragons] ──> [Dragon Locations] + └─> [Rumors] ─────────> [Quest Hook] +``` + """) + + with gr.Accordion("AI-Assisted Dialogue Generation", open=False): + gr.Markdown("*Generate dialogue trees for NPCs using AI*") + dlg_gen_npc = gr.Dropdown( + label="Select NPC", + choices=[], + interactive=True + ) + dlg_gen_topic = gr.Textbox( + label="Dialogue Topic/Description", + placeholder="Ask about rumors in town, what they think of the new Jarl", + lines=2 + ) + dlg_gen_model = gr.Dropdown( + label="Model", + choices=[ + ("Qwen3 4B (Recommended)", "Qwen/Qwen3-4B"), + ("Llama 3.2 1B (Fast)", "meta-llama/Llama-3.2-1B-Instruct") + ], + value="Qwen/Qwen3-4B" + ) + generate_dialogue_btn = gr.Button("Generate Dialogue", variant="primary") + dlg_gen_preview = gr.Code(label="Generated Dialogue Preview", language="json", interactive=False) + dlg_gen_status = gr.Markdown("") + add_generated_dialogue_btn = gr.Button("Add to Database", variant="secondary") + + # ==================== TAB 8: SKILL & PROGRESSION ==================== + with gr.Tab("Skills & Perks"): + gr.Markdown("### Skill Trees & Character Progression") + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("#### Add Skill") + skill_id = gr.Textbox(label="Skill ID") + skill_name = gr.Textbox(label="Skill Name", placeholder="One-Handed") + skill_category = gr.Dropdown( + choices=["combat", "magic", "stealth", "crafting"], + value="combat", + label="Category" + ) + skill_description = gr.Textbox( + label="Description", + placeholder="The art of fighting with one-handed weapons." + ) + skill_attribute = gr.Dropdown( + choices=["strength", "intelligence", "dexterity", "constitution", "charisma", "wisdom"], + value="strength", + label="Governing Attribute" + ) + skill_xp_actions = gr.Textbox( + label="XP Actions (JSON)", + lines=3, + placeholder='[{"action": "hit_enemy", "xp": 5}, {"action": "kill_enemy", "xp": 20}]' + ) + with gr.Row(): + add_skill_btn = gr.Button("Add/Update Skill", variant="primary") + delete_skill_btn = gr.Button("Delete Selected", variant="stop") + skill_status = gr.Markdown("") + + with gr.Column(scale=1): + gr.Markdown("#### Skill List") + skill_list_output = gr.Dataframe( + headers=["ID", "Name", "Category", "Attribute"], + datatype=["str", "str", "str", "str"], + interactive=False + ) + refresh_skills_btn = gr.Button("Refresh List") + + with gr.Accordion("Add Perk", open=False): + gr.Markdown("#### Perk Definition") + perk_id = gr.Textbox(label="Perk ID") + perk_name = gr.Textbox(label="Perk Name", placeholder="Armsman") + perk_skill = gr.Textbox(label="Parent Skill ID", placeholder="one_handed") + perk_skill_req = gr.Number(label="Skill Level Required", value=0) + perk_description = gr.Textbox( + label="Description", + placeholder="One-handed weapons do 20% more damage." + ) + perk_effects = gr.Textbox( + label="Effects (JSON)", + lines=2, + placeholder='[{"type": "damage_mult", "skill": "one_handed", "value": 1.2}]' + ) + perk_prereqs = gr.Textbox( + label="Prerequisite Perks (comma-separated)", + placeholder="armsman_1, armsman_2" + ) + perk_cost = gr.Number(label="Perk Point Cost", value=1) + add_perk_btn = gr.Button("Add Perk", variant="primary") + perk_status = gr.Markdown("") + + with gr.Accordion("Skill System Templates", open=False): + gr.Markdown(""" +**Standard Skill Categories:** + +**Combat Skills:** +- One-Handed, Two-Handed, Archery, Block, Heavy Armor, Light Armor + +**Magic Skills:** +- Destruction, Restoration, Alteration, Conjuration, Illusion, Enchanting + +**Stealth Skills:** +- Sneak, Lockpicking, Pickpocket, Speech, Alchemy + +**Example Perk Tree (One-Handed):** +``` + [Armsman 5/5] (20/40/60/80/100) + │ + ┌──────────┼──────────┐ + │ │ │ + [Bladesman] [Bone Breaker] [Dual Flurry] + │ │ │ + [Critical] [Paralyze] [Dual Savagery] +``` + +**XP Formulas:** +```python +# Skill XP from action +xp_gained = base_xp * (1 + skill_level * 0.01) + +# Level from skill sum (Skyrim-style) +level = floor((skill_sum - 272) / 18) + 1 +``` + """) + + # ==================== EVENT HANDLERS ==================== + + def update_stats(): + stats = db.get_stats() + return f"**Database:** {stats['locations']} locations | {stats['npcs']} NPCs | {stats['quests']} quests | {stats['items']} items | {stats['factions']} factions | {stats['skills']} skills" + + def add_entity_handler(entity_type, entity_json): + if not entity_json: + return "Error: JSON is required" + try: + data = json.loads(entity_json) + if entity_type == "Location": + loc = Location(**data) + db.add_location(loc) + return f"Added location: {loc.name} ({loc.id})" + elif entity_type == "NPC": + npc = NPC(**data) + db.add_npc(npc) + return f"Added NPC: {npc.name} ({npc.id})" + elif entity_type == "Quest": + # Handle stages specially + stages = data.pop("stages", []) + quest_stages = [QuestStage(**s) for s in stages] + quest = Quest(**data, stages=quest_stages) + db.add_quest(quest) + return f"Added quest: {quest.name} ({quest.id})" + elif entity_type == "Item": + item = Item(**data) + db.add_item(item) + return f"Added item: {item.name} ({item.id})" + elif entity_type == "Faction": + faction_id = data.get("id", data.get("name", "unknown").lower().replace(" ", "_")) + db.factions[faction_id] = data + return f"Added faction: {data.get('name', faction_id)}" + else: + return f"Unknown entity type: {entity_type}" + except Exception as e: + return f"Error: {str(e)}" + + def add_location_handler(id_val, name, region, x, y, loc_type, population, connections): + if not name: + return "Error: Name is required", gr.update() + try: + loc = parse_location_form(id_val, name, region, x, y, loc_type, population, connections) + db.add_location(loc) + return f"Added location: {loc.name} ({loc.id})", get_location_list() + except Exception as e: + return f"Error: {str(e)}", gr.update() + + def get_location_list(): + rows = [] + for loc in db.locations.values(): + rows.append([loc.id, loc.name, loc.location_type, loc.region, + loc.coordinates["x"], loc.coordinates["y"], loc.population]) + return rows if rows else [["", "", "", "", 0, 0, 0]] + + def add_npc_handler(id_val, name, race, role, level, essential, home, factions, schedule): + if not name: + return "Error: Name is required", gr.update() + try: + npc = parse_npc_form(id_val, name, race, role, level, essential, home, factions, schedule) + db.add_npc(npc) + return f"Added NPC: {npc.name} ({npc.id})", get_npc_list() + except Exception as e: + return f"Error: {str(e)}", gr.update() + + def get_npc_list(): + rows = [] + for npc in db.npcs.values(): + rows.append([npc.id, npc.name, npc.race, npc.role, npc.level, npc.home_location, npc.essential]) + return rows if rows else [["", "", "", "", 0, "", False]] + + def add_quest_handler(id_val, name, quest_type, stages, rewards, prereqs): + if not name: + return "Error: Name is required", gr.update() + try: + quest = parse_quest_form(id_val, name, quest_type, stages, rewards, prereqs) + db.add_quest(quest) + return f"Added quest: {quest.name} ({quest.id})", get_quest_list() + except Exception as e: + return f"Error: {str(e)}", gr.update() + + def get_quest_list(): + rows = [] + for q in db.quests.values(): + rows.append([q.id, q.name, q.quest_type, len(q.stages), ", ".join(q.prerequisites)]) + return rows if rows else [["", "", "", 0, ""]] + + def export_database(): + return db.to_json() + + def import_database(json_str): + if not json_str: + return "Error: No JSON provided" + success, message = db.from_json(json_str) + if success: + stats = db.get_stats() + return f"{message}. Imported: {stats['locations']} locations, {stats['npcs']} NPCs, {stats['quests']} quests, {stats['items']} items" + return message + + def clear_database(): + db.clear() + return "Database cleared" + + # Connect event handlers + refresh_stats_btn.click(fn=update_stats, outputs=[stats_display]) + + add_entity_btn.click( + fn=add_entity_handler, + inputs=[entity_type_select, entity_json_input], + outputs=[add_status] + ) + + export_btn.click(fn=export_database, outputs=[db_export_output]) + import_btn.click(fn=import_database, inputs=[import_json_input], outputs=[import_status]) + clear_db_btn.click(fn=clear_database, outputs=[import_status]) + + # Export to playable game config + def export_game_config(): + if not db.locations: + return "", "No data to export. Load the demo or add entities first." + try: + config_json = db.export_to_game_config() + stats = db.get_stats() + loc_count = stats['locations'] + npc_count = stats['npcs'] + dlg_count = stats['dialogues'] + return config_json, f"Exported {loc_count} locations with {npc_count} NPCs and {dlg_count} dialogue states. Copy to Game Preview tab to play!" + except Exception as e: + return "", f"Export error: {str(e)}" + + export_game_config_btn.click( + fn=export_game_config, + outputs=[game_config_output, export_config_status] + ) + + add_location_btn.click( + fn=add_location_handler, + inputs=[loc_id, loc_name, loc_region, loc_x, loc_y, loc_type, loc_population, loc_connections], + outputs=[loc_status, location_list_output] + ) + refresh_locations_btn.click(fn=get_location_list, outputs=[location_list_output]) + + add_npc_btn.click( + fn=add_npc_handler, + inputs=[npc_id, npc_name, npc_race, npc_role, npc_level, npc_essential, npc_home, npc_factions, npc_schedule], + outputs=[npc_status, npc_list_output] + ) + refresh_npcs_btn.click(fn=get_npc_list, outputs=[npc_list_output]) + + # NPC Schedule Timeline handlers + def update_timeline_npc_choices(): + choices = [(f"{npc.name} ({npc.id})", npc.id) for npc in db.npcs.values()] + return gr.update(choices=choices) + + # Timeline event handlers moved after generate_schedule_timeline_svg definition + + add_quest_btn.click( + fn=add_quest_handler, + inputs=[quest_id, quest_name, quest_type, quest_stages, quest_rewards, quest_prereqs], + outputs=[quest_status, quest_list_output] + ) + refresh_quests_btn.click(fn=get_quest_list, outputs=[quest_list_output]) + + # ==================== RADIANT QUEST HANDLERS ==================== + _radiant_generated_quest = [None] # Store generated quest for adding + + def update_template_info(template_id): + """Update the template info markdown based on selection.""" + info_map = { + "tg_burglary": """**tg_burglary**: Steal {TARGET_ITEM} from {TARGET_NPC}'s residence +- Requires: NPCs with merchant/noble roles, items with value > 50 +- Rewards: 200 gold, Thieves Guild reputation""", + "comp_exterminate": """**comp_exterminate**: Clear {TARGET_LOCATION} of {ENEMY_TYPE} +- Requires: Locations with type dungeon/cave/ruin +- Rewards: 300 gold, Companions reputation""", + "misc_delivery": """**misc_delivery**: Deliver {DELIVERY_ITEM} to {TARGET_NPC} +- Requires: NPCs (merchant/innkeeper), misc items with weight < 5 +- Rewards: 75 gold""", + "bounty_hunt": """**bounty_hunt**: Hunt down {TARGET_NPC} +- Requires: Non-essential NPCs with thief/warrior role +- Rewards: Random bounty (100-500 gold)""" + } + return info_map.get(template_id, "Select a template") + + def generate_radiant_quest_handler(template_id): + """Generate a quest from the selected template.""" + if not template_id: + return "", "Select a template first" + + quest, message = generate_radiant_quest_instance(template_id, db) + + if quest is None: + _radiant_generated_quest[0] = None + return "", f"**Error:** {message}" + + _radiant_generated_quest[0] = quest + + # Format quest as JSON preview + quest_dict = { + "id": quest.id, + "name": quest.name, + "quest_type": quest.quest_type, + "stages": quest.stages, + "rewards": quest.rewards, + "prerequisites": quest.prerequisites + } + preview = json.dumps(quest_dict, indent=2) + + return preview, f"**Generated:** {quest.name}" + + def add_radiant_quest_handler(): + """Add the generated quest to the database.""" + if _radiant_generated_quest[0] is None: + return "", "Generate a quest first", gr.update() + + quest = _radiant_generated_quest[0] + db.add_quest(quest) + _radiant_generated_quest[0] = None + + return "", f"**Added to database:** {quest.name} ({quest.id})", get_quest_list() + + radiant_template_dropdown.change( + fn=update_template_info, + inputs=[radiant_template_dropdown], + outputs=[radiant_template_info] + ) + + generate_radiant_btn.click( + fn=generate_radiant_quest_handler, + inputs=[radiant_template_dropdown], + outputs=[radiant_preview_json, radiant_status] + ) + + add_radiant_quest_btn.click( + fn=add_radiant_quest_handler, + outputs=[radiant_preview_json, radiant_status, quest_list_output] + ) + + # ==================== ITEM & CRAFTING HANDLERS ==================== + def add_item_handler(id_val, name, item_type, weight, value, properties_json): + if not name: + return "Error: Name is required", gr.update() + try: + props = json.loads(properties_json) if properties_json else {} + item = Item( + id=id_val or name.lower().replace(" ", "_"), + name=name, + item_type=item_type, + weight=float(weight or 0), + value=int(value or 0), + properties=props + ) + db.add_item(item) + return f"Added item: {item.name} ({item.id})", get_item_list() + except Exception as e: + return f"Error: {str(e)}", gr.update() + + def get_item_list(): + rows = [] + for item in db.items.values(): + rows.append([item.id, item.name, item.item_type, item.weight, item.value]) + return rows if rows else [["", "", "", 0, 0]] + + def add_recipe_handler(id_val, name, rtype, ingredients_json, result_json, workstation): + if not name: + return "Error: Name is required" + try: + ingredients = json.loads(ingredients_json) if ingredients_json else [] + result = json.loads(result_json) if result_json else {} + recipe = Recipe( + id=id_val or name.lower().replace(" ", "_"), + name=name, + recipe_type=rtype, + ingredients=ingredients, + result=result, + workstation=workstation + ) + db.add_recipe(recipe) + return f"Added recipe: {recipe.name} ({recipe.id})" + except Exception as e: + return f"Error: {str(e)}" + + add_item_btn.click( + fn=add_item_handler, + inputs=[item_id, item_name, item_type_select, item_weight, item_value, item_properties], + outputs=[item_status, item_list_output] + ) + refresh_items_btn.click(fn=get_item_list, outputs=[item_list_output]) + add_recipe_btn.click( + fn=add_recipe_handler, + inputs=[recipe_id, recipe_name, recipe_type, recipe_ingredients, recipe_result, recipe_workstation], + outputs=[recipe_status] + ) + + # Economy Balance Analyzer handler moved after analyze_item_economy definition + + # ==================== FACTION HANDLERS ==================== + def add_faction_handler(id_val, name, ftype, hq, ranks_json, relationships_json): + if not name: + return "Error: Name is required", gr.update() + try: + ranks = json.loads(ranks_json) if ranks_json else [] + rels = json.loads(relationships_json) if relationships_json else {} + faction = Faction( + id=id_val or name.lower().replace(" ", "_"), + name=name, + faction_type=ftype, + headquarters=hq, + ranks=ranks, + relationships=rels + ) + db.add_faction(faction) + return f"Added faction: {faction.name} ({faction.id})", get_faction_list() + except Exception as e: + return f"Error: {str(e)}", gr.update() + + def get_faction_list(): + rows = [] + for fac in db.factions.values(): + rows.append([fac.id, fac.name, fac.faction_type, fac.headquarters, len(fac.ranks)]) + return rows if rows else [["", "", "", "", 0]] + + add_faction_btn.click( + fn=add_faction_handler, + inputs=[faction_id, faction_name, faction_type, faction_hq, faction_ranks, faction_relationships], + outputs=[faction_status, faction_list_output] + ) + refresh_factions_btn.click(fn=get_faction_list, outputs=[faction_list_output]) + # Faction graph handler moved after generate_faction_relationship_svg definition + + # ==================== DIALOGUE HANDLERS ==================== + def add_dialogue_handler(id_val, name, speaker, priority, conditions_json, response, effects_json, next_topics): + if not id_val and not name: + return "Error: ID or Name is required", gr.update() + try: + conditions = json.loads(conditions_json) if conditions_json else [] + effects = json.loads(effects_json) if effects_json else [] + topic = DialogueTopic( + id=id_val or name.lower().replace(" ", "_").replace(".", ""), + name=name, + speaker_id=speaker, + priority=int(priority or 50), + conditions=conditions, + response_text=response, + effects=effects, + next_topics=[t.strip() for t in next_topics.split(",") if t.strip()] + ) + db.add_dialogue(topic) + return f"Added dialogue: {topic.name} ({topic.id})", get_dialogue_list() + except Exception as e: + return f"Error: {str(e)}", gr.update() + + def get_dialogue_list(): + rows = [] + for dlg in db.dialogues.values(): + rows.append([dlg.id, dlg.name, dlg.speaker_id, dlg.priority, len(dlg.conditions) > 0]) + return rows if rows else [["", "", "", 0, False]] + + add_dialogue_btn.click( + fn=add_dialogue_handler, + inputs=[dlg_id, dlg_name, dlg_speaker, dlg_priority, dlg_conditions, dlg_response, dlg_effects, dlg_next], + outputs=[dlg_status, dialogue_list_output] + ) + refresh_dialogue_btn.click(fn=get_dialogue_list, outputs=[dialogue_list_output]) + + # ==================== LLM GENERATION HANDLERS ==================== + _generated_npc = [None] + _generated_quest = [None] + _generated_dialogue = [None] + + def npc_gen_handler(prompt, model_name, enable_thinking): + """Handle NPC generation.""" + if not prompt: + return "", "Please enter a description" + preview, status = generate_npc_from_prompt(prompt, db, model_name, enable_thinking) + if preview: + try: + _generated_npc[0] = json.loads(preview) + except: + _generated_npc[0] = None + return preview, status + + def add_gen_npc_handler(): + """Add generated NPC to database.""" + if not _generated_npc[0]: + return "", "Generate an NPC first", gr.update() + try: + data = _generated_npc[0] + npc = NPC( + id=data.get("id", ""), + name=data.get("name", ""), + race=data.get("race", "human"), + role=data.get("role", "civilian"), + level=data.get("level", 1), + essential=data.get("essential", False), + home_location=data.get("home_location", ""), + factions=data.get("factions", []), + schedule=data.get("schedule", {}) + ) + db.add_npc(npc) + _generated_npc[0] = None + return "", f"**Added:** {npc.name} ({npc.id})", get_npc_list() + except Exception as e: + return "", f"**Error:** {str(e)}", gr.update() + + def quest_gen_handler(prompt, model_name): + """Handle quest generation.""" + if not prompt: + return "", "Please enter a description" + preview, status = generate_quest_from_prompt(prompt, db, model_name) + if preview: + try: + _generated_quest[0] = json.loads(preview) + except: + _generated_quest[0] = None + return preview, status + + def add_gen_quest_handler(): + """Add generated quest to database.""" + if not _generated_quest[0]: + return "", "Generate a quest first", gr.update() + try: + data = _generated_quest[0] + quest = Quest( + id=data.get("id", ""), + name=data.get("name", ""), + quest_type=data.get("quest_type", "side"), + stages=data.get("stages", []), + rewards=data.get("rewards", {}), + prerequisites=data.get("prerequisites", []) + ) + db.add_quest(quest) + _generated_quest[0] = None + return "", f"**Added:** {quest.name} ({quest.id})", get_quest_list() + except Exception as e: + return "", f"**Error:** {str(e)}", gr.update() + + def dialogue_gen_handler(npc_id, topic, model_name): + """Handle dialogue generation.""" + if not npc_id: + return "", "Please select an NPC" + if not topic: + return "", "Please enter a topic" + preview, status = generate_dialogue_from_prompt(npc_id, topic, db, model_name) + if preview: + try: + _generated_dialogue[0] = json.loads(preview) + except: + _generated_dialogue[0] = None + return preview, status + + def add_gen_dialogue_handler(): + """Add generated dialogue to database.""" + if not _generated_dialogue[0]: + return "", "Generate dialogue first", gr.update() + try: + data = _generated_dialogue[0] + # Create dialogue entry + dialogue = Dialogue( + id=f"dlg_{data.get('npc_id', 'unknown')}_{data.get('topic', 'topic').replace(' ', '_').lower()[:20]}", + name=data.get("topic", "Generated Dialogue"), + speaker_id=data.get("npc_id", ""), + priority=50, + conditions=[], + response=data.get("initial_line", ""), + effects=[], + next_topics=[] + ) + db.add_dialogue(dialogue) + _generated_dialogue[0] = None + return "", f"**Added:** {dialogue.name} ({dialogue.id})", get_dialogue_list() + except Exception as e: + return "", f"**Error:** {str(e)}", gr.update() + + def update_dlg_gen_npc_choices(): + """Update NPC dropdown for dialogue generation.""" + choices = [(f"{npc.name} ({npc.id})", npc.id) for npc in db.npcs.values()] + return gr.update(choices=choices) + + # NPC generation handlers + generate_npc_btn.click( + fn=npc_gen_handler, + inputs=[npc_gen_prompt, npc_gen_model, npc_gen_thinking], + outputs=[npc_gen_preview, npc_gen_status] + ) + add_generated_npc_btn.click( + fn=add_gen_npc_handler, + outputs=[npc_gen_preview, npc_gen_status, npc_list_output] + ) + + # Quest generation handlers + generate_quest_btn.click( + fn=quest_gen_handler, + inputs=[quest_gen_prompt, quest_gen_model], + outputs=[quest_gen_preview, quest_gen_status] + ) + add_generated_quest_btn.click( + fn=add_gen_quest_handler, + outputs=[quest_gen_preview, quest_gen_status, quest_list_output] + ) + + # Dialogue generation handlers + generate_dialogue_btn.click( + fn=dialogue_gen_handler, + inputs=[dlg_gen_npc, dlg_gen_topic, dlg_gen_model], + outputs=[dlg_gen_preview, dlg_gen_status] + ) + add_generated_dialogue_btn.click( + fn=add_gen_dialogue_handler, + outputs=[dlg_gen_preview, dlg_gen_status, dialogue_list_output] + ) + + # Update dialogue NPC dropdown when NPCs change + refresh_npcs_btn.click( + fn=update_dlg_gen_npc_choices, + outputs=[dlg_gen_npc] + ) + + # ==================== SKILL & PERK HANDLERS ==================== + def add_skill_handler(id_val, name, category, description, attribute, xp_actions_json): + if not name: + return "Error: Name is required", gr.update() + try: + xp_actions = json.loads(xp_actions_json) if xp_actions_json else [] + skill = Skill( + id=id_val or name.lower().replace(" ", "_").replace("-", "_"), + name=name, + category=category, + description=description, + governing_attribute=attribute, + xp_actions=xp_actions + ) + db.add_skill(skill) + return f"Added skill: {skill.name} ({skill.id})", get_skill_list() + except Exception as e: + return f"Error: {str(e)}", gr.update() + + def get_skill_list(): + rows = [] + for skill in db.skills.values(): + rows.append([skill.id, skill.name, skill.category, skill.governing_attribute]) + return rows if rows else [["", "", "", ""]] + + def add_perk_handler(id_val, name, parent_skill, skill_req, description, effects_json, prereqs, cost): + if not name: + return "Error: Name is required" + try: + effects = json.loads(effects_json) if effects_json else [] + perk = Perk( + id=id_val or name.lower().replace(" ", "_"), + name=name, + skill_id=parent_skill, + skill_requirement=int(skill_req or 0), + description=description, + effects=effects, + prerequisites=[p.strip() for p in prereqs.split(",") if p.strip()], + cost=int(cost or 1) + ) + db.add_perk(perk) + return f"Added perk: {perk.name} ({perk.id})" + except Exception as e: + return f"Error: {str(e)}" + + add_skill_btn.click( + fn=add_skill_handler, + inputs=[skill_id, skill_name, skill_category, skill_description, skill_attribute, skill_xp_actions], + outputs=[skill_status, skill_list_output] + ) + refresh_skills_btn.click(fn=get_skill_list, outputs=[skill_list_output]) + add_perk_btn.click( + fn=add_perk_handler, + inputs=[perk_id, perk_name, perk_skill, perk_skill_req, perk_description, perk_effects, perk_prereqs, perk_cost], + outputs=[perk_status] + ) + + # ==================== DEMO & VALIDATION HANDLERS ==================== + + def load_demo_handler(): + stats = db.load_demo_data() + return ( + f"Demo loaded: {stats['locations']} locations, {stats['npcs']} NPCs, {stats['quests']} quests, " + f"{stats['items']} items, {stats['factions']} factions, {stats['skills']} skills, {stats['dialogues']} dialogues" + ) + + def run_validation(): + issues = db.validate() + errors_md = "" + warnings_md = "" + info_md = "" + + if issues["errors"]: + errors_md = "\n".join([f"- {e}" for e in issues["errors"]]) + else: + errors_md = "No errors found." + + if issues["warnings"]: + warnings_md = "\n".join([f"- {w}" for w in issues["warnings"]]) + else: + warnings_md = "No warnings." + + if issues["info"]: + info_md = "\n".join([f"- {i}" for i in issues["info"]]) + + return errors_md, warnings_md, info_md + + def analyze_derivative_handler(entity_type, entity_id): + """Analyze derivative changes for an entity.""" + if not entity_id or not entity_id.strip(): + return "*Enter an entity ID to analyze*" + analysis = db.analyze_derivative_changes(entity_type, entity_id.strip()) + return db.format_derivative_report(analysis) + + load_demo_btn.click(fn=load_demo_handler, outputs=[demo_status]) + validate_btn.click(fn=run_validation, outputs=[validation_errors, validation_warnings, validation_info]) + analyze_derivative_btn.click( + fn=analyze_derivative_handler, + inputs=[derivative_entity_type, derivative_entity_id], + outputs=[derivative_report] + ) + + # ==================== WORLD MAP VISUALIZATION ==================== + + def generate_world_map(): + """Generate SVG map of locations with connections.""" + if not db.locations: + return "
No locations to display. Add locations or load demo data.
" + + # Find bounds + xs = [loc.coordinates["x"] for loc in db.locations.values()] + ys = [loc.coordinates["y"] for loc in db.locations.values()] + min_x, max_x = min(xs), max(xs) + min_y, max_y = min(ys), max(ys) + + # Add padding + padding = 50 + width = 600 + height = 400 + + # Scale function + def scale_x(x): + if max_x == min_x: + return width / 2 + return padding + (x - min_x) / (max_x - min_x) * (width - 2 * padding) + + def scale_y(y): + if max_y == min_y: + return height / 2 + return padding + (y - min_y) / (max_y - min_y) * (height - 2 * padding) + + # Color by type + type_colors = { + "city": "#FFD700", # Gold + "town": "#90EE90", # Light green + "village": "#98FB98", # Pale green + "dungeon": "#DC143C", # Crimson + "camp": "#FFA500", # Orange + "landmark": "#87CEEB", # Sky blue + "cave": "#8B4513", # Saddle brown + "ruin": "#708090", # Slate gray + "fort": "#B22222", # Fire brick + } + + svg_parts = [f''] + + # Draw connections first (so they're behind dots) + for loc in db.locations.values(): + x1 = scale_x(loc.coordinates["x"]) + y1 = scale_y(loc.coordinates["y"]) + for conn_id in loc.connected_to: + if conn_id in db.locations: + conn = db.locations[conn_id] + x2 = scale_x(conn.coordinates["x"]) + y2 = scale_y(conn.coordinates["y"]) + svg_parts.append(f'') + + # Draw locations + for loc in db.locations.values(): + x = scale_x(loc.coordinates["x"]) + y = scale_y(loc.coordinates["y"]) + color = type_colors.get(loc.location_type, "#FFFFFF") + radius = 8 if loc.location_type == "city" else 6 if loc.location_type in ["town", "fort"] else 4 + + # Circle + svg_parts.append(f'') + + # Label + svg_parts.append(f'{loc.name}') + + # Legend + legend_y = height - 30 + legend_items = [("city", "City"), ("town", "Town"), ("dungeon", "Dungeon"), ("landmark", "Landmark")] + for i, (ltype, label) in enumerate(legend_items): + lx = 20 + i * 100 + color = type_colors.get(ltype, "#FFF") + svg_parts.append(f'') + svg_parts.append(f'{label}') + + svg_parts.append('') + return "".join(svg_parts) + + refresh_map_btn.click(fn=generate_world_map, outputs=[map_preview]) + + # ==================== NPC SCHEDULE TIMELINE VISUALIZATION ==================== + + def generate_schedule_timeline_svg(npc_id: str) -> str: + """Generate 24-hour timeline SVG for NPC schedule.""" + if not npc_id or npc_id not in db.npcs: + return "
Select an NPC to view their schedule
" + + npc = db.npcs[npc_id] + if not npc.schedule: + return f"
{npc.name} has no schedule defined
" + + width = 800 + height = 140 + hour_width = (width - 80) / 24 # 80px for labels + + # Color by action type + action_colors = { + "sleep": "#1a1a4e", # Dark blue + "eat": "#ff9900", # Orange + "work": "#4caf50", # Green + "train": "#e91e63", # Pink + "patrol": "#2196f3", # Blue + "wander": "#9c27b0", # Purple + "sit_throne": "#ffd700", # Gold + "meditate": "#00bcd4", # Cyan + "guard": "#795548", # Brown + "relax": "#8bc34a", # Light green + "teach": "#ff5722", # Deep orange + "idle": "#607d8b", # Blue gray + } + + svg_parts = [f''] + + # Title + svg_parts.append(f'{npc.name}\'s Daily Schedule') + svg_parts.append(f'{npc.race.title()} {npc.role.title()}') + + # Hour markers + for hour in range(0, 25, 4): + x = 50 + hour * hour_width + svg_parts.append(f'{hour:02d}:00') + svg_parts.append(f'') + + # Background bar + svg_parts.append(f'') + + # Schedule blocks + y_start = 67 + bar_height = 24 + + for entry in npc.schedule: + hours_str = entry.get("hours", "0-0") + action = entry.get("action", "idle") + location = entry.get("location", "") + + # Parse hours like "0-8" or "8-20" + try: + start_h, end_h = map(int, hours_str.split("-")) + except: + continue + + x_start = 50 + start_h * hour_width + block_width = (end_h - start_h) * hour_width + color = action_colors.get(action, "#666") + + # Rectangle + svg_parts.append(f'') + + # Label (if wide enough) + if block_width > 40: + label = action[:8] + svg_parts.append(f'{label}') + + # Legend + legend_y = height - 18 + legend_items = [("sleep", "Sleep"), ("work", "Work"), ("eat", "Eat"), ("patrol", "Patrol"), ("train", "Train")] + for i, (action, label) in enumerate(legend_items): + lx = 50 + i * 100 + color = action_colors.get(action, "#666") + svg_parts.append(f'') + svg_parts.append(f'{label}') + + svg_parts.append('') + return "".join(svg_parts) + + def get_npc_choices_for_timeline(): + """Get list of NPCs for dropdown.""" + return [(f"{npc.name} ({npc.id})", npc.id) for npc in db.npcs.values()] + + # Timeline event handlers (after function definition) + timeline_npc_select.change(fn=generate_schedule_timeline_svg, inputs=[timeline_npc_select], outputs=[timeline_preview]) + refresh_timeline_btn.click( + fn=lambda npc_id: generate_schedule_timeline_svg(npc_id), + inputs=[timeline_npc_select], + outputs=[timeline_preview] + ) + + # ==================== FACTION RELATIONSHIP GRAPH ==================== + + def generate_faction_relationship_svg() -> str: + """Generate SVG graph of faction relationships.""" + if not db.factions: + return "
No factions defined. Add factions or load demo data.
" + + width = 600 + height = 400 + factions = list(db.factions.values()) + n = len(factions) + + # Position factions in a circle + center_x, center_y = width / 2, height / 2 + radius = min(width, height) / 3 + + positions = {} + for i, faction in enumerate(factions): + angle = (2 * math.pi * i / n) - math.pi / 2 + x = center_x + radius * math.cos(angle) + y = center_y + radius * math.sin(angle) + positions[faction.id] = (x, y) + + # Color by faction type + type_colors = { + "guild": "#4CAF50", # Green + "criminal": "#f44336", # Red + "government": "#2196F3", # Blue + "military": "#795548", # Brown + "religious": "#9C27B0", # Purple + "merchant": "#FF9800", # Orange + "noble_house": "#FFD700", # Gold + } + + svg_parts = [f''] + + # Title + svg_parts.append(f'Faction Relationships') + + # Draw relationship lines first (behind nodes) + drawn_pairs = set() + for faction in factions: + x1, y1 = positions[faction.id] + for related_id, disposition in faction.relationships.items(): + if related_id not in positions: + continue + + # Avoid duplicate lines + pair = tuple(sorted([faction.id, related_id])) + if pair in drawn_pairs: + continue + drawn_pairs.add(pair) + + x2, y2 = positions[related_id] + + # Color by disposition + if disposition >= 50: + color = "#4CAF50" # Green - allied + elif disposition >= 0: + color = "#FFC107" # Yellow - neutral/friendly + elif disposition > -50: + color = "#FF9800" # Orange - unfriendly + else: + color = "#f44336" # Red - hostile + + stroke_width = max(1, min(4, abs(disposition) / 30)) + dash = "" if disposition >= 0 else "stroke-dasharray='5,3'" + + svg_parts.append(f'') + + # Show disposition value at midpoint + mid_x = (x1 + x2) / 2 + mid_y = (y1 + y2) / 2 + svg_parts.append(f'{disposition:+d}') + + # Draw faction nodes + for faction in factions: + x, y = positions[faction.id] + color = type_colors.get(faction.faction_type, "#607D8B") + + # Node circle + svg_parts.append(f'') + + # Faction name (truncated) + name = faction.name[:12] + svg_parts.append(f'{name}') + + # Type label below + svg_parts.append(f'{faction.faction_type}') + + # Legend + legend_y = height - 25 + legend_items = [ + ("#4CAF50", "Allied (+50+)"), + ("#FFC107", "Friendly (0-49)"), + ("#FF9800", "Tense (-1 to -49)"), + ("#f44336", "Hostile (-50-)"), + ] + for i, (color, label) in enumerate(legend_items): + lx = 30 + i * 145 + svg_parts.append(f'') + svg_parts.append(f'{label}') + + svg_parts.append('') + return "".join(svg_parts) + + # Faction graph handler (after function definition) + refresh_faction_graph_btn.click(fn=generate_faction_relationship_svg, outputs=[faction_graph_preview]) + + # ==================== ECONOMY BALANCE ANALYZER ==================== + + def analyze_item_economy() -> Tuple[str, str]: + """Analyze item values and detect balance issues.""" + if not db.items: + return "No items to analyze. Add items or load demo data.", "
No items to chart
" + + items = list(db.items.values()) + values = [item.value for item in items] + weights = [item.weight for item in items if item.weight > 0] + + # Statistics + avg_value = statistics.mean(values) if values else 0 + median_value = statistics.median(values) if values else 0 + max_value = max(values) if values else 0 + min_value = min(values) if values else 0 + stdev = statistics.stdev(values) if len(values) > 1 else 0 + + # Detect outliers (>2 stdev from mean) + outliers = [] + for item in items: + if stdev > 0 and abs(item.value - avg_value) > 2 * stdev: + outliers.append(f"- **{item.name}**: {item.value}g") + + # Value per weight analysis + value_weight_ratios = [] + for item in items: + if item.weight > 0: + ratio = item.value / item.weight + value_weight_ratios.append((item.name, ratio, item.value, item.weight)) + + value_weight_ratios.sort(key=lambda x: x[1], reverse=True) + + # Group by type + by_type = {} + for item in items: + if item.item_type not in by_type: + by_type[item.item_type] = [] + by_type[item.item_type].append(item.value) + + # Build report + report = f"""## Economy Analysis + +### Value Distribution +| Metric | Value | +|--------|-------| +| Total Items | {len(items)} | +| Average Value | {avg_value:.1f}g | +| Median Value | {median_value:.1f}g | +| Range | {min_value}g - {max_value}g | +| Std Deviation | {stdev:.1f} | + +### Value by Type +""" + + for item_type, type_values in sorted(by_type.items()): + avg = sum(type_values) / len(type_values) + report += f"- **{item_type}**: {len(type_values)} items, avg {avg:.1f}g\n" + + if outliers: + report += f"\n### Outliers ({len(outliers)} items)\n" + report += "\n".join(outliers[:5]) + if len(outliers) > 5: + report += f"\n- ...and {len(outliers) - 5} more" + + if value_weight_ratios: + report += "\n\n### Best Value/Weight Ratios\n" + for name, ratio, value, weight in value_weight_ratios[:5]: + report += f"- **{name}**: {ratio:.1f} g/unit ({value}g / {weight}wt)\n" + + # Generate SVG bar chart + svg = _generate_value_distribution_svg(items) + + return report, svg + + def _generate_value_distribution_svg(items: list) -> str: + """Generate SVG bar chart of item values.""" + if not items: + return "
No items
" + + # Group items into value buckets + buckets = {"0-10": 0, "11-50": 0, "51-100": 0, "101-500": 0, "500+": 0} + for item in items: + if item.value <= 10: + buckets["0-10"] += 1 + elif item.value <= 50: + buckets["11-50"] += 1 + elif item.value <= 100: + buckets["51-100"] += 1 + elif item.value <= 500: + buckets["101-500"] += 1 + else: + buckets["500+"] += 1 + + width = 450 + height = 220 + bar_width = 65 + max_count = max(buckets.values()) if buckets.values() else 1 + + svg_parts = [f''] + svg_parts.append(f'Item Value Distribution') + + x = 35 + chart_bottom = 175 + chart_height = 130 + + for label, count in buckets.items(): + bar_height = (count / max_count) * chart_height if max_count > 0 else 0 + y = chart_bottom - bar_height + + # Bar + color = "#4CAF50" if count > 0 else "#333" + svg_parts.append(f'') + + # Count on top + if count > 0: + svg_parts.append(f'{count}') + + # Label below + svg_parts.append(f'{label}g') + + x += bar_width + 12 + + svg_parts.append('') + return "".join(svg_parts) + + # Economy analyzer handler (after function definition) + analyze_economy_btn.click(fn=analyze_item_economy, outputs=[economy_report, economy_chart]) + + # ==================== DELETE HANDLERS ==================== + + # Store selected IDs for deletion + selected_location_id = gr.State("") + selected_npc_id = gr.State("") + selected_quest_id = gr.State("") + selected_item_id = gr.State("") + selected_faction_id = gr.State("") + selected_dialogue_id = gr.State("") + selected_skill_id = gr.State("") + + def delete_location_handler(loc_id_to_delete): + if not loc_id_to_delete: + return "No location selected to delete", get_location_list() + if db.delete_location(loc_id_to_delete): + return f"Deleted location: {loc_id_to_delete}", get_location_list() + return f"Location not found: {loc_id_to_delete}", get_location_list() + + def delete_npc_handler(npc_id_to_delete): + if not npc_id_to_delete: + return "No NPC selected to delete", get_npc_list() + if db.delete_npc(npc_id_to_delete): + return f"Deleted NPC: {npc_id_to_delete}", get_npc_list() + return f"NPC not found: {npc_id_to_delete}", get_npc_list() + + def delete_quest_handler(quest_id_to_delete): + if not quest_id_to_delete: + return "No quest selected to delete", get_quest_list() + if db.delete_quest(quest_id_to_delete): + return f"Deleted quest: {quest_id_to_delete}", get_quest_list() + return f"Quest not found: {quest_id_to_delete}", get_quest_list() + + def delete_item_handler(item_id_to_delete): + if not item_id_to_delete: + return "No item selected to delete", get_item_list() + if db.delete_item(item_id_to_delete): + return f"Deleted item: {item_id_to_delete}", get_item_list() + return f"Item not found: {item_id_to_delete}", get_item_list() + + def delete_faction_handler(fac_id_to_delete): + if not fac_id_to_delete: + return "No faction selected to delete", get_faction_list() + if db.delete_faction(fac_id_to_delete): + return f"Deleted faction: {fac_id_to_delete}", get_faction_list() + return f"Faction not found: {fac_id_to_delete}", get_faction_list() + + def delete_dialogue_handler(dlg_id_to_delete): + if not dlg_id_to_delete: + return "No dialogue selected to delete", get_dialogue_list() + if db.delete_dialogue(dlg_id_to_delete): + return f"Deleted dialogue: {dlg_id_to_delete}", get_dialogue_list() + return f"Dialogue not found: {dlg_id_to_delete}", get_dialogue_list() + + def delete_skill_handler(skill_id_to_delete): + if not skill_id_to_delete: + return "No skill selected to delete", get_skill_list() + if db.delete_skill(skill_id_to_delete): + return f"Deleted skill: {skill_id_to_delete}", get_skill_list() + return f"Skill not found: {skill_id_to_delete}", get_skill_list() + + # Connect delete handlers - use the ID field as source + delete_location_btn.click( + fn=delete_location_handler, + inputs=[loc_id], + outputs=[loc_status, location_list_output] + ) + delete_npc_btn.click( + fn=delete_npc_handler, + inputs=[npc_id], + outputs=[npc_status, npc_list_output] + ) + delete_quest_btn.click( + fn=delete_quest_handler, + inputs=[quest_id], + outputs=[quest_status, quest_list_output] + ) + delete_item_btn.click( + fn=delete_item_handler, + inputs=[item_id], + outputs=[item_status, item_list_output] + ) + delete_faction_btn.click( + fn=delete_faction_handler, + inputs=[faction_id], + outputs=[faction_status, faction_list_output] + ) + delete_dialogue_btn.click( + fn=delete_dialogue_handler, + inputs=[dlg_id], + outputs=[dlg_status, dialogue_list_output] + ) + delete_skill_btn.click( + fn=delete_skill_handler, + inputs=[skill_id], + outputs=[skill_status, skill_list_output] + ) + + # ==================== REFRESH ALL ON DEMO LOAD ==================== + + def refresh_all_after_demo(): + stats = db.load_demo_data() + # Get NPC choices for dropdowns + npc_choices = [(f"{npc.name} ({npc.id})", npc.id) for npc in db.npcs.values()] + return ( + f"Demo loaded: {stats['locations']} locations, {stats['npcs']} NPCs, {stats['quests']} quests, " + f"{stats['items']} items, {stats['factions']} factions, {stats['skills']} skills", + update_stats(), + get_location_list(), + get_npc_list(), + get_quest_list(), + get_item_list(), + get_faction_list(), + get_dialogue_list(), + get_skill_list(), + generate_world_map(), + gr.update(choices=npc_choices), + generate_faction_relationship_svg(), + gr.update(choices=npc_choices) # Also update dialogue gen NPC dropdown + ) + + load_demo_btn.click( + fn=refresh_all_after_demo, + outputs=[ + demo_status, + stats_display, + location_list_output, + npc_list_output, + quest_list_output, + item_list_output, + faction_list_output, + dialogue_list_output, + skill_list_output, + map_preview, + timeline_npc_select, + faction_graph_preview, + dlg_gen_npc + ] + ) diff --git a/ui_tabs/config_analysis_tab.py b/ui_tabs/config_analysis_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..cc45eaec1c83f0eea71bd662abc614c25c8dab50 --- /dev/null +++ b/ui_tabs/config_analysis_tab.py @@ -0,0 +1,1712 @@ +"""Config Analysis & Improvement Tab - Merged Analysis Dashboard. + +This module combines Config Health, Config Improvement, and Prompts & Examples +into a unified workflow with shared config input. +""" + +import gradio as gr +import json +import spaces +from typing import Dict, Any, List, Tuple + +from config_linting import ( + get_config_health_summary, + generate_mermaid_diagram, + generate_story_flags +) + +# Import prompt templates and examples +from prompts_and_outputs import ( + WFStage2prompt, + WFStage2prompt2, + SimpleStructureTimeline1stPrompt, + Outputof1stSimpleStructurePrompt, + MoreRobustTimeline1stPrompt, + Outputof1stMoreRobustPrompt, + TimeRelatedConceptsForIdeaGeneration, + Nonlinearprogressionideas, + Adjectivebasedcombinationideatexts, + Adjectivebasedcombinationideatextsv2, +) + +from story_elements import ( + sterotypicalstoryendings, + InterpersonalConflictIdeasBetraylacksympathyunfairexpectations, +) + +from game_configs import ( + originalconfigatbeinningofthisspace, + claude3_5_06072024configtips, + tipsupdatedconfigatbeinningofthisspace, +) + + +# ==================== STARTING POINT DETECTION & CHECKLISTS ==================== + +STARTING_POINT_CHECKLISTS = { + "world_geography": { + "name": "World Facts / Geography", + "icon": "🌍", + "checks": [ + ("Location connectivity", "Are all locations reachable? Check for orphaned areas."), + ("Spatial descriptions", "Do descriptions mention directions, distances, landmarks?"), + ("Exploration rewards", "Are there secrets or discoveries in each location?"), + ("Environmental variety", "Different biomes, weather, or atmospheres across locations?"), + ("Travel consequences", "Does moving between locations have meaningful effects?"), + ("Map coherence", "Would this make sense as a physical map?"), + ] + }, + "video_writer": { + "name": "Video/Film Ideas (Writer)", + "icon": "🎬", + "checks": [ + ("Beat structure", "Does the narrative follow clear act breaks (setup, confrontation, resolution)?"), + ("Pacing", "Are high-tension moments balanced with breathing room?"), + ("Character arcs", "Do characters change from beginning to end?"), + ("Dramatic question", "Is there a central question driving the story?"), + ("Scene purpose", "Does each state advance plot, reveal character, or both?"), + ("Climax placement", "Is there a clear climactic moment near the end?"), + ] + }, + "video_videographer": { + "name": "Video/Film Ideas (Videographer)", + "icon": "📹", + "checks": [ + ("Visual descriptions", "Do descriptions paint visual scenes (framing, lighting, composition)?"), + ("Camera notes", "Are there implied camera movements or angles in the text?"), + ("Transition flow", "Do scene changes have visual logic (cuts, fades, match cuts)?"), + ("Shot variety", "Mix of wide establishing shots, medium, and close-ups implied?"), + ("Visual motifs", "Are there recurring visual elements for thematic connection?"), + ("Media prompts", "Do media fields contain cinematographic direction?"), + ] + }, + "song_lyrics": { + "name": "Song / Lyric Ideas", + "icon": "🎵", + "checks": [ + ("Emotional arc", "Does the mood progress through verses/states?"), + ("Verse-to-state mapping", "Does each state correspond to a lyrical section?"), + ("Refrain/chorus", "Are there recurring states that echo the chorus?"), + ("Rhythm in text", "Does the prose have musicality or poetic flow?"), + ("Sensory language", "Rich use of sound, texture, and feeling words?"), + ("Thematic unity", "Does everything connect to the song's central theme?"), + ] + }, + "timeline_events": { + "name": "Timeline Events", + "icon": "⏱️", + "checks": [ + ("Temporal markers", "Do descriptions include time references (morning, later, after)?"), + ("Cause-effect chains", "Do earlier choices affect later states?"), + ("Event sequencing", "Is the order of events logical and clear?"), + ("Parallel timelines", "If branching, do timelines stay internally consistent?"), + ("Urgency mechanics", "Are there time-sensitive choices or deadlines?"), + ("Flashback/forward", "If non-linear, are jumps clearly marked?"), + ] + }, + "existing_structures": { + "name": "Existing Structures (D&D, Templates)", + "icon": "📚", + "checks": [ + ("Source fidelity", "Does the adaptation honor the source material?"), + ("Mechanical translation", "Are game mechanics (stats, rolls) represented in choices?"), + ("Lore consistency", "Do names, places, and terms match the source?"), + ("Balance", "Are encounters/challenges appropriately difficult?"), + ("Completeness", "Are all key elements from the source included?"), + ("Adaptation creativity", "What new elements make this unique?"), + ] + }, + "character_psychology": { + "name": "Character Psychology", + "icon": "🧠", + "checks": [ + ("Motivation clarity", "Are character wants and needs clear?"), + ("Psychological depth", "Do characters have flaws, fears, contradictions?"), + ("Dialogue authenticity", "Does each character have a distinct voice?"), + ("Relationship dynamics", "Are interpersonal tensions and alliances present?"), + ("Character agency", "Do NPCs make choices independent of player?"), + ("Internal conflict", "Do characters struggle with themselves, not just external?"), + ] + }, + "mystery_depth": { + "name": "Mystery / Hidden Depth", + "icon": "🔮", + "checks": [ + ("Information layering", "Is truth revealed gradually across layers?"), + ("Red herrings", "Are there misleading clues that feel fair?"), + ("Reveal pacing", "Are revelations spaced for maximum impact?"), + ("Unreliable elements", "Can the player trust what they're told?"), + ("Hidden connections", "Do seemingly unrelated elements connect?"), + ("Core truth", "Is there a satisfying 'abyss' truth worth discovering?"), + ] + }, + "faction_politics": { + "name": "Faction Politics", + "icon": "⚔️", + "checks": [ + ("Faction definition", "Are factions clearly distinct with different goals?"), + ("Reputation tracking", "Do choices affect standing with groups?"), + ("Alliance consequences", "Does joining one faction affect others?"), + ("Political complexity", "Are there nuanced positions, not just good/evil?"), + ("Power dynamics", "Is there a balance of power that can shift?"), + ("Betrayal potential", "Can allegiances change or be tested?"), + ] + }, +} + + +# ==================== IMPROVEMENT WORKFLOWS ==================== + +IMPROVEMENT_WORKFLOWS = { + "branching_first": { + "name": "Branching First", + "description": "Best for linear configs that need more player agency", + "steps": [ + {"round": 1, "focus": "choices", "instruction": "Add 3-4 choices per state"}, + {"round": 2, "focus": "choices", "instruction": "Add conditional/hidden choices"}, + {"round": 3, "focus": "detail", "instruction": "Flesh out descriptions"}, + {"round": 4, "focus": "characters", "instruction": "Add NPC motivations"}, + ] + }, + "detail_first": { + "name": "Detail First", + "description": "Best for skeleton configs with basic structure", + "steps": [ + {"round": 1, "focus": "detail", "instruction": "Rich descriptions (3-4 sentences)"}, + {"round": 2, "focus": "environment", "instruction": "Environmental storytelling"}, + {"round": 3, "focus": "choices", "instruction": "Meaningful branches"}, + {"round": 4, "focus": "tension", "instruction": "Add stakes and pressure"}, + ] + }, + "non_branching": { + "name": "Non-Branching Depth", + "description": "Best for configs where you want depth, not breadth", + "steps": [ + {"round": 1, "focus": "detail", "instruction": "Deepen existing paths"}, + {"round": 2, "focus": "characters", "instruction": "NPC dialogue and reactions"}, + {"round": 3, "focus": "environment", "instruction": "Discoverable lore"}, + {"round": 4, "focus": "tension", "instruction": "Emotional beats"}, + ] + }, + "mystery_deepening": { + "name": "Mystery Deepening", + "description": "Best for mystery/investigation configs", + "steps": [ + {"round": 1, "focus": "environment", "instruction": "Plant clues in descriptions"}, + {"round": 2, "focus": "characters", "instruction": "Hidden motivations"}, + {"round": 3, "focus": "choices", "instruction": "Investigation options"}, + {"round": 4, "focus": "tension", "instruction": "Red herrings and reveals"}, + ] + }, +} + +# Focus area definitions for prompt generation +FOCUS_AREAS = { + "balanced": { + "name": "Balanced", + "instruction": """Improve the config with balanced attention to: +- Richer descriptions (2-3 sentences per state) +- 3-4 meaningful choices per state where appropriate +- Clear character motivations +- Environmental details that support the mood""" + }, + "detail": { + "name": "Add Detail", + "instruction": """Focus on ADDING DETAIL to descriptions: +- Expand each description to 3-4 vivid sentences +- Include sensory details (sight, sound, smell, touch) +- Add atmospheric elements (weather, lighting, ambient sounds) +- Include small environmental storytelling details +- Make locations feel lived-in and real""" + }, + "choices": { + "name": "Add Choices", + "instruction": """Focus on ADDING MORE CHOICES and branches: +- Ensure each state has 3-4 distinct choices +- Add choices that reflect different playstyles (cautious, bold, clever) +- Include hidden or conditional choices +- Create meaningful branches that reconverge later +- Add optional side paths that reward exploration""" + }, + "characters": { + "name": "Deepen Characters", + "instruction": """Focus on DEEPENING CHARACTER MOTIVATIONS: +- Give NPCs clear wants, fears, and secrets +- Add dialogue that reveals personality +- Create relationship dynamics between characters +- Include character-specific choices and reactions +- Show character growth through the narrative""" + }, + "environment": { + "name": "Environmental Storytelling", + "instruction": """Focus on ENVIRONMENTAL STORYTELLING: +- Add details that imply history without exposition +- Include objects that tell stories (old letters, worn paths, abandoned items) +- Use weather and time of day to set mood +- Create spaces that feel connected to events +- Add discoverable lore through exploration""" + }, + "tension": { + "name": "Increase Tension", + "instruction": """Focus on INCREASING DRAMATIC TENSION: +- Add time pressure or urgency where appropriate +- Include moments of difficult moral choice +- Create setbacks that raise stakes +- Add foreshadowing of dangers +- Include moments of false safety before reveals""" + }, +} + +# Config type instructions for prompt context +CONFIG_TYPE_INSTRUCTIONS = { + "world": """This is a WORLD/GEOGRAPHY exploration game config. The story unfolds through location discovery. +Key aspects: Location atmosphere, natural connections, hidden areas, NPC placement, environmental storytelling.""", + "timeline": """This is a TIMELINE/CHRONOLOGICAL narrative config. Events unfold in time sequence. +Key aspects: Time-based mood, cause-effect, pacing, flashbacks/forwards, time pressure.""", + "song": """This is an EMOTIONAL JOURNEY config based on musical/lyrical structure. +Key aspects: Emotional transitions, verse/chorus structure, perspective shifts, sensory details.""", + "mystery": """This is a MYSTERY/INVESTIGATION config using the iceberg model. +Key aspects: Layered clues, red herrings, reveal pacing, unreliable elements, core truth.""", + "dnd": """This is a D&D/FANTASY ADVENTURE config with encounters and exploration. +Key aspects: Combat variety, social encounters, treasure, boss encounters, resource management.""", + "film": """This is a FILM/VIDEO SCENES config structured around visual storytelling. +Key aspects: Visual hooks, camera direction, transitions, dialogue subtext, visual motifs.""", + "beats": """This is a STORY BEATS config following professional screenplay structure. +Key aspects: Beat turns, character-driven plot, escalating stakes, emotional weight, payoffs.""", +} + + +def detect_starting_point(config_json: str) -> Tuple[str, Dict[str, float]]: + """ + Analyze config structure to guess the starting point type. + Returns (best_guess, confidence_scores). + """ + try: + config = json.loads(config_json) + except json.JSONDecodeError: + return "unknown", {} + + scores = { + "world_geography": 0.0, + "video_writer": 0.0, + "video_videographer": 0.0, + "song_lyrics": 0.0, + "timeline_events": 0.0, + "existing_structures": 0.0, + "character_psychology": 0.0, + "mystery_depth": 0.0, + "faction_politics": 0.0, + } + + all_text = "" + location_count = 0 + state_count = 0 + + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + location_count += 1 + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + state_count += 1 + desc = state_data.get("description", "") + all_text += f" {desc} " + for choice in state_data.get("choices", []): + if isinstance(choice, dict): + all_text += f" {choice.get('text', '')} " + + all_text_lower = all_text.lower() + + # World/Geography signals + geo_keywords = ["north", "south", "east", "west", "path", "road", "village", "city", "forest", "mountain", "cave", "door", "corridor", "room", "building", "outside", "inside", "enter", "exit", "travel", "journey"] + scores["world_geography"] = sum(1 for k in geo_keywords if k in all_text_lower) / len(geo_keywords) + if location_count >= 5: + scores["world_geography"] += 0.2 + + # Video Writer signals (dramatic structure) + drama_keywords = ["suddenly", "meanwhile", "finally", "climax", "tension", "reveal", "twist", "confrontation", "resolution", "act", "scene", "dramatic", "stakes"] + scores["video_writer"] = sum(1 for k in drama_keywords if k in all_text_lower) / len(drama_keywords) + + # Video Videographer signals (visual) + visual_keywords = ["camera", "shot", "frame", "close-up", "wide", "pan", "zoom", "angle", "lighting", "shadow", "silhouette", "composition", "visual", "fade", "cut to"] + scores["video_videographer"] = sum(1 for k in visual_keywords if k in all_text_lower) / len(visual_keywords) + + # Song/Lyrics signals (emotional, poetic) + song_keywords = ["verse", "chorus", "rhythm", "melody", "heart", "soul", "feeling", "dream", "echo", "silence", "cry", "whisper", "voice", "song", "music"] + scores["song_lyrics"] = sum(1 for k in song_keywords if k in all_text_lower) / len(song_keywords) + + # Timeline signals + time_keywords = ["morning", "evening", "night", "day", "hour", "later", "before", "after", "then", "now", "yesterday", "tomorrow", "clock", "time", "wait", "moment"] + scores["timeline_events"] = sum(1 for k in time_keywords if k in all_text_lower) / len(time_keywords) + + # D&D/Existing structures signals + dnd_keywords = ["hp", "damage", "attack", "spell", "magic", "roll", "dice", "armor", "weapon", "potion", "dungeon", "dragon", "goblin", "quest", "level"] + scores["existing_structures"] = sum(1 for k in dnd_keywords if k in all_text_lower) / len(dnd_keywords) + + # Character Psychology signals + psych_keywords = ["feel", "think", "believe", "fear", "hope", "want", "need", "memory", "emotion", "trust", "betray", "guilt", "regret", "desire", "conflict"] + scores["character_psychology"] = sum(1 for k in psych_keywords if k in all_text_lower) / len(psych_keywords) + + # Mystery/Depth signals + mystery_keywords = ["secret", "hidden", "clue", "mystery", "discover", "reveal", "truth", "lie", "suspect", "evidence", "investigate", "puzzle", "unknown", "shadow"] + scores["mystery_depth"] = sum(1 for k in mystery_keywords if k in all_text_lower) / len(mystery_keywords) + + # Faction signals + faction_keywords = ["faction", "alliance", "reputation", "loyalty", "betray", "guild", "clan", "tribe", "kingdom", "empire", "rebel", "join", "side", "enemy", "ally"] + scores["faction_politics"] = sum(1 for k in faction_keywords if k in all_text_lower) / len(faction_keywords) + + # Normalize and find best match + max_score = max(scores.values()) if scores.values() else 0 + if max_score > 0: + scores = {k: round(v / max_score, 2) for k, v in scores.items()} + + best_guess = max(scores, key=scores.get) if max_score > 0 else "world_geography" + return best_guess, scores + + +def format_starting_point_checklist(starting_point: str) -> str: + """Format the checklist for a starting point as markdown.""" + if starting_point not in STARTING_POINT_CHECKLISTS: + return "Unknown starting point selected." + + checklist = STARTING_POINT_CHECKLISTS[starting_point] + lines = [f"## {checklist['icon']} {checklist['name']} Checklist\n"] + lines.append("Review these criteria based on your creative approach:\n") + + for i, (check_name, check_desc) in enumerate(checklist["checks"], 1): + lines.append(f"- [ ] **{check_name}**: {check_desc}") + + return "\n".join(lines) + + +def format_detection_results(config_json: str) -> Tuple[str, str, str]: + """ + Detect starting point and return formatted results. + Returns (detected_type, confidence_display, checklist). + """ + if not config_json or not config_json.strip(): + return "world_geography", "*Paste a config to auto-detect*", "" + + detected, scores = detect_starting_point(config_json) + + # Format confidence display + confidence_lines = ["**Detection Confidence:**\n"] + sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True) + for sp_key, score in sorted_scores[:5]: # Top 5 + sp_info = STARTING_POINT_CHECKLISTS.get(sp_key, {}) + icon = sp_info.get("icon", "") + name = sp_info.get("name", sp_key) + bar = "█" * int(score * 10) + "░" * (10 - int(score * 10)) + confidence_lines.append(f"{icon} {name}: {bar} ({int(score*100)}%)") + + checklist = format_starting_point_checklist(detected) + + return detected, "\n".join(confidence_lines), checklist + + +# ==================== METRICS FUNCTIONS ==================== + +def compute_config_metrics(config_json: str) -> Dict[str, Any]: + """Compute quality metrics from a game config.""" + try: + config = json.loads(config_json) + except json.JSONDecodeError: + return {"error": "Invalid JSON"} + + # Count states and locations + total_states = 0 + total_locations = 0 + all_choices = [] + all_descriptions = [] + states_with_consequences = 0 + states_with_media = 0 + + for location_key, location_data in config.items(): + if isinstance(location_data, dict): + total_locations += 1 + for state_key, state_data in location_data.items(): + if isinstance(state_data, dict): + total_states += 1 + + # Count choices + choices = state_data.get("choices", []) + if isinstance(choices, list): + all_choices.append(len(choices)) + + # Count description length + desc = state_data.get("description", "") + if desc: + all_descriptions.append(len(desc.split())) + + # Check for consequences + if state_data.get("consequences") or state_data.get("on_enter"): + states_with_consequences += 1 + + # Check for media + if state_data.get("media") and len(state_data.get("media", [])) > 0: + states_with_media += 1 + + # Calculate averages + avg_choices = sum(all_choices) / len(all_choices) if all_choices else 0 + avg_desc_length = sum(all_descriptions) / len(all_descriptions) if all_descriptions else 0 + media_coverage = states_with_media / total_states if total_states > 0 else 0 + + return { + "total_states": total_states, + "total_locations": total_locations, + "avg_choices_per_state": round(avg_choices, 2), + "states_with_consequences": states_with_consequences, + "avg_description_length": round(avg_desc_length, 1), + "media_coverage": round(media_coverage * 100, 1), + "states_with_media": states_with_media, + } + + +def compute_improvement_suggestions(config_json: str) -> str: + """Generate improvement suggestions based on computed metrics.""" + metrics = compute_config_metrics(config_json) + + if "error" in metrics: + return f"**Error:** {metrics['error']}" + + suggestions = [] + + # Check state variety + if metrics["total_states"] < 10: + suggestions.append(f"**Limited content**: Only {metrics['total_states']} states. Consider adding more locations and states for variety.") + + # Check choice variety + if metrics["avg_choices_per_state"] < 2.5: + suggestions.append(f"**Low choice variety**: Average {metrics['avg_choices_per_state']} choices/state. Add more choices to reduce linearity.") + + # Check media coverage + if metrics["media_coverage"] < 20: + suggestions.append(f"**Limited media**: Only {metrics['media_coverage']}% of states have media. Add images/sounds for engagement.") + + # Check description length + if metrics["avg_description_length"] < 15: + suggestions.append(f"**Short descriptions**: Average {metrics['avg_description_length']} words. Expand for richer narrative.") + + # Check consequences + consequence_ratio = metrics["states_with_consequences"] / metrics["total_states"] if metrics["total_states"] > 0 else 0 + if consequence_ratio < 0.3: + suggestions.append(f"**Few consequences**: Only {metrics['states_with_consequences']} states have consequences. Add more for player agency.") + + if not suggestions: + return "**Config looks good!** No major issues detected based on structural metrics." + + return "## Improvement Suggestions\n\n" + "\n\n".join(suggestions) + + +def format_metrics_display(config_json: str) -> str: + """Format metrics as a readable markdown display.""" + metrics = compute_config_metrics(config_json) + + if "error" in metrics: + return f"**Error:** {metrics['error']}" + + return f"""## Config Metrics + +| Metric | Value | Target | +|--------|-------|--------| +| Total States | {metrics['total_states']} | 15+ | +| Total Locations | {metrics['total_locations']} | 5+ | +| Avg Choices/State | {metrics['avg_choices_per_state']} | 2.5+ | +| States with Consequences | {metrics['states_with_consequences']} | 30%+ | +| Avg Description Length | {metrics['avg_description_length']} words | 15+ | +| Media Coverage | {metrics['media_coverage']}% | 50%+ | +""" + + +# ==================== ZERO GPU GENERATION ==================== + +# Lazy model loading (shared with D&D GM tab) +_analysis_model = None +_analysis_tokenizer = None +_analysis_model_name = None + + +def _ensure_torch(): + """Lazy import torch.""" + import torch + return torch + + +def _ensure_transformers(): + """Lazy import transformers.""" + import transformers + return transformers + + +def _ensure_analysis_model(model_name: str = "Qwen/Qwen3-4B"): + """Lazy load model for config generation.""" + global _analysis_model, _analysis_tokenizer, _analysis_model_name + + if _analysis_model is None or _analysis_model_name != model_name: + tf = _ensure_transformers() + print(f"Loading config generation model: {model_name}") + + if _analysis_model is not None: + del _analysis_model + del _analysis_tokenizer + _torch = _ensure_torch() + if _torch.cuda.is_available(): + _torch.cuda.empty_cache() + + _analysis_tokenizer = tf.AutoTokenizer.from_pretrained(model_name) + _analysis_model = tf.AutoModelForCausalLM.from_pretrained( + model_name, torch_dtype="auto", device_map="auto" + ) + _analysis_model_name = model_name + + return _analysis_model, _analysis_tokenizer + + +@spaces.GPU +def generate_config_from_theme(theme: str, prompt_template: str) -> str: + """Generate a game config from a theme using Zero GPU.""" + if not theme.strip(): + return "Please enter a theme." + + _torch = _ensure_torch() + model, tokenizer = _ensure_analysis_model() + + zero = _torch.Tensor([0]).cuda() + model.to(zero.device) + + # Build the prompt + full_prompt = f"{prompt_template}\n\nTheme: {theme}\n\nGenerate a complete game config JSON:" + + messages = [{"role": "user", "content": full_prompt}] + text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) + + inputs = tokenizer([text], return_tensors="pt").to(zero.device) + + with _torch.no_grad(): + outputs = model.generate( + **inputs, + max_new_tokens=2048, + temperature=0.7, + do_sample=True, + pad_token_id=tokenizer.eos_token_id, + ) + + generated = tokenizer.decode(outputs[0], skip_special_tokens=True) + + # Extract JSON from response + if "```json" in generated: + start = generated.find("```json") + 7 + end = generated.find("```", start) + if end > start: + return generated[start:end].strip() + + if "{" in generated: + start = generated.find("{") + # Find matching closing brace + depth = 0 + for i, char in enumerate(generated[start:], start): + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + if depth == 0: + return generated[start:i+1] + + return generated + + +# ==================== TAB CREATION ==================== + +def create_config_analysis_tab(): + """Create the Config Analysis & Improvement tab.""" + with gr.Tab("Config Analysis & Improvement"): + gr.Markdown("## Config Analysis & Improvement") + gr.Markdown("Analyze, improve, and generate game configs. Paste your config below or generate a new one.") + + # Shared config input at top + with gr.Row(): + config_input = gr.Textbox( + label="Config JSON", + lines=12, + placeholder='{"location": {"state": {"description": "...", "choices": [...]}}}', + scale=3 + ) + with gr.Column(scale=1): + analyze_btn = gr.Button("Analyze Config", variant="primary", size="lg") + clear_btn = gr.Button("Clear", size="sm") + + with gr.Tabs(): + # ==================== SUB-TAB 1: ANALYZE ==================== + with gr.TabItem("Analyze"): + gr.Markdown("### Health Report & Metrics") + + with gr.Row(): + with gr.Column(scale=1): + health_report_output = gr.Markdown(value="*Paste config and click Analyze*") + + with gr.Column(scale=1): + metrics_output = gr.Markdown(value="*Metrics will appear here*") + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("### State Machine Diagram") + gr.Markdown("*Copy to [mermaid.live](https://mermaid.live) to visualize*") + mermaid_output = gr.Code(label="Mermaid Code", language=None, lines=12) + mermaid_btn = gr.Button("Generate Diagram") + + with gr.Column(scale=1): + gr.Markdown("### Story Flags") + gr.Markdown("*Potential narrative issues*") + flags_output = gr.Dataframe( + headers=["Severity", "State", "Issue"], + label="Story Flags", + wrap=True + ) + + with gr.Accordion("Legend", open=False): + gr.Markdown(""" +**Diagram Colors:** +- Green: Start state +- Pink: End state +- Yellow: Dead-end (no exits) +- Red: Orphaned (unreachable) + +**Severity Levels:** +- Warning: Should probably fix +- Info: Consider reviewing + +**Common Issues:** +- *Disconnected choices*: Description doesn't mention the available choices +- *Sparse content*: Very short descriptions +- *Length outlier*: Description much longer/shorter than average +- *Inconsistent media*: Some states have media, others don't + """) + + # ==================== SUB-TAB 2: IMPROVEMENT GUIDE ==================== + with gr.TabItem("Improvement Guide"): + gr.Markdown("### Improvement Suggestions") + + suggestions_output = gr.Markdown(value="*Analyze a config to see suggestions*") + + # ==================== STARTING POINT CONTEXT ==================== + with gr.Accordion("Starting Point Context (Tailored Checklists)", open=True): + gr.Markdown("*Different creative approaches need different quality checks. Select or auto-detect your starting point.*") + + with gr.Row(): + with gr.Column(scale=1): + detect_btn = gr.Button("🔍 Auto-Detect from Config", variant="secondary") + starting_point_dropdown = gr.Dropdown( + choices=[ + ("🌍 World Facts / Geography", "world_geography"), + ("🎬 Video/Film (Writer)", "video_writer"), + ("📹 Video/Film (Videographer)", "video_videographer"), + ("🎵 Song / Lyric Ideas", "song_lyrics"), + ("⏱️ Timeline Events", "timeline_events"), + ("📚 Existing Structures (D&D)", "existing_structures"), + ("🧠 Character Psychology", "character_psychology"), + ("🔮 Mystery / Hidden Depth", "mystery_depth"), + ("⚔️ Faction Politics", "faction_politics"), + ], + value="world_geography", + label="Select Starting Point", + interactive=True + ) + with gr.Column(scale=1): + detection_confidence = gr.Markdown(value="*Click 'Auto-Detect' to analyze your config*") + + checklist_output = gr.Markdown(value="*Select a starting point or auto-detect to see tailored checklist*") + + with gr.Accordion("Quick Evaluation Method", open=False): + gr.Markdown(""" +**Prompt to use with any SOTA LLM:** + +> This config is for a basic text based game engine. I don't have any structural metrics to assess the quality of the config. What JSON things can we look at to see if it may be too bland for a person testing the game? + +Then paste your config with the prompt. + """) + gr.Markdown(""" +**What to look for (Claude 3.5 analysis):** +- **State variety**: More locations and states = more diversity +- **Average choices per state**: Higher = less linear feel +- **Consequences**: More = greater sense of agency +- **Description length**: Longer = richer narrative +- **Media usage**: Images/sounds = more engagement +- **Narrative branching**: More paths = better replayability + """) + + with gr.Accordion("Before/After Example", open=False): + gr.Markdown("Example of config improvement using LLM suggestions:") + display_original = originalconfigatbeinningofthisspace.replace(' ', ' ').replace('\n', '
') + display_tips = claude3_5_06072024configtips.replace(' ', ' ').replace('\n', '
') + display_improved = tipsupdatedconfigatbeinningofthisspace.replace(' ', ' ').replace('\n', '
') + gr.HTML(f"""
+
+ Original Config:
{display_original} +
+
+ Claude 3.5 Tips:
{display_tips} +
+
+ Improved Config:
{display_improved} +
+
""") + + # ==================== SUB-TAB 3: PROMPTS & TEMPLATES ==================== + with gr.TabItem("Prompts & Templates"): + gr.Markdown("### LLM Prompts for Config Generation") + gr.Markdown("Use these prompts with external LLMs or generate directly with Zero GPU.") + + with gr.Accordion("Config Creation Prompts", open=True): + gr.Code(WFStage2prompt, label="Stage 2: Story to JSON (works on 70B+)") + + with gr.Row(): + theme_input = gr.Textbox( + label="Theme for Generation", + placeholder="A haunted mansion mystery...", + scale=3 + ) + generate_btn = gr.Button("Generate with Zero GPU", variant="primary", scale=1) + + generated_config_output = gr.Code( + label="Generated Config", + language="json", + lines=15, + visible=True + ) + + gr.Code(WFStage2prompt2, label="Alternate Stage 2 prompt (simpler)") + gr.Code("Lets critique this JSON to find areas to fix", label="Config Critique Prompt") + gr.Code("Lets make a list for the prompts we will use to make media objects in this JSON. Make one for a person to interpret and one for direct media generators that focus on keywords:", label="Media Prompt Generation") + + with gr.Accordion("Ending & Conflict Ideas", open=False): + gr.Code(sterotypicalstoryendings, label="30 Stereotypical Story Endings (llama-3.3-70B)") + gr.Code(InterpersonalConflictIdeasBetraylacksympathyunfairexpectations, label="Interpersonal Conflict Ideas (o1)") + + with gr.Accordion("Concept Brainstorm Texts", open=False): + with gr.Row(): + gr.Textbox(value=TimeRelatedConceptsForIdeaGeneration, label="Time-Related Concepts", lines=15) + gr.Textbox(value=Nonlinearprogressionideas, label="Non-linear Progression Ideas", lines=15) + with gr.Row(): + gr.Textbox(value=Adjectivebasedcombinationideatextsv2, label="Adjective Combinations v2", lines=15) + gr.Textbox(value=Adjectivebasedcombinationideatexts, label="Adjective Combinations v1", lines=15) + + with gr.Accordion("Example Workflow Outputs", open=False): + gr.Markdown("**Simple Structure Workflow:**") + gr.Code(SimpleStructureTimeline1stPrompt, label="Simple Structure - Input Prompt") + gr.Code(Outputof1stSimpleStructurePrompt, label="Simple Structure - Output") + + gr.Markdown("**More Robust Workflow:**") + gr.Code(MoreRobustTimeline1stPrompt, label="More Robust - 1st Prompt") + gr.Code(Outputof1stMoreRobustPrompt, label="More Robust - 1st Output") + + # ==================== SUB-TAB 4: LLM IMPROVEMENT LOOP ==================== + with gr.TabItem("LLM Improvement Loop"): + gr.Markdown("### Iterative Config Improvement with External LLM") + gr.Markdown("Generate prompts for ChatGPT/Claude to improve your config round by round. Track changes and metrics across iterations.") + + # Session state + improvement_session = gr.State(value={ + "rounds": [], + "current_config": "", + "original_config": "", + "session_active": False, + "current_round": 0 + }) + + # Config Input Section + with gr.Group(): + gr.Markdown("#### 1. Load Config") + with gr.Row(): + loop_config_input = gr.Textbox( + label="Config JSON", + lines=8, + placeholder="Paste your config here or load from playtest...", + scale=3 + ) + with gr.Column(scale=1): + gr.Markdown("**Demo Options:**") + load_demo_btn = gr.Button("Load Starter (7 states)", variant="secondary", size="sm") + load_complete_demo_btn = gr.Button("Load Complete (29 states)", variant="secondary", size="sm") + load_demo_session_btn = gr.Button("Load Full Demo Session", variant="primary", size="sm") + gr.Markdown("**Your Config:**") + load_from_playtest_btn = gr.Button("Load from Playtest", variant="secondary", size="sm") + start_session_btn = gr.Button("Start New Session", variant="primary") + session_status = gr.Markdown("*No active session*") + + # Side-by-Side Demo Comparison - All 5 Rounds + with gr.Accordion("Demo Comparison: All Rounds Side-by-Side", open=False): + gr.Markdown("**See how each round builds on the previous - state by state progression:**") + + # Metrics overview + demo_metrics_display = gr.Markdown("*Click 'Load Comparison' to see metrics*") + load_comparison_btn = gr.Button("Load All Rounds", variant="secondary") + + # State selector dropdown + gr.Markdown("---") + gr.Markdown("**Select a State to Compare Across All Rounds:**") + with gr.Row(): + state_selector = gr.Dropdown( + choices=[], + value=None, + label="Select State", + scale=2 + ) + with gr.Column(scale=1): + state_status = gr.Markdown("*Load comparison first*") + + # 5-column round view + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("##### R0: Starter") + round0_view = gr.Textbox(lines=12, interactive=False, show_label=False, placeholder="Base state") + with gr.Column(scale=1): + gr.Markdown("##### R1: Environment") + round1_view = gr.Textbox(lines=12, interactive=False, show_label=False, placeholder="+ clues, details") + with gr.Column(scale=1): + gr.Markdown("##### R2: Characters") + round2_view = gr.Textbox(lines=12, interactive=False, show_label=False, placeholder="+ motivations") + with gr.Column(scale=1): + gr.Markdown("##### R3: Choices") + round3_view = gr.Textbox(lines=12, interactive=False, show_label=False, placeholder="+ options") + with gr.Column(scale=1): + gr.Markdown("##### R4: Tension") + round4_view = gr.Textbox(lines=12, interactive=False, show_label=False, placeholder="+ reveals") + + # Changes summary for selected state + state_changes_display = gr.Markdown("*Select a state above to see progression across rounds*") + + # Suggested Workflows Section + with gr.Accordion("Suggested Workflows (click step to auto-set focus)", open=True): + gr.Markdown("*Choose a workflow path or manually select focus areas below*") + + with gr.Row(): + # Branching First + with gr.Column(scale=1): + gr.Markdown("**Branching First**\n*For linear configs needing player agency*") + wf_branch_1 = gr.Button("1: choices", size="sm") + wf_branch_2 = gr.Button("2: choices", size="sm") + wf_branch_3 = gr.Button("3: detail", size="sm") + wf_branch_4 = gr.Button("4: characters", size="sm") + + # Detail First + with gr.Column(scale=1): + gr.Markdown("**Detail First**\n*For skeleton configs with basic structure*") + wf_detail_1 = gr.Button("1: detail", size="sm") + wf_detail_2 = gr.Button("2: environment", size="sm") + wf_detail_3 = gr.Button("3: choices", size="sm") + wf_detail_4 = gr.Button("4: tension", size="sm") + + # Non-Branching + with gr.Column(scale=1): + gr.Markdown("**Non-Branching Depth**\n*For depth over breadth*") + wf_nonbranch_1 = gr.Button("1: detail", size="sm") + wf_nonbranch_2 = gr.Button("2: characters", size="sm") + wf_nonbranch_3 = gr.Button("3: environment", size="sm") + wf_nonbranch_4 = gr.Button("4: tension", size="sm") + + # Mystery + with gr.Column(scale=1): + gr.Markdown("**Mystery Deepening**\n*For investigation configs*") + wf_mystery_1 = gr.Button("1: environment", size="sm") + wf_mystery_2 = gr.Button("2: characters", size="sm") + wf_mystery_3 = gr.Button("3: choices", size="sm") + wf_mystery_4 = gr.Button("4: tension", size="sm") + + # Current Round Section + with gr.Group(): + gr.Markdown("#### 2. Generate Improvement Prompt") + round_indicator = gr.Markdown("**Current Round: Not started**") + + with gr.Row(): + loop_config_type = gr.Dropdown( + choices=[ + ("Auto-detect", "auto"), + ("World/Geography", "world"), + ("Timeline", "timeline"), + ("Emotional Journey", "song"), + ("Mystery", "mystery"), + ("D&D/Fantasy", "dnd"), + ("Film/Video", "film"), + ("Story Beats", "beats"), + ], + value="auto", + label="Config Type", + scale=1 + ) + loop_focus_area = gr.Dropdown( + choices=[ + ("Balanced", "balanced"), + ("Add Detail", "detail"), + ("Add Choices", "choices"), + ("Deepen Characters", "characters"), + ("Environmental Storytelling", "environment"), + ("Increase Tension", "tension"), + ], + value="balanced", + label="Focus Area", + scale=1 + ) + generate_loop_prompt_btn = gr.Button("Generate Prompt", variant="primary", scale=1) + + loop_prompt_output = gr.Code( + label="AI Prompt (copy to ChatGPT/Claude)", + language=None, + lines=15 + ) + + # After LLM Response Section + with gr.Group(): + gr.Markdown("#### 3. Paste LLM Response & Accept") + loop_response_input = gr.Textbox( + label="Paste Improved Config from LLM", + lines=8, + placeholder="After getting the improved config from ChatGPT/Claude, paste it here..." + ) + with gr.Row(): + accept_round_btn = gr.Button("Accept & Start Next Round", variant="primary", scale=2) + with gr.Column(scale=1): + revert_dropdown = gr.Dropdown( + choices=[], + value=None, + label="Revert to Round", + interactive=True, + allow_custom_value=True + ) + revert_btn = gr.Button("Revert", variant="secondary", size="sm") + + # Metrics comparison + metrics_comparison = gr.Markdown("*Metrics comparison will appear after accepting a round*") + + # Round History Section + with gr.Accordion("Round History", open=False): + history_display = gr.Markdown("*No rounds completed yet*") + with gr.Row(): + export_history_btn = gr.Button("Export Session History", variant="secondary") + export_history_file = gr.File(label="Download", interactive=False) + + # ==================== EVENT HANDLERS ==================== + + def get_flags_as_table(config): + """Convert story flags to table format.""" + flags = generate_story_flags(config) + if not flags or (isinstance(flags, list) and len(flags) > 0 and isinstance(flags[0], str)): + return [["Info", "N/A", str(flags[0]) if flags else "No config to analyze"]] + rows = [] + for f in flags: + rows.append([ + f.get("severity", "info").capitalize(), + f.get("state", ""), + f.get("message", "") + ]) + return rows if rows else [["Info", "N/A", "No issues detected"]] + + def analyze_all(config_json): + """Run all analysis functions.""" + health = get_config_health_summary(config_json) + metrics = format_metrics_display(config_json) + suggestions = compute_improvement_suggestions(config_json) + flags = get_flags_as_table(config_json) + return health, metrics, suggestions, flags + + # Main analyze button updates all outputs + analyze_btn.click( + fn=analyze_all, + inputs=[config_input], + outputs=[health_report_output, metrics_output, suggestions_output, flags_output] + ) + + # Mermaid diagram generation + mermaid_btn.click( + fn=generate_mermaid_diagram, + inputs=[config_input], + outputs=[mermaid_output] + ) + + # Clear button + clear_btn.click( + fn=lambda: ("", "*Paste config and click Analyze*", "*Metrics will appear here*", + "*Analyze a config to see suggestions*", "", [["", "", ""]]), + outputs=[config_input, health_report_output, metrics_output, + suggestions_output, mermaid_output, flags_output] + ) + + # Zero GPU generation + generate_btn.click( + fn=lambda theme: generate_config_from_theme(theme, WFStage2prompt), + inputs=[theme_input], + outputs=[generated_config_output] + ) + + # Starting point detection + def run_detection(config_json): + """Auto-detect starting point and update UI.""" + detected, confidence, checklist = format_detection_results(config_json) + return detected, confidence, checklist + + detect_btn.click( + fn=run_detection, + inputs=[config_input], + outputs=[starting_point_dropdown, detection_confidence, checklist_output] + ) + + # Dropdown selection updates checklist + starting_point_dropdown.change( + fn=format_starting_point_checklist, + inputs=[starting_point_dropdown], + outputs=[checklist_output] + ) + + # ==================== LLM IMPROVEMENT LOOP HANDLERS ==================== + + def detect_config_type(config_json: str) -> str: + """Auto-detect config type from content.""" + if not config_json: + return "film" + config_lower = config_json.lower() + if "world" in config_lower or "location_" in config_lower or "terrain" in config_lower: + return "world" + elif "timeline" in config_lower or "event_" in config_lower: + return "timeline" + elif "emotional_journey" in config_lower or "moment_" in config_lower: + return "song" + elif "investigation" in config_lower or "clue_" in config_lower: + return "mystery" + elif "adventure" in config_lower or "encounter_" in config_lower: + return "dnd" + elif "beat_" in config_lower or "act_" in config_lower: + return "beats" + return "film" + + def start_improvement_session(config_json, session): + """Initialize a new improvement session.""" + if not config_json or not config_json.strip(): + return ( + session, + "**Error:** Please paste a config first", + "**Current Round: Not started**", + [], + "*No rounds completed yet*" + ) + + # Validate JSON + try: + json.loads(config_json) + except json.JSONDecodeError as e: + return ( + session, + f"**Error:** Invalid JSON - {str(e)}", + "**Current Round: Not started**", + [], + "*No rounds completed yet*" + ) + + # Initialize new session + new_session = { + "rounds": [], + "current_config": config_json, + "original_config": config_json, + "session_active": True, + "current_round": 1 + } + + metrics = compute_config_metrics(config_json) + status = f"**Session Started!** Original config: {metrics.get('total_states', 0)} states, {metrics.get('avg_choices_per_state', 0)} avg choices" + + return ( + new_session, + status, + "**Current Round: 1** (Ready to generate prompt)", + [], + "*No rounds completed yet*" + ) + + def generate_improvement_prompt(config_json, config_type, focus_area, session): + """Generate a context-aware improvement prompt.""" + if not session.get("session_active"): + return "Please start a session first by clicking 'Start New Session'." + + current_config = session.get("current_config", config_json) + if not current_config: + return "No config available. Please start a session." + + # Auto-detect type if needed + actual_type = config_type if config_type != "auto" else detect_config_type(current_config) + + # Get type instructions + type_instruction = CONFIG_TYPE_INSTRUCTIONS.get(actual_type, CONFIG_TYPE_INSTRUCTIONS["film"]) + + # Get focus instructions + focus_instruction = FOCUS_AREAS.get(focus_area, FOCUS_AREAS["balanced"])["instruction"] + + # Count states + try: + config = json.loads(current_config) + state_count = sum(len(loc) for loc in config.values() if isinstance(loc, dict)) + except: + state_count = "unknown" + + # Build round context + round_num = session.get("current_round", 1) + round_context = "" + if round_num > 1 and session.get("rounds"): + prev_focuses = [r.get("focus_area", "unknown") for r in session["rounds"]] + round_context = f"\n**Previous rounds:** {', '.join(prev_focuses)}\nThis is round {round_num}. Build on previous improvements." + + prompt = f"""I have a game config JSON for an interactive narrative game. Please help me improve it. + +**CONFIG TYPE:** {actual_type.upper()} +**CURRENT SIZE:** {state_count} states +**FOCUS FOR THIS ROUND:** {FOCUS_AREAS.get(focus_area, {}).get('name', focus_area)} +**ROUND:** {round_num}{round_context} + +{type_instruction} + +{focus_instruction} + +**IMPORTANT RULES:** +1. Return ONLY valid JSON - no explanations before or after +2. Keep the exact same structure (location > state > properties) +3. Every state MUST have: "description", "choices" (list), "transitions" (dict mapping choices to state IDs) +4. Do NOT rename existing state IDs - only add new ones +5. Ensure all transitions point to valid state IDs +6. Keep "media" and "media_prompt" fields if present + +**HERE IS MY CURRENT CONFIG:** +```json +{current_config} +``` + +Please return an improved version of this config with the enhancements described above. Remember: ONLY return valid JSON, nothing else.""" + + return prompt + + def accept_round_result(new_config_json, session): + """Accept the LLM result and advance to next round.""" + if not session.get("session_active"): + return ( + session, + "*Start a session first*", + "**Current Round: Not started**", + [], + "*No rounds completed yet*", + "" # Clear the input + ) + + if not new_config_json or not new_config_json.strip(): + return ( + session, + "**Error:** Please paste the improved config from the LLM", + f"**Current Round: {session.get('current_round', 1)}**", + gr.update(), + gr.update(), + gr.update() + ) + + # Validate JSON + try: + json.loads(new_config_json) + except json.JSONDecodeError as e: + return ( + session, + f"**Error:** Invalid JSON - {str(e)}", + f"**Current Round: {session.get('current_round', 1)}**", + gr.update(), + gr.update(), + gr.update() + ) + + # Compute metrics before and after + old_metrics = compute_config_metrics(session.get("current_config", "")) + new_metrics = compute_config_metrics(new_config_json) + + # Create round record + round_data = { + "round_number": session.get("current_round", 1), + "config_before": session.get("current_config", ""), + "config_after": new_config_json, + "metrics_before": old_metrics, + "metrics_after": new_metrics, + } + + # Update session + new_session = session.copy() + new_session["rounds"] = session.get("rounds", []) + [round_data] + new_session["current_config"] = new_config_json + new_session["current_round"] = session.get("current_round", 1) + 1 + + # Format metrics comparison + comparison = f"""### Round {round_data['round_number']} Metrics Change + +| Metric | Before | After | Change | +|--------|--------|-------|--------| +| States | {old_metrics.get('total_states', 0)} | {new_metrics.get('total_states', 0)} | {new_metrics.get('total_states', 0) - old_metrics.get('total_states', 0):+d} | +| Avg Choices | {old_metrics.get('avg_choices_per_state', 0)} | {new_metrics.get('avg_choices_per_state', 0)} | {new_metrics.get('avg_choices_per_state', 0) - old_metrics.get('avg_choices_per_state', 0):+.2f} | +| Avg Desc Length | {old_metrics.get('avg_description_length', 0)} | {new_metrics.get('avg_description_length', 0)} | {new_metrics.get('avg_description_length', 0) - old_metrics.get('avg_description_length', 0):+.1f} | +| Media Coverage | {old_metrics.get('media_coverage', 0)}% | {new_metrics.get('media_coverage', 0)}% | {new_metrics.get('media_coverage', 0) - old_metrics.get('media_coverage', 0):+.1f}% | +""" + + # Format history + history_lines = ["### Improvement History\n"] + for r in new_session["rounds"]: + rn = r.get("round_number", "?") + states_change = r.get("metrics_after", {}).get("total_states", 0) - r.get("metrics_before", {}).get("total_states", 0) + history_lines.append(f"**Round {rn}:** +{states_change} states") + + # Update revert dropdown choices + revert_choices = [(f"Round {r['round_number']}", r['round_number']) for r in new_session["rounds"]] + + return ( + new_session, + comparison, + f"**Current Round: {new_session['current_round']}** (Ready for next improvement)", + revert_choices, + "\n".join(history_lines), + "" # Clear the response input + ) + + def revert_to_round(round_num, session): + """Revert to a previous round's config.""" + if not session.get("session_active") or not session.get("rounds"): + return session, "*No rounds to revert to*", gr.update() + + # Find the round + target_round = None + for r in session["rounds"]: + if r.get("round_number") == round_num: + target_round = r + break + + if not target_round: + return session, f"*Round {round_num} not found*", gr.update() + + # Revert session + new_session = session.copy() + new_session["current_config"] = target_round.get("config_after", "") + new_session["current_round"] = round_num + 1 + # Keep only rounds up to and including the reverted round + new_session["rounds"] = [r for r in session["rounds"] if r.get("round_number", 0) <= round_num] + + return ( + new_session, + f"*Reverted to Round {round_num}. Continue from Round {round_num + 1}*", + target_round.get("config_after", "") + ) + + def export_session_history(session): + """Export session history as JSON file.""" + if not session.get("rounds"): + return None + + import tempfile + from datetime import datetime + + export_data = { + "export_date": datetime.now().isoformat(), + "original_config": session.get("original_config", ""), + "final_config": session.get("current_config", ""), + "total_rounds": len(session.get("rounds", [])), + "rounds": session.get("rounds", []) + } + + # Write to temp file + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"improvement_session_{timestamp}.json" + filepath = f"{tempfile.gettempdir()}/{filename}" + + with open(filepath, "w") as f: + json.dump(export_data, f, indent=2) + + return filepath + + def set_focus_area(focus_value): + """Set the focus area dropdown.""" + return focus_value + + # Wire up LLM Improvement Loop handlers + start_session_btn.click( + fn=start_improvement_session, + inputs=[loop_config_input, improvement_session], + outputs=[improvement_session, session_status, round_indicator, revert_dropdown, history_display] + ) + + generate_loop_prompt_btn.click( + fn=generate_improvement_prompt, + inputs=[loop_config_input, loop_config_type, loop_focus_area, improvement_session], + outputs=[loop_prompt_output] + ) + + accept_round_btn.click( + fn=accept_round_result, + inputs=[loop_response_input, improvement_session], + outputs=[improvement_session, metrics_comparison, round_indicator, revert_dropdown, history_display, loop_response_input] + ) + + revert_btn.click( + fn=revert_to_round, + inputs=[revert_dropdown, improvement_session], + outputs=[improvement_session, session_status, loop_config_input] + ) + + export_history_btn.click( + fn=export_session_history, + inputs=[improvement_session], + outputs=[export_history_file] + ) + + # Load from main config input + load_from_playtest_btn.click( + fn=lambda x: x, + inputs=[config_input], + outputs=[loop_config_input] + ) + + # Load demo mystery config + def load_demo_mystery(): + """Load the mystery demo config for the workflow tutorial.""" + return '''{ + "investigation": { + "crime_scene": { + "description": "A dimly lit office. Papers scattered everywhere. A broken window.", + "choices": ["Examine the desk", "Check the window", "Leave"], + "transitions": { + "Examine the desk": "desk_clue", + "Check the window": "window_clue", + "Leave": "hallway" + } + }, + "desk_clue": { + "description": "The desk has a half-written letter and an empty coffee cup.", + "choices": ["Read the letter", "Back to scene"], + "transitions": { + "Read the letter": "letter_reveal", + "Back to scene": "crime_scene" + } + }, + "window_clue": { + "description": "Glass shards on the inside. Someone broke in from outside.", + "choices": ["Look outside", "Back to scene"], + "transitions": { + "Look outside": "outside_view", + "Back to scene": "crime_scene" + } + }, + "letter_reveal": { + "description": "The letter mentions a meeting tonight. It's unfinished.", + "choices": ["Continue investigating"], + "transitions": { + "Continue investigating": "crime_scene" + } + }, + "outside_view": { + "description": "Footprints in the mud lead toward the parking lot.", + "choices": ["Follow the footprints", "Back inside"], + "transitions": { + "Follow the footprints": "hallway", + "Back inside": "crime_scene" + } + }, + "hallway": { + "description": "The hallway is quiet. A security guard approaches.", + "choices": ["Talk to guard", "Return to office"], + "transitions": { + "Talk to guard": "guard_talk", + "Return to office": "crime_scene" + } + }, + "guard_talk": { + "description": "The guard says he heard nothing unusual tonight.", + "choices": ["Press for details", "Thank him and leave"], + "transitions": { + "Press for details": "guard_talk", + "Thank him and leave": "hallway" + } + } + } +}''' + + load_demo_btn.click( + fn=load_demo_mystery, + outputs=[loop_config_input] + ) + + # Load complete mystery demo (after 4 rounds of improvement) + def load_complete_demo(): + """Load the completed mystery demo showing the end result after 4 rounds.""" + import os + demo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "demo_mystery_complete.json") + try: + with open(demo_path, 'r') as f: + return f.read() + except: + # Fallback to inline minimal version + return '{"investigation": {"crime_scene": {"description": "Complete demo file not found. Please ensure demo_mystery_complete.json exists.", "choices": ["OK"], "transitions": {"OK": "crime_scene"}}}}' + + load_complete_demo_btn.click( + fn=load_complete_demo, + outputs=[loop_config_input] + ) + + # Load full demo session with pre-populated round history + def load_demo_session(): + """Load a complete demo session showing 4 rounds of improvement.""" + import os + from demo_llm_loop_examples import DEMO_MYSTERY_STARTER, DEMO_AFTER_ROUND_1 + + # Load the complete demo + demo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "demo_mystery_complete.json") + try: + with open(demo_path, 'r') as f: + complete_config = f.read() + except: + complete_config = DEMO_AFTER_ROUND_1 + + # Create simulated round history + demo_session = { + "rounds": [ + { + "round_number": 1, + "focus_area": "environment", + "config_type": "mystery", + "metrics_before": {"total_states": 7, "avg_choices": 2.0, "avg_desc_words": 12}, + "metrics_after": {"total_states": 18, "avg_choices": 2.8, "avg_desc_words": 45}, + "prompt_summary": "Focus: Environmental storytelling - plant clues in descriptions, add sensory details" + }, + { + "round_number": 2, + "focus_area": "characters", + "config_type": "mystery", + "metrics_before": {"total_states": 18, "avg_choices": 2.8, "avg_desc_words": 45}, + "metrics_after": {"total_states": 22, "avg_choices": 3.1, "avg_desc_words": 52}, + "prompt_summary": "Focus: Character depth - NPC motivations, dialogue, relationships" + }, + { + "round_number": 3, + "focus_area": "choices", + "config_type": "mystery", + "metrics_before": {"total_states": 22, "avg_choices": 3.1, "avg_desc_words": 52}, + "metrics_after": {"total_states": 25, "avg_choices": 3.6, "avg_desc_words": 55}, + "prompt_summary": "Focus: Investigation options - multiple approaches, hidden paths" + }, + { + "round_number": 4, + "focus_area": "tension", + "config_type": "mystery", + "metrics_before": {"total_states": 25, "avg_choices": 3.6, "avg_desc_words": 55}, + "metrics_after": {"total_states": 29, "avg_choices": 3.8, "avg_desc_words": 62}, + "prompt_summary": "Focus: Tension & stakes - red herrings, dramatic reveals, time pressure" + } + ], + "current_config": complete_config, + "original_config": DEMO_MYSTERY_STARTER, + "session_active": True, + "current_round": 5 + } + + # Format history display + history_lines = ["## Demo Session: Mystery Deepening Workflow\n"] + history_lines.append("| Round | Focus | States | Choices | Desc Length |") + history_lines.append("|-------|-------|--------|---------|-------------|") + history_lines.append("| Start | - | 7 | 2.0 | 12 words |") + + for r in demo_session["rounds"]: + m = r["metrics_after"] + history_lines.append(f"| R{r['round_number']} | {r['focus_area']} | {m['total_states']} | {m['avg_choices']} | {m['avg_desc_words']} words |") + + history_lines.append("\n### Round Details\n") + for r in demo_session["rounds"]: + delta_states = r["metrics_after"]["total_states"] - r["metrics_before"]["total_states"] + history_lines.append(f"**Round {r['round_number']}: {r['focus_area'].title()}**") + history_lines.append(f"- {r['prompt_summary']}") + history_lines.append(f"- Added {delta_states} new states\n") + + history_text = "\n".join(history_lines) + + # Revert dropdown options + revert_options = [f"Round {r['round_number']}" for r in demo_session["rounds"]] + + return ( + demo_session, + complete_config, + "**Demo Session Loaded!** 4 rounds of Mystery Deepening workflow. See Round History below.", + "**Current Round: 5** (Demo complete - review history or start fresh)", + revert_options, + history_text + ) + + load_demo_session_btn.click( + fn=load_demo_session, + outputs=[improvement_session, loop_config_input, session_status, round_indicator, revert_dropdown, history_display] + ) + + # Demo comparison state storage - now holds all 5 rounds + demo_comparison_data = gr.State(value={"rounds": [{}, {}, {}, {}, {}], "all_states": []}) + + # Load comparison data for all 5 rounds + def load_demo_comparison_data(): + """Load all 5 demo rounds and prepare comparison data.""" + from demo_llm_loop_examples import ( + DEMO_MYSTERY_STARTER, DEMO_AFTER_ROUND_1, + DEMO_AFTER_ROUND_2, DEMO_AFTER_ROUND_3, DEMO_AFTER_ROUND_4 + ) + + # Parse all rounds + try: + rounds = [ + json.loads(DEMO_MYSTERY_STARTER), + json.loads(DEMO_AFTER_ROUND_1), + json.loads(DEMO_AFTER_ROUND_2), + json.loads(DEMO_AFTER_ROUND_3), + json.loads(DEMO_AFTER_ROUND_4), + ] + except Exception as e: + return ( + {"rounds": [{}, {}, {}, {}, {}], "all_states": []}, + f"Error loading demo configs: {e}", + gr.update(choices=[], value=None), + "*Error loading data*" + ) + + # Get investigation states from each round + round_states = [r.get("investigation", {}) for r in rounds] + + # Collect all state names across all rounds + all_state_names = set() + for rs in round_states: + all_state_names.update(rs.keys()) + + # Find states in R0 (starter) for "original" classification + r0_states = set(round_states[0].keys()) + r4_states = set(round_states[4].keys()) + + # Classify states + original_states = sorted(r0_states) # States that existed from the start + new_states = sorted(all_state_names - r0_states) # States added in later rounds + + # Build choices - original first, then new + state_choices = [] + for state in original_states: + state_choices.append((f"📝 {state} (original)", state)) + for state in new_states: + # Find which round it first appeared in + first_round = "?" + for i, rs in enumerate(round_states): + if state in rs: + first_round = f"R{i}" + break + state_choices.append((f"✨ {state} (added {first_round})", state)) + + # Calculate metrics for each round + def calc_metrics(states_dict): + if not states_dict: + return 0, 0, 0 + count = len(states_dict) + total_choices = sum(len(s.get('choices', [])) for s in states_dict.values()) + total_words = sum(len(s.get('description', '').split()) for s in states_dict.values()) + return count, round(total_choices / count, 1) if count else 0, round(total_words / count, 0) if count else 0 + + metrics = [calc_metrics(rs) for rs in round_states] + + metrics_md = f"""## Round-by-Round Metrics + +| Round | Focus | States | Avg Choices | Avg Words | +|-------|-------|--------|-------------|-----------| +| **R0** | Starter | {metrics[0][0]} | {metrics[0][1]} | {int(metrics[0][2])} | +| **R1** | Environment | {metrics[1][0]} | {metrics[1][1]} | {int(metrics[1][2])} | +| **R2** | Characters | {metrics[2][0]} | {metrics[2][1]} | {int(metrics[2][2])} | +| **R3** | Choices | {metrics[3][0]} | {metrics[3][1]} | {int(metrics[3][2])} | +| **R4** | Tension | {metrics[4][0]} | {metrics[4][1]} | {int(metrics[4][2])} | + +**Legend:** 📝 Original state | ✨ Added in later round + +--- +*Select a state below to see its progression across all 5 rounds.* +""" + comparison_data = { + "rounds": round_states, + "all_states": sorted(all_state_names) + } + + # Default to first original state for meaningful comparison + default_state = original_states[0] if original_states else (list(all_state_names)[0] if all_state_names else None) + + return ( + comparison_data, + metrics_md, + gr.update(choices=state_choices, value=default_state), + f"*{len(original_states)} original, {len(new_states)} added*" + ) + + def format_state_view(state_data): + """Format a single state for display.""" + if not state_data: + return "(not present)" + text = f"{state_data.get('description', 'N/A')}\n\n" + choices = state_data.get('choices', []) + text += f"Choices ({len(choices)}):\n" + for i, c in enumerate(choices, 1): + text += f" {i}. {c}\n" + return text.strip() + + def show_state_comparison(state_name, comparison_data): + """Show state across all 5 rounds.""" + if not state_name or not comparison_data or not comparison_data.get("rounds"): + empty = "(select a state)" + return empty, empty, empty, empty, empty, "*Select a state to compare*" + + rounds = comparison_data.get("rounds", [{}, {}, {}, {}, {}]) + + # Get state from each round + views = [] + word_counts = [] + choice_counts = [] + for i, rs in enumerate(rounds): + state_data = rs.get(state_name) + views.append(format_state_view(state_data)) + if state_data: + word_counts.append(len(state_data.get('description', '').split())) + choice_counts.append(len(state_data.get('choices', []))) + else: + word_counts.append(0) + choice_counts.append(0) + + # Build progression summary + first_appearance = next((i for i, rs in enumerate(rounds) if state_name in rs), None) + if first_appearance is None: + summary = f"**`{state_name}`** - State not found in any round" + elif first_appearance == 0: + summary = f"""**`{state_name}`** - Original state, evolved through all rounds + +| Metric | R0 | R1 | R2 | R3 | R4 | +|--------|----|----|----|----|----| +| Words | {word_counts[0]} | {word_counts[1]} | {word_counts[2]} | {word_counts[3]} | {word_counts[4]} | +| Choices | {choice_counts[0]} | {choice_counts[1]} | {choice_counts[2]} | {choice_counts[3]} | {choice_counts[4]} | +""" + else: + summary = f"""**`{state_name}`** - Added in Round {first_appearance} + +| Metric | R0 | R1 | R2 | R3 | R4 | +|--------|----|----|----|----|----| +| Words | {word_counts[0] or '-'} | {word_counts[1] or '-'} | {word_counts[2] or '-'} | {word_counts[3] or '-'} | {word_counts[4] or '-'} | +| Choices | {choice_counts[0] or '-'} | {choice_counts[1] or '-'} | {choice_counts[2] or '-'} | {choice_counts[3] or '-'} | {choice_counts[4] or '-'} | +""" + + return views[0], views[1], views[2], views[3], views[4], summary + + load_comparison_btn.click( + fn=load_demo_comparison_data, + outputs=[demo_comparison_data, demo_metrics_display, state_selector, state_status] + ) + + state_selector.change( + fn=show_state_comparison, + inputs=[state_selector, demo_comparison_data], + outputs=[round0_view, round1_view, round2_view, round3_view, round4_view, state_changes_display] + ) + + # Workflow step buttons - auto-set focus area + for btn, focus in [ + (wf_branch_1, "choices"), (wf_branch_2, "choices"), (wf_branch_3, "detail"), (wf_branch_4, "characters"), + (wf_detail_1, "detail"), (wf_detail_2, "environment"), (wf_detail_3, "choices"), (wf_detail_4, "tension"), + (wf_nonbranch_1, "detail"), (wf_nonbranch_2, "characters"), (wf_nonbranch_3, "environment"), (wf_nonbranch_4, "tension"), + (wf_mystery_1, "environment"), (wf_mystery_2, "characters"), (wf_mystery_3, "choices"), (wf_mystery_4, "tension"), + ]: + btn.click(fn=lambda f=focus: f, outputs=[loop_focus_area]) diff --git a/ui_tabs/dnd_gm_tab.py b/ui_tabs/dnd_gm_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..4028622e593aed154e722c56d00e881627a07b8d --- /dev/null +++ b/ui_tabs/dnd_gm_tab.py @@ -0,0 +1,1150 @@ +""" +D&D Game Master Tab - Interactive D&D 5e with LLM-powered Game Master. +Uses Zero GPU for inference on HuggingFace Spaces. +Includes Open5e API integration for SRD content lookup. +""" + +import json +import re +import gradio as gr +import spaces +import requests +from typing import Optional, Dict, Any, List + +# ==================== OPEN5E API INTEGRATION ==================== +OPEN5E_BASE_URL = "https://api.open5e.com/v1" + +def fetch_open5e(endpoint: str, search: str = "", limit: int = 10) -> Dict[str, Any]: + """Fetch data from Open5e API. + + Args: + endpoint: API endpoint (monsters, spells, conditions, etc.) + search: Search term to filter results + limit: Maximum results to return + + Returns: + API response as dictionary + """ + try: + params = {"limit": limit} + if search: + params["search"] = search + + response = requests.get(f"{OPEN5E_BASE_URL}/{endpoint}/", params=params, timeout=10) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"error": str(e), "results": []} + +def fetch_open5e_detail(endpoint: str, slug: str) -> Dict[str, Any]: + """Fetch detailed data for a specific item. + + Args: + endpoint: API endpoint (monsters, spells, etc.) + slug: Unique identifier for the item + + Returns: + Detailed item data + """ + try: + response = requests.get(f"{OPEN5E_BASE_URL}/{endpoint}/{slug}/", timeout=10) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"error": str(e)} + +def search_monsters(query: str, limit: int = 10) -> str: + """Search for D&D 5e monsters.""" + data = fetch_open5e("monsters", query, limit) + if "error" in data: + return f"**Error:** {data['error']}" + + if not data.get("results"): + return f"No monsters found matching '{query}'" + + results = [] + for monster in data["results"]: + cr = monster.get("cr", "?") + hp = monster.get("hit_points", "?") + ac = monster.get("armor_class", "?") + mtype = monster.get("type", "unknown") + results.append(f"**{monster['name']}** (CR {cr}) - {mtype.title()}, AC {ac}, HP {hp}") + + return "\n".join(results) + +def get_monster_details(slug: str) -> str: + """Get full details for a monster.""" + data = fetch_open5e_detail("monsters", slug) + if "error" in data: + return f"**Error:** {data['error']}" + + md = f"# {data.get('name', 'Unknown')}\n" + md += f"*{data.get('size', '')} {data.get('type', '')}, {data.get('alignment', '')}*\n\n" + md += f"**Armor Class** {data.get('armor_class', '?')}\n" + md += f"**Hit Points** {data.get('hit_points', '?')} ({data.get('hit_dice', '')})\n" + md += f"**Speed** {data.get('speed', {})}\n" + md += f"**Challenge** {data.get('cr', '?')}\n\n" + + # Stats + md += "| STR | DEX | CON | INT | WIS | CHA |\n" + md += "|-----|-----|-----|-----|-----|-----|\n" + md += f"| {data.get('strength', '?')} | {data.get('dexterity', '?')} | {data.get('constitution', '?')} | " + md += f"{data.get('intelligence', '?')} | {data.get('wisdom', '?')} | {data.get('charisma', '?')} |\n\n" + + # Special abilities + if data.get("special_abilities"): + md += "## Special Abilities\n" + for ability in data["special_abilities"]: + md += f"**{ability.get('name', '')}:** {ability.get('desc', '')}\n\n" + + # Actions + if data.get("actions"): + md += "## Actions\n" + for action in data["actions"]: + md += f"**{action.get('name', '')}:** {action.get('desc', '')}\n\n" + + return md + +def search_spells(query: str, limit: int = 10) -> str: + """Search for D&D 5e spells.""" + data = fetch_open5e("spells", query, limit) + if "error" in data: + return f"**Error:** {data['error']}" + + if not data.get("results"): + return f"No spells found matching '{query}'" + + results = [] + for spell in data["results"]: + level = spell.get("level_int", 0) + level_str = "Cantrip" if level == 0 else f"Level {level}" + school = spell.get("school", "unknown") + results.append(f"**{spell['name']}** ({level_str} {school})") + + return "\n".join(results) + +def get_spell_details(slug: str) -> str: + """Get full details for a spell.""" + data = fetch_open5e_detail("spells", slug) + if "error" in data: + return f"**Error:** {data['error']}" + + level = data.get("level_int", 0) + level_str = "Cantrip" if level == 0 else f"{level}{'st' if level==1 else 'nd' if level==2 else 'rd' if level==3 else 'th'}-level" + + md = f"# {data.get('name', 'Unknown')}\n" + md += f"*{level_str} {data.get('school', '')}*\n\n" + md += f"**Casting Time:** {data.get('casting_time', '?')}\n" + md += f"**Range:** {data.get('range', '?')}\n" + md += f"**Components:** {data.get('components', '?')}\n" + md += f"**Duration:** {data.get('duration', '?')}\n" + md += f"**Classes:** {data.get('dnd_class', '?')}\n\n" + md += data.get("desc", "No description available.") + + if data.get("higher_level"): + md += f"\n\n**At Higher Levels:** {data['higher_level']}" + + return md + +def search_conditions(query: str = "") -> str: + """Get D&D 5e conditions.""" + data = fetch_open5e("conditions", query, 20) + if "error" in data: + return f"**Error:** {data['error']}" + + if not data.get("results"): + return "No conditions found" + + results = [] + for condition in data["results"]: + desc = condition.get("desc", "")[:100] + "..." if len(condition.get("desc", "")) > 100 else condition.get("desc", "") + results.append(f"**{condition['name']}:** {desc}") + + return "\n\n".join(results) + +def search_classes(query: str = "") -> str: + """Search for D&D 5e classes.""" + data = fetch_open5e("classes", query, 20) + if "error" in data: + return f"**Error:** {data['error']}" + + if not data.get("results"): + return "No classes found" + + results = [] + for cls in data["results"]: + hd = cls.get("hit_dice", "?") + results.append(f"**{cls['name']}** - Hit Die: d{hd}") + + return "\n".join(results) + +def search_magicitems(query: str, limit: int = 10) -> str: + """Search for D&D 5e magic items.""" + data = fetch_open5e("magicitems", query, limit) + if "error" in data: + return f"**Error:** {data['error']}" + + if not data.get("results"): + return f"No magic items found matching '{query}'" + + results = [] + for item in data["results"]: + rarity = item.get("rarity", "unknown") + itype = item.get("type", "") + results.append(f"**{item['name']}** ({rarity}) - {itype}") + + return "\n".join(results) + +def get_magicitem_details(slug: str) -> str: + """Get full details for a magic item.""" + data = fetch_open5e_detail("magicitems", slug) + if "error" in data: + return f"**Error:** {data['error']}" + + md = f"# {data.get('name', 'Unknown')}\n" + md += f"*{data.get('type', '')} ({data.get('rarity', '')})*\n\n" + + if data.get("requires_attunement"): + md += f"**Requires Attunement:** {data.get('requires_attunement', 'Yes')}\n\n" + + md += data.get("desc", "No description available.") + + return md + + +# Lazy imports for performance +_torch = None +_transformers = None + +def _ensure_torch(): + global _torch + if _torch is None: + import torch + _torch = torch + return _torch + +def _ensure_transformers(): + global _transformers + if _transformers is None: + import transformers + _transformers = transformers + return _transformers + +# Global model storage (loaded once per model) +_gm_model = None +_gm_tokenizer = None +_gm_model_name = None + +def _ensure_gm_model(model_name="Qwen/Qwen3-4B"): + """Lazy load the GM model, reloading if model changes.""" + global _gm_model, _gm_tokenizer, _gm_model_name + + # Reload if model changed + if _gm_model is None or _gm_model_name != model_name: + tf = _ensure_transformers() + print(f"Loading D&D GM model: {model_name}") + + # Clear old model from memory + if _gm_model is not None: + del _gm_model + del _gm_tokenizer + _torch = _ensure_torch() + if _torch.cuda.is_available(): + _torch.cuda.empty_cache() + + _gm_tokenizer = tf.AutoTokenizer.from_pretrained(model_name) + _gm_model = tf.AutoModelForCausalLM.from_pretrained( + model_name, + torch_dtype="auto", + device_map="auto" + ) + _gm_model_name = model_name + print(f"GM model loaded successfully: {model_name}") + + return _gm_model, _gm_tokenizer + + +@spaces.GPU +def generate_scene_image_local(prompt: str) -> str: + """Generate image using local SDXL Turbo model with Zero GPU.""" + try: + from diffusers import AutoPipelineForText2Image + _torch = _ensure_torch() + + pipe = AutoPipelineForText2Image.from_pretrained( + "stabilityai/sdxl-turbo", + torch_dtype=_torch.float16, + variant="fp16" + ) + pipe.to("cuda") + + image = pipe( + prompt, + num_inference_steps=4, + guidance_scale=0.0, + height=512, + width=512, + ).images[0] + + # Save to temp file and return path + import tempfile + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: + image.save(f.name) + return f.name + except Exception as e: + print(f"Local SDXL Turbo error: {e}") + import traceback + traceback.print_exc() + return None + + +def generate_scene_image(scene_description: str, image_model: str) -> str: + """Generate an image for the current scene using the selected model.""" + # Build a D&D-style image prompt + prompt = f"Fantasy tavern scene, D&D style, atmospheric lighting: {scene_description[:200]}" + + if image_model == "local_sdxl": + return generate_scene_image_local(prompt) + else: + # Use external HuggingFace Space via gradio_client + try: + from gradio_client import Client + + if "Z-Image-Turbo" in image_model: + print(f"Connecting to Z-Image-Turbo...") + client = Client("mrfakename/Z-Image-Turbo") + result = client.predict( + prompt=prompt, + seed=0, + randomize_seed=True, + width=512, + height=512, + num_inference_steps=4, + api_name="/infer" + ) + if isinstance(result, tuple): + return result[0] + return result + + elif "Qwen" in image_model: + print(f"Connecting to Qwen-Image...") + client = Client("Qwen/Qwen-Image-2512") + result = client.predict( + prompt=prompt, + api_name="/generate" + ) + if isinstance(result, tuple): + return result[0] + return result + + except Exception as e: + print(f"External image generation error: {e}") + import traceback + traceback.print_exc() + return None + + return None + + +# Import D&D components +from dnd_game_master_hfport.models.character import Character, PRESET_CHARACTERS +from dnd_game_master_hfport.models.scene import Scene, PRESET_SCENES +from dnd_game_master_hfport.engine.dice import roll_d20, RollMode +from dnd_game_master_hfport.engine.modifiers import get_skill_modifier, get_ability_modifier +from dnd_game_master_hfport.prompts.gm_prompts import ( + build_gm_system_prompt, + build_scene_context, + build_roll_outcome_prompt, +) + + +def format_character_display(character: Character) -> str: + """Format character info for display.""" + if not character: + return "No character selected" + + ability_mod = lambda x: f"+{(x-10)//2}" if (x-10)//2 >= 0 else str((x-10)//2) + + skills_display = [] + for skill in character.proficient_skills[:6]: # Show first 6 skills + skill_name = skill.replace("_", " ").title() + expertise = " (E)" if character.has_expertise(skill) else "" + skills_display.append(f" {skill_name}{expertise}") + + return f"""**{character.name}** +Level {character.level} {character.race} {character.character_class} +*{character.background}* | {character.alignment} + +**HP:** {character.current_hp}/{character.max_hp} | **AC:** {character.armor_class} | **Speed:** {character.speed}ft + +**Abilities:** +STR: {character.strength} ({ability_mod(character.strength)}) | DEX: {character.dexterity} ({ability_mod(character.dexterity)}) | CON: {character.constitution} ({ability_mod(character.constitution)}) +INT: {character.intelligence} ({ability_mod(character.intelligence)}) | WIS: {character.wisdom} ({ability_mod(character.wisdom)}) | CHA: {character.charisma} ({ability_mod(character.charisma)}) + +**Proficient Skills:** +{chr(10).join(skills_display)} + +**Equipment:** {', '.join(character.equipment[:4])}... +""" + + +def format_scene_display(scene: Scene) -> str: + """Format scene info for display.""" + if not scene: + return "No scene loaded" + + npc_list = "" + if scene.npcs: + npc_list = "\n**NPCs Present:**\n" + "\n".join([ + f" - {npc.name} ({npc.attitude.value})" + for npc in scene.npcs + ]) + + objects_list = "" + if scene.objects: + objects_list = "\n**Notable Objects:** " + ", ".join(scene.objects) + + exits_list = "" + if scene.exits: + exits_list = "\n**Exits:** " + ", ".join(scene.exits) + + return f"""## {scene.location} +*{scene.time_of_day.value.title()}, {scene.lighting.value} lighting* + +{scene.description} +{npc_list}{objects_list}{exits_list} +""" + + +def parse_gm_response(response: str) -> dict: + """Parse JSON from GM response, handling common LLM output issues.""" + + def clean_json_string(s: str) -> str: + """Fix common JSON issues from LLM output.""" + # Remove trailing commas before } or ] + s = re.sub(r',\s*([}\]])', r'\1', s) + # Remove any control characters + s = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', s) + return s.strip() + + try: + # Try to extract JSON from markdown code block + json_match = re.search(r'```(?:json)?\s*([\s\S]*?)```', response) + if json_match: + cleaned = clean_json_string(json_match.group(1)) + return json.loads(cleaned) + + # Try to find JSON object + json_match = re.search(r'\{[\s\S]*\}', response) + if json_match: + cleaned = clean_json_string(json_match.group()) + return json.loads(cleaned) + + # Fallback: treat as narration + return { + "decision": "auto_success", + "narration": response.strip() + } + except json.JSONDecodeError: + # Try one more time with aggressive cleaning + try: + json_match = re.search(r'\{[\s\S]*\}', response) + if json_match: + cleaned = clean_json_string(json_match.group()) + # Try to extract at least the narration field + narration_match = re.search(r'"narration"\s*:\s*"([^"]*)"', cleaned) + if narration_match: + return { + "decision": "auto_success", + "narration": narration_match.group(1) + } + except: + pass + + return { + "decision": "auto_success", + "narration": response.strip() + } + + +@spaces.GPU +def gm_process_action( + player_action: str, + character_json: str, + scene_json: str, + event_log: str, + model_name: str = "unsloth/Llama-3.2-1B-Instruct", + enable_thinking: bool = False +) -> tuple: + """ + Process player action using LLM and return GM decision. + + Returns: (gm_response, updated_event_log, roll_needed, roll_info) + """ + if not player_action or not player_action.strip(): + return "Please enter an action.", event_log, False, "{}" + + # Load character and scene from JSON + try: + char_data = json.loads(character_json) + scene_data = json.loads(scene_json) + except json.JSONDecodeError: + return "Error: Invalid game state.", event_log, False, "{}" + + # Recreate character object + character = Character(**{k: v for k, v in char_data.items() + if k in Character.__dataclass_fields__}) + + # Build scene from data + scene = Scene( + location=scene_data.get("location", "Unknown"), + description=scene_data.get("description", ""), + ) + + # Get model and tokenizer + model, tokenizer = _ensure_gm_model(model_name) + + # Move to GPU + _torch = _ensure_torch() + zero = _torch.Tensor([0]).cuda() + model.to(zero.device) + + # Build prompt + system_prompt = build_gm_system_prompt() + context = build_scene_context(character, scene) + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": f"{context}\n\n---\n\nPlayer action: {player_action}"} + ] + + # Configure thinking mode for Qwen3 models + chat_kwargs = { + "tokenize": False, + "add_generation_prompt": True, + } + is_qwen3 = "qwen3" in model_name.lower() or "Qwen3" in model_name + if is_qwen3: + chat_kwargs["enable_thinking"] = enable_thinking + + text = tokenizer.apply_chat_template(messages, **chat_kwargs) + + model_inputs = tokenizer([text], return_tensors="pt").to(zero.device) + + # Use more tokens when thinking is enabled to allow for both thinking and response + max_tokens = 1024 if (is_qwen3 and enable_thinking) else 512 + + generated_ids = model.generate( + model_inputs.input_ids, + max_new_tokens=max_tokens, + temperature=0.8, + do_sample=True, + pad_token_id=tokenizer.eos_token_id + ) + + generated_ids = [ + output_ids[len(input_ids):] + for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) + ] + response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] + + # Strip Qwen3 thinking tags if present (in case of partial output) + response = re.sub(r'.*?', '', response, flags=re.DOTALL).strip() + + # Parse response + decision = parse_gm_response(response) + decision_type = decision.get("decision", "auto_success") + narration = decision.get("narration", response) + + # Normalize decision type - LLM may return non-standard values + # If it has skill + dc, treat as needs_roll + has_skill = "skill" in decision and decision["skill"] + has_dc = "dc" in decision and isinstance(decision.get("dc"), (int, float)) + if has_skill and has_dc and decision_type not in ["auto_success", "auto_fail", "impossible"]: + decision_type = "needs_roll" + + # Extract suggestions if present + suggestions = decision.get("suggestions", []) + suggestions_text = "" + if suggestions and isinstance(suggestions, list): + suggestions_text = "\n\n**Suggestions:**\n" + "\n".join([f"- {s}" for s in suggestions[:4]]) + + # Update event log + new_log = event_log + f"\n[PLAYER] {player_action}" + + if decision_type == "needs_roll": + skill = decision.get("skill", "perception") + dc = int(decision.get("dc", 15)) + roll_info = json.dumps({ + "skill": skill, + "dc": dc, + "action": player_action, + "advantage": decision.get("advantage", False), + "disadvantage": decision.get("disadvantage", False) + }) + new_log += f"\n[GM] {narration}\n[ROLL NEEDED] {skill.title()} DC {dc}" + return f"**Roll Required:** {skill.title()} (DC {dc})\n\n{narration}{suggestions_text}", new_log, True, roll_info + else: + new_log += f"\n[GM] {narration}" + return f"{narration}{suggestions_text}", new_log, False, "{}" + + +@spaces.GPU +def gm_execute_roll( + roll_info_json: str, + character_json: str, + scene_json: str, + event_log: str, + model_name: str = "unsloth/Llama-3.2-1B-Instruct", + enable_thinking: bool = False +) -> tuple: + """ + Execute a skill roll and get narration. + + Returns: (roll_result_display, narration, updated_event_log) + """ + try: + roll_info = json.loads(roll_info_json) + char_data = json.loads(character_json) + except json.JSONDecodeError: + return "Error: Invalid data", "Could not execute roll.", event_log + + # Recreate character + character = Character(**{k: v for k, v in char_data.items() + if k in Character.__dataclass_fields__}) + + skill = roll_info.get("skill", "perception") + dc = roll_info.get("dc", 15) + action = roll_info.get("action", "attempt something") + + # Calculate modifier + modifier = get_skill_modifier( + ability_scores=character.get_ability_scores(), + skill=skill, + level=character.level, + is_proficient=character.is_proficient(skill), + has_expertise=character.has_expertise(skill), + ) + + # Determine roll mode + mode = RollMode.NORMAL + if roll_info.get("advantage"): + mode = RollMode.ADVANTAGE + elif roll_info.get("disadvantage"): + mode = RollMode.DISADVANTAGE + + # Roll! + roll_result = roll_d20(modifier, mode) + natural = roll_result.natural_roll + total = roll_result.total + success = total >= dc or natural == 20 + if natural == 1: + success = False + + critical = natural == 20 or natural == 1 + + # Build roll display + result_emoji = "SUCCESS" if success else "FAILURE" + if natural == 20: + result_emoji = "CRITICAL SUCCESS!" + elif natural == 1: + result_emoji = "CRITICAL FAILURE!" + + roll_display = f"""### Roll Result +**{skill.title()}:** {natural} + {modifier} = **{total}** vs DC {dc} +**Result:** {result_emoji} +""" + + # Get narration from LLM + model, tokenizer = _ensure_gm_model(model_name) + _torch = _ensure_torch() + zero = _torch.Tensor([0]).cuda() + model.to(zero.device) + + outcome_prompt = build_roll_outcome_prompt( + action=action, + skill=skill, + roll_total=total, + dc=dc, + success=success, + critical=critical, + natural_roll=natural, + ) + + system_prompt = build_gm_system_prompt() + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": outcome_prompt} + ] + + # Configure thinking mode for Qwen3 models + chat_kwargs = { + "tokenize": False, + "add_generation_prompt": True, + } + is_qwen3 = "qwen3" in model_name.lower() or "Qwen3" in model_name + if is_qwen3: + chat_kwargs["enable_thinking"] = enable_thinking + + text = tokenizer.apply_chat_template(messages, **chat_kwargs) + + model_inputs = tokenizer([text], return_tensors="pt").to(zero.device) + + # Use more tokens when thinking is enabled + max_tokens = 768 if (is_qwen3 and enable_thinking) else 384 + + generated_ids = model.generate( + model_inputs.input_ids, + max_new_tokens=max_tokens, + temperature=0.8, + do_sample=True, + pad_token_id=tokenizer.eos_token_id + ) + + generated_ids = [ + output_ids[len(input_ids):] + for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) + ] + response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] + + # Strip Qwen3 thinking tags if present (in case of partial output) + response = re.sub(r'.*?', '', response, flags=re.DOTALL).strip() + + # Parse narration + parsed = parse_gm_response(response) + narration = parsed.get("narration", response) + + # Update event log + roll_summary = f"{skill.title()}: {natural}+{modifier}={total} vs DC {dc} - {result_emoji}" + new_log = event_log + f"\n[ROLL] {roll_summary}\n[GM] {narration}" + + return roll_display, narration, new_log + + +def convert_event_log_to_config(event_log: str, location_name: str = "adventure") -> str: + """ + Convert event log to game config JSON format. + """ + if not event_log.strip(): + return json.dumps({"error": "No events to convert"}, indent=2) + + lines = event_log.strip().split("\n") + config = {} + current_location = location_name.lower().replace(" ", "_") + state_counter = 0 + + config[current_location] = {} + + current_state = None + current_description = "" + current_choices = [] + + for line in lines: + line = line.strip() + if not line: + continue + + if line.startswith("[SCENE]"): + # New scene = new state + desc = line.replace("[SCENE]", "").strip() + state_counter += 1 + current_state = f"state_{state_counter}" + config[current_location][current_state] = { + "description": desc, + "choices": [], + "transitions": {} + } + current_description = desc + + elif line.startswith("[PLAYER]"): + # Player action = choice + action = line.replace("[PLAYER]", "").strip() + if current_state and current_state in config[current_location]: + config[current_location][current_state]["choices"].append(action) + + elif line.startswith("[GM]"): + # GM response = description for next state + narration = line.replace("[GM]", "").strip() + if current_state: + # Create a new state for this outcome + state_counter += 1 + new_state = f"state_{state_counter}" + config[current_location][new_state] = { + "description": narration, + "choices": [], + "transitions": {} + } + # Link previous choices to this state + if config[current_location].get(current_state, {}).get("choices"): + last_choice = config[current_location][current_state]["choices"][-1] + config[current_location][current_state]["transitions"][last_choice] = f"{current_location}/{new_state}" + current_state = new_state + + elif line.startswith("[ROLL]"): + # Roll info - could add to choice_config + pass + + elif line.startswith("[ROLL NEEDED]"): + # Roll requirement - add to choice_config + pass + + # Clean up empty states + for loc in list(config.keys()): + for state in list(config[loc].keys()): + if not config[loc][state].get("description"): + del config[loc][state] + + return json.dumps(config, indent=2) + + +def create_dnd_gm_tab(): + """Create the D&D Game Master tab.""" + + with gr.Tab("D&D Game Master"): + gr.Markdown("""## D&D 5e Game Master +Play D&D with an AI Game Master! Select a character and scene, then describe what you want to do. +The GM will interpret your action, request skill checks when needed, and narrate the outcome. + """) + + # Game state storage + character_state = gr.State(value="{}") + scene_state = gr.State(value="{}") + roll_info_state = gr.State(value="{}") + roll_pending = gr.State(value=False) + + with gr.Row(): + # Setup controls + with gr.Column(scale=1): + character_dropdown = gr.Dropdown( + label="Select Character", + choices=list(PRESET_CHARACTERS.keys()), + value="Kira (Rogue)" + ) + scene_dropdown = gr.Dropdown( + label="Select Scene", + choices=list(PRESET_SCENES.keys()), + value="The Dusty Tankard (Tavern)" + ) + model_dropdown = gr.Dropdown( + label="LLM Model", + choices=[ + ("Llama 3.2 1B (Fast)", "unsloth/Llama-3.2-1B-Instruct"), + ("Qwen 3 4B (Recommended)", "Qwen/Qwen3-4B"), + ("Qwen 3 8B (Best)", "Qwen/Qwen3-8B"), + ], + value="Qwen/Qwen3-4B" + ) + enable_thinking = gr.Checkbox( + label="Enable Thinking (Qwen3)", + value=False, + info="Let model reason before responding (uses more tokens)" + ) + enable_images = gr.Checkbox( + label="Enable Image Generation", + value=False + ) + image_model_dropdown = gr.Dropdown( + label="Image Model", + choices=[ + ("Z-Image-Turbo (Fast)", "mrfakename/Z-Image-Turbo"), + ("Qwen-Image-2512 (Better)", "Qwen/Qwen-Image-2512"), + ("Local SDXL Turbo (Zero GPU)", "local_sdxl"), + ], + value="mrfakename/Z-Image-Turbo", + visible=False + ) + start_btn = gr.Button("Start Adventure", variant="primary") + reset_btn = gr.Button("Reset Game", variant="secondary") + + # Toggle image model visibility + enable_images.change( + fn=lambda x: gr.update(visible=x), + inputs=[enable_images], + outputs=[image_model_dropdown] + ) + + with gr.Row(): + # Left column - Character info and scene image + with gr.Column(scale=1): + character_display = gr.Markdown( + value="*Select a character and click Start Adventure*", + label="Character" + ) + scene_image = gr.Image( + label="Scene", + visible=False, + height=256 + ) + generate_image_btn = gr.Button("Generate Scene Image", visible=False, size="sm") + + # Right column - Scene and interaction + with gr.Column(scale=2): + scene_display = gr.Markdown( + value="*Select a scene and click Start Adventure*", + label="Scene" + ) + + gr.Markdown("### Your Action") + action_input = gr.Textbox( + label="What do you do?", + placeholder="I search the room carefully...", + lines=2 + ) + with gr.Row(): + submit_btn = gr.Button("Submit Action", variant="primary") + roll_btn = gr.Button("Roll Dice", variant="secondary", visible=False) + + gm_response = gr.Markdown( + value="*Waiting for your action...*", + label="GM Response" + ) + + roll_result = gr.Markdown(value="", visible=False) + + with gr.Row(): + with gr.Column(): + gr.Markdown("### Event Log") + event_log = gr.Textbox( + label="Event Log", + lines=10, + value="", + interactive=False + ) + with gr.Row(): + export_btn = gr.Button("Export to Game Config") + config_output = gr.Code(label="Generated Config", language="json", visible=False) + + # Event handlers + def start_adventure(char_name, scene_name, images_enabled): + """Initialize the adventure.""" + character = PRESET_CHARACTERS[char_name]() + scene = PRESET_SCENES[scene_name]() + + char_json = json.dumps(character.to_dict()) + scene_json = scene.to_dict() + + char_display = format_character_display(character) + scene_display_text = format_scene_display(scene) + + initial_log = f"[SCENE] {scene.location}: {scene.description}" + + return ( + char_json, + json.dumps(scene_json), + char_display, + scene_display_text, + initial_log, + "*Your adventure begins... What do you do?*", + gr.update(visible=False), + "", + gr.update(visible=images_enabled), # scene_image visibility + gr.update(visible=images_enabled), # generate_image_btn visibility + ) + + def handle_generate_image(scene_json, image_model): + """Generate an image for the current scene.""" + try: + scene_data = json.loads(scene_json) + description = scene_data.get("description", "A fantasy scene") + location = scene_data.get("location", "Unknown location") + lighting = scene_data.get("lighting", "dim") + time_of_day = scene_data.get("time_of_day", "evening") + + # Build rich prompt from scene data + full_description = f"{location}. {description} {time_of_day} lighting, {lighting} atmosphere." + + image_path = generate_scene_image(full_description, image_model) + if image_path: + return gr.update(value=image_path, visible=True) + else: + return gr.update(visible=True, value=None) + except Exception as e: + print(f"Image generation error: {e}") + return gr.update(visible=True, value=None) + + def handle_action(action, char_json, scene_json, event_log_text, model_name, thinking_enabled): + """Handle player action submission.""" + response, new_log, needs_roll, roll_info = gm_process_action( + action, char_json, scene_json, event_log_text, model_name, thinking_enabled + ) + + return ( + response, + new_log, + gr.update(visible=needs_roll), + roll_info, + needs_roll + ) + + def handle_roll(roll_info_json, char_json, scene_json, event_log_text, model_name, thinking_enabled): + """Handle dice roll.""" + roll_display, narration, new_log = gm_execute_roll( + roll_info_json, char_json, scene_json, event_log_text, model_name, thinking_enabled + ) + + return ( + f"{roll_display}\n\n{narration}", + new_log, + gr.update(visible=False), + gr.update(visible=True, value=roll_display), + False + ) + + def reset_game(): + """Reset the game state.""" + return ( + "{}", + "{}", + "*Select a character and click Start Adventure*", + "*Select a scene and click Start Adventure*", + "", + "*Waiting for your action...*", + gr.update(visible=False), + "", + False, + gr.update(visible=False), + gr.update(visible=False, value=None), # scene_image + gr.update(visible=False), # generate_image_btn + ) + + def export_to_config(event_log_text): + """Export event log to game config.""" + config = convert_event_log_to_config(event_log_text) + return gr.update(visible=True, value=config) + + # Wire up events + start_btn.click( + fn=start_adventure, + inputs=[character_dropdown, scene_dropdown, enable_images], + outputs=[ + character_state, scene_state, + character_display, scene_display, + event_log, gm_response, + roll_btn, roll_info_state, + scene_image, generate_image_btn + ] + ) + + generate_image_btn.click( + fn=handle_generate_image, + inputs=[scene_state, image_model_dropdown], + outputs=[scene_image] + ) + + submit_btn.click( + fn=handle_action, + inputs=[action_input, character_state, scene_state, event_log, model_dropdown, enable_thinking], + outputs=[gm_response, event_log, roll_btn, roll_info_state, roll_pending] + ) + + roll_btn.click( + fn=handle_roll, + inputs=[roll_info_state, character_state, scene_state, event_log, model_dropdown, enable_thinking], + outputs=[gm_response, event_log, roll_btn, roll_result, roll_pending] + ) + + reset_btn.click( + fn=reset_game, + outputs=[ + character_state, scene_state, + character_display, scene_display, + event_log, gm_response, + roll_btn, roll_info_state, + roll_pending, roll_result, + scene_image, generate_image_btn + ] + ) + + export_btn.click( + fn=export_to_config, + inputs=[event_log], + outputs=[config_output] + ) + + gr.Markdown(""" +--- +**Tips:** +- Describe your actions naturally: "I sneak past the guard" or "I examine the strange runes" +- The GM will decide if you need to roll dice based on the action's difficulty +- Your character's skills and abilities affect your chances of success +- Export your playthrough to a game config to build on it later! + """) + + # ==================== D&D 5e SRD LOOKUP ==================== + with gr.Accordion("D&D 5e SRD Lookup (Open5e API)", open=False): + gr.Markdown("*Search the D&D 5e System Reference Document for monsters, spells, magic items, and more.*") + + with gr.Row(): + srd_category = gr.Dropdown( + label="Category", + choices=[ + ("Monsters", "monsters"), + ("Spells", "spells"), + ("Magic Items", "magicitems"), + ("Conditions", "conditions"), + ("Classes", "classes") + ], + value="monsters" + ) + srd_search = gr.Textbox( + label="Search", + placeholder="goblin, fireball, vorpal...", + scale=2 + ) + srd_search_btn = gr.Button("Search", variant="primary") + + srd_results = gr.Markdown("*Enter a search term and click Search*") + + with gr.Row(): + srd_detail_slug = gr.Textbox( + label="Get Details (slug)", + placeholder="goblin, fireball, vorpal-sword...", + info="Enter the item's slug (lowercase, hyphenated name)" + ) + srd_detail_btn = gr.Button("Get Details") + + srd_detail_output = gr.Markdown("") + + def srd_search_handler(category, query): + """Handle SRD search.""" + if category == "monsters": + return search_monsters(query) + elif category == "spells": + return search_spells(query) + elif category == "magicitems": + return search_magicitems(query) + elif category == "conditions": + return search_conditions(query) + elif category == "classes": + return search_classes(query) + return "Select a category" + + def srd_detail_handler(category, slug): + """Handle SRD detail lookup.""" + if not slug: + return "Enter a slug to get details" + slug = slug.lower().strip().replace(" ", "-") + if category == "monsters": + return get_monster_details(slug) + elif category == "spells": + return get_spell_details(slug) + elif category == "magicitems": + return get_magicitem_details(slug) + return "Detail view not available for this category" + + srd_search_btn.click( + fn=srd_search_handler, + inputs=[srd_category, srd_search], + outputs=[srd_results] + ) + + srd_detail_btn.click( + fn=srd_detail_handler, + inputs=[srd_category, srd_detail_slug], + outputs=[srd_detail_output] + ) + + gr.Markdown(""" +--- +*Data provided by [Open5e API](https://open5e.com/) - Open source D&D 5e SRD content.* + """) diff --git a/ui_tabs/llm_playtest_tab.py b/ui_tabs/llm_playtest_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..4a325f3e14b3583acf4fded1ba1a64e9fff0c1c9 --- /dev/null +++ b/ui_tabs/llm_playtest_tab.py @@ -0,0 +1,344 @@ +"""LLM Playtesting Tab - Automated Game Testing with AI. + +This module provides the LLM Playtesting tab for automated game config testing +using local language models and external LLM prompt generation. +""" + +import gradio as gr +import json +from llm_playtester import run_llm_playtest + + +# Playtest perspectives with their biases and what they find +PLAYTEST_PERSPECTIVES = { + "completionist": { + "name": "The Completionist", + "description": "Wants to see everything, explore every path", + "bias": "Methodically explores all options, backtracks frequently", + "finds": "Dead ends, unreachable states, missing content, incomplete branches", + "blind_instruction": "You are a completionist player who MUST try every option. Keep mental notes of paths you haven't explored yet. If you feel like you're missing content, say so. Express frustration at dead ends or when you can't go back to explore other options.", + "full_instruction": "Analyze this config as a completionist. Identify: (1) States that are unreachable from the start, (2) Dead ends with no way to continue or return, (3) Branches that feel underdeveloped compared to others, (4) Content that most players would miss." + }, + "story_lover": { + "name": "The Story Lover", + "description": "Focuses on narrative coherence and emotional beats", + "bias": "Prioritizes story choices over gameplay, notices character inconsistencies", + "finds": "Plot holes, character inconsistencies, tone breaks, narrative dead ends", + "blind_instruction": "You are deeply invested in the story. Comment on character motivations, notice when something doesn't make narrative sense, and express when the tone shifts unexpectedly. If a character acts inconsistently, point it out. Care about WHY things happen, not just what happens.", + "full_instruction": "Analyze this config for narrative quality. Identify: (1) Character inconsistencies across states, (2) Plot holes or logical gaps, (3) Tonal inconsistencies, (4) Missing emotional beats, (5) Choices that don't make narrative sense." + }, + "skeptic": { + "name": "The Skeptic", + "description": "Questions everything, looks for logical problems", + "bias": "Asks 'why would I do that?', notices contrivances", + "finds": "Forced choices, illogical transitions, motivation gaps, plot contrivances", + "blind_instruction": "You are a skeptical player who questions everything. For each choice, ask yourself 'Why would my character do this?' If something feels contrived or forced, say so. Notice when the game railroads you into choices that don't make sense. Be critical but fair.", + "full_instruction": "Analyze this config with a critical eye. Identify: (1) Choices that no reasonable player would make, (2) Forced/railroaded paths, (3) Transitions that don't logically follow, (4) Situations where player motivation is unclear, (5) Contrived scenarios." + }, + "speedrunner": { + "name": "The Speed Runner", + "description": "Looks for the fastest path, notices pacing issues", + "bias": "Skips content, finds shortcuts, notices filler", + "finds": "Shortest paths, pacing problems, unnecessary content, optimal routes", + "blind_instruction": "You want to finish as fast as possible. Skip flavor text, choose the most direct options, and notice when the game forces you through unnecessary steps. Comment on pacing - is it too slow? Are there sections that feel like padding?", + "full_instruction": "Analyze this config for pacing and efficiency. Identify: (1) The shortest path through the game, (2) Content that feels like padding or filler, (3) Sections that could be streamlined, (4) Bottlenecks where all paths converge unnecessarily." + }, + "immersionist": { + "name": "The Immersionist", + "description": "Wants atmosphere and believable world", + "bias": "Notices description quality, mood inconsistencies", + "finds": "Thin descriptions, atmosphere breaks, world-building gaps", + "blind_instruction": "You want to feel immersed in this world. Notice the quality of descriptions - are they vivid enough? Does the atmosphere feel consistent? When something breaks your immersion, say exactly what pulled you out. Comment on sensory details (or lack thereof).", + "full_instruction": "Analyze this config for immersion quality. Identify: (1) States with thin or generic descriptions, (2) Atmosphere/mood inconsistencies between connected states, (3) Missing environmental details, (4) Moments that would break player immersion, (5) World-building gaps." + }, + "first_timer": { + "name": "The First-Timer", + "description": "New to games, easily confused", + "bias": "Needs clear guidance, notices assumed knowledge", + "finds": "Unclear directions, jargon, confusing choices, assumed knowledge", + "blind_instruction": "You've never played a game like this before. If ANYTHING is confusing, say so immediately. If you don't understand a choice, pick randomly and explain your confusion. Notice when the game assumes you know something you weren't told. Get frustrated at unclear directions.", + "full_instruction": "Analyze this config from a newcomer's perspective. Identify: (1) Jargon or terms that aren't explained, (2) Choices where the consequences are unclear, (3) Assumed knowledge about the world/story, (4) Confusing navigation or unclear directions, (5) Missing context for decisions." + }, + "replayer": { + "name": "The Replayer", + "description": "Has played before, looking for variety", + "bias": "Notices repetition, seeks new content", + "finds": "Repetitive content, lack of meaningful branches, same outcomes", + "blind_instruction": "You've played this game before (pretend you have). Look for new content you might have missed. Notice when different choices lead to the same outcome. Express disappointment at repetitive content. Comment on whether choices feel meaningful or cosmetic.", + "full_instruction": "Analyze this config for replayability. Identify: (1) Choices that lead to the same outcome (false choices), (2) Repetitive descriptions or situations, (3) Lack of meaningful branching, (4) Content that's identical regardless of path, (5) Missing alternate routes." + }, + "edge_finder": { + "name": "The Edge-Case Finder", + "description": "Tries to break the game", + "bias": "Makes unusual choices, looks for contradictions", + "finds": "Loops, contradictions, impossible states, broken transitions", + "blind_instruction": "You want to break this game. Make the weirdest choices possible. Try to find loops, contradictions, or situations that don't make sense. If you can get the game into a broken state, that's a win. Notice when the game doesn't account for unusual player behavior.", + "full_instruction": "Analyze this config for edge cases and bugs. Identify: (1) Infinite loops or cycles, (2) States that contradict each other, (3) Transitions to non-existent states, (4) Flags or inventory items that create impossible situations, (5) Softlocks where progress becomes impossible." + } +} + + +def generate_playtest_prompt(config_json: str, perspective: str, is_blind: bool, num_steps: int = 10) -> str: + """Generate a playtest prompt for external LLM.""" + if not config_json or not config_json.strip(): + return "Error: Please paste a config JSON first." + + try: + config = json.loads(config_json) + except json.JSONDecodeError as e: + return f"Error: Invalid JSON - {str(e)}" + + persp = PLAYTEST_PERSPECTIVES.get(perspective, PLAYTEST_PERSPECTIVES["completionist"]) + + # Count states for context + total_states = sum(len(loc) for loc in config.values() if isinstance(loc, dict)) + + # Get starting point + first_location = next(iter(config.keys())) + first_state = next(iter(config[first_location].keys())) + start_desc = config[first_location][first_state].get("description", "No description") + start_choices = config[first_location][first_state].get("choices", []) + + if is_blind: + # Blind playtest - LLM only sees current state, plays step by step + choices_formatted = "\n".join([f" {i+1}. {c}" for i, c in enumerate(start_choices)]) + + prompt = f"""# Blind Playtest Request: {persp['name']} + +You are playtesting a text adventure game AS IF you were a real player. You cannot see the whole game - only what's presented to you. + +**Your Player Persona:** {persp['name']} +- {persp['description']} +- Bias: {persp['bias']} +- You typically find: {persp['finds']} + +**How to Play:** +{persp['blind_instruction']} + +--- + +## Starting Scene + +**Location:** {first_location} +**Description:** {start_desc} + +**Your choices:** +{choices_formatted} + +--- + +## Instructions + +Play through this game for approximately {num_steps} steps. For each step: + +1. **React in character** - What does {persp['name']} think/feel about this scene? +2. **Choose** - Pick a choice and explain WHY (in character) +3. **Note issues** - Mention any problems you notice from your perspective + +After I give you the result of your choice, continue playing. At the end, provide a summary of issues found from the {persp['name']} perspective. + +**Begin your playthrough now. What do you do?**""" + + else: + # Full config visible - structural analysis + prompt = f"""# Full Config Playtest Analysis: {persp['name']} + +You have access to the COMPLETE game config. Analyze it thoroughly from the perspective of {persp['name']}. + +**Analysis Persona:** {persp['name']} +- {persp['description']} +- Focus: {persp['finds']} + +**Your Task:** +{persp['full_instruction']} + +--- + +## Config Statistics +- Total states: {total_states} +- Starting point: {first_location}/{first_state} + +## Complete Config JSON + +```json +{config_json} +``` + +--- + +## Required Analysis + +Provide a detailed report with: + +1. **Critical Issues** - Problems that break the game or severely impact experience +2. **Major Concerns** - Significant issues from the {persp['name']} perspective +3. **Minor Notes** - Small improvements that would help +4. **Positive Observations** - What works well + +For each issue, cite the specific state(s) involved. + +**Begin your analysis:**""" + + return prompt + + +def create_llm_playtest_tab(modelnames): + """Create the LLM Playtesting tab. + + Args: + modelnames: List of available LLM model names + """ + with gr.Tab("LLM Playtesting"): + gr.Markdown("## Game Playtesting with AI") + gr.Markdown("Test your game config using local AI or generate prompts for external LLMs (ChatGPT/Claude).") + + # ==================== EXTERNAL LLM PLAYTEST PROMPTS ==================== + with gr.Accordion("External LLM Playtest (ChatGPT/Claude)", open=True): + gr.Markdown("### Perspective-Based Playtest Prompts") + gr.Markdown("Generate prompts that ask an external LLM to playtest your game from different player perspectives. Each perspective finds different issues.") + + with gr.Row(): + ext_config_input = gr.Textbox( + label="Config JSON", + lines=6, + placeholder="Paste your game config here...", + scale=2 + ) + with gr.Column(scale=1): + perspective_dropdown = gr.Dropdown( + choices=[ + ("The Completionist - finds dead ends, missing content", "completionist"), + ("The Story Lover - finds plot holes, inconsistencies", "story_lover"), + ("The Skeptic - finds illogical choices, contrivances", "skeptic"), + ("The Speed Runner - finds pacing issues, filler", "speedrunner"), + ("The Immersionist - finds atmosphere breaks, thin descriptions", "immersionist"), + ("The First-Timer - finds confusing parts, unclear directions", "first_timer"), + ("The Replayer - finds repetition, false choices", "replayer"), + ("The Edge-Case Finder - finds loops, contradictions, bugs", "edge_finder"), + ], + value="completionist", + label="Playtest Perspective" + ) + + visibility_toggle = gr.Radio( + choices=[ + ("Blind (like real player)", "blind"), + ("Full Config (structural analysis)", "full") + ], + value="blind", + label="Config Visibility" + ) + + playtest_steps = gr.Slider( + minimum=5, maximum=30, value=10, step=1, + label="Steps (blind mode only)" + ) + + gr.Markdown(""" + **Visibility modes:** + - **Blind**: LLM plays step-by-step like a real player who hasn't seen the game before + - **Full Config**: LLM sees entire structure, can analyze dead ends and unreachable content + """) + + generate_playtest_btn = gr.Button("Generate Playtest Prompt", variant="primary") + + playtest_prompt_output = gr.Code( + label="Copy this prompt to ChatGPT/Claude", + language=None, + lines=20 + ) + + # Perspective info display + with gr.Accordion("Perspective Details", open=False): + perspective_info = gr.Markdown(""" + **Select a perspective above to see details.** + + Each perspective has different biases and finds different issues: + - Completionist: Dead ends, unreachable content + - Story Lover: Plot holes, character issues + - Skeptic: Logic problems, forced choices + - Speed Runner: Pacing, filler content + - Immersionist: Atmosphere, description quality + - First-Timer: Confusion, unclear directions + - Replayer: Repetition, false choices + - Edge-Case Finder: Bugs, contradictions + """) + + def update_perspective_info(perspective): + persp = PLAYTEST_PERSPECTIVES.get(perspective, {}) + return f"""**{persp.get('name', 'Unknown')}** + +*{persp.get('description', '')}* + +**Bias:** {persp.get('bias', '')} + +**Typically finds:** {persp.get('finds', '')} + +**Blind mode instruction:** {persp.get('blind_instruction', '')[:200]}... +""" + + perspective_dropdown.change( + fn=update_perspective_info, + inputs=[perspective_dropdown], + outputs=[perspective_info] + ) + + def generate_prompt_wrapper(config, perspective, visibility, steps): + is_blind = visibility == "blind" + return generate_playtest_prompt(config, perspective, is_blind, steps) + + generate_playtest_btn.click( + fn=generate_prompt_wrapper, + inputs=[ext_config_input, perspective_dropdown, visibility_toggle, playtest_steps], + outputs=[playtest_prompt_output] + ) + + # ==================== LOCAL LLM PLAYTEST (ZeroGPU) ==================== + with gr.Accordion("Local LLM Playtest (ZeroGPU)", open=False): + gr.Markdown("### Automated Playthrough with Local AI") + gr.Markdown("Use a local LLM to automatically play through your game from different perspectives. *Note: Limited by ZeroGPU availability.*") + + with gr.Row(): + # Use same models as Media Studio + default_playtest_model = "unsloth/Llama-3.2-1B-Instruct" if "unsloth/Llama-3.2-1B-Instruct" in modelnames else modelnames[0] + llm_model_selector = gr.Dropdown( + choices=modelnames, + value=default_playtest_model, + label="Text Model", + scale=2 + ) + local_perspective_dropdown = gr.Dropdown( + choices=[ + ("Default Playtester", "default"), + ("The Completionist - explore everything", "completionist"), + ("The Story Lover - narrative focus", "story_lover"), + ("The Skeptic - questions logic", "skeptic"), + ("The Speed Runner - efficiency focus", "speedrunner"), + ("The Immersionist - atmosphere focus", "immersionist"), + ("The First-Timer - easily confused", "first_timer"), + ("The Replayer - seeks variety", "replayer"), + ("The Edge-Case Finder - tries to break it", "edge_finder"), + ], + value="default", + label="Playtest Perspective", + scale=2 + ) + max_steps_slider = gr.Slider(minimum=5, maximum=50, value=20, step=1, label="Max Steps", scale=1) + + llm_config_input = gr.Textbox(label="Config JSON (paste your config here)", lines=5) + llm_run_btn = gr.Button("Run LLM Playtest", variant="primary") + + with gr.Row(): + with gr.Column(): + llm_playthrough_log = gr.Textbox(label="Playthrough Log", lines=15, interactive=False) + with gr.Column(): + llm_findings = gr.Textbox(label="Issues & Findings", lines=15, interactive=False) + + # Wire up the playtest button with perspective + llm_run_btn.click( + fn=run_llm_playtest, + inputs=[llm_config_input, llm_model_selector, max_steps_slider, local_perspective_dropdown], + outputs=[llm_playthrough_log, llm_findings] + ) + + gr.Markdown("*Model auto-loads on first run. Each perspective looks for different issues.*") diff --git a/ui_tabs/mechanic_translation_tab.py b/ui_tabs/mechanic_translation_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..e8ae09bc9a245fb8a046e48e9f5e76ae662a5276 --- /dev/null +++ b/ui_tabs/mechanic_translation_tab.py @@ -0,0 +1,328 @@ +"""Mechanic Translation Tab - 3D/2D/1D Conversion. + +This module provides the Mechanic Translation tab for translating game mechanics +across dimensions (3D ↔ 2D ↔ 1D). +""" + +import json +import gradio as gr + + +# Translation data structures - supports both downgrade (3D→2D→1D) and upgrade (1D→2D→3D) +MECHANIC_TRANSLATIONS = { + "Combat": { + "3d": "Free movement, hitboxes, combos, dodging in 3D space", + "2d": "Side-view or top-down sprites, attack patterns, i-frames", + "1d": "Choice + stat checks, risk/reward options, health tracking", + "variables": ["health", "stamina", "damage", "defense"], + "input_notes": { + "Timing": "Add timed choice windows or quick-time events", + "Precision": "Add accuracy modifiers based on choices", + "Strategy": "Add positioning/stance choices before attacks", + "Memory": "Track combo states, remember enemy patterns" + }, + "upgrade_1d_to_2d": "Add sprite animations, visual hit feedback, enemy sprites with attack tells, health bars", + "upgrade_2d_to_3d": "Add Z-axis movement, 3D hitboxes, camera lock-on, spatial dodge/roll mechanics" + }, + "Exploration": { + "3d": "Open world, verticality, physics-based traversal", + "2d": "Tile maps, screen transitions, platforming sections", + "1d": "Node graph, location descriptions, area reveals", + "variables": ["discovered_areas", "current_location", "items_found"], + "input_notes": { + "Memory": "Track visited locations, remember landmarks", + "Strategy": "Choose paths, manage resources for journey", + "Precision": "N/A - abstracted in 1D", + "Timing": "Time-limited exploration events" + }, + "upgrade_1d_to_2d": "Add tile-based map, player sprite, fog of war, visible collectibles, screen scrolling", + "upgrade_2d_to_3d": "Add verticality (climbing, flying), first/third person view, physics traversal, seamless world" + }, + "Puzzle": { + "3d": "Physics objects, 3D spatial manipulation, perspective tricks", + "2d": "Tile sliding, pattern matching, block pushing", + "1d": "Logic choices, state tracking, item combinations", + "variables": ["puzzle_state", "inventory", "switches_activated"], + "input_notes": { + "Memory": "Remember clues, recall earlier information", + "Strategy": "Plan sequence of actions", + "Precision": "N/A - abstracted in 1D", + "Timing": "Timed puzzles add pressure" + }, + "upgrade_1d_to_2d": "Add visual puzzle elements, draggable pieces, grid-based manipulation, animated feedback", + "upgrade_2d_to_3d": "Add physics simulation, perspective-based puzzles, 3D object rotation, spatial reasoning" + }, + "Stealth": { + "3d": "Cone vision, sound radius, cover system, verticality", + "2d": "Line-of-sight tiles, shadow zones, patrol patterns", + "1d": "Awareness meter, timed choices, detection risk", + "variables": ["detection_level", "noise_made", "disguise_status"], + "input_notes": { + "Timing": "Windows of opportunity to move", + "Strategy": "Route planning, distraction choices", + "Memory": "Patrol pattern memorization", + "Precision": "N/A - abstracted to pass/fail" + }, + "upgrade_1d_to_2d": "Add visible guard cones, shadow sprites, noise indicators, patrol path visualization", + "upgrade_2d_to_3d": "Add 3D cover system, sound propagation, verticality (vents, rooftops), first-person stealth" + }, + "Economy": { + "3d": "3D shops, physical trading posts, auction houses", + "2d": "Menu-based shops, shop UIs, inventory grids", + "1d": "Resource numbers, buy/sell choices, trade text", + "variables": ["gold", "inventory_value", "reputation_discount"], + "input_notes": { + "Strategy": "Buy low/sell high decisions", + "Memory": "Remember prices, track markets", + "Timing": "Limited-time deals", + "Precision": "N/A" + }, + "upgrade_1d_to_2d": "Add visual inventory grid, item sprites, shop UI with merchant portraits, drag-drop trading", + "upgrade_2d_to_3d": "Add 3D shop environments, NPC merchants, physical item display, immersive haggling" + }, + "Racing": { + "3d": "Vehicle physics, 3D tracks, drifting, collisions", + "2d": "Top-down or side-scrolling, lane-based, obstacle avoidance", + "1d": "Speed state, risk/reward choices, position tracking", + "variables": ["speed", "position", "fuel", "damage"], + "input_notes": { + "Timing": "React to obstacles, shift timing", + "Strategy": "When to boost, pit stop decisions", + "Precision": "Abstracted to choice outcomes", + "Memory": "Track layout memorization" + }, + "upgrade_1d_to_2d": "Add track visualization, car sprites, obstacle sprites, speed lines, position indicators", + "upgrade_2d_to_3d": "Add 3D vehicle physics, cockpit/chase camera, realistic tracks, drifting mechanics" + }, + "Crafting": { + "3d": "3D workbenches, item manipulation, resource nodes", + "2d": "Grid inventory, recipe UI, crafting menus", + "1d": "Item list + combination choices, recipe discovery", + "variables": ["materials", "known_recipes", "tool_quality"], + "input_notes": { + "Memory": "Remember recipes, material locations", + "Strategy": "Resource allocation, crafting order", + "Timing": "Time-based crafting, freshness", + "Precision": "N/A" + }, + "upgrade_1d_to_2d": "Add visual recipe book, ingredient sprites, crafting animation, result preview", + "upgrade_2d_to_3d": "Add 3D workbench interaction, physical item placement, minigame crafting, visible results" + }, + "Social": { + "3d": "Motion capture dialogue, NPC schedules, relationship scenes", + "2d": "Portrait + text, emote sprites, affection meters", + "1d": "Dialogue choices, disposition tracking, relationship text", + "variables": ["npc_affection", "reputation", "dialogue_flags"], + "input_notes": { + "Strategy": "Gift giving, dialogue choices", + "Memory": "Remember NPC preferences, past conversations", + "Timing": "Time-limited events, schedules", + "Precision": "N/A" + }, + "upgrade_1d_to_2d": "Add character portraits, expression sprites, visual affection meters, gift animations", + "upgrade_2d_to_3d": "Add motion capture expressions, full body language, dynamic scenes, voice acting" + } +} + +PRESET_EXAMPLES = { + "Tekken Combo": {"source": "3D", "type": "Combat", "inputs": ["Timing", "Memory"], "desc": "Frame-perfect combo inputs", "loop": "Input → Attack → Chain → Finish"}, + "GTA Shooting": {"source": "3D", "type": "Combat", "inputs": ["Precision", "Reaction"], "desc": "Third-person cover shooter", "loop": "Aim → Shoot → Take Cover → Reload"}, + "Zelda Puzzle": {"source": "3D", "type": "Puzzle", "inputs": ["Memory", "Strategy"], "desc": "Physics-based dungeon puzzles", "loop": "Observe → Experiment → Solve → Progress"}, + "Pokemon Battle": {"source": "2D", "type": "Combat", "inputs": ["Strategy", "Memory"], "desc": "Turn-based type matchups", "loop": "Choose Move → Execute → React → Repeat"}, + "Dark Souls": {"source": "3D", "type": "Combat", "inputs": ["Timing", "Strategy"], "desc": "Stamina-based action combat", "loop": "Approach → Attack/Dodge → Manage Stamina → Punish"}, + "Hollow Knight": {"source": "2D", "type": "Exploration", "inputs": ["Precision", "Memory"], "desc": "Metroidvania exploration", "loop": "Explore → Find Ability → Unlock Area → Repeat"}, + "Stardew Valley": {"source": "2D", "type": "Economy", "inputs": ["Strategy", "Planning"], "desc": "Farm economy simulation", "loop": "Plant → Tend → Harvest → Sell → Upgrade"}, + "Ace Attorney": {"source": "2D", "type": "Social", "inputs": ["Memory", "Strategy"], "desc": "Investigation and courtroom drama", "loop": "Gather Evidence → Present → React → Conclude"} +} + + +def translate_mechanic(source_dim, mechanic_type, description, core_loop, input_types, feedback_types, stakes): + """Translate a mechanic between dimensions.""" + if mechanic_type not in MECHANIC_TRANSLATIONS: + return "Select a mechanic type", "Select a mechanic type", "Select a mechanic type", "{}" + + data = MECHANIC_TRANSLATIONS[mechanic_type] + dim_order = ["1D", "2D", "3D"] + source_idx = dim_order.index(source_dim) + + # Build DOWNGRADE translations (→ lower dimensions) + downgrade_text = f"## ⬇️ Simplify ({source_dim} → Lower)\n\n" + if source_dim == "3D": + downgrade_text += f"**→ 2D:** {data['2d']}\n" + downgrade_text += "- Flatten verticality to layers\n" + downgrade_text += "- Replace physics with sprite collision\n\n" + downgrade_text += f"**→ 1D:** {data['1d']}\n" + downgrade_text += f"- Variables: {', '.join(data['variables'])}\n" + elif source_dim == "2D": + downgrade_text += f"**→ 1D:** {data['1d']}\n" + downgrade_text += f"- Variables: {', '.join(data['variables'])}\n" + for inp in input_types: + if inp in data['input_notes']: + downgrade_text += f"- {inp}: {data['input_notes'][inp]}\n" + else: + downgrade_text += "*Already at 1D - cannot simplify further*\n" + + # Build UPGRADE translations (→ higher dimensions) + upgrade_text = f"## ⬆️ Expand ({source_dim} → Higher)\n\n" + if source_dim == "1D": + upgrade_text += f"**→ 2D:** {data['upgrade_1d_to_2d']}\n\n" + upgrade_text += f"**→ 3D:** {data['upgrade_2d_to_3d']}\n" + elif source_dim == "2D": + upgrade_text += f"**→ 3D:** {data['upgrade_2d_to_3d']}\n" + else: + upgrade_text += "*Already at 3D - maximum dimension*\n" + + # Build current dimension description + current_text = f"## 📍 Current: {source_dim}\n\n" + current_text += f"**Implementation:** {data[source_dim.lower()]}\n\n" + current_text += f"**Core Loop:** {core_loop if core_loop else 'Not specified'}\n\n" + current_text += f"**Stakes ({stakes}):**\n" + stakes_map = { + "Life/Death": "Game over on bad choices, checkpoint system", + "Resources": "Lose items/gold, can recover", + "Progress": "Sent back, lose time but not items", + "Time": "Miss opportunities, story continues", + "Reputation": "NPCs react differently, doors close" + } + current_text += f"- {stakes_map.get(stakes, 'Define consequences')}\n" + + # Generate sample config (for 1D) + sample_config = { + mechanic_type.lower(): { + "start": { + "description": description[:100] if description else f"Begin {mechanic_type.lower()} encounter", + "choices": ["approach_carefully", "rush_in", "retreat"], + "transitions": { + "approach_carefully": f"{mechanic_type.lower()}_careful", + "rush_in": f"{mechanic_type.lower()}_aggressive", + "retreat": "previous_area" + }, + "variables_affected": data['variables'][:2] + } + } + } + + return current_text, downgrade_text, upgrade_text, json.dumps(sample_config, indent=2) + + +def load_preset(preset_name): + """Load a preset example configuration.""" + if preset_name not in PRESET_EXAMPLES: + return "3D", "Combat", "", "", [], [], "Life/Death" + p = PRESET_EXAMPLES[preset_name] + return p["source"], p["type"], p["desc"], p["loop"], p["inputs"], ["Visual", "Audio"], "Life/Death" + + +def create_mechanic_translation_tab(): + """Create the Mechanic Translation tab.""" + with gr.Tab("Mechanic Translation"): + gr.Markdown("## Mechanic Translator: 3D ↔ 2D ↔ 1D") + gr.Markdown("Translate game mechanics **bidirectionally** across dimensions. Simplify 3D→2D→1D or expand 1D→2D→3D. Understand what each dimension adds or removes.") + + # UI Layout + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("### Step 1: Source Mechanic") + mt_preset = gr.Dropdown( + label="Quick Load Preset", + choices=["Custom"] + list(PRESET_EXAMPLES.keys()), + value="Custom" + ) + mt_source_dim = gr.Radio( + label="Source Dimension", + choices=["3D", "2D", "1D"], + value="3D" + ) + mt_mechanic_type = gr.Dropdown( + label="Mechanic Type", + choices=list(MECHANIC_TRANSLATIONS.keys()), + value="Combat" + ) + mt_description = gr.Textbox( + label="Description", + placeholder="Describe the mechanic (e.g., 'Player aims with mouse and shoots enemies')", + lines=2 + ) + mt_core_loop = gr.Textbox( + label="Core Loop", + placeholder="Action cycle (e.g., 'Aim → Shoot → Reload → Repeat')", + lines=1 + ) + + with gr.Column(scale=1): + gr.Markdown("### Step 2: Components") + mt_inputs = gr.CheckboxGroup( + label="Player Input Types", + choices=["Timing", "Precision", "Memory", "Strategy", "Reaction", "Planning"], + value=["Timing", "Strategy"] + ) + mt_feedback = gr.CheckboxGroup( + label="Feedback Types", + choices=["Visual", "Audio", "Score", "Progression", "Narrative"], + value=["Visual", "Audio"] + ) + mt_stakes = gr.Dropdown( + label="Stakes", + choices=["Life/Death", "Resources", "Progress", "Time", "Reputation"], + value="Life/Death" + ) + mt_generate_btn = gr.Button("Generate Translations", variant="primary") + + gr.Markdown("---") + gr.Markdown("### Translation Results (Bidirectional)") + + with gr.Row(): + with gr.Column(): + mt_result_current = gr.Markdown(value="*Click Generate to analyze*") + with gr.Column(): + mt_result_downgrade = gr.Markdown(value="*⬇️ Simplify options*") + with gr.Column(): + mt_result_upgrade = gr.Markdown(value="*⬆️ Expand options*") + + with gr.Accordion("Generated 1D Config Snippet", open=False): + mt_config_output = gr.Code(label="Sample Config JSON", language="json") + + # Event handlers + mt_preset.change( + fn=load_preset, + inputs=[mt_preset], + outputs=[mt_source_dim, mt_mechanic_type, mt_description, mt_core_loop, mt_inputs, mt_feedback, mt_stakes] + ) + + mt_generate_btn.click( + fn=translate_mechanic, + inputs=[mt_source_dim, mt_mechanic_type, mt_description, mt_core_loop, mt_inputs, mt_feedback, mt_stakes], + outputs=[mt_result_current, mt_result_downgrade, mt_result_upgrade, mt_config_output] + ) + + # Reference tables + with gr.Accordion("Mechanic Translation Reference Table", open=False): + gr.Markdown(""" +| Mechanic | 3D Implementation | 2D Implementation | 1D Implementation | +|----------|-------------------|-------------------|-------------------| +| Combat | Free movement, hitboxes, combos | Side-view/top-down sprites, patterns | Choice + stat checks, risk/reward | +| Exploration | Open world, verticality, physics | Tile maps, screen transitions | Node graph, location descriptions | +| Puzzle | Physics objects, 3D manipulation | Tile sliding, pattern matching | Logic choices, state tracking | +| Stealth | Cone vision, sound radius, cover | Line-of-sight tiles, shadows | Awareness meter, timed choices | +| Economy | 3D shops, trading posts | Menu screens, shop UIs | Resource numbers, buy/sell choices | +| Racing | Vehicle physics, 3D tracks | Top-down/side scroll, lanes | Speed state, risk/reward choices | +| Crafting | 3D workbenches, item models | Grid inventory, recipe UI | Item list + combinations | +| Social | Motion capture, expressions | Portrait + text, emotes | Dialogue choices, disposition | + +**Dimension Characteristics:** +| Dimension | Spatial | Timing | Precision | Typical Input | +|-----------|---------|--------|-----------|---------------| +| 3D | Full XYZ | Real-time | High | Controller/Mouse | +| 2D | XY plane | Real-time/Turn | Medium | D-pad/Keys | +| 1D | States | Turn-based | Low | Clicks/Taps | + """) + + with gr.Accordion("Original Notes", open=False): + gr.HTML("Interactive Fun (one form) = Avoiding something you dont want to happen in as dramatic fashion as possible = in '1D' format that can mean avoiding deadends and if you stack stories on stories the deadends can pile up") + gr.Markdown("1D games fun = ??? (Havent figured it out yet) same as a navigation game? Reaching the end is the current end aka the game is essentially a number password and wrong input means a deadend where the full password = full story - chained stories with deadends = same missing back story in a long story = a punishment to some and a reward to others") + gr.HTML("Ideas for options based on existing 2D / 3D games") + gr.HTML("Most game mechanics = button press + animation == in 1D animations = text description / picture") + gr.HTML("Tekken - Kick and Punch in extreme detail + complex input buttons") + gr.HTML("Gta - Driving = Move the 3d world or the camera angle, Shooting = Aim (camera) + shoot (animation of player and npcs), Meeting = marker + video, Shopping = menu") + gr.HTML("Dave the diver - Swimming and Flying are the same with contextual actions - in 2D so 1D = Shortcuts in options") + gr.HTML("Story progression mechanics - Player must scope / prepare a location, Player must find someone / deliver a message from any npc / do something to a npc, Player must source a item for the story characters, player must be present to guide the character to do something") diff --git a/ui_tabs/narrative_engine_tab.py b/ui_tabs/narrative_engine_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..1e4a1341a5f9df36304d5393c1a3e997f16e155e --- /dev/null +++ b/ui_tabs/narrative_engine_tab.py @@ -0,0 +1,446 @@ +""" +Narrative Engine Tab - Layered storytelling with mystery iceberg. + +Uses the Narrative Engine for generating narratives with: +- 5-layer mystery iceberg (surface → abyss) +- Character generation with psychological depth +- Demo stories across genres +- Custom narrative creation with mystery layers +""" + +import gradio as gr +import json +from typing import Dict, Any, List + +# Import narrative engine components +from narrativeengine_hfport import ( + NarrativeEngine, NarrativeMode, NodeType, DepthLevel, + get_demo, list_demos, DEMO_STORIES, + CharacterGenerator, GeneratedCharacter +) + + +def format_story_setup(engine: NarrativeEngine) -> str: + """Format story setup as markdown.""" + if not engine.story_setup: + return "No story setup available." + + setup = engine.story_setup + return f"""## {setup.title} + +### Setting +**{setup.place['name']}**: {setup.place['description']} + +**Key Locations**: {', '.join(setup.place.get('key_locations', []))} + +### Time +{setup.time['period']} - {setup.time['specific_moment']} + +*{setup.time['tension']}* + +### Protagonist +**{setup.protagonist['name']}**: {setup.protagonist['description']} + +- **Motivation**: {setup.protagonist['motivation']} +- **Flaw**: {setup.protagonist['flaw']} + +### Hook +> {setup.hook} + +### Stakes +{setup.stakes} +""" + + +def format_characters(engine: NarrativeEngine) -> str: + """Format characters as markdown.""" + if not engine.characters: + return "No characters registered." + + lines = ["## Characters\n"] + for char_id, char in engine.characters.items(): + lines.append(f"### {char.name}") + lines.append(f"**Role**: {char.role}") + lines.append(f"**Background**: {char.background}") + if char.wants: + lines.append("\n**Wants**:") + for want_id, want in char.wants.items(): + lines.append(f"- {want['description']}") + lines.append("") + + return "\n".join(lines) + + +def format_clues(engine: NarrativeEngine) -> str: + """Format story clues as markdown.""" + if not engine.story_clues: + return "No clues planted." + + lines = ["## Story Clues\n"] + for clue in sorted(engine.story_clues, key=lambda c: c.planted_at): + visibility = "Obvious" if clue.noticeability > 0.6 else "Subtle" if clue.noticeability > 0.3 else "Hidden" + lines.append(f"- **[{clue.clue_type.upper()}]** ({visibility}) {clue.content}") + + return "\n".join(lines) + + +def format_iceberg(iceberg: Dict[str, Any]) -> str: + """Format iceberg layers as markdown.""" + if not iceberg: + return "No iceberg data available." + + lines = ["## Narrative Iceberg\n"] + emoji_map = { + "surface": "Surface", + "shallow": "Shallow", + "mid": "Mid-depth", + "deep": "Deep", + "abyss": "Abyss" + } + + for level, data in iceberg.items(): + label = emoji_map.get(level, level.title()) + lines.append(f"### {label}: {data['name']}") + for item in data['content'][:5]: + lines.append(f"- {item}") + if len(data['content']) > 5: + lines.append(f"- *... and {len(data['content']) - 5} more*") + lines.append("") + + return "\n".join(lines) + + +def format_generated_character(char: GeneratedCharacter) -> Dict[str, Any]: + """Format generated character as dict.""" + return { + "id": char.id, + "name": char.name, + "role": char.role, + "background": char.background, + "skill": char.skill, + "trait": char.trait, + "equipment": char.equipment, + "context": char.context, + "psychological_profile": char.psychological_profile, + "personality_traits": char.personality_traits + } + + +def create_narrative_engine_tab(): + """Create the Narrative Engine tab.""" + + with gr.Tab("Narrative Engine"): + gr.Markdown("""## Narrative Engine - Layered Mystery & Character Systems + +A comprehensive narrative generation system with 7 storytelling paradigms: +- **Struggle Propagation** - Character wants with butterfly effects +- **Mystery Iceberg** - 5-layer depth (Surface to Abyss) +- **Reveal Ripple Observer** - Information timing control +- **Emotional Mechanics** - Setup, Trigger, Sustain, Kill patterns + """) + + with gr.Tabs(): + # === TAB 1: DEMO STORIES === + with gr.TabItem("Demo Stories"): + gr.Markdown("### Pre-built Narrative Demonstrations") + gr.Markdown("Explore 6 complete story structures with mystery layers, character dynamics, and planted clues.") + + with gr.Row(): + demo_dropdown = gr.Dropdown( + choices=[ + ("The Hartfield Affair (Political Thriller)", "hartfield"), + ("The Vanishing Code (Sci-Fi)", "scifi"), + ("The Last Performance (Romance/Drama)", "romance"), + ("The Gilded Cage (Heist)", "heist"), + ("The Hollow Season (Folk Horror)", "horror"), + ("The Redemption (Classic Arc)", "redemption") + ], + label="Select Demo Story", + value="hartfield" + ) + demo_btn = gr.Button("Generate Story", variant="primary") + + with gr.Row(): + with gr.Column(): + demo_narrative = gr.Markdown(label="Story Narrative") + with gr.Column(): + demo_iceberg = gr.Markdown(label="Narrative Iceberg") + + demo_json = gr.JSON(label="Full Story Data") + + def run_demo_story(story_id: str) -> tuple: + """Run a demo story.""" + try: + engine = get_demo(story_id) + + # Execute sample actions + action_map = { + "hartfield": [("detective", "acquire", "letter"), ("mayor", "pressure", "testimony")], + "scifi": [("engineer", "reveal", "manifest"), ("ai", "compromise", "escape_pods")], + "romance": [("isabella", "reveal", "letters"), ("lucia", "acquire", "role")], + "heist": [("saint", "acquire", "security"), ("viktor", "reveal", "painting")], + "horror": [("rowan", "reveal", "ritual"), ("agnes", "pressure", "mark")], + "redemption": [("hero", "reveal", "soul"), ("emperor", "pressure", "darkside")] + } + + for char, action, target in action_map.get(story_id, []): + engine.execute_action(char, action, target) + + iceberg = engine.generate_full_iceberg() + + setup_md = format_story_setup(engine) + chars_md = format_characters(engine) + clues_md = format_clues(engine) + iceberg_md = format_iceberg(iceberg) + + narrative_md = f"{setup_md}\n\n---\n\n{chars_md}\n\n---\n\n{clues_md}" + full_data = engine.to_dict() + + return narrative_md, iceberg_md, full_data + + except Exception as e: + error_msg = f"Error: {str(e)}" + return error_msg, error_msg, {"error": str(e)} + + demo_btn.click( + fn=run_demo_story, + inputs=[demo_dropdown], + outputs=[demo_narrative, demo_iceberg, demo_json] + ) + + # === TAB 2: CHARACTER GENERATOR === + with gr.TabItem("Character Generator"): + gr.Markdown("### Generate Characters with Psychological Depth") + + with gr.Row(): + char_context = gr.Dropdown( + choices=[ + ("Detective/Mystery", "detective"), + ("Action/Adventure", "action"), + ("Horror", "horror"), + ("Fantasy", "fantasy"), + ("Sci-Fi", "sci_fi"), + ("Romance", "romance"), + ("Spy/Espionage", "spy"), + ("Crime/Heist", "crime") + ], + label="Character Context", + value="detective" + ) + char_count = gr.Slider( + minimum=1, maximum=10, value=3, step=1, + label="Number of Characters" + ) + char_btn = gr.Button("Generate Characters", variant="primary") + + char_output = gr.JSON(label="Generated Characters") + + def generate_characters(context: str, count: int) -> Dict[str, Any]: + """Generate characters.""" + try: + generator = CharacterGenerator(context) + characters = generator.generate_multiple(int(count)) + return { + "context": context, + "count": len(characters), + "characters": [format_generated_character(c) for c in characters] + } + except Exception as e: + return {"error": str(e)} + + char_btn.click( + fn=generate_characters, + inputs=[char_context, char_count], + outputs=[char_output] + ) + + # === TAB 3: CUSTOM NARRATIVE === + with gr.TabItem("Custom Narrative"): + gr.Markdown("### Create Your Own Narrative Structure") + + with gr.Row(): + with gr.Column(): + custom_title = gr.Textbox(label="Story Title", value="Untitled Story") + custom_setting = gr.Textbox(label="Setting", value="A city on the edge of change") + custom_mode = gr.Dropdown( + choices=[ + "Struggle (Plot-driven)", + "Compressed (Emotional density)", + "Meditative (Presence-based)", + "Hybrid (Combined)" + ], + label="Narrative Mode", + value="Struggle (Plot-driven)" + ) + + with gr.Column(): + gr.Markdown("**Protagonist**") + protag_name = gr.Textbox(label="Name", value="Alex") + protag_want = gr.Textbox(label="What they want", value="To find the truth") + protag_struggle = gr.Textbox(label="Their struggle", value="Haunted by the past") + + with gr.Column(): + gr.Markdown("**Antagonist**") + antag_name = gr.Textbox(label="Name", value="Morgan") + antag_want = gr.Textbox(label="What they want", value="To keep secrets buried") + + custom_btn = gr.Button("Generate Narrative", variant="primary") + + with gr.Row(): + custom_narrative = gr.Markdown(label="Generated Narrative") + custom_iceberg = gr.Markdown(label="Iceberg Layers") + + custom_json = gr.JSON(label="Full Data") + + def create_custom_narrative(title, protag_name_val, protag_want_val, protag_struggle_val, + antag_name_val, antag_want_val, setting, mode) -> tuple: + """Create custom narrative.""" + try: + engine = NarrativeEngine() + + mode_map = { + "Struggle (Plot-driven)": NarrativeMode.STRUGGLE, + "Compressed (Emotional density)": NarrativeMode.COMPRESSED, + "Meditative (Presence-based)": NarrativeMode.MEDITATIVE, + "Hybrid (Combined)": NarrativeMode.HYBRID + } + engine.set_mode(mode_map.get(mode, NarrativeMode.STRUGGLE)) + + engine.register_character("protagonist", protag_name_val, "protagonist") + engine.register_character("antagonist", antag_name_val, "antagonist") + + engine.add_want("protagonist", "main_goal", protag_want_val) + engine.add_want("antagonist", "main_goal", antag_want_val) + + engine.create_node("objective", NodeType.RESOURCE, "The Objective", + description=f"What both {protag_name_val} and {antag_name_val} seek") + engine.create_node("setting", NodeType.LOCATION, setting) + engine.create_node("deadline", NodeType.TIME_WINDOW, "The Deadline", + metadata={"remaining_time": 24, "unit": "hours"}) + + engine.connect_nodes("objective", "setting") + engine.connect_nodes("deadline", "objective") + + engine.execute_action("protagonist", "reveal", "objective", + narrative_context=protag_struggle_val) + engine.execute_action("antagonist", "pressure", "objective", + narrative_context="opposing the protagonist") + + iceberg = engine.generate_full_iceberg() + + narrative_lines = [f"# {title}\n"] + narrative_lines.append(f"**Setting**: {setting}\n") + narrative_lines.append(f"**Mode**: {mode}\n") + narrative_lines.append("\n## Characters\n") + narrative_lines.append(f"**{protag_name_val}** (Protagonist)") + narrative_lines.append(f"- Want: {protag_want_val}") + narrative_lines.append(f"- Struggle: {protag_struggle_val}\n") + narrative_lines.append(f"**{antag_name_val}** (Antagonist)") + narrative_lines.append(f"- Want: {antag_want_val}\n") + narrative_lines.append("\n## Narrative Events\n") + for entry in engine.narrative_log: + narrative_lines.append(f"- {entry['surface_event']}") + + narrative_md = "\n".join(narrative_lines) + iceberg_md = format_iceberg(iceberg) + + return narrative_md, iceberg_md, engine.to_dict() + + except Exception as e: + error_msg = f"Error: {str(e)}" + return error_msg, error_msg, {"error": str(e)} + + custom_btn.click( + fn=create_custom_narrative, + inputs=[custom_title, protag_name, protag_want, protag_struggle, + antag_name, antag_want, custom_setting, custom_mode], + outputs=[custom_narrative, custom_iceberg, custom_json] + ) + + # === TAB 4: MYSTERY LAYERS === + with gr.TabItem("Mystery Layers"): + gr.Markdown("### Add Mystery Depth to Any Event") + gr.Markdown("Transform a simple surface event into layered narrative with multiple interpretations, hidden connections, and unresolved threads.") + + with gr.Row(): + with gr.Column(): + mystery_event = gr.Textbox( + label="Surface Event", + value="The mayor suddenly resigns from office", + lines=2 + ) + with gr.Row(): + mystery_char1 = gr.Textbox(label="Character 1", value="The Mayor") + mystery_char2 = gr.Textbox(label="Character 2", value="The Reporter") + + mystery_interps = gr.Slider( + minimum=2, maximum=5, value=3, step=1, + label="Number of Interpretations" + ) + mystery_btn = gr.Button("Add Mystery Layers", variant="primary") + + mystery_output = gr.JSON(label="Layered Event") + + def add_mystery_layers(surface_event, char1, char2, num_interps) -> Dict[str, Any]: + """Add mystery layers to event.""" + try: + engine = NarrativeEngine() + engine.register_character("char1", char1) + engine.register_character("char2", char2) + + layered = engine.mystery.add_mystery_layers( + surface_event, + character_ids=["char1", "char2"], + num_interpretations=int(num_interps), + include_hidden_connection=True, + include_unresolved_thread=True + ) + + iceberg = engine.mystery.generate_iceberg_summary(layered.id) + + return { + "surface_event": surface_event, + "characters": [char1, char2], + "interpretations": [ + { + "reading": i.reading, + "description": i.description, + "plausibility": round(i.plausibility, 2), + "darkness_level": round(i.darkness_level, 2) + } for i in layered.interpretations + ], + "evidence": [ + { + "type": e.evidence_type, + "content": e.content, + "is_red_herring": e.is_red_herring + } for e in layered.evidence + ], + "hidden_connections": [ + { + "type": c.connection_type, + "description": c.description + } for c in layered.hidden_connections + ], + "unresolved_threads": [ + { + "element": t.element, + "speculation_hooks": t.speculation_hooks + } for t in layered.unresolved_threads + ], + "iceberg": iceberg + } + except Exception as e: + return {"error": str(e)} + + mystery_btn.click( + fn=add_mystery_layers, + inputs=[mystery_event, mystery_char1, mystery_char2, mystery_interps], + outputs=[mystery_output] + ) + + gr.Markdown(""" +--- +**Narrative Engine** integrates 7 storytelling paradigms for rich, layered narratives. +All generation is deterministic - no external APIs required. + """) diff --git a/ui_tabs/resources_hub_tab.py b/ui_tabs/resources_hub_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..ec279b949a10509180847317524c0e859b975104 --- /dev/null +++ b/ui_tabs/resources_hub_tab.py @@ -0,0 +1,501 @@ +"""Resources Hub Tab. + +This module provides links to resources, engine documentation, and tutorials. +""" + +import gradio as gr + +# Import test media HTML from relatively_constant_variables +try: + from relatively_constant_variables import TestmedialoadinHTML +except ImportError: + TestmedialoadinHTML = '
Test media loading HTML not available
' + + +def create_resources_hub_tab(): + """Create the Resources Hub tab.""" + with gr.Tab("Resources Hub"): + gr.Markdown("## Consolidated Resources for Game Development") + gr.Markdown("All documentation, tools, tutorials, and references organized by category.") + + with gr.Tab("Engine & Mechanics"): + gr.HTML("There is an issue with the loading of the choices above 4 - only 3 load max it seems") + gr.HTML("Placeholder for explanations of Player and Game Session") + with gr.Tab("Endless Commerce support"): + gr.HTML("Need to be able to support this type of code for upgrades - https://www.decisionproblem.com/paperclips/index2.html - https://www.reddit.com/r/incremental_games/comments/rc7ks7/the_unique_storytelling_of_universal_paperclips/
https://huggingface.co/spaces/osanseviero/TheMLGame/blob/main/main.js") + with gr.Tab("Current '1D Engine' Defects"): + gr.HTML("To test the config idea I (with llm assistance) had to make an 'engine' that was based around the config - so there are many potholes ") + gr.HTML("All realtime events - Text still needs realtime as well") + with gr.Tab("Inventory and Skill Support"): + gr.HTML("Each decision affects Skills or inventory") + with gr.Tab("NPC Support"): + gr.HTML("Shared timeline that the player interfere with") + with gr.Tab("Economics Support"): + gr.HTML("Style Idea for a Basic Idea - Endless Economy (Tiny Tower as well) - Paperclip maximiser and inspirations - https://huggingface.co/spaces/osanseviero/TheMLGame") + with gr.Tab("Time Support"): + gr.HTML("No urgency / patience mechanics") + with gr.Tab("LLM play testing"): + gr.Markdown("*Full LLM playtesting is now in the **Test** tab at root level.*") + + with gr.Tab("Real World & Robotics Controllers"): + gr.Markdown("### Real World Game Extensions") + gr.HTML("Side Quests can be to build with a real world 3D printer - eg. Map or speculating how uncharted territories are. Or Star wars view pinpointer (rey)") + gr.HTML("3D printed trophies or game items as real items") + gr.HTML("Smart Watch and Phone - Notifications to watch and eg. character messages, authorisation requests, in game internet - Cloud computing / SBC or controller project ideas") + gr.HTML("Some Image to 3D options (As Position Refernece) - https://huggingface.co/spaces/FrozenBurning/3DTopia-XL") + gr.HTML("Sites to use (Majority Free / Free Tier) - https://stephaneginier.com/sculptgl/ (Sculpting Editor No Login) | https://www.tinkercad.com/ (Editor) | https://app.vectary.com/ (Editor) | https://clara.io/ (Editor and huge models library) | Sketchfab (Model Library) | https://www.figuro.io/Home/Welcome (Editor and some tutorials) | Spline (Editor and Presentation) | https://www.selfcad.com/ (Editor)") + gr.Markdown("### LLM/Robotics as Custom Controllers") + gr.HTML("https://www.reddit.com/r/singularity/comments/1fm7fup/ihmc_and_boardwalk_robotics_show_their_humanoid/") + gr.HTML("Controls changed the scope of the game eg. mouse vs keyboard vs console controller vs remote vs touch screen
LLM can be vision/surveilance based controler (eg. MGS/GTA camera gauged by an actual camera in real life) or it can be a companion (offline/off console game progrssion ideas)") + gr.HTML("https://github.com/Shaka-Labs/ACT $250 imitation learning/teleoperation - eg. a win loss result alert / NPC 'scout' telling you go or stay") + gr.HTML("https://huggingface.co/posts/thomwolf/809364796644704") + gr.HTML("Robotics - https://www.nvidia.com/en-us/autonomous-machines/embedded-systems/jetson-orin/ https://huggingface.co/lerobot https://github.com/tonyzhaozh/aloha https://github.com/Shaka-Labs/ACT https://github.com/OpenTeleVision/TeleVision https://www.stereolabs.com/ ") + gr.HTML("https://www.reddit.com/r/singularity/comments/1f88z58/the_first_ever_agent_civilization_1000_truly/") + with gr.Tab("Existing Game Developemnt Resources"): + gr.HTML("https://enginesdatabase.com/") + gr.HTML("https://develop.games/#nav-tools-engine ") + with gr.Tab("Other Considerations"): + with gr.Tab("General"): + gr.HTML("https://www.reddit.com/r/singularity/comments/1fiugew/wonderworld_a_novel_framework_for_interactive_3d/") + + gr.HTML("https://huggingface.co/docs/hub/api - daily papers is an endpoint so you can turn paper abstract into games with the help of LLM") + + gr.HTML("Experiment for https://huggingface.co/spaces/ysharma/open-interpreter/blob/main/app.py inplementation with gradio client api") + + gr.HTML("https://huggingface.co/spaces/HuggingFaceTB/SmolLM-135M-Instruct-WebGPU") + + gr.HTML("Useful Spaces and links: https://huggingface.co/spaces/artificialguybr/Stable-Audio-Open-Zero https://huggingface.co/spaces/stabilityai/TripoSR https://huggingface.co/spaces/wangfuyun/AnimateLCM-SVD https://huggingface.co/spaces/multimodalart/face-to-all https://huggingface.co/spaces/facebook/MusicGen https://huggingface.co/spaces/Doubiiu/tooncrafter") + + gr.HTML("langchain docs as awareness for alot of the integration use cases and providers that are possible - https://python.langchain.com/v0.2/docs/integrations/tools/") + + gr.HTML("https://huggingface.co/spaces/linoyts/scribble-sdxl-flash as map planner") + + gr.HTML("---------------------------------------Gameplay Ideas-------------------------------") + gr.HTML("https://huggingface.co/spaces/Lin-Chen/ShareCaptioner-Video - game use example police questions a event with multiple eye witnesses needs to give as close to the caption description to win") + with gr.Tab("State management through huggingface?"): + gr.HTML("Huggingface as the login provider? - https://huggingface.co/spaces/Wauplin/gradio-user-history/tree/main https://huggingface.co/spaces/AP123/IllusionDiffusion https://huggingface.co/docs/hub/en/spaces-oauth https://huggingface.co/docs/hub/en/oauth, persistent storage - https://huggingface.co/docs/hub/en/spaces-storage") + with gr.Tab("Finetuning options"): + gr.HTML("https://docs.mistral.ai/guides/finetuning/
https://openpipe.ai/blog/fine-tuning-best-practices-chapter-2-models") + gr.HTML("Unsloth and Colab? - https://github.com/unslothai/unsloth https://huggingface.co/unsloth
Mistral Nemo Base - https://huggingface.co/unsloth/Mistral-Nemo-Base-2407 - https://colab.research.google.com/drive/17d3U-CAIwzmbDRqbZ9NnpHxCkmXB6LZ0?usp=sharing
Llama 3 8B https://huggingface.co/unsloth/llama-3-8b-Instruct-bnb-4bit") + gr.HTML("Price - https://openpipe.ai/pricing") + with gr.Tab("Backend and/or Hosting?"): + gr.HTML("Deployemnt options - https://huggingface.co/SpacesExamples", "https://huggingface.co/templates") + gr.HTML("Prototyping and freemium
free api
HF Pro subscription") + gr.HTML("GPU (Data privacy) = No Rate limits? - https://replicate.com/pricing, https://lambdalabs.com/service/gpu-cloud https://huggingface.co/pricing#endpoints https://tensordock.com/cloud-gpus", "https://massedcompute.com/home/pricing/" ) + gr.HTML("Speed - Groq, SambaNova, https://www.etched.com/announcing-etched ") + gr.HTML("Price - Coding - https://aider.chat/docs/leaderboards/ - https://www.deepseek.com/ 0.3 per million - is this per token or chinese character as that means converting code to chinese if possible can save api cost?") + gr.HTML("Llama 3.1 405B - https://ai.meta.com/blog/meta-llama-3-1/ https://replicate.com/meta/meta-llama-3.1-405b-instruct https://fireworks.ai/pricing https://www.ibm.com/products/watsonx-ai/foundation-models") + gr.HTML("Covered by Anythingllm - https://github.com/Mintplex-Labs/anything-llm : https://repocloud.io/details/?app_id=276, https://render.com/pricing, https://docs.railway.app/reference/pricing/free-trial, https://repocloud.io/pricing, https://elest.io/pricing ") + with gr.Tab("HF Spaces Build Options"): + gr.Markdown("### HuggingFace Spaces Build Configuration") + gr.Markdown("Options for configuring builds, handling timeouts, and managing heavy dependencies.") + + with gr.Accordion("SDK Options", open=True): + gr.Markdown(""" +**1. Gradio SDK** (Simple, default) +```yaml +sdk: gradio +python_version: 3.10 +``` +- Uses `requirements.txt` for dependencies +- Build timeout: ~30 min default +- Simple but limited control + +**2. Docker SDK** (Full control) +```yaml +sdk: docker +app_port: 7860 +``` +- Full `Dockerfile` control +- Can use pre-built images +- Better layer caching + """) + + with gr.Accordion("Timeout & Build Settings", open=True): + gr.Markdown(""" +**Extend Startup Timeout** (in README.md YAML): +```yaml +startup_duration_timeout: 1h +``` +- Default: 30 minutes +- Max: varies by plan + +**Build Variables**: +```yaml +env: + - MY_VAR=value +``` +Passed as build-args to Docker. + +**Secrets**: Set in Settings tab, expose in Dockerfile with `RUN --mount=type=secret` + """) + + with gr.Accordion("Heavy Dependencies (like dlib)", open=True): + gr.Markdown(""" +| Approach | Pros | Cons | +|----------|------|------| +| **Docker + pre-built wheel** | Fast build | Need to find/host wheel | +| **Runtime install** | No build timeout | Slow first run | +| **API fallback** | No deps needed | External service reliability | +| **Duplicate Space** | Pre-built available | Maintenance overhead | + +**Example: Pre-built dlib in Dockerfile**: +```dockerfile +FROM python:3.10 +RUN pip install dlib --find-links https://example.com/dlib-wheel.whl +``` + +**Runtime Install Pattern**: +```python +@spaces.GPU +def my_function(): + import subprocess + subprocess.run(["pip", "install", "heavy-package"]) + # Then use it... +``` + """) + + with gr.Accordion("Useful Links", open=False): + gr.HTML(""" + Docker Spaces Docs
+ Spaces Config Reference
+ Spaces Overview
+ Spaces Examples
+ Space Templates + """) + with gr.Tab("Some Interesting Git Repos"): + gr.HTML("https://github.com/NVIDIA/Megatron-LM https://github.com/OpenGVLab/EfficientQAT https://github.com/evintunador/minLlama3/blob/main/model.py https://github.com/evintunador/micro-GPT-sandbox") + with gr.Tab("Old Ideas"): + gr.HTML("""
Main ideas for this space is (June 2024) (Custom component planning?):
+
+
We can generate almost any media data and more
+
A program exist around data
+
Time moves in a straight so all considerations are flattend by the nature of time
+
llms good at short questions
+
HF + Gradio allows for api use so this my prototype tool for tool use test
+
""") + with gr.Tab("Licensing"): + gr.HTML("Need to find the press release to see license eg. https://blackforestlabs.ai/announcing-black-forest-labs/") + with gr.Tabs("Links to go over when free"): + gr.HTML("https://www.reddit.com/r/singularity/comments/1ecuu8j/you_can_now_use_ai_for_3d_model_creation/ | ") + + with gr.Tab("Audio Resources"): + gr.Markdown("### Audio Generation & Sound Design Resources") + gr.Markdown("Resources for music, sound effects, and voice generation for games.") + + with gr.Accordion("AI Audio Generation", open=True): + gr.HTML(""" + HuggingFace Spaces:
+ Stable Audio - Sound effects & ambient
+ MusicGen - Music generation
+ Kokoro-82M - Fast, natural voice synthesis
+ Supertonic-2 - High-quality expressive TTS
+ GLM-TTS - Multilingual TTS with voice cloning
+ Fish Speech - Voice cloning
+ Bark - Text to speech with emotion
+
Talking Portraits:
+ Hallo - Talking portrait animation
+ LivePortrait - Portrait animation + """) + + with gr.Accordion("3rd Party Audio Tools", open=False): + gr.HTML(""" + Music Generation:
+ Suno - AI music creation
+ Udio - AI music generation
+
Sound Libraries:
+ Freesound - Free sound effects
+ ZapSplat - Free SFX library
+ Sonniss GDC - Game audio bundles + """) + + with gr.Accordion("Audio Categories for Games", open=False): + gr.Markdown(""" +**Music Types:** +- Background/Ambient - Sets mood, loops seamlessly +- Interactive - Changes based on gameplay +- Cutscene - Narrative-driven, linear +- Menu - UI navigation, branded + +**Sound Effects:** +- Environmental - Weather, nature, machinery +- Character - Footsteps, voice, actions +- Action - Combat, items, abilities +- UI - Clicks, notifications, feedback + +**Speech:** +- Dialogue - Character conversations +- Narration - Story exposition +- Voiceover - Instructions, tutorials + """) + + with gr.Accordion("Music Generation Methods (Pre vs Post 2023)", open=True): + gr.Markdown(""" +**Comparison of AI Music Generation Methods:** + +| Method | Era | VRAM | Duration | Quality | Open Source | +|--------|-----|------|----------|---------|-------------| +| **Jukebox** | 2020 | 16GB+ | Slow (hours) | High | Yes | +| **MusicVAE** | 2018 | 4-8GB | Fast | Low-Med | Yes | +| **MuseNet** | 2019 | API only | Fast | Medium | No | +| **Riffusion** | 2022 | 6-8GB | Fast | Medium | Yes | +| **MusicGen** | 2023 | 8-16GB | Medium | High | Yes | +| **Stable Audio** | 2023 | 8-12GB | Medium | High | Partial | +| **AudioCraft** | 2023 | 8-16GB | Medium | High | Yes | +| **MusicLM** | 2023 | API only | Fast | Very High | No | +| **Suno v3** | 2024 | API only | Fast | Very High | No | +| **Udio** | 2024 | API only | Fast | Very High | No | + +--- + +**Pre-2023 Methods (GPU Efficient, Lower Quality):** + +*Jukebox (OpenAI, 2020)* +- VQ-VAE based, generates raw audio with vocals +- Can continue/extend existing songs +- ~16GB+ VRAM, extremely slow (hours per minute of audio) +- Quality: Good but often "dreamy"/artifacts +- Good for: Experimental, background ambience + +*MusicVAE (Magenta, 2018)* +- MIDI-based, learns latent space of melodies +- Fast, controllable interpolation between styles +- ~4-8GB VRAM, real-time capable +- Quality: Basic MIDI, needs good synths +- Good for: Procedural game music, variations + +*Riffusion (2022)* +- Fine-tuned Stable Diffusion on spectrograms +- Novel approach: image diffusion for audio +- ~6-8GB VRAM (same as SD 1.5) +- Quality: Medium, 5-second clips +- Good for: Quick sound effects, loops + +*MuseNet (OpenAI, 2019)* +- Transformer-based MIDI generation +- Multi-instrument, various styles +- API only (no local) +- Good for: Classical/jazz style compositions + +--- + +**Post-2023 Methods (Higher Quality, More Resources):** + +*MusicGen (Meta, 2023)* +- Transformer + EnCodec neural codec +- Text-to-music with melody conditioning +- Small: 300M (~4GB), Medium: 1.5B (~8GB), Large: 3.3B (~16GB) +- Quality: High, coherent structure +- Good for: Game soundtracks, ambient music +- **Runs on ZeroGPU with small/medium models** + +*Stable Audio (Stability AI, 2023)* +- Latent diffusion for audio +- Long-form generation (up to 90s) +- ~8-12GB VRAM +- Quality: High, especially for SFX +- Good for: Sound effects, ambient, loops +- **Runs on ZeroGPU** + +*AudioCraft/MusicGen-Stereo (Meta, 2023)* +- Stereo output, better quality +- Same architecture as MusicGen +- ~10-16GB VRAM for stereo +- Good for: Production-ready game audio + +*Suno v3/v4 (2024)* +- Full songs with vocals and lyrics +- Extremely high quality, human-like +- API only, commercial service +- Good for: Trailer music, title themes +- Limitation: API costs, usage rights + +*Udio (2024)* +- Competing with Suno, similar quality +- Better at certain genres +- API only, commercial service +- Good for: Professional game soundtracks + +--- + +**Recommendations for ZeroGPU/HF Spaces:** + +| Use Case | Recommendation | VRAM | +|----------|---------------|------| +| Quick SFX | Stable Audio Open | ~8GB | +| Background music | MusicGen Small | ~4GB | +| Higher quality music | MusicGen Medium | ~8GB | +| Production soundtracks | Suno/Udio API | N/A | +| Procedural variations | MusicVAE | ~4GB | + +**Code Example for MusicGen on ZeroGPU:** +```python +@spaces.GPU(duration=120) +def generate_music(prompt, duration=10): + from audiocraft.models import MusicGen + model = MusicGen.get_pretrained('facebook/musicgen-small') + model.set_generation_params(duration=duration) + wav = model.generate([prompt]) + return wav[0].cpu().numpy() +``` + +**HuggingFace Spaces for Music:** +- [MusicGen](https://huggingface.co/spaces/facebook/MusicGen) +- [Stable Audio](https://huggingface.co/spaces/artificialguybr/Stable-Audio-Open-Zero) +- [AudioCraft](https://huggingface.co/spaces/facebook/audiocraft) +- [Riffusion](https://huggingface.co/spaces/riffusion/riffusion-playground) + """) + + with gr.Tab("GPU & Video Generation"): + gr.Markdown("### ZeroGPU & Video Generation Reference") + gr.Markdown("Technical specifications and methods for GPU-based generation on HuggingFace Spaces.") + + with gr.Accordion("ZeroGPU Specifications & Limitations", open=True): + gr.Markdown(""" +**Hardware:** NVIDIA H200 GPU with ~70GB VRAM (shared/virtualized) + +**Default Timeout:** 60 seconds per `@spaces.GPU` decorated function call + +**Extending Timeout:** +```python +@spaces.GPU(duration=180) # 3 minutes max recommended for video +def my_video_function(): + ... +``` + +**Key Constraints:** +| Resource | Limit | Notes | +|----------|-------|-------| +| VRAM | ~70GB shared | May vary based on concurrent users | +| Default timeout | 60s | Extendable with duration parameter | +| Max timeout | ~300s | Longer tasks may be killed | +| Concurrent jobs | Limited | Queue system recommended | + +**Common "GPU Task Aborted" Causes:** +1. **Timeout exceeded** - Task took longer than duration limit +2. **CUDA operations outside @spaces.GPU** - All GPU code must be inside decorated functions +3. **Memory pressure** - Too much VRAM used, try CPU offload + +**Best Practices for ZeroGPU:** +```python +# Enable CPU offload to reduce VRAM usage +pipe.enable_model_cpu_offload() + +# Enable VAE tiling for large images/videos +if hasattr(pipe, 'vae'): + pipe.vae.enable_tiling() + +# Use lower settings for video generation +# - Fewer steps (4-8 instead of 25+) +# - Shorter duration (2-3 seconds) +# - Lower resolution (512x320) +``` + +**Working Examples on ZeroGPU:** +- GPT-OSS 20B: 512 tokens generation works +- Falcon-H1R 7B: 1000 tokens generation works +- Video models: Need low settings (4 steps, 2s, 512x320) + """) + + with gr.Accordion("Talking Head Video Generation Methods", open=True): + gr.Markdown(""" +**Comparison of Audio-Driven Talking Head Methods (2024-2026):** + +| Method | VRAM | Speed | Quality | Best For | +|--------|------|-------|---------|----------| +| **Wav2Lip** | 4-6GB | Fast | Medium | Lip sync only, any face | +| **FOMM** | 4-6GB | Fast | Medium | Simple motion transfer | +| **SadTalker** | 6-8GB | Medium | Good | Full head motion + lip sync | +| **LivePortrait** | 10-12GB | Medium | High | Expression transfer | +| **Hallo** | 20-24GB+ | Slow | Very High | High quality portraits | +| **EDTalk** | 8-12GB | Medium | High | Efficient, good balance | +| **EMO** | 24GB+ | Slow | Very High | Emotional expressions | + +**Old Methods (GPU Efficient, Lower Quality):** + +*Wav2Lip (2020)* +- Only modifies lip region, keeps rest of face static +- Works with any face video/image +- ~4-6GB VRAM, real-time capable +- Good for: Quick lip sync, game NPCs + +*First Order Motion Model (FOMM, 2019)* +- Transfers motion from driving video to source image +- Simple architecture, fast inference +- ~4-6GB VRAM +- Good for: Simple animations, avatars + +*SadTalker (2023)* +- Generates 3D motion coefficients from audio +- Full head movement + lip sync +- ~6-8GB VRAM, slower than Wav2Lip +- Good for: Realistic NPCs with head motion + +**New Methods (High Quality, GPU Hungry):** + +*LivePortrait (2024)* +- Stitching and retargeting for expression control +- High quality output, controllable +- ~10-12GB VRAM +- Good for: High quality cutscenes + +*Hallo (2024)* +- Hierarchical audio-driven synthesis +- Best quality for portrait animation +- ~20-24GB VRAM, slow +- Good for: Hero characters, cinematics + +*EDTalk (2024)* +- Efficient design, competitive quality +- Better VRAM/quality tradeoff +- ~8-12GB VRAM +- Good for: Production use with limited GPU + +**Recommendations for ZeroGPU:** +1. **Wav2Lip or SadTalker** - If you need it to run on Spaces +2. **API-based Hallo** - Use existing HF Space via API for quality +3. **LivePortrait** - Good balance if you have 12GB available +4. **EDTalk** - Best new method that might fit ZeroGPU constraints + +**HuggingFace Spaces for Talking Heads:** +- [Hallo](https://huggingface.co/spaces/fffiloni/tts-hallo-talking-portrait) +- [LivePortrait](https://huggingface.co/spaces/KwaiVGI/LivePortrait) +- [SadTalker](https://huggingface.co/spaces/vinthony/SadTalker) + """) + + with gr.Accordion("Video Generation Models on ZeroGPU", open=False): + gr.Markdown(""" +**Tested Video Models & Settings:** + +| Model | Type | Recommended Settings | Notes | +|-------|------|---------------------|-------| +| LTX-Video-0.9.7-distilled | T2V | 4 steps, 2s, 512x320 | Fast, works well | +| HunyuanVideo | T2V | 4 steps, 2s, 512x320 | Needs CPU offload | +| Wan2.2-T2V-1.3B | T2V | 4 steps, 2s, 480x320 | Smaller model | +| Wan2.2-I2V-14B | I2V | 4 steps, 2s | Use with CPU offload | + +**Pipeline Classes (diffusers):** +```python +from diffusers import ( + LTXPipeline, # NOT LTXConditionPipeline + HunyuanVideoPipeline, + WanPipeline, # For T2V + WanImageToVideoPipeline # For I2V +) +``` + +**Model IDs:** +- LTX: `Lightricks/LTX-Video-0.9.7-distilled` +- Hunyuan: `hunyuanvideo-community/HunyuanVideo` +- Wan T2V: `Wan-AI/Wan2.2-T2V-1.3B-Diffusers` +- Wan I2V: `Wan-AI/Wan2.2-I2V-14B-480P-Diffusers` + """) + + with gr.Tab("Asset loading test"): + gr.HTML("SDXL (linoyts/scribble-sdxl-flash), SVD and Stable Audio used for the test assets (For commercial use need a licence)
testmedia/") + with gr.Row(): + gr.Image(value="testmedia/Flash scribble SDXL - random squiggles as roads.webp") + gr.Video(value="testmedia/SVD - random squiggles as roads video 004484.mp4") + gr.Audio(value="testmedia/Stable Audio - Raindrops, output.wav") + gr.HTML(TestmedialoadinHTML) # imported from relatively_constant_variables + diff --git a/ui_tabs/story_architect_tab.py b/ui_tabs/story_architect_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..b7022afbdfc307e1c98db71e40a433a7b1aa3f6d --- /dev/null +++ b/ui_tabs/story_architect_tab.py @@ -0,0 +1,285 @@ +""" +Story Architect Tab - Story structure and beat templates. + +Uses the StoryArchitect data models for creating story outlines with +professional beat templates (90-minute movie, 30-minute TV, YouTube videos, shorts). +""" + +import gradio as gr +import json +from typing import List, Dict, Any + +# Import story data models +from storygenattempt_hfport.story_data import ( + Beat, StoryFoundation, SeriesFoundation, + TEMPLATES, TEMPLATE_DESCRIPTIONS, TRANSITION_TYPES, + ADJECTIVES, SENSORY_DETAILS, RELATIONSHIP_LOOPS, + VISUAL_GRAMMAR, CHARACTER_FLAWS, DIALOGUE_SUBTEXT, + PAYOFF_SEEDS, ENVIRONMENTAL_VIBES, TWIST_MECHANICS, + DEMO_STORIES, + create_beats_from_template, export_story_json +) + + +def format_beats_display(beats: List[Beat], foundation: StoryFoundation = None) -> str: + """Format beats as markdown for display.""" + lines = [] + + if foundation: + lines.append("## Story Foundation") + lines.append(f"**Crux:** {foundation.crux}") + lines.append(f"**Struggle:** {foundation.struggle}") + lines.append(f"**Emotional Tone:** {foundation.emotional_specifics}") + lines.append(f"**Arc:** {foundation.initial_state} → {foundation.ending_state}") + lines.append(f"**Transition Type:** {foundation.transition_type}") + if foundation.characters: + lines.append(f"**Characters:** {foundation.characters}") + lines.append("") + lines.append("---") + lines.append("") + + lines.append("## Story Beats") + lines.append("") + + for i, beat in enumerate(beats, 1): + beat_line = f"**{i}.** {beat.verb}" + if beat.adjective: + beat_line += f" *({beat.adjective})*" + if beat.modifier: + beat_line += f" - {beat.modifier}" + lines.append(beat_line) + + if beat.reasoning: + lines.append(f" > {beat.reasoning}") + if beat.script_text: + lines.append(f" Script: *{beat.script_text}*") + if beat.visual_flow: + lines.append(f" Visual: {beat.visual_flow}") + + return "\n".join(lines) + + +def format_resource_list(title: str, items: List[str], numbered: bool = False) -> str: + """Format a list of resources as markdown.""" + lines = [f"## {title}", ""] + for i, item in enumerate(items, 1): + if numbered: + lines.append(f"{i}. {item}") + else: + lines.append(f"- {item}") + return "\n".join(lines) + + +def create_story_architect_tab(): + """Create the Story Architect tab.""" + + with gr.Tab("Story Architect"): + gr.Markdown("""## Story Architect - Beat Templates & Narrative Tools + +Create professional story outlines using industry-standard beat templates. +Choose from 90-minute movies, 30-minute TV episodes, YouTube videos, or short-form content. + """) + + with gr.Tabs(): + # === TAB 1: BEAT TEMPLATES === + with gr.TabItem("Beat Templates"): + gr.Markdown("### Generate Story Beats") + gr.Markdown("Select a template and optionally fill in story foundation details.") + + with gr.Row(): + with gr.Column(scale=1): + template_dropdown = gr.Dropdown( + label="Template", + choices=[ + ("90-Minute Feature Film (93 beats)", "movie_90"), + ("30-Minute TV Episode (29 beats)", "tv_30"), + ("9-Minute YouTube Video (9 beats)", "youtube_9"), + ("30-Second Short (6 beats)", "short_30s"), + ], + value="youtube_9" + ) + + transition_dropdown = gr.Dropdown( + label="Transition Type", + choices=TRANSITION_TYPES, + value="The Quest (Stasis to Change)" + ) + + with gr.Accordion("Story Foundation (Optional)", open=False): + crux_input = gr.Textbox( + label="Crux (Core Concept)", + placeholder="A woman discovers her mentor has been manipulating her entire career.", + lines=2 + ) + struggle_input = gr.Textbox( + label="Struggle", + placeholder="Rules: Trust is earned. Challenges: Everyone has hidden motives.", + lines=2 + ) + emotional_input = gr.Textbox( + label="Emotional Specifics", + placeholder="Tense, Claustrophobic, Revelatory", + ) + initial_state = gr.Textbox( + label="Initial State", + placeholder="Naive, trusting" + ) + ending_state = gr.Textbox( + label="Ending State", + placeholder="Wiser, independent" + ) + characters_input = gr.Textbox( + label="Main Characters", + placeholder="Maya, Dr. Chen, The Board" + ) + + generate_btn = gr.Button("Generate Beats", variant="primary") + + with gr.Column(scale=2): + beats_output = gr.Markdown(label="Generated Beats") + + with gr.Row(): + export_btn = gr.Button("Export JSON") + json_output = gr.Code(label="JSON Export", language="json", visible=False) + + # State storage + current_beats = gr.State(value=[]) + current_foundation = gr.State(value=None) + + def generate_beats(template_key, transition, crux, struggle, emotional, + initial, ending, characters): + """Generate beats from template.""" + beats = create_beats_from_template(template_key) + + foundation = None + if crux or struggle: + foundation = StoryFoundation( + crux=crux, + struggle=struggle, + emotional_specifics=emotional, + initial_state=initial, + ending_state=ending, + transition_type=transition, + characters=characters + ) + + display = format_beats_display(beats, foundation) + return display, beats, foundation + + def export_json(beats_list, foundation): + """Export to JSON.""" + if not beats_list: + return gr.update(visible=True, value='{"error": "No beats generated"}') + + # Convert state to Beat objects if needed + beats = [] + for b in beats_list: + if isinstance(b, dict): + beats.append(Beat(**b)) + else: + beats.append(b) + + # Convert foundation + if foundation and isinstance(foundation, dict): + foundation = StoryFoundation(**foundation) + elif foundation is None: + foundation = StoryFoundation() + + json_str = export_story_json(foundation, beats) + return gr.update(visible=True, value=json_str) + + generate_btn.click( + fn=generate_beats, + inputs=[template_dropdown, transition_dropdown, crux_input, struggle_input, + emotional_input, initial_state, ending_state, characters_input], + outputs=[beats_output, current_beats, current_foundation] + ) + + export_btn.click( + fn=export_json, + inputs=[current_beats, current_foundation], + outputs=[json_output] + ) + + # === TAB 2: DEMO STORIES === + with gr.TabItem("Demo Stories"): + gr.Markdown("### Pre-built Story Examples") + gr.Markdown("Explore complete story outlines with filled-in beat details.") + + with gr.Row(): + demo_dropdown = gr.Dropdown( + label="Select Demo", + choices=[ + ("The Heist - Rebirth Arc", "heist"), + ("The Coup - Tragedy Arc", "coup"), + ("The Battery - Survival Drama", "battery"), + ], + value="heist" + ) + load_demo_btn = gr.Button("Load Demo", variant="primary") + + demo_output = gr.Markdown(label="Demo Story") + + def load_demo(demo_key): + """Load a demo story.""" + if demo_key not in DEMO_STORIES: + return "Demo not found." + + demo = DEMO_STORIES[demo_key] + template_key = demo["template"] + foundation_data = demo["foundation"] + + foundation = StoryFoundation(**foundation_data) + beats = create_beats_from_template(template_key) + + return format_beats_display(beats, foundation) + + load_demo_btn.click( + fn=load_demo, + inputs=[demo_dropdown], + outputs=[demo_output] + ) + + # === TAB 3: WRITING RESOURCES === + with gr.TabItem("Writing Resources"): + gr.Markdown("### Narrative Building Blocks") + gr.Markdown("Reference materials for enriching your story beats.") + + with gr.Tabs(): + with gr.TabItem("Adjectives"): + gr.Markdown(format_resource_list("Emotional Tone Adjectives", ADJECTIVES[:60])) + with gr.Accordion("More Adjectives", open=False): + gr.Markdown(format_resource_list("", ADJECTIVES[60:])) + + with gr.TabItem("Sensory Details"): + gr.Markdown(format_resource_list("Atmospheric Details", SENSORY_DETAILS)) + + with gr.TabItem("Relationship Loops"): + gr.Markdown(format_resource_list("Character Relationship Friction", RELATIONSHIP_LOOPS, numbered=True)) + + with gr.TabItem("Visual Grammar"): + gr.Markdown(format_resource_list("Camera & Direction Techniques", VISUAL_GRAMMAR, numbered=True)) + + with gr.TabItem("Character Flaws"): + gr.Markdown(format_resource_list("Psychological Flaws", CHARACTER_FLAWS, numbered=True)) + + with gr.TabItem("Dialogue Subtext"): + gr.Markdown(format_resource_list("Hidden Meaning Patterns", DIALOGUE_SUBTEXT, numbered=True)) + + with gr.TabItem("Payoff Seeds"): + gr.Markdown(format_resource_list("Setup/Payoff Patterns", PAYOFF_SEEDS, numbered=True)) + + with gr.TabItem("Environments"): + gr.Markdown(format_resource_list("Environmental Vibes", ENVIRONMENTAL_VIBES, numbered=True)) + + with gr.TabItem("Twist Mechanics"): + gr.Markdown(format_resource_list("Plot Twist Patterns", TWIST_MECHANICS, numbered=True)) + + gr.Markdown(""" +--- +**Story Architect** provides professional story structure templates used in film, TV, and digital content. +- **90-Minute Movie**: Full three-act structure with 93 precise beats +- **30-Minute TV**: Single episode arc with A/B story integration +- **YouTube 9**: Optimized for retention and engagement +- **30-Second Short**: Micro-storytelling for social media + """) diff --git a/ui_tabs/story_graph_tab.py b/ui_tabs/story_graph_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..86ffff728445232f1d9a7a0c58d534d82502eeea --- /dev/null +++ b/ui_tabs/story_graph_tab.py @@ -0,0 +1,256 @@ +"""Story Graph Tab - Visual Narrative Planning. + +This module provides the Story Graph tab for visual story beat planning. +""" + +import json +import gradio as gr + + +def load_story_graph_html(beats_json=None): + """Load the story graph HTML file, optionally with pre-loaded beats.""" + try: + with open('story-graph-v3.html', 'r', encoding='utf-8') as f: + html_content = f.read() + + # If beats provided, inject them to load on startup + if beats_json and beats_json.strip() and not beats_json.startswith("Error"): + injection_script = f''' + // Auto-load imported beats + (function() {{ + const importedBeats = {beats_json}; + if (importedBeats && importedBeats.length > 0) {{ + beats = []; + connections = []; + nodesContainer.innerHTML = ''; + importedBeats.forEach((b, i) => {{ + const beat = {{ + id: Date.now() + i, + minute: b.minute || i * 5, + verb: b.verb || b.id || 'Beat ' + i, + location: b.location || '', + time: b.time || '', + weather: b.weather || '', + person: b.person || '', + emotion: b.emotion || '', + goal: b.goal || b.description || '', + obstacle: b.obstacle || '', + tone: b.tone || '', + stakes: b.stakes || '', + power: b.power || '', + ideal: b.ideal || '', + object: b.object || '', + question: b.question || '', + x: getXForMinute(b.minute || i * 5), + y: 100 + (i % 4) * 160, + expanded: false + }}; + beats.push(beat); + }}); + beats.forEach(b => renderBeat(b)); + renderConnections(); + }} + }})();''' + html_content = html_content.replace( + '// Load rich example on start\n loadRichExample();', + injection_script + ) + + # Escape for srcdoc attribute + html_escaped = html_content.replace('&', '&').replace('"', '"') + return f'' + except Exception as e: + return f'
Error loading story graph: {str(e)}
' + + +def config_to_story_beats(config_json): + """Convert game config states to story beat format. + + Handles nested format: {location: {state: {description, choices, transitions}}} + Also handles flat format: {states: [{id, description, choices}]} + """ + try: + config = json.loads(config_json) + beats = [] + beat_index = 0 + + # Check if it's the nested location format + if 'states' not in config: + # Nested format: {location: {state: {...}}} + for location_name, states in config.items(): + if isinstance(states, dict): + for state_name, state_data in states.items(): + if isinstance(state_data, dict) and 'description' in state_data: + choices = state_data.get('choices', []) + choices_str = ', '.join(choices[:3]) if choices else '' + + beat = { + 'minute': beat_index * 5, + 'verb': state_name.replace('_', ' ').title(), + 'location': location_name.replace('_', ' ').title(), + 'person': '', + 'emotion': '', + 'goal': state_data.get('description', '')[:100], + 'obstacle': f"Choices: {choices_str}" if choices_str else '', + 'tone': '', + 'stakes': '', + 'object': ', '.join(state_data.get('media', [])[:1]) if state_data.get('media') else '', + } + beats.append(beat) + beat_index += 1 + else: + # Flat format: {states: [...]} + states = config.get('states', []) + for i, state in enumerate(states): + beat = { + 'minute': i * 5, + 'verb': state.get('id', f'State {i}'), + 'location': '', + 'person': '', + 'emotion': '', + 'goal': state.get('description', '')[:100] if state.get('description') else '', + 'obstacle': '', + 'tone': '', + 'stakes': '', + } + beats.append(beat) + + return json.dumps(beats) + except Exception as e: + return f"Error: {str(e)}" + + +def story_beats_to_config(beats_json): + """Convert story beats to nested game config format. + + Output: {location: {state: {description, choices, transitions}}} + """ + try: + data = json.loads(beats_json) + beats_list = data.get('beats', []) if isinstance(data, dict) else data + + # Group beats by location + config = {} + for i, beat in enumerate(beats_list): + location = beat.get('location', 'main').lower().replace(' ', '_') or 'main' + verb = beat.get('verb', f'beat_{i}') + state_id = verb.lower().replace(' ', '_') + + if location not in config: + config[location] = {} + + # Determine next state for transitions + next_beats = beats_list[i+1:i+2] if i < len(beats_list)-1 else [] + next_state = None + if next_beats: + next_loc = next_beats[0].get('location', 'main').lower().replace(' ', '_') or 'main' + next_verb = next_beats[0].get('verb', 'end').lower().replace(' ', '_') + next_state = f"{next_loc}_{next_verb}" + + state = { + 'description': beat.get('goal', '') or beat.get('verb', ''), + 'choices': ['continue'] if next_state else [], + 'transitions': {'continue': next_state} if next_state else {}, + 'media': [] + } + + config[location][state_id] = state + + # Add end state + config['end'] = { + 'end': { + 'description': 'The End. Your adventure concludes here.', + 'choices': [], + 'transitions': {}, + 'media': [] + } + } + + return json.dumps(config, indent=2) + except Exception as e: + return f'{{"error": "{str(e)}"}}' + + +def create_story_graph_tab(): + """Create and return the Story Graph tab components.""" + with gr.Tab("Story Graph"): + gr.Markdown("## Story Beat Graph - Visual Narrative Planning") + gr.Markdown("Create and connect story beats visually. Use the graph's **Export** button (bottom toolbar) to get JSON.") + + storygraph_html_content = load_story_graph_html() + + with gr.Row(): + with gr.Column(scale=3): + storygraph_iframe = gr.HTML(value=storygraph_html_content) + + with gr.Column(scale=1): + gr.Markdown("### Import Config → Graph") + + sg_config_input = gr.Textbox( + label="Game Config JSON", + placeholder='{"states": [{"id": "start", "description": "Beginning..."}]}', + lines=8 + ) + sg_import_btn = gr.Button("Load Config into Graph", variant="primary") + sg_import_status = gr.Textbox(label="Status", lines=1, interactive=False) + + gr.Markdown("---") + gr.Markdown("### Export Graph → Config") + gr.Markdown("1. Click **Export** in the graph's bottom toolbar") + gr.Markdown("2. Copy the JSON from the popup") + gr.Markdown("3. Paste below to convert to game config:") + + sg_beats_input = gr.Textbox( + label="Paste Exported Beats JSON", + placeholder='Paste the JSON from the graph Export button here...', + lines=6 + ) + sg_convert_btn = gr.Button("Convert to Game Config", variant="secondary") + sg_config_output = gr.Textbox( + label="Generated Game Config", + lines=8, + interactive=False + ) + + gr.Markdown("---") + sg_reload_btn = gr.Button("Reset Graph (Load Default Example)") + + gr.Markdown(""" + **Graph Tips:** + - Drag beats to reposition + - Click • dots to connect beats + - Click ▼ to expand beat details + - Use bottom toolbar: Auto Layout, Export, Clear + """) + + # Event handlers + def handle_import(config_json): + if not config_json or not config_json.strip(): + return storygraph_html_content, "Please paste a config JSON first" + beats_json = config_to_story_beats(config_json) + if beats_json.startswith("Error"): + return storygraph_html_content, beats_json + try: + beats_count = len(json.loads(beats_json)) + new_iframe = load_story_graph_html(beats_json) + return new_iframe, f"Loaded {beats_count} beats into graph" + except Exception as e: + return storygraph_html_content, f"Error: {str(e)}" + + sg_import_btn.click( + fn=handle_import, + inputs=[sg_config_input], + outputs=[storygraph_iframe, sg_import_status] + ) + + sg_convert_btn.click( + fn=story_beats_to_config, + inputs=[sg_beats_input], + outputs=[sg_config_output] + ) + + sg_reload_btn.click( + fn=lambda: (load_story_graph_html(), "Graph reset to default example"), + inputs=[], + outputs=[storygraph_iframe, sg_import_status] + ) diff --git a/ui_tabs/study_3d_tab.py b/ui_tabs/study_3d_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..eb57d3d102346470e36e7253d9a8b1d1206d5808 --- /dev/null +++ b/ui_tabs/study_3d_tab.py @@ -0,0 +1,497 @@ +"""3D Study and Tutorials Tab. + +This module provides educational resources and tutorials for 3D development. +""" + +import gradio as gr + + +def create_3d_study_tab(): + """Create the 3D Study and Tutorials tab.""" + with gr.Tab("3D Study and Tutorials"): + gr.Markdown("## 3D Development Learning Resources & Tutorials") + gr.Markdown("A collection of resources for learning 3D game development, modeling, and related technologies.") + + with gr.Tab("Getting Started & Tools"): + gr.Markdown("""### 3D Fundamentals + +**Core Concepts to Learn:** +- Coordinate systems (world space, local space, screen space) +- Meshes, vertices, and polygons +- Materials, textures, and shaders +- Lighting and shadows +- Camera systems and projections + +**Recommended Learning Path:** +1. Basic 3D math (vectors, matrices, quaternions) +2. Simple 3D scene setup +3. Loading and displaying 3D models +4. Basic lighting and materials +5. Camera controls and navigation + +--- + +### Popular 3D Game Engines + +| Engine | Type | Pros | Cons | Links | +|--------|------|------|------|-------| +| **PlayCanvas** | Web-based (JS) | No download, browser-based, good docs | Performance limits for complex scenes | [Editor](https://playcanvas.com/) / [Tutorials](https://developer.playcanvas.com/tutorials/) | +| **Unity** | Full engine (C#) | Large community, asset store, cross-platform | Steeper learning curve, runtime fees | [Unity Learn](https://learn.unity.com/) | +| **Unreal** | AAA engine (C++/Blueprints) | High-quality graphics, visual scripting | Heavy resources, complex for beginners | [Learning Portal](https://dev.epicgames.com/community/unreal-engine/learning) | +| **Godot** | Open-source (GDScript/C#) | Free, lightweight, good 2D, improving 3D | Smaller community | [Docs](https://docs.godotengine.org/) | +| **Three.js** | JS library | Web-native, flexible, good for viz | Not a full engine, manual setup | [Docs](https://threejs.org/docs/) / [Journey](https://threejs-journey.com/) | + +--- + +### 3D Modeling Software + +**Blender (Free & Open Source)** - Best for modeling, sculpting, animation, rendering +- [Blender Guru - Donut Tutorial](https://www.youtube.com/playlist?list=PLjEaoINr3zgFX8ZsChQVQsuDSjEqdWMAD) +- [Official Tutorials](https://www.blender.org/support/tutorials/) +- [Grant Abbitt YouTube](https://www.youtube.com/@grabbitt) + +**Free 3D Asset Sources:** +- [Sketchfab](https://sketchfab.com/) - Many free models +- [Poly Haven](https://polyhaven.com/) - Free HDRIs, textures, models +- [Kenney Assets](https://kenney.nl/assets) - Game-ready assets +- [Mixamo](https://www.mixamo.com/) - Free character animations +- [Quaternius](https://quaternius.com/) - Low-poly game assets + +--- + +### AI-Assisted 3D Development + +**Emerging AI Tools:** +- **Text-to-3D:** Generate 3D models from text descriptions +- **Image-to-3D:** Convert 2D images to 3D models +- **AI Texturing:** Auto-generate textures and materials +- **Motion Capture Alternatives:** AI-based animation from video + +**Notable AI 3D Tools:** +- Meshy, Tripo3D, Luma AI - Text/Image to 3D +- Stable Zero123 - Single image to 3D +- Point-E, Shap-E (OpenAI) - 3D generation research +- NeRF/Gaussian Splatting - 3D from photos/video + +**Recommended Viewing:** +- [No BS Conversation about AI & 3D with Andrew Price (Blender Guru)](https://www.youtube.com/watch?v=E5IIMNQ4rl4) + """) + + with gr.Tab("YouTubers & Creators"): + gr.Markdown("""### 3D & AI YouTubers to Follow + +**AI + 3D Focus:** + +| Creator | Focus | Link | +|---------|-------|------| +| **Stefan 3D AI Lab** | AI for 3D, tutorials, workflows | [YouTube](https://www.youtube.com/@stefan3dailab) | +| **Corridor Crew** | VFX breakdowns, AI experiments | [YouTube](https://www.youtube.com/@CorridorCrew) | +| **Two Minute Papers** | AI research explained | [YouTube](https://www.youtube.com/@TwoMinutePapers) | +| **Latent Space** | AI art & 3D generation | [YouTube](https://www.youtube.com/@LatentSpaceTV) | + +**Blender & 3D Modeling:** + +| Creator | Focus | Link | +|---------|-------|------| +| **Blender Guru** | Tutorials, industry insights | [YouTube](https://www.youtube.com/@blaboratories) | +| **Grant Abbitt** | Beginner-friendly Blender | [YouTube](https://www.youtube.com/@grabbitt) | +| **CG Geek** | Blender tutorials, simulations | [YouTube](https://www.youtube.com/@CGGeek) | +| **Ducky 3D** | Blender procedural, geometry nodes | [YouTube](https://www.youtube.com/@TheDucky3D) | +| **Default Cube** | Short Blender tips | [YouTube](https://www.youtube.com/@DefaultCube) | +| **Polygon Runway** | Motion graphics, Blender | [YouTube](https://www.youtube.com/@PolygonRunway) | +| **Polyfjord** | Stylized 3D, tutorials | [YouTube](https://www.youtube.com/@Polyfjord) | + +**Game Dev & Engines:** + +| Creator | Focus | Link | +|---------|-------|------| +| **Brackeys** | Unity tutorials (classic) | [YouTube](https://www.youtube.com/@Brackeys) | +| **Game Dev TV** | Unity/Unreal courses | [YouTube](https://www.youtube.com/@GameDevTV) | +| **GDQuest** | Godot tutorials | [YouTube](https://www.youtube.com/@GDQuest) | +| **Fireship** | Web dev, quick tutorials | [YouTube](https://www.youtube.com/@Fireship) | +| **Sebastian Lague** | Coding adventures, algorithms | [YouTube](https://www.youtube.com/@SebastianLague) | + +**Technical Art & Shaders:** + +| Creator | Focus | Link | +|---------|-------|------| +| **Ben Cloward** | Shader tutorials | [YouTube](https://www.youtube.com/@BenCloward) | +| **Acerola** | Graphics programming | [YouTube](https://www.youtube.com/@Acerola_t) | +| **The Art of Code** | Shader coding | [YouTube](https://www.youtube.com/@TheArtofCodeIsCool) | + """) + + with gr.Tab("Animations & Measurements"): + gr.Markdown("## Animation Keyframes & Real-World Measurements") + gr.Markdown("Understanding motion through keyframes and translating physical measurements to digital.") + + with gr.Tab("Animation Keyframes"): + gr.Markdown("### Breaking Down Animations into Keyframes") + gr.Markdown("Every animation consists of key poses that define the motion.") + + with gr.Row(): + with gr.Column(): + gr.Markdown("""**Walk Cycle (8-frame)** +1. **Contact** - Front heel touches, back toe down +2. **Down** - Weight shifts, lowest point +3. **Passing** - Back leg passes under body +4. **Up** - Push off, highest point +5-8. Mirror for opposite leg + +*Arms swing opposite to legs, slight torso rotation, head stays stable (focus point), hip sway for natural movement*""") + + with gr.Column(): + gr.Markdown("""**Run Cycle (faster, airborne)** +1. **Contact** - Front foot strikes +2. **Compression** - Absorb impact +3. **Drive** - Push off, power +4. **Flight** - Both feet off ground +5-8. Mirror for opposite leg + +*More forward lean, higher knee lift, arms pump*""") + + with gr.Row(): + with gr.Column(): + gr.Markdown("""**Eat Animation** +1. **Idle** - Hand at rest +2. **Reach** - Move toward food +3. **Grasp** - Fingers close +4. **Lift** - Bring to face +5. **Approach** - Near mouth +6. **Bite** - Food enters mouth +7. **Chew** - Jaw motion starts +8. **Return** - Hand returns + +*Variations: finger food vs utensil, standing vs sitting posture, speed (casual vs hungry)*""") + + with gr.Column(): + gr.Markdown("""**Sleep Animation** +1. **Tired** - Eyes drooping +2. **Settling** - Finding position +3. **Eyes Close** - Final adjustment +4. **Asleep** - Relaxed, breathing +5. **Deep Sleep** - Subtle shifts +6. **Stir** - Minor movements + +*Breathing loop (overlay): chest/belly rise & fall, 3-4 sec cycle, subtle shoulder movement* +*Wake: stretch, yawn, eye rub*""") + + gr.Markdown("---") + gr.Markdown("### Custom Animation Breakdown") + with gr.Row(): + custom_anim_name = gr.Textbox(label="Animation Name", placeholder="e.g., Jump, Climb, Dance", scale=1) + with gr.Row(): + custom_anim_keyframes = gr.Textbox( + label="Keyframe Breakdown", + placeholder="1. Starting pose\n2. Anticipation\n3. Action\n4. Follow-through\n5. Recovery", + lines=6, + scale=2 + ) + custom_anim_notes = gr.Textbox( + label="Timing & Notes", + placeholder="Frame counts, easing...", + lines=6, + scale=1 + ) + + with gr.Tab("Measurements & Proportions"): + gr.Markdown("### Real-World Measurements for Digital Creation") + + with gr.Row(): + with gr.Column(): + gr.Markdown("""**Body Measurements** + +| Measurement | How to Take | Range | +|-------------|-------------|-------| +| Chest/Bust | Fullest part, under arms | 80-120 cm | +| Waist | Natural waistline | 60-100 cm | +| Hips | Fullest part | 85-125 cm | +| Shoulder Width | Point to point | 35-50 cm | +| Arm Length | Shoulder to wrist | 55-70 cm | +| Inseam | Crotch to ankle | 70-85 cm | +| Torso | Neck base to waist | 40-50 cm | + +*Tips: Use flexible tape, measure over light clothing, keep snug not tight. Measure twice, cut once!*""") + + with gr.Column(): + gr.Markdown("""**Glove Measurements** + +| Measurement | Location | Range | +|-------------|----------|-------| +| Hand Circumference | Palm at knuckles | 17-24 cm | +| Hand Length | Wrist to middle finger | 17-21 cm | +| Thumb Length | Web to tip | 5-7 cm | +| Wrist | Around bone | 14-19 cm | + +*Finger lengths: Index 6-8cm, Middle 7-9cm, Ring 6-8cm, Pinky 5-7cm* + +**Size Chart:** +| Circumference | Size | +|---------------|------| +| 17-18 cm | XS/7 | +| 18-19 cm | S/7.5 | +| 19-20 cm | M/8 | +| 21-22 cm | L/8.5 | +| 23-24 cm | XL/9 |""") + + with gr.Row(): + with gr.Column(): + gr.Markdown("""**Human Proportions** + +- **Realistic adult:** 7.5-8 heads tall +- **Heroic:** 8-8.5 heads tall +- **Fashion:** 9-10 heads tall +- **Chibi:** 2-4 heads tall + +**Key Ratios:** +- Arm span = Height +- Elbow at waist level +- Wrist at crotch level +- Knee at mid-leg +- Eyes at head midpoint + +**Golden Ratio (1:1.618):** Appears in faces, shells, architecture. Guides pleasing compositions.""") + + with gr.Column(): + gr.Markdown("""**Capturing Measurements** + +**Manual:** Calipers (0.1mm), tape measure, reference objects (coins, rulers in photos) + +**Photogrammetry:** 50-200 photos, include scale reference +- Meshroom (free), RealityCapture, Agisoft +- Output: Textured 3D mesh with real-world scale + +**LiDAR:** iPhone 12+ Pro / iPad Pro +- Polycam, Scaniverse, 3D Scanner App (~1-2 cm) + +**Structured Light:** Desktop scanners, higher accuracy than LiDAR + +**Reference Modeling:** Photo with ruler, scale in software""") + + gr.Markdown("---") + gr.Markdown("### Scale Calculator") + with gr.Row(): + real_measurement = gr.Number(label="Real-world size (cm)", value=100) + desired_scale = gr.Dropdown( + label="Target Scale", + choices=["1:1 (Full size)", "1:10", "1:24 (Dollhouse)", "1:64 (Hot Wheels)", "1:100", "Custom"], + value="1:1 (Full size)" + ) + custom_scale = gr.Number(label="Custom denominator", value=1) + with gr.Row(): + calc_btn = gr.Button("Calculate", scale=1) + calc_result = gr.Textbox(label="Scaled size", interactive=False, scale=2) + + def calculate_scale(real_size, scale, custom): + scale_map = { + "1:1 (Full size)": 1, + "1:10": 10, + "1:24 (Dollhouse)": 24, + "1:64 (Hot Wheels)": 64, + "1:100": 100, + "Custom": custom if custom > 0 else 1 + } + divisor = scale_map.get(scale, 1) + result = real_size / divisor + return f"{result:.2f} cm ({result*10:.1f} mm)" + + calc_btn.click( + fn=calculate_scale, + inputs=[real_measurement, desired_scale, custom_scale], + outputs=[calc_result] + ) + + with gr.Tab("Animation Resources"): + gr.Markdown("## Free Animation & Motion Capture Resources") + gr.Markdown("Sources for free character animations, motion capture data, and rigging tools.") + + with gr.Row(): + with gr.Column(): + gr.Markdown("""**Animation Platforms** +- [Mixamo](https://www.mixamo.com/) - Characters + anims, .fbx/.dae, free, 2500+ animations +- [Actorcore](https://actorcore.reallusion.com/) - Motion library, .fbx/.bvh, free tier +- [Rokoko Studio](https://www.rokoko.com/products/studio) - Mocap, .fbx/.bvh, free tier +- [Cascadeur](https://cascadeur.com/) - Physics-based, .fbx/.dae, AI-assisted +- [Plask](https://plask.ai/) - Video to mocap, .fbx/.bvh/.glb, free tier + +**Mocap Databases** +- [CMU Mocap](http://mocap.cs.cmu.edu/) - .bvh/.c3d, public domain +- [Bandai Namco Research](https://github.com/BandaiNamcoResearchInc/Bandai-Namco-Research-Motiondataset) - .bvh, free research +- [LAFAN1](https://github.com/ubisoft/ubisoft-laforge-animation-dataset) - Locomotion, .bvh +- [AMASS](https://amass.is.tue.mpg.de/) - Unified archive, .npz""") + + with gr.Column(): + gr.Markdown("""**Pre-Animated Models** +- [Sketchfab](https://sketchfab.com/) - Filter "animated", .glb/.gltf/.fbx +- [Poly Pizza](https://poly.pizza/) - Google Poly archive, .glb +- [Ready Player Me](https://readyplayer.me/) - Avatars + anims, .glb +- [CGTrader](https://www.cgtrader.com/) - Filter "free" + "animated" +- [TurboSquid](https://www.turbosquid.com/) - Some free animated +- [Free3D](https://free3d.com/) - Community models + +**Rigged Characters** +- [Mixamo Fuse](https://www.mixamo.com/) - Create → Auto-rig → Apply +- [MakeHuman](http://www.makehumancommunity.org/) - Free, open source +- [VRoid Studio](https://vroid.com/en/studio) - Anime, VRM format +- [Daz3D](https://www.daz3d.com/) - Realistic humans, free base""") + + with gr.Column(): + gr.Markdown("""**Tools & Retargeting** +- [Blender](https://www.blender.org/) - Retarget, convert, Rigify addon +- [AccuRig](https://actorcore.reallusion.com/auto-rig) - Free auto-rigging +- [Rokoko Studio](https://www.rokoko.com/) - Free retargeting app + +**Format Conversion** +- .bvh → Motion only, apply in Blender +- .fbx → Most universal format +- .glb/.gltf → Web-ready (Three.js, PlayCanvas) + +**DIY Motion Capture** +- [Plask](https://plask.ai/) - Webcam video → animation +- [DeepMotion](https://www.deepmotion.com/) - Free tier +- [Move.ai](https://www.move.ai/) - Phone app, multi-phone +- [MediaPipe](https://mediapipe.dev/) - Google, free, self-host + +**Game-Ready Packs (CC0)** +- [Quaternius](https://quaternius.com/) - Low-poly anims +- [Kenney](https://kenney.nl/assets) - Simple/casual +- [OpenGameArt](https://opengameart.org/) - Community""") + + with gr.Tab("3D Background with UI"): + gr.Markdown("## Using 3D as Background for Story Games") + gr.Markdown("Add immersive 3D backgrounds while keeping the choice-based UI overlay.") + + with gr.Row(): + with gr.Column(): + gr.Markdown("""**Image to 3D Tools** + +[3DTopia-XL](https://huggingface.co/spaces/FrozenBurning/3DTopia-XL) | [TRELLIS](https://huggingface.co/spaces/JeffreyXiang/TRELLIS) | [InstantMesh](https://huggingface.co/spaces/TencentARC/InstantMesh) + +**More 3D Generation:** +- [Hunyuan3D-2mini-Turbo](https://huggingface.co/spaces/tencent/Hunyuan3D-2mini-Turbo) - With texture, glb/obj/ply/stl +- [Hi3DGen](https://huggingface.co/spaces/Stable-X/Hi3DGen) - glb/obj/ply/stl +- [Stable Virtual Camera](https://huggingface.co/spaces/stabilityai/stable-virtual-camera) - 3D video reconstruction +- [VGGT (Facebook)](https://huggingface.co/spaces/facebook/vggt) - High speed, single angle +- [GRM Demo](https://huggingface.co/spaces/GRM-demo/GRM) + +**Research:** +- [3D Research Tracker](https://huggingface.co/spaces/dylanebert/research-tracker) +- [nunif/iw3](https://github.com/nagadomi/nunif/blob/master/iw3/README.md)""") + + with gr.Column(): + gr.Markdown("""**3D Editors (Free/Free Tier)** +- [SculptGL](https://stephaneginier.com/sculptgl/) - Sculpting, no login +- [Tinkercad](https://www.tinkercad.com/) - Simple editor +- [Vectary](https://app.vectary.com/) - Web 3D editor +- [Clara.io](https://clara.io/) - Editor + model library +- [Sketchfab](https://sketchfab.com/) - Model library +- [Figuro](https://www.figuro.io/Home/Welcome) - Editor + tutorials +- [Spline](https://spline.design/) - 3D design + presentation +- [SelfCAD](https://www.selfcad.com/) - Full 3D editor + +**Utilities:** +- [GLB to PLY Converter](https://imagetostl.com/convert/file/glb/to/ply) +- [PlayCanvas Free Assets](https://forum.playcanvas.com/t/assets-free-3d-models-sprites-icons-and-sounds-for-your-games/19199)""") + + with gr.Column(): + gr.Markdown("""**Adding UI to 3D Exports** + +**PlayCanvas:** 2D Screen entity → Text/Button children → wire clicks + +**Godot:** CanvasLayer → Label/Button → connect signals + +**Unreal:** Widget Blueprint → Text/Button → bind state + +**PlayCanvas Code:** +```javascript +var UiManager = pc.createScript('uiManager'); +UiManager.prototype.initialize = function() { + this.descText = this.app.root.findByName('DescriptionText'); + this.buttons = this.app.root.findByName('ChoiceContainer').children; +}; +UiManager.prototype.updateUI = function(state) { + this.descText.element.text = state.description; + state.choices.forEach((c, i) => { + if (this.buttons[i]) this.buttons[i].element.text = c; + }); +}; +```""") + + with gr.Tab("AR/XR Development"): + gr.Markdown("## AR/XR Integration & Development") + + with gr.Row(): + with gr.Column(): + gr.Markdown("""**AR Filter Platforms** + +| Tool | Platform | Formats | +|------|----------|---------| +| [Lens Studio](https://lensstudio.snapchat.com/) | Snapchat | .fbx .obj .gltf .glb | +| [Meta Spark](https://spark.meta.com/) | IG/FB | .fbx .obj .gltf .dae | +| [Effect House](https://effecthouse.tiktok.com/) | TikTok | .fbx .obj .gltf .glb | +| [Adobe Aero](https://www.adobe.com/products/aero.html) | Cross | .usdz .glb .gltf .fbx | +| [Reality Composer](https://developer.apple.com/augmented-reality/tools/) | Apple | .usdz .reality | +| [8th Wall](https://www.8thwall.com/) | Web | .glb .gltf | + +**Key Features:** +- **Lens Studio**: Face/body/hand tracking, SnapML, 300+ templates, Spectacles support +- **Meta Spark**: Patch Editor + Script API, audio analyzer, IG/FB distribution +- **Effect House**: Visual scripting, segmentation (hair/skin/bg), massive reach +- **Adobe Aero**: No-code, PS/AI integration, USDZ export +- **8th Wall**: WebAR (no app!), Three.js/A-Frame, 3B+ devices""") + + with gr.Column(): + gr.Markdown("""**Platform Workflows** + +**Snapchat:** [Lens Studio](https://lensstudio.snapchat.com/download) +1. Build lens → 2. Webcam preview → 3. Pair phone (Settings) → 4. Send to Snapchat → 5. Test in app → 6. Submit for review + +**Instagram/FB:** [Meta Spark](https://spark.meta.com/download) +1. Build effect → 2. Webcam preview → 3. Download Spark Player app → 4. Test on Device → 5. Submit via Spark Hub + +**TikTok:** [Effect House](https://effecthouse.tiktok.com/download) +1. Build effect → 2. Webcam preview → 3. QR code → 4. Scan in TikTok → 5. Submit for review + +**WebAR:** [8th Wall](https://www.8thwall.com/) +1. Code in editor → 2. Browser preview → 3. Localhost test → 4. Deploy → 5. Share URL (no app needed!)""") + + with gr.Column(): + gr.Markdown("""**Testing & WebXR** + +| Platform | Test App | +|----------|----------| +| Snapchat | Snapchat app | +| Instagram | Spark Player + IG | +| TikTok | TikTok app | +| Apple | AR Quick Look | +| WebAR | Any browser | + +**WebXR Frameworks:** +- [A-Frame](https://aframe.io/) - Web VR/AR +- [Three.js](https://threejs.org/) - WebXR support +- [AR.js](https://ar-js-org.github.io/AR.js-Docs/) - Open source +- [Model Viewer](https://modelviewer.dev/) - Google 3D/AR +- [PlayCanvas WebXR](https://developer.playcanvas.com/tutorials/webxr-hello-world/) + +**Key Insight:** Snapchat & TikTok test in same app users use. 8th Wall: browser = production.""") + + with gr.Column(): + gr.Markdown("""**Publishing & Docs** + +**Review Times:** +- Snapchat: 24-48 hrs +- Instagram/FB: Days to weeks +- TikTok: Days +- WebAR: No review! + +**Documentation:** +- [Lens Studio Docs](https://docs.snap.com/lens-studio) +- [Meta Spark Learn](https://sparkar.facebook.com/ar-studio/learn) +- [Effect House Learn](https://effecthouse.tiktok.com/learn) +- [8th Wall Docs](https://www.8thwall.com/docs) +- [Adobe Aero Help](https://helpx.adobe.com/aero/user-guide.html) + +**Game Config Integration:** +- AR triggers at game states +- Face filters for character transformation +- Location-based ARG triggers +- QR codes → game state changes""") diff --git a/unreal_exporter.py b/unreal_exporter.py new file mode 100644 index 0000000000000000000000000000000000000000..8665d5cd70e7b7599bf08d3903e28f5a2f6a987f --- /dev/null +++ b/unreal_exporter.py @@ -0,0 +1,771 @@ +""" +Unreal Engine Exporter - Converts game configs to Unreal Python script. + +Generates a Python script that can be run in Unreal's Python console to: +- Create GameState as a Python class +- Create 3D placeholder actors for each location/choice +- Set up navigation and click handling +- Implement the full condition/effect/transition system + +Target: Unreal Engine 5.x with Python plugin enabled +Requires: Editor Scripting Utilities plugin +""" + +import json + + +def generate_gamestate_py(): + """Generate Python GameState class for Unreal.""" + return ''' +# ============================================================ +# GAME STATE +# ============================================================ +class GameState: + """Singleton game state tracking.""" + + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if self._initialized: + return + self._initialized = True + + self.inventory = [] + self.money = 20 + self.flags = {} + self.counters = {} + self.people_met = set() + self.locations_visited = set() + self.current_location = "" + self.current_state = "" + + # ==================== Inventory ==================== + def add_item(self, item): + if item not in self.inventory: + self.inventory.append(item) + log_event("effect_applied", f"add_item={item}") + + def remove_item(self, item): + if item in self.inventory: + self.inventory.remove(item) + log_event("effect_applied", f"remove_item={item}") + + def has_item(self, item): + return item in self.inventory + + def add_items(self, items): + for item in items: + self.add_item(item) + + # ==================== Money ==================== + def add_money(self, amount): + self.money += amount + log_event("effect_applied", f"add_money={amount}") + + def remove_money(self, amount): + self.money -= amount + log_event("effect_applied", f"remove_money={amount}") + + def set_money(self, amount): + self.money = amount + + # ==================== Flags ==================== + def set_flag(self, name, value=True): + self.flags[name] = value + log_event("effect_applied", f"set_flag={name}:{value}") + + def clear_flag(self, name): + self.flags[name] = False + + def toggle_flag(self, name): + self.flags[name] = not self.flags.get(name, False) + + def has_flag(self, name): + return self.flags.get(name, False) + + # ==================== Counters ==================== + def set_counter(self, name, value): + self.counters[name] = value + log_event("effect_applied", f"set_counter={name}:{value}") + + def get_counter(self, name): + return self.counters.get(name, 0) + + def increment_counter(self, name, amount=1): + self.counters[name] = self.counters.get(name, 0) + amount + log_event("effect_applied", f"increment={name}:{amount}") + + def decrement_counter(self, name, amount=1): + self.counters[name] = self.counters.get(name, 0) - amount + + # ==================== People ==================== + def meet_person(self, name): + if name not in self.people_met: + self.people_met.add(name) + log_event("effect_applied", f"meet_person={name}") + + def has_met(self, name): + return name in self.people_met + + # ==================== Locations ==================== + def visit_location(self, name): + self.locations_visited.add(name) + + def has_visited(self, name): + return name in self.locations_visited + + def discover_location(self, name): + self.visit_location(name) + + def has_discovered(self, name): + return self.has_visited(name) + + # ==================== State Summary ==================== + def get_summary(self): + return f"""=== Game State === +Location: {self.current_location} / {self.current_state} +Money: {self.money} +Inventory: {self.inventory} +Flags: {self.flags} +Counters: {self.counters} +People Met: {self.people_met} +""" + + def reset(self): + self.inventory.clear() + self.money = 20 + self.flags.clear() + self.counters.clear() + self.people_met.clear() + self.locations_visited.clear() + self.current_location = "" + self.current_state = "" + + +# Global instance +game_state = GameState() +''' + + +def generate_condition_evaluator_py(): + """Generate Python Condition Evaluator for Unreal.""" + return ''' +# ============================================================ +# CONDITION EVALUATOR +# ============================================================ +def evaluate_condition(condition): + """Evaluate a condition expression against game state.""" + # No condition = always true + if condition is None or condition == {}: + return True + + # String = flag check + if isinstance(condition, str): + return game_state.has_flag(condition) + + if not isinstance(condition, dict): + return False + + # Compound conditions + if "and" in condition: + return all(evaluate_condition(c) for c in condition["and"]) + + if "or" in condition: + return any(evaluate_condition(c) for c in condition["or"]) + + if "not" in condition: + return not evaluate_condition(condition["not"]) + + # Atomic conditions + return _evaluate_atomic(condition) + + +def _evaluate_atomic(condition): + """Evaluate a single atomic condition.""" + # Inventory checks + if "has_item" in condition: + return game_state.has_item(condition["has_item"]) + if "not_has_item" in condition: + return not game_state.has_item(condition["not_has_item"]) + + # Flag checks + if "flag" in condition: + return game_state.has_flag(condition["flag"]) + if "not_flag" in condition: + return not game_state.has_flag(condition["not_flag"]) + + # Money checks + if "money" in condition: + return _compare_numeric(game_state.money, condition["money"]) + + # Counter checks + if "counter" in condition: + for name, check in condition["counter"].items(): + if not _compare_numeric(game_state.get_counter(name), check): + return False + return True + + # People checks + if "met_person" in condition: + return game_state.has_met(condition["met_person"]) + if "not_met_person" in condition: + return not game_state.has_met(condition["not_met_person"]) + + # Location checks + if "visited" in condition: + return game_state.has_visited(condition["visited"]) + if "not_visited" in condition: + return not game_state.has_visited(condition["not_visited"]) + + return True + + +def _compare_numeric(actual, comparison): + """Evaluate numeric comparisons.""" + if isinstance(comparison, (int, float)): + return actual >= comparison + + if isinstance(comparison, dict): + if "gte" in comparison: + return actual >= comparison["gte"] + if "gt" in comparison: + return actual > comparison["gt"] + if "lte" in comparison: + return actual <= comparison["lte"] + if "lt" in comparison: + return actual < comparison["lt"] + if "eq" in comparison: + return actual == comparison["eq"] + if "neq" in comparison: + return actual != comparison["neq"] + + return True +''' + + +def generate_effect_applicator_py(): + """Generate Python Effect Applicator for Unreal.""" + return ''' +# ============================================================ +# EFFECT APPLICATOR +# ============================================================ +def apply_effects(effects): + """Apply a set of effects to the game state.""" + if not effects: + return + + # Inventory effects + if "add_item" in effects: + items = effects["add_item"] + if isinstance(items, list): + game_state.add_items(items) + else: + game_state.add_item(items) + + if "remove_item" in effects: + items = effects["remove_item"] + if isinstance(items, list): + for item in items: + game_state.remove_item(item) + else: + game_state.remove_item(items) + + # Money effects + if "add_money" in effects: + game_state.add_money(effects["add_money"]) + if "remove_money" in effects: + game_state.remove_money(effects["remove_money"]) + if "set_money" in effects: + game_state.set_money(effects["set_money"]) + + # Flag effects + if "set_flag" in effects: + flag = effects["set_flag"] + if isinstance(flag, str): + game_state.set_flag(flag) + elif isinstance(flag, list): + for f in flag: + game_state.set_flag(f) + elif isinstance(flag, dict): + for f, v in flag.items(): + game_state.set_flag(f, v) + + if "clear_flag" in effects: + flags = effects["clear_flag"] + if isinstance(flags, list): + for f in flags: + game_state.clear_flag(f) + else: + game_state.clear_flag(flags) + + if "toggle_flag" in effects: + flags = effects["toggle_flag"] + if isinstance(flags, list): + for f in flags: + game_state.toggle_flag(f) + else: + game_state.toggle_flag(flags) + + # Counter effects + if "set_counter" in effects: + for name, value in effects["set_counter"].items(): + game_state.set_counter(name, value) + + if "increment" in effects: + for name, amount in effects["increment"].items(): + game_state.increment_counter(name, amount) + + if "decrement" in effects: + for name, amount in effects["decrement"].items(): + game_state.decrement_counter(name, amount) + + # People effects + if "add_person" in effects: + people = effects["add_person"] + if isinstance(people, list): + for p in people: + game_state.meet_person(p) + else: + game_state.meet_person(people) + + # Location effects + if "add_location" in effects: + locs = effects["add_location"] + if isinstance(locs, list): + for loc in locs: + game_state.discover_location(loc) + else: + game_state.discover_location(locs) + + if "visit_location" in effects: + locs = effects["visit_location"] + if isinstance(locs, list): + for loc in locs: + game_state.visit_location(loc) + else: + game_state.visit_location(locs) +''' + + +def generate_transition_resolver_py(): + """Generate Python Transition Resolver for Unreal.""" + return ''' +# ============================================================ +# TRANSITION RESOLVER +# ============================================================ +import random + +def resolve_transition(transition): + """Resolve a transition specification to a target state.""" + # Simple string + if isinstance(transition, str): + return transition + + if not isinstance(transition, dict): + return None + + # Weighted random + if "random" in transition: + return _resolve_weighted_random(transition["random"]) + + # Equal probability pool + if "random_from" in transition: + pool = transition["random_from"] + return random.choice(pool) if pool else None + + # Conditional if/then/else + if "if" in transition: + if evaluate_condition(transition["if"]): + return resolve_transition(transition.get("then")) + else: + return resolve_transition(transition.get("else")) + + # Chained conditions + if "conditions" in transition: + for cond in transition["conditions"]: + if "default" in cond: + return resolve_transition(cond["default"]) + if evaluate_condition(cond.get("if")): + return resolve_transition(cond.get("then")) + + return None + + +def _resolve_weighted_random(weights): + """Select from weighted options.""" + if not weights: + return None + + states = [w[0] for w in weights] + probs = [w[1] for w in weights] + total = sum(probs) + + if total <= 0: + return None + + # Normalize + probs = [p / total for p in probs] + return random.choices(states, weights=probs, k=1)[0] +''' + + +def generate_event_logging_py(): + """Generate event logging utilities.""" + return ''' +# ============================================================ +# EVENT LOGGING +# ============================================================ +def log_event(event_type, details): + """Log game events for debugging and feedback.""" + print(f"[GAME_EVENT] {event_type}: {details}") +''' + + +def generate_scene_builder_py(config): + """Generate Python code to build Unreal scene.""" + config_json = json.dumps(config) + + code = f''' +# ============================================================ +# SCENE BUILDER +# ============================================================ +import unreal + +# Game configuration +GAME_CONFIG = {config_json} + +# Actor references +location_actors = {{}} +choice_actors = {{}} +current_location = None + + +def create_scene(): + """Create the full 3D scene from game config.""" + global location_actors, choice_actors + + editor = unreal.EditorLevelLibrary + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + + # Get or create cube mesh for choices + cube_mesh = unreal.EditorAssetLibrary.load_asset("/Engine/BasicShapes/Cube") + plane_mesh = unreal.EditorAssetLibrary.load_asset("/Engine/BasicShapes/Plane") + + state_index = 0 + + for location, states in GAME_CONFIG.items(): + if not isinstance(states, dict): + continue + + for state_name, state_data in states.items(): + if not isinstance(state_data, dict): + continue + + entity_name = f"{{location}}_{{state_name}}" + x_pos = state_index * 2500 # 25 meters apart + + # Create location container actor + loc_actor = editor.spawn_actor_from_class( + unreal.Actor, + unreal.Vector(x_pos, 0, 0) + ) + loc_actor.set_actor_label(f"Loc_{{entity_name}}") + loc_actor.set_actor_hidden_in_game(True) + location_actors[entity_name] = loc_actor + + # Create ground plane + ground = editor.spawn_actor_from_class( + unreal.StaticMeshActor, + unreal.Vector(x_pos, 0, 0) + ) + ground.static_mesh_component.set_static_mesh(plane_mesh) + ground.set_actor_scale3d(unreal.Vector(15, 15, 1)) + ground.set_actor_label(f"Ground_{{entity_name}}") + ground.attach_to_actor(loc_actor, "", unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, False) + + # Create choices + choices = state_data.get("choices", []) + choice_config = state_data.get("choice_config", {{}}) + transitions = state_data.get("transitions", {{}}) + effects = state_data.get("effects", {{}}) + + for idx, choice_text in enumerate(choices): + x_offset = (idx - len(choices)/2) * 300 # 3 meters apart + + # Create choice cube + choice_actor = editor.spawn_actor_from_class( + unreal.StaticMeshActor, + unreal.Vector(x_pos + x_offset, -500, 100) + ) + choice_actor.static_mesh_component.set_static_mesh(cube_mesh) + choice_actor.set_actor_label(f"Choice_{{entity_name}}_{{idx}}") + choice_actor.attach_to_actor(loc_actor, "", unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, False) + + # Store choice data + choice_id = f"{{entity_name}}_choice_{{idx}}" + choice_actors[choice_id] = {{ + "actor": choice_actor, + "text": choice_text, + "condition": choice_config.get(choice_text, {{}}).get("condition"), + "transition": transitions.get(choice_text, ""), + "effects": effects.get(choice_text, {{}}) + }} + + state_index += 1 + + # Create camera + camera = editor.spawn_actor_from_class( + unreal.CameraActor, + unreal.Vector(0, 1000, 500) + ) + camera.set_actor_rotation(unreal.Rotator(-25, -90, 0), False) + camera.set_actor_label("GameCamera") + + # Create directional light + light = editor.spawn_actor_from_class( + unreal.DirectionalLight, + unreal.Vector(0, 0, 1000) + ) + light.set_actor_rotation(unreal.Rotator(-45, -30, 0), False) + light.set_actor_label("GameLight") + + print(f"Created {{len(location_actors)}} locations with {{len(choice_actors)}} total choices") +''' + + return code + + +def generate_game_controller_py(config): + """Generate the game controller logic.""" + # Find starting location/state + first_location = next(iter(config.keys())) + first_state = next(iter(config[first_location].keys())) + + return f''' +# ============================================================ +# GAME CONTROLLER +# ============================================================ +def navigate_to(location, state): + """Navigate to a specific location/state.""" + global current_location + + entity_name = f"{{location}}_{{state}}" + old_location = f"{{game_state.current_location}}_{{game_state.current_state}}" + + log_event("transition", f"{{old_location}} -> {{entity_name}}") + + # Hide current location + if current_location and current_location in location_actors: + location_actors[current_location].set_actor_hidden_in_game(True) + + # Show new location + if entity_name in location_actors: + location_actors[entity_name].set_actor_hidden_in_game(False) + current_location = entity_name + + # Update state + game_state.current_location = location + game_state.current_state = state + game_state.visit_location(location) + + # Apply on_enter effects + state_data = GAME_CONFIG.get(location, {{}}).get(state, {{}}) + if "on_enter" in state_data: + apply_effects(state_data["on_enter"]) + + # Update choice visibility + update_choice_visibility() + + # Print description + if "description" in state_data: + print(f"Description: {{state_data['description']}}") + else: + print(f"Error: Location not found: {{entity_name}}") + + +def update_choice_visibility(): + """Update visibility of choices based on conditions.""" + entity_name = f"{{game_state.current_location}}_{{game_state.current_state}}" + + for choice_id, choice_data in choice_actors.items(): + if choice_id.startswith(entity_name + "_choice_"): + condition = choice_data.get("condition") + visible = evaluate_condition(condition) + choice_data["actor"].set_actor_hidden_in_game(not visible) + + +def select_choice(choice_id): + """Handle choice selection.""" + if choice_id not in choice_actors: + print(f"Choice not found: {{choice_id}}") + return + + choice = choice_actors[choice_id] + + # Check condition + if not evaluate_condition(choice.get("condition")): + print("Condition not met") + return + + log_event("choice_selected", choice["text"]) + + # Apply effects + apply_effects(choice.get("effects", {{}})) + + # Resolve transition + target = resolve_transition(choice.get("transition", "")) + if target: + parts = target.rsplit("_", 1) + if len(parts) >= 2: + navigate_to(parts[0], parts[1]) + + +def start_game(): + """Initialize and start the game.""" + game_state.reset() + create_scene() + navigate_to("{first_location}", "{first_state}") + print("Game started! Use select_choice('location_state_choice_N') to make choices.") + print("Use print(game_state.get_summary()) to see current state.") + + +def list_choices(): + """List available choices for current location.""" + entity_name = f"{{game_state.current_location}}_{{game_state.current_state}}" + print(f"\\nChoices at {{entity_name}}:") + + for choice_id, choice_data in choice_actors.items(): + if choice_id.startswith(entity_name + "_choice_"): + condition_met = evaluate_condition(choice_data.get("condition")) + status = "available" if condition_met else "locked" + print(f" {{choice_id}}: {{choice_data['text']}} [{{status}}]") +''' + + +def generate_usage_instructions(): + """Generate usage instructions for the Unreal script.""" + return ''' +# ============================================================ +# USAGE INSTRUCTIONS +# ============================================================ +""" +Unreal Engine Python Script Usage: + +1. PREREQUISITES: + - Unreal Engine 5.x with Python Editor Script Plugin enabled + - Go to Edit -> Plugins -> search "Python" -> enable "Python Editor Script Plugin" + - Restart the editor + +2. RUNNING THE SCRIPT: + - Open Output Log (Window -> Developer Tools -> Output Log) + - Go to Window -> Developer Tools -> Python Console (or use Output Log's Python input) + - Paste this entire script into the Python console + +3. STARTING THE GAME: + >>> start_game() + +4. PLAYING: + - View current state: + >>> print(game_state.get_summary()) + + - List available choices: + >>> list_choices() + + - Make a choice: + >>> select_choice('location_state_choice_0') + +5. DEBUGGING: + - All game events are logged with [GAME_EVENT] prefix + - Check Output Log for event history + +6. RESETTING: + - To reset and start over: + >>> game_state.reset() + >>> start_game() + +NOTE: This creates placeholder cube meshes. Replace them with your actual 3D assets +by modifying the cube_mesh reference in create_scene(). +""" +''' + + +def export_to_unreal(config_json): + """ + Main export function - converts game config to Unreal Python script. + + Args: + config_json: JSON string of the game config + + Returns: + tuple: (explanation, python_code) + """ + try: + config = json.loads(config_json) + except json.JSONDecodeError as e: + return f"JSON Error: {e}", "" + + # Generate all parts + parts = [ + '"""', + 'UNREAL ENGINE GAME EXPORT', + 'Generated from game config', + 'Paste this into Unreal Python Console', + '"""', + '', + generate_usage_instructions(), + generate_event_logging_py(), + generate_gamestate_py(), + generate_condition_evaluator_py(), + generate_effect_applicator_py(), + generate_transition_resolver_py(), + generate_scene_builder_py(config), + generate_game_controller_py(config), + '', + '# Auto-start when script is executed', + '# Uncomment the line below to auto-start:', + '# start_game()', + '', + 'print("Unreal Game Script loaded!")', + 'print("Run start_game() to begin.")', + ] + + full_code = '\n'.join(parts) + + explanation = """Unreal Engine 5 Export Generated! + +Instructions: +1. Enable the Python Editor Script Plugin in Unreal: + Edit -> Plugins -> search "Python" -> enable "Python Editor Script Plugin" + Restart the editor + +2. Open the Python Console: + Window -> Developer Tools -> Python Console + +3. Paste the generated script into the Python console + +4. Start the game: + >>> start_game() + +5. Play the game: + >>> list_choices() # See available choices + >>> select_choice('location_state_choice_0') # Make a choice + >>> print(game_state.get_summary()) # View state + +The generated code includes: +- GameState: Singleton tracking inventory, money, flags, counters +- Condition Evaluator: Checks conditions (has_item, flag, money, etc.) +- Effect Applicator: Applies effects (add_item, set_flag, increment, etc.) +- Transition Resolver: Handles random/conditional transitions +- Scene Builder: Creates placeholder cube actors for each choice +- Game Controller: Navigation and choice handling + +All events are logged with [GAME_EVENT] prefix for debugging. +""" + + return explanation, full_code diff --git a/world_scenarios.py b/world_scenarios.py new file mode 100644 index 0000000000000000000000000000000000000000..89e8b434e832d003e6cd27ebde81ad119e6362a5 --- /dev/null +++ b/world_scenarios.py @@ -0,0 +1,192 @@ +"""World/Geography scenario data and sequence generation. + +This module provides: +- WORLD_LOCATIONS: Dictionary of location templates by terrain type +- generate_world_sequence: Function to generate exploration sequences +""" + +import json +import random + +# Location templates organized by terrain type +WORLD_LOCATIONS = { + "forest": [ + ("Ancient Grove", "Towering trees block out the sun, their gnarled roots forming natural archways", ["herbs", "wildlife"]), + ("Hidden Glade", "A peaceful clearing where sunlight streams through the canopy", ["spring", "flowers"]), + ("Dark Thicket", "Dense undergrowth and twisted branches create an oppressive maze", ["danger", "secrets"]), + ("Ranger's Outpost", "A wooden platform high in the trees serves as a lookout point", ["shelter", "supplies"]), + ("Sacred Tree", "An enormous tree covered in ancient carvings and offerings", ["magic", "lore"]), + ("Mushroom Circle", "Bioluminescent fungi cast an eerie glow in the darkness", ["alchemy", "fey"]), + ], + "mountain": [ + ("Rocky Pass", "A narrow path winds between sheer cliff faces", ["ambush", "difficult"]), + ("Summit Vista", "The peak offers breathtaking views of the lands below", ["discovery", "rest"]), + ("Cave Entrance", "A dark opening in the mountainside hints at depths unknown", ["exploration", "danger"]), + ("Abandoned Mine", "Rusted equipment and collapsed tunnels tell of past industry", ["treasure", "hazard"]), + ("Mountain Shrine", "A weathered stone altar sits at a windswept overlook", ["blessing", "lore"]), + ("Hot Springs", "Steam rises from naturally heated pools carved in rock", ["healing", "rest"]), + ], + "desert": [ + ("Oasis Haven", "Palm trees and clear water offer respite from the endless sand", ["water", "trade"]), + ("Buried Ruins", "Half-buried stone columns hint at a civilization swallowed by sand", ["treasure", "puzzle"]), + ("Salt Flats", "Blinding white crystals stretch to the horizon under a merciless sun", ["danger", "resource"]), + ("Canyon Shelter", "Deep ravines offer shade and protection from sandstorms", ["shelter", "ambush"]), + ("Nomad Camp", "Colorful tents cluster around a central fire pit", ["trade", "information"]), + ("Mirage Crossing", "Heat shimmers create illusions that confuse travelers", ["hazard", "magic"]), + ], + "coastal": [ + ("Fishing Village", "Simple huts line a beach where boats rest on the sand", ["trade", "information"]), + ("Lighthouse Point", "A tall beacon guides ships past treacherous rocks", ["safety", "mystery"]), + ("Sea Cave", "Waves crash into a cavern that opens to hidden chambers", ["treasure", "danger"]), + ("Shipwreck Cove", "The skeleton of a great vessel lies half-submerged", ["salvage", "ghosts"]), + ("Tidal Pools", "Colorful sea life inhabits shallow rock formations", ["discovery", "alchemy"]), + ("Smuggler's Landing", "A hidden beach accessible only at low tide", ["secrets", "trade"]), + ], + "urban": [ + ("Market Square", "Vendors hawk wares while crowds bustle between stalls", ["trade", "information"]), + ("Noble District", "Grand mansions line cobblestone streets patrolled by guards", ["wealth", "intrigue"]), + ("Thieves' Quarter", "Narrow alleys and shadowy doorways hide illicit dealings", ["danger", "contacts"]), + ("Temple District", "Spires of various faiths rise above incense-filled streets", ["healing", "quests"]), + ("Dockside", "Sailors, merchants, and rogues mingle near moored vessels", ["travel", "rumors"]), + ("Underground", "Tunnels and sewers form a city beneath the city", ["secrets", "danger"]), + ], + "swamp": [ + ("Murky Depths", "Dark water hides what lurks beneath the surface", ["danger", "mystery"]), + ("Witch's Hut", "A crooked dwelling stands on stilts above the mire", ["magic", "trade"]), + ("Sunken Temple", "Ancient stones rise from the swamp, covered in moss", ["lore", "treasure"]), + ("Will-o-Wisp Path", "Dancing lights lead travelers deeper into the bog", ["trap", "magic"]), + ("Hermit's Island", "A small patch of solid ground hosts a reclusive inhabitant", ["information", "quest"]), + ("Dead Woods", "Skeletal trees rise from stagnant water, draped in moss", ["undead", "atmosphere"]), + ], + "tundra": [ + ("Frozen Lake", "A vast expanse of ice stretches beneath gray skies", ["crossing", "danger"]), + ("Ice Caves", "Crystal formations glitter in caverns carved by ancient glaciers", ["beauty", "secrets"]), + ("Mammoth Graveyard", "Enormous bones protrude from the permafrost", ["resources", "spirits"]), + ("Northern Lights Camp", "A sheltered spot where the aurora dances overhead", ["rest", "magic"]), + ("Frost Giant Ruins", "Massive stone structures built for beings twice human height", ["danger", "treasure"]), + ("Hot Vent", "Volcanic heat creates a pocket of warmth in the frozen waste", ["shelter", "unique"]), + ], +} + +# Connection types between locations +CONNECTION_TYPES = [ + "A worn path leads to", + "Through dense terrain you find", + "Following the sound of water reveals", + "Ancient markers point toward", + "Local rumors speak of", + "A hidden trail connects to", +] + + +def generate_world_sequence(location_count, include_forest, include_mountain, include_desert, + include_coastal, include_urban, include_swamp, include_tundra, + force_urban_start=False): + """Generate a world exploration sequence. + + Args: + location_count: Number of locations (3-12) + include_*: Include specific terrain types + force_urban_start: Start the sequence in an urban location + + Returns: + Tuple of (list_output, json_output, prompts_output) + """ + available = [] + if include_forest: + available.extend([(l[0], l[1], "forest", l[2]) for l in WORLD_LOCATIONS["forest"]]) + if include_mountain: + available.extend([(l[0], l[1], "mountain", l[2]) for l in WORLD_LOCATIONS["mountain"]]) + if include_desert: + available.extend([(l[0], l[1], "desert", l[2]) for l in WORLD_LOCATIONS["desert"]]) + if include_coastal: + available.extend([(l[0], l[1], "coastal", l[2]) for l in WORLD_LOCATIONS["coastal"]]) + if include_urban: + available.extend([(l[0], l[1], "urban", l[2]) for l in WORLD_LOCATIONS["urban"]]) + if include_swamp: + available.extend([(l[0], l[1], "swamp", l[2]) for l in WORLD_LOCATIONS["swamp"]]) + if include_tundra: + available.extend([(l[0], l[1], "tundra", l[2]) for l in WORLD_LOCATIONS["tundra"]]) + + if not available: + return "Select at least one terrain type!", "{}", "" + + sequence = [] + + # Force urban start if requested + if force_urban_start and include_urban: + urban_locs = [l for l in available if l[2] == "urban"] + if urban_locs: + start = random.choice(urban_locs) + sequence.append(start) + available = [l for l in available if l != start] + location_count -= 1 + + # Fill with random locations + random.shuffle(available) + sequence.extend(available[:location_count]) + + if not sequence: + return "No locations available!", "{}", "" + + # Format as list + list_output = "## World Exploration Map\n\n" + for i, (name, desc, terrain, features) in enumerate(sequence, 1): + features_str = ", ".join(features) + list_output += f"**{i}. {name}** [{terrain}]\n{desc}\n*Features: {features_str}*\n\n" + + # Format as config JSON + config = {"world": {}} + for i, (name, desc, terrain, features) in enumerate(sequence, 1): + state_name = name.lower().replace(" ", "_").replace("'", "").replace("-", "_") + current_state_id = f"location_{i}_{state_name}" + + features_str = ", ".join(features) + + # Create choices - can go to adjacent locations (2-3 options) + choices = [] + transitions = {} + + # Add exploration option + choices.append("Explore this area") + transitions["Explore this area"] = current_state_id # Stay and explore + + # Add travel options to next locations + num_connections = min(3, len(sequence) - 1) + other_locs = [j for j in range(len(sequence)) if j != i - 1] + random.shuffle(other_locs) + + for j in other_locs[:num_connections]: + next_loc = sequence[j] + next_name = next_loc[0] + next_state_name = next_name.lower().replace(" ", "_").replace("'", "").replace("-", "_") + next_state_id = f"location_{j+1}_{next_state_name}" + + connection = random.choice(CONNECTION_TYPES) + choice_text = f"Travel to {next_name}" + choices.append(choice_text) + transitions[choice_text] = next_state_id + + config["world"][current_state_id] = { + "description": f"[{terrain.upper()}] {desc}\n\nYou notice: {features_str}", + "media_prompt": f"Fantasy landscape: {name} - {terrain} terrain, {desc}, featuring {features_str}", + "choices": choices, + "transitions": transitions + } + + json_output = json.dumps(config, indent=2) + + # Format prompts + prompts_output = "## World Location Prompts\n\n" + for i, (name, desc, terrain, features) in enumerate(sequence, 1): + features_str = ", ".join(features) + prompts_output += f"Fantasy landscape: {name} - {terrain} terrain, {desc}\n" + + return list_output, json_output, prompts_output + + +if __name__ == "__main__": + list_out, json_out, prompts_out = generate_world_sequence( + 5, True, True, False, True, True, False, False, True + ) + print(list_out)