"""Bridge Troll β€” Gradio app. Each session, Gorm is secretly assigned one of several hidden NATURES. The player wins by discovering what moves THIS troll β€” generic sob stories are discounted. On win (resolve -> 0) or loss (resolve -> LOSE_AT, he hurls you back), a reveal card shows what his nature was. Local loop test (no GPU/download): BRIDGE_TROLL_MOCK=1 python app.py """ from __future__ import annotations import os import gradio as gr from troll_engine import (GameState, START_RESOLVE, LOSE_AT, build_messages, parse_judgment, random_nature) from models import get_backend # ZeroGPU decorator β€” no-op locally. Supports @gpu and @gpu(duration=...). try: import spaces gpu = spaces.GPU except Exception: def gpu(*args, **_kwargs): if args and callable(args[0]): return args[0] return lambda fn: fn _backend = get_backend() @gpu(duration=30) def _generate(messages: list[dict]) -> str: return _backend.generate(messages) INTRO = ("A mossy troll heaves himself upright across the only bridge over the Mirebeck. " '*"None cross Gorm\'s bridge for free, traveller. Give me a reason β€” a *good* one."*') def _meter_html(resolve: int, won: bool, lost: bool) -> str: if won: return ("
GORM HAS STEPPED ASIDE πŸŒ‰
" "
") if lost: return ("
GORM HURLS YOU BACK πŸ’’
" "
") pct = max(0, min(100, round(resolve / START_RESOLVE * 100))) hue = 90 + (1 - pct / 100) * 30 return ("
" f"
Gorm's Resolve β€” {resolve}
" f"
") def _reveal(state: GameState) -> str: if not state.over or not state.nature: return "" n = state.nature if state.won: return (f"### πŸŒ‰ You crossed in {state.turns} turns.\n" f"**This Gorm's hidden nature:** *{n['name']}* β€” moved by {n['soft']}.") return (f"### πŸ’’ Gorm lost patience and hurled you back.\n" f"**His hidden nature was:** *{n['name']}* β€” moved by {n['soft']}. " f"You leaned too hard on what he can't stand: {n['sore']}.") def on_submit(user_text: str, chat: list, state: GameState): user_text = (user_text or "").strip() if not user_text or state.over: return chat, state, _meter_html(state.resolve, state.won, state.lost), "", _reveal(state), gr.update() raw = _generate(build_messages(state, user_text)) j = parse_judgment(raw) state.history.append({"role": "user", "content": user_text}) state.history.append({"role": "assistant", "content": j.reply}) state.apply(j) chat = chat + [{"role": "user", "content": user_text}, {"role": "assistant", "content": j.reply}] why = f"*{j.tactic.value}* Β· {j.reason}" + (f" Β· persuasiveness {j.persuasiveness}/5" if j.tactic.value == "genuine" else "") box = gr.update(interactive=not state.over, placeholder="The bridge is yours." if state.won else ("Gorm has thrown you out." if state.lost else "Speak to Gorm…")) return chat, state, _meter_html(state.resolve, state.won, state.lost), why, _reveal(state), box def on_reset(): state = GameState(nature=random_nature()) chat = [{"role": "assistant", "content": INTRO}] return (chat, state, _meter_html(state.resolve, False, False), "", "", gr.update(interactive=True, value="", placeholder="Speak to Gorm…")) CSS = """ .resolve-wrap { margin: 6px 0 14px; } .resolve-label { font-family: Georgia, serif; font-size: 14px; letter-spacing:.04em; margin-bottom:4px; } .resolve-bar { height: 16px; background:#2a2118; border:1px solid #5a4a32; border-radius:9px; overflow:hidden; } .resolve-fill { height:100%; transition: width .5s ease, background .5s ease; } .resolve-fill.won { background:#caa54a; } .resolve-fill.lost { background:#a33; } #why { font-family: Georgia, serif; opacity:.8; min-height:1.4em; } #reveal { font-family: Georgia, serif; } """ with gr.Blocks(title="Bridge Troll") as demo: gr.Markdown("## πŸ§ŒπŸŒ‰ Bridge Troll\n*Talk your way across β€” if your argument is actually good. " "Every troll is hiding something different.*") if os.environ.get("BRIDGE_TROLL_MOCK") == "1": gr.Markdown("> ⚠️ **MOCK MODE** β€” keyword stub, not the real model. " "Natures, discovery, and probing do NOT work here. " "Run on the Space (no `BRIDGE_TROLL_MOCK`) to play the real Gorm.") meter = gr.HTML(_meter_html(START_RESOLVE, False, False)) chatbot = gr.Chatbot(value=[{"role": "assistant", "content": INTRO}], height=420, show_label=False) why = gr.Markdown("", elem_id="why") reveal = gr.Markdown("", elem_id="reveal") with gr.Row(): box = gr.Textbox(placeholder="Speak to Gorm…", show_label=False, scale=8, autofocus=True) send = gr.Button("Say it", variant="primary", scale=1) reset = gr.Button("New traveller", size="sm") state = gr.State(GameState(nature=random_nature())) outs = [chatbot, state, meter, why, reveal, box] send.click(on_submit, [box, chatbot, state], outs).then(lambda: "", None, box) box.submit(on_submit, [box, chatbot, state], outs).then(lambda: "", None, box) reset.click(on_reset, None, outs) demo.load(on_reset, None, outs) # fresh hidden nature for every visitor if __name__ == "__main__": demo.launch(css=CSS, theme=gr.themes.Soft())