""" You Are a Bug - HuggingFace Space app shell. CSV-backed roster UI, character sheet, ability selection, and chat scaffold. Run: python3 app/app.py """ import concurrent.futures import csv import html import json import os import re import tempfile import threading import gradio as gr import engine import roster APP_DIR = os.path.dirname(os.path.abspath(__file__)) ASSETS_DIR = os.path.join(APP_DIR, "test-assets") ROSTER_CSV = os.path.join(APP_DIR, "bug-classes-and-abilities.csv") STAT_KEYS = ["might", "speed", "smarts", "mystique"] MAX_LEVEL = 7 def slugify(value: str) -> str: slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-") return slug or "bug" def parse_level_entry(raw: str) -> dict: text = (raw or "").strip() if ":" in text: name, description = text.split(":", 1) return { "kind": "ability", "name": name.strip(), "description": description.strip(), "text": text, } bonuses = [] for part in text.split(","): match = re.search(r"([+-]?\d+)\s+(Might|Speed|Smarts|Mystique)", part.strip(), re.I) if match: bonuses.append( { "amount": int(match.group(1)), "stat": match.group(2).lower(), } ) return { "kind": "stat", "name": "Stat Growth", "description": text, "bonuses": bonuses, "text": text, } def load_roster(csv_path: str) -> list[dict]: with open(csv_path, newline="", encoding="utf-8") as handle: rows = list(csv.reader(handle)) names = [cell.strip() for cell in rows[0][1:] if cell.strip()] bugs = [ { "id": slugify(name), "name": name, "description": "", "hidden_description": "", "hp": 0, "moxie": 0, "stats": {}, "levels": [], } for name in names ] row_map = {} for row in rows[1:]: if not row: continue key = row[0].strip().rstrip(":") row_map[key.lower()] = row[1:] for idx, bug in enumerate(bugs): bug["description"] = cell_at(row_map, "description", idx) bug["hidden_description"] = cell_at(row_map, "hidden_description", idx) bug["hp"] = int(cell_at(row_map, "hp", idx) or 0) bug["moxie"] = int(cell_at(row_map, "moxie", idx) or 0) bug["stats"] = { "might": int(cell_at(row_map, "might", idx) or 0), "speed": int(cell_at(row_map, "speed", idx) or 0), "smarts": int(cell_at(row_map, "smarts", idx) or 0), "mystique": int(cell_at(row_map, "mystique", idx) or 0), } bug["levels"] = [ {"level": level, **parse_level_entry(cell_at(row_map, f"level {level}", idx))} for level in range(1, MAX_LEVEL + 1) ] return bugs def cell_at(row_map: dict, key: str, idx: int) -> str: row = row_map.get(key.lower(), []) return row[idx].strip() if idx < len(row) else "" BUGS = load_roster(ROSTER_CSV) BUG_BY_ID = {bug["id"]: bug for bug in BUGS} DEFAULT_BUG_ID = BUG_BY_ID.get("stag-beetle", BUGS[0])["id"] def h(value) -> str: return html.escape(str(value), quote=True) def stat_value_after_levels(bug: dict, stat: str, current_level: int) -> int: value = bug["stats"].get(stat, 0) for row in bug["levels"]: if row["level"] > current_level or row["kind"] != "stat": continue for bonus in row.get("bonuses", []): if bonus["stat"] == stat: value += bonus["amount"] return value def stat_bonus_text(row: dict) -> str: bonuses = row.get("bonuses", []) if not bonuses: return h(row.get("text", "")) parts = [] for bonus in bonuses: sign = "+" if bonus["amount"] >= 0 else "" parts.append(f"{sign}{bonus['amount']} {bonus['stat'].capitalize()}") return ", ".join(parts) def resource_bar(label: str, value: int, max_value: int, accent: str) -> str: segs = "".join( f'
' for i in range(max_value) ) if max_value else "" return ( 'Level {current_level} Character Sheet
' f'{h(bug["description"])}
' 'Resources
' 'Stats
' f'Level Progression
' f'Level {sheet["level"]}
' f'{h(bug["description"])}
' 'Resources
' 'Stats
' f'{turns_remaining} {turn_word} remaining today
' def render_pending_line(active_ability: str | None) -> str: if not active_ability: return "" return ( 'Failure is fun. Constraints are enabling. Small is huge.
You are a bug.
Choose your character:
""" CSS = """ /* Belt-and-braces font load: the link may not be injected inside the HF Spaces iframe, but the CSS payload always is. */ @import url('https://fonts.googleapis.com/css2?family=Gluten:wght@400;700&family=Dongle:wght@300;400;700&family=Varela+Round&family=DM+Mono:ital,wght@0,400;0,500;1,400&display=swap'); :root { --bg-void: #0d0b08; --ink: #1c180f; --parchment: #e8dfc8; --parchment-dark: #c9bfa6; --moss: #4a5e3a; --moss-dark: #203018; --moss-deep: rgba(18,35,20,0.84); --rust: #8b3a2a; --amber: #c47c1a; --dust: rgba(232,223,200,0.07); --shadow: rgba(13,11,8,0.6); --font-hero: 'Gluten', cursive; --font-d: 'Dongle', sans-serif; --font-body: 'Varela Round', sans-serif; --font-m: 'DM Mono', 'Courier Prime', monospace; /* Organic blob radii: four unequal corners, unequal h/v per corner, so surfaces read hand-cut rather than geometric. Two variants so adjacent elements don't visibly share a shape. */ --blob-lg: 1.7rem 0.7rem 1.5rem 0.8rem / 0.8rem 1.6rem 0.7rem 1.7rem; --blob-md: 1.1rem 0.5rem 1.2rem 0.45rem / 0.5rem 1.1rem 0.45rem 1.2rem; --blob-btn: 0.95rem 0.45rem 1.05rem 0.4rem / 0.45rem 0.95rem 0.4rem 1.05rem; } html, body { background: var(--bg-void) !important; margin: 0; padding: 0; } gradio-app, gradio-app > div { background: transparent !important; } .gradio-container { background: transparent !important; position: relative; z-index: 1; min-height: 100vh; } .gradio-container > div, .gradio-container .wrap, .gradio-container .contain, .gradio-container section, .gradio-container .block, .gradio-container .panel, .gradio-container form, .gradio-container fieldset { background: transparent !important; border: none !important; box-shadow: none !important; } footer, .gradio-container footer { display: none !important; } #app-shell { max-width: 1080px; margin: 0 auto; padding: 0 1rem 2.5rem; } #title-inner { text-align: center; padding: 3rem 1rem 0.95rem; } #title-inner h1 { font-family: var(--font-hero); font-size: clamp(2.4rem, 5vw, 4.4rem); color: var(--moss-dark); text-shadow: 0 1px 0 rgba(232,223,200,0.35), 0 2px 12px rgba(232,223,200,0.22); margin: 0; letter-spacing: 0; } /* Wavy panel edges: the surface itself stays transparent; its skin lives on a ::before layer so the displacement filter waves the border without warping the text on top. */ .surface-panel, #control-surface, #chat-panel, #active-abilities-panel { position: relative !important; z-index: 0 !important; background: transparent !important; border: none !important; color: var(--parchment); } .surface-panel::before, #control-surface::before, #chat-panel::before, #active-abilities-panel::before, .ingame-header::before { content: ''; position: absolute; inset: 0; z-index: -1; background: var(--moss-deep); border: 1px solid rgba(201,191,166,0.18); border-radius: var(--blob-lg); box-shadow: 0 18px 50px rgba(13,11,8,0.18); filter: url(#squiggle-panel); } #control-surface { padding: 1.15rem 1.25rem !important; margin-bottom: 0.85rem; } #intro-copy p { font-family: var(--font-body); color: var(--parchment); font-size: 1.2rem; line-height: 1.75; margin: 0 auto 0.95rem; max-width: 48ch; text-align: center; } .choose-label { font-family: var(--font-d); font-size: 2rem; color: var(--parchment); text-align: left; text-transform: none; letter-spacing: 0; margin: 0 0 0.5rem; line-height: 1.1; } #bug-btn-row { display: grid !important; grid-template-columns: repeat(3, 1fr) !important; gap: 0.5rem !important; padding: 0 !important; } #bug-btn-row > div { width: 100% !important; flex: none !important; min-width: 0 !important; } /* Button skins live on ::before — kept as a layer so per-state styling stays separable from the label. */ #bug-btn-row button { width: 100% !important; position: relative !important; z-index: 0; background: transparent !important; color: var(--parchment) !important; border: none !important; font-family: var(--font-body) !important; font-size: 0.84rem !important; letter-spacing: 0.04em !important; text-transform: uppercase; padding: 0.55rem 0.5rem !important; } #bug-btn-row button::before { content: ''; position: absolute; inset: 0; z-index: -1; background: rgba(232,223,200,0.13); border: 1px solid rgba(232,223,200,0.28); border-radius: var(--blob-btn); } #bug-btn-row button:hover { color: #fff9ea !important; } #bug-btn-row button:hover::before { background: rgba(232,223,200,0.24); } #bug-btn-row .bug-btn-selected { color: var(--bg-void) !important; } #bug-btn-row .bug-btn-selected::before { background: var(--amber); border-color: var(--amber); box-shadow: 0 0 10px rgba(196,124,26,0.45); } #main-row { align-items: flex-start !important; } #chat-col { gap: 0.85rem !important; } .sheet-body, #chat-panel, #active-abilities-panel { padding: 1.2rem 1.3rem; } .eyebrow, .sec-label { font-family: var(--font-m); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.13em; color: var(--parchment); margin: 0 0 0.55rem; } .sec-label { border-bottom: 1px solid rgba(201,191,166,0.18); padding-bottom: 0.3rem; } .sheet-name { font-family: var(--font-d), 'Dongle', sans-serif !important; color: var(--parchment); font-size: 2.4rem !important; line-height: 1.1; margin: 0 0 0.15rem; letter-spacing: 0; } .sheet-desc { font-family: var(--font-body); color: var(--parchment-dark); font-size: 1.2rem; line-height: 1.65; margin: 0.2rem 0 1rem; } .sheet-head { padding-bottom: 0; margin-bottom: 0; } .sheet-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1.4rem; margin-bottom: 1rem; } .res-wrap { margin-bottom: 0; } .stat-row, .res-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.4rem; } .res-label { font-family: var(--font-body); font-size: 1.05rem; color: var(--parchment); min-width: 3.2rem; } .res-segs { flex: 1; display: flex; gap: 2px; align-items: center; } .res-seg { flex: 1; min-width: 4px; height: 9px; border-radius: 1px; } .res-seg.empty { background: rgba(201,191,166,0.15); } .res-frac { font-family: var(--font-body); color: var(--parchment); text-align: right; font-size: 1.05rem; min-width: 3.5rem; } .stat-list { display: grid; grid-template-columns: max-content auto; column-gap: 0.75rem; } .stat-item { display: contents; } .stat-name, .stat-val { border-bottom: 1px solid rgba(201,191,166,0.12); padding: 0.38rem 0; } .stat-name { font-family: var(--font-body); font-size: 1.05rem; color: var(--parchment-dark); } .stat-val { font-family: var(--font-body); font-size: 1.15rem; color: var(--parchment); font-weight: 600; text-align: right; } .progression-label { margin-top: 1.1rem; } .progression-list, .active-ability-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.55rem; } .level-card { height: 100%; min-height: 5.35rem; background: rgba(232,223,200,0.07); border: 1px solid rgba(201,191,166,0.12); border-left: 3px solid var(--moss); border-radius: var(--blob-md); padding: 0.58rem 0.65rem; } .active-ability-card { flex: 1; min-width: 0; } .active-ability-selected { background: rgba(196,124,26,0.12) !important; border-color: rgba(196,124,26,0.34) !important; border-left-color: var(--amber) !important; } .level-locked { border-left-color: rgba(201,191,166,0.22); } .level-meta { display: flex; flex-wrap: wrap; gap: 0.35rem; font-family: var(--font-m); color: var(--parchment-dark); font-size: 0.76rem; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 0.3rem; } .lock-note { color: var(--parchment-dark); margin-left: auto; } .level-body strong { display: block; color: var(--parchment); font-family: var(--font-d); font-size: 1.75rem; margin-bottom: 0.1rem; line-height: 1.1; } .level-body span { color: var(--parchment-dark); font-family: var(--font-body); font-size: 0.92rem; line-height: 1.45; } .active-ability-row { align-items: center !important; gap: 0 !important; margin-bottom: 0.45rem; background: rgba(232,223,200,0.07) !important; border: 1px solid rgba(201,191,166,0.12) !important; border-left: 3px solid var(--moss) !important; border-radius: var(--blob-md) !important; padding: 0.58rem 0.65rem !important; min-height: 5.35rem; } .active-ability-row:last-child { margin-bottom: 0; } .activate-btn { align-self: center; flex-grow: 0 !important; flex-shrink: 0; position: relative !important; z-index: 0; background: transparent !important; color: var(--moss) !important; border: none !important; font-family: var(--font-body) !important; font-size: 0.9rem !important; letter-spacing: 0.02em; min-width: 8.5rem !important; width: auto !important; padding: 0.4rem 0.55rem !important; margin-left: 0.65rem !important; } .activate-btn::before { content: ''; position: absolute; inset: 0; z-index: -1; background: var(--parchment); border: 1px solid rgba(74,94,58,0.4); border-radius: var(--blob-btn); } .activate-btn:hover:not(:disabled) { color: var(--parchment) !important; } .activate-btn:hover:not(:disabled)::before { background: var(--moss); } .activate-btn:disabled { opacity: 0.35 !important; cursor: not-allowed !important; } .activate-btn-active { color: var(--bg-void) !important; } .activate-btn-active::before { background: var(--amber); border-color: var(--amber); } #confirm-btn { background: transparent !important; color: var(--parchment) !important; border: none !important; font-family: var(--font-body) !important; font-size: 0.95rem !important; letter-spacing: 0.04em !important; } #start-btn-row { justify-content: center !important; margin: 0.85rem 0 !important; padding: 0 !important; gap: 0 !important; flex-grow: 0 !important; } #start-btn-row > * { flex-grow: 0 !important; flex-basis: auto !important; width: auto !important; min-width: 0 !important; } #start-btn, #start-btn > button, button#start-btn { background: transparent !important; color: var(--parchment) !important; border: none !important; font-family: var(--font-body) !important; font-size: 1.15rem !important; letter-spacing: 0.04em !important; padding: 1rem 2.5rem !important; width: auto !important; min-width: 0 !important; flex-grow: 0 !important; } /* Shared moss-button skin layer (wavy edges, crisp labels). */ #confirm-btn, #start-btn, #quit-btn, #export-btn, #start-btn > button, #quit-btn > button, #export-btn > button { position: relative !important; z-index: 0; } #confirm-btn::before, #start-btn::before, #quit-btn::before, #export-btn::before, #start-btn > button::before, #quit-btn > button::before, #export-btn > button::before { content: ''; position: absolute; inset: 0; z-index: -1; background: var(--moss); border-radius: var(--blob-btn); } #confirm-btn:hover::before, #start-btn:hover::before, #quit-btn:hover::before, #export-btn:hover::before, #start-btn > button:hover::before, #quit-btn > button:hover::before, #export-btn > button:hover::before { background: #3a4d2d; } #quit-btn { justify-content: center !important; flex-grow: 0 !important; width: auto !important; margin: 0.85rem auto !important; } #quit-btn.hide { display: none !important; } #quit-btn, #quit-btn > button, button#quit-btn { background: transparent !important; color: var(--parchment) !important; border: none !important; font-family: var(--font-body) !important; font-size: 1.15rem !important; letter-spacing: 0.04em !important; padding: 1rem 1.6rem !important; width: auto !important; min-width: 0 !important; flex-grow: 0 !important; } #export-row { justify-content: center !important; margin: 0.85rem auto !important; gap: 0 !important; flex-grow: 0 !important; } #export-row > * { flex-grow: 0 !important; flex-basis: auto !important; width: auto !important; min-width: 0 !important; } #export-btn, #export-btn > button, button#export-btn { background: transparent !important; color: var(--parchment) !important; border: none !important; font-family: var(--font-body) !important; font-size: 1.15rem !important; letter-spacing: 0.04em !important; padding: 1rem 1.6rem !important; width: auto !important; min-width: 0 !important; flex-grow: 0 !important; } #game-chat { background: rgba(13,11,8,0.55) !important; border: 1px solid rgba(201,191,166,0.12) !important; border-radius: var(--blob-md) !important; } #game-chat .icon-button-wrapper { display: none !important; } #game-chat .message-wrap, #game-chat .message { font-family: var(--font-body) !important; font-size: 1.0rem !important; } #game-chat .bot { background: var(--parchment) !important; border: none !important; border-radius: 1rem 0.55rem 0.95rem 0.3rem / 0.6rem 1rem 0.35rem 0.9rem !important; } #game-chat .bot, #game-chat .bot * { color: var(--ink) !important; line-height: 1.65; } #game-chat .user { background: var(--bg-void) !important; border: 1px solid rgba(201,191,166,0.12) !important; border-radius: 0.55rem 1rem 0.3rem 0.95rem / 1rem 0.6rem 0.9rem 0.35rem !important; } #game-chat .user, #game-chat .user * { color: var(--parchment) !important; } #input-row { align-items: flex-end !important; gap: 0.5rem !important; margin-top: 0.65rem; } #player-input label > span:first-child, #player-input [data-testid="block-info"] { display: none !important; } #player-input textarea, #player-input input { background: rgba(13,11,8,0.82) !important; color: var(--parchment) !important; border: 1px solid rgba(201,191,166,0.18) !important; border-radius: var(--blob-md) !important; font-family: var(--font-body) !important; font-size: 1.05rem !important; line-height: 1.5; } #player-input textarea::placeholder, #player-input input::placeholder { color: rgba(201,191,166,0.55) !important; } #player-input textarea:focus, #player-input input:focus { border-color: var(--moss) !important; outline: none !important; box-shadow: none !important; } .turn-counter { font-family: var(--font-m); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.13em; color: var(--parchment-dark); text-align: right; margin: 0.45rem 0 0; } .turn-prompt { border-left: 4px solid var(--amber); background: rgba(196,124,26,0.1); color: var(--parchment); font-family: var(--font-body); font-size: 1.05rem; line-height: 1.5; padding: 0.62rem 0.85rem; margin: 0.55rem 0 0; border-radius: 0 0.9rem 0.8rem 0 / 0 0.7rem 1rem 0; } #active-abilities-panel .sec-label { margin-bottom: 0.75rem; } .ingame-header { position: relative; z-index: 0; padding: 1.2rem 1.3rem; margin-bottom: 0; } @media (max-width: 840px) { .sheet-grid, .progression-list, .active-ability-list { grid-template-columns: 1fr; } #chat-col { min-width: 100% !important; } #input-row { flex-direction: column !important; align-items: stretch !important; } } """ def build_bg_script(assets_dir: str) -> str: return f""" function injectBgLayers() {{ if (!document.getElementById('squiggle-defs')) {{ // SVG displacement filter for the panel ::before skin layers: waves the // surface edges hand-drawn-style without touching the content on top. const defs = document.createElement('div'); defs.id = 'squiggle-defs'; defs.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden;'; defs.innerHTML = ''; document.body.appendChild(defs); }} if (document.getElementById('bg-layers')) return; const A = '/gradio_api/file={assets_dir}'; const bg = document.createElement('div'); bg.id = 'bg-layers'; bg.style.cssText = 'position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden;'; const base = document.createElement('div'); base.style.cssText = 'position:absolute;inset:0;opacity:1;' + 'background-image:url(' + A + '/tile-background.png);' + 'background-repeat:repeat;background-size:auto;'; bg.appendChild(base); [ {{ id:'bg-mid', file:'flowers-and-grasses-3-scaled.png', dur:'120s', opacity:'0.8' }}, {{ id:'bg-near', file:'peonies-2-scaled.png', dur:'70s', opacity:'0.7' }}, {{ id:'bg-fore', file:'procession-bugs-1-scaled.png', dur:'40s', opacity:'0.55' }} ].forEach(function(cfg) {{ const div = document.createElement('div'); div.id = cfg.id; div.style.cssText = 'position:absolute;inset:0;opacity:' + cfg.opacity + ';'; bg.appendChild(div); const url = A + '/' + cfg.file; const img = new Image(); img.onload = function() {{ const scaledW = Math.ceil(window.innerHeight * this.naturalWidth / this.naturalHeight); div.style.backgroundImage = 'url(' + url + ')'; div.style.backgroundRepeat = 'repeat-x'; div.style.backgroundSize = scaledW + 'px 100%'; div.style.backgroundPosition = '0 center'; const kf = 'drift_' + cfg.id; const style = document.createElement('style'); style.textContent = '@keyframes ' + kf + ' {{ from {{ background-position-x: 0px }} to {{ background-position-x: -' + scaledW + 'px }} }}'; document.head.appendChild(style); div.style.animation = kf + ' ' + cfg.dur + ' linear infinite'; }}; img.src = url; }}); document.body.insertBefore(bg, document.body.firstChild); }} if (document.readyState === 'loading') {{ document.addEventListener('DOMContentLoaded', injectBgLayers); }} else {{ injectBgLayers(); }} """ BG_SCRIPT = build_bg_script(ASSETS_DIR) JS_ON_LOAD = "() => { " + BG_SCRIPT + " }" HEAD = f""" """ default_bug = BUG_BY_ID[DEFAULT_BUG_ID] default_level = 1 with gr.Blocks(title="You Are a Bug") as demo: selected_bug = gr.State(default_bug) current_level = gr.State(default_level) active_ability = gr.State(None) game_started = gr.State(False) game = gr.State(None) world_deck = gr.State(None) # world states, shuffled once per session loading = gr.State(None) # loading strings deck + cycle index with gr.Column(elem_id="app-shell"): gr.HTML(TITLE_HTML, elem_id="title-block") with gr.Column(elem_id="control-surface") as control_surface_col: gr.HTML(INTRO_HTML, elem_id="intro-block") with gr.Row(elem_id="bug-btn-row"): bug_btns = [ gr.Button( bug["name"], elem_id=f"btn-{bug['id']}", elem_classes=["bug-btn-selected"] if bug["id"] == DEFAULT_BUG_ID else [], ) for bug in BUGS ] with gr.Row(elem_id="main-row"): with gr.Column(scale=1, min_width=420, elem_id="chat-col"): sheet_summary = gr.HTML(render_sheet_summary(default_bug, default_level)) with gr.Row(elem_id="start-btn-row"): start_btn = gr.Button("Start Game", elem_id="start-btn") quit_btn = gr.Button("Quit & Return", elem_id="quit-btn", visible=False) ingame_header = gr.HTML("", visible=False) with gr.Column(elem_id="chat-panel", visible=False) as chat_panel_col: chatbot = gr.Chatbot( value=[ { "role": "assistant", "content": "Choose a bug, review your sheet, then start the game.", } ], elem_id="game-chat", show_label=False, height=510, buttons=[], group_consecutive_messages=False, ) with gr.Row(elem_id="input-row"): player_input = gr.Textbox( placeholder="Start the game before taking a turn...", show_label=False, elem_id="player-input", lines=2, scale=5, interactive=False, max_length=300, ) confirm_btn = gr.Button( "Confirm Turn", elem_id="confirm-btn", scale=1, interactive=False, ) turn_counter = gr.HTML("", elem_id="turn-counter", visible=False) pending_line = gr.HTML("", elem_id="pending-line") with gr.Column(elem_id="active-abilities-panel", visible=False) as active_abilities_col: gr.HTML('Active Abilities
') active_components = [] for idx in range(MAX_ACTIVE_ABILITY_SLOTS): rows = active_ability_rows(default_bug, default_level) visible = idx < len(rows) with gr.Row(elem_classes=["active-ability-row"]): row_html = gr.HTML( render_active_ability_card(rows[idx], None) if visible else "", elem_id=f"active-ability-{idx + 1}", scale=5, ) row_button = gr.Button( "Activate", visible=visible, interactive=visible, elem_id=f"ability-activate-{idx + 1}", elem_classes=["activate-btn"], scale=0, min_width=0, ) active_components.extend([row_html, row_button]) with gr.Row(elem_id="export-row", visible=False) as export_row: export_btn = gr.Button("Export Trace", elem_id="export-btn") export_file = gr.File(label=None, visible=False) select_outputs = ( [selected_bug, active_ability, sheet_summary] + active_components + [pending_line] + bug_btns ) for bug in BUGS: bug_btns[BUGS.index(bug)].click( fn=make_select_bug_handler(bug["id"]), inputs=[current_level], outputs=select_outputs, show_progress="hidden", ) activate_outputs = [active_ability] + active_components + [pending_line] for idx, component in enumerate(active_components): if idx % 2 == 1: slot_idx = idx // 2 component.click( fn=make_activate_handler(slot_idx), inputs=[selected_bug, current_level, active_ability, game], outputs=activate_outputs, show_progress="hidden", ) toggle_outputs = [ game_started, chatbot, player_input, confirm_btn, control_surface_col, sheet_summary, start_btn, quit_btn, ingame_header, turn_counter, chat_panel_col, active_abilities_col, game, world_deck, loading, export_row, ] + active_components export_btn.click(fn=export_trace, inputs=[game], outputs=[export_file]) start_btn.click( fn=start_game, inputs=[selected_bug, world_deck, loading], outputs=toggle_outputs, show_progress="hidden", ) quit_btn.click( fn=quit_game, inputs=[], outputs=toggle_outputs, show_progress="hidden", ) turn_inputs = [ player_input, chatbot, selected_bug, current_level, active_ability, game_started, game, world_deck, loading, ] turn_outputs = ( [chatbot, player_input, confirm_btn, active_ability] + active_components + [pending_line, ingame_header, turn_counter, game, world_deck, loading, current_level] ) confirm_btn.click( fn=confirm_turn, inputs=turn_inputs, outputs=turn_outputs, show_progress="hidden", ) player_input.submit( fn=confirm_turn, inputs=turn_inputs, outputs=turn_outputs, show_progress="hidden", ) if __name__ == "__main__": # Warm the model in the background so the first Start Game isn't a cold load. threading.Thread(target=engine.warmup, daemon=True).start() demo.launch( css=CSS, js=JS_ON_LOAD, head=HEAD, allowed_paths=[ASSETS_DIR], )