Spaces:
Running on Zero
Running on Zero
| """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.*") | |