Spaces:
Runtime error
Runtime error
| """Rune Goblin — Gradio game UI (plan section 12). | |
| Feels like a tiny cursed dungeon, not a chatbot. Runs on the deterministic | |
| rule engine until a fine-tuned adapter is present, then automatically uses | |
| the model (see ``rune_goblin.inference``). | |
| Run:: | |
| uv run python app/app.py | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| from pathlib import Path | |
| # allow running the file directly without installing the package | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) | |
| import gradio as gr # noqa: E402 | |
| from rune_goblin.game import Game # noqa: E402 | |
| from rune_goblin.runelang import GLYPHS # noqa: E402 | |
| USE_MODEL = os.environ.get("RG_USE_MODEL", "0") == "1" | |
| CSS = """ | |
| .gradio-container {background: #0e0b14; color: #e7d9ff;} | |
| #title {text-align:center; font-family: monospace;} | |
| .rune-btn button {font-size: 1.4rem !important; min-width: 64px;} | |
| #log textarea {background:#1a1426; color:#b7f5c2; font-family:monospace;} | |
| """ | |
| def _enemy_md(snap: dict) -> str: | |
| e = snap["enemy"] | |
| bar = "█" * e["hp"] + "░" * (e["max_hp"] - e["hp"]) | |
| return ( | |
| f"### 👹 {e['name']}\n" | |
| f"HP: `{bar}` {e['hp']}/{e['max_hp']}\n\n" | |
| f"Weakness: **{', '.join(e['weakness'])}** · Resists: {', '.join(e['resistance'])}\n\n" | |
| f"Mood: *{e['mood']}*" | |
| ) | |
| def _player_md(snap: dict) -> str: | |
| p = snap["player"] | |
| bar = "❤" * p["hp"] + "·" * (p["max_hp"] - p["hp"]) | |
| inv = ", ".join(p["inventory"]) or "empty" | |
| r = snap["room"] | |
| return ( | |
| f"### 🧙 You — Room {r['index'] + 1}/{r['total']}: {r['name']}\n" | |
| f"HP: {bar} {p['hp']}/{p['max_hp']} · Score: **{snap['score']}**\n\n" | |
| f"Inventory: {inv}" | |
| ) | |
| def new_game(): | |
| game = Game.new(use_text_model=USE_MODEL) | |
| snap = game.snapshot() | |
| return (game, _enemy_md(snap), _player_md(snap), "\n".join(snap["log"]), | |
| "", "*No spell cast yet.*") | |
| def add_rune(selected: str, key: str): | |
| parts = [p for p in selected.split(" + ") if p] | |
| if len(parts) >= 4: | |
| return selected # max 4 runes | |
| parts.append(key) | |
| return " + ".join(parts) | |
| def clear_runes(): | |
| return "" | |
| def cast(game: Game, selected: str): | |
| runes = [p for p in selected.split(" + ") if p] | |
| if not game or game.over or not runes: | |
| snap = game.snapshot() if game else {} | |
| return (game, gr.update(), gr.update(), gr.update(), "", | |
| "*Pick 2–4 runes first (or start a new run).*") | |
| spell = game.cast(runes) | |
| snap = game.snapshot() | |
| result_md = ( | |
| f"### ✨ {spell.spell_name} \n" | |
| f"*{spell.flavor}*\n\n" | |
| f"**Effect:** {spell.effect}\n\n" | |
| + (f"**Side effect:** {spell.side_effect}\n\n" if spell.side_effect else "") | |
| + f"`enemy {spell.enemy_hp_delta:+d}` · `player {spell.player_hp_delta:+d}` · " | |
| f"chaos {spell.chaos}/10 · {', '.join(spell.status_effects) or 'no statuses'}" | |
| ) | |
| return (game, _enemy_md(snap), _player_md(snap), "\n".join(snap["log"]), | |
| "", result_md) | |
| def build() -> gr.Blocks: | |
| with gr.Blocks(css=CSS, title="Rune Goblin", theme=gr.themes.Base()) as demo: | |
| game_state = gr.State() | |
| gr.Markdown("# 🪄 Rune Goblin\nDraw forbidden spells. Regret efficiently.", elem_id="title") | |
| with gr.Row(): | |
| enemy_md = gr.Markdown() | |
| player_md = gr.Markdown() | |
| selected = gr.Textbox(label="Selected spell", interactive=False) | |
| gr.Markdown("#### Rune Board — tap 2–4 runes") | |
| with gr.Row(): | |
| for key, g in GLYPHS.items(): | |
| btn = gr.Button(f"{g.symbol} {g.label}", elem_classes="rune-btn") | |
| btn.click(add_rune, inputs=[selected, gr.State(key)], outputs=selected) | |
| with gr.Row(): | |
| cast_btn = gr.Button("🔮 CAST SPELL", variant="primary") | |
| clear_btn = gr.Button("Clear") | |
| new_btn = gr.Button("New Run") | |
| result_md = gr.Markdown() | |
| log_box = gr.Textbox(label="Dungeon Log", lines=12, interactive=False, elem_id="log") | |
| clear_btn.click(clear_runes, outputs=selected) | |
| cast_btn.click(cast, inputs=[game_state, selected], | |
| outputs=[game_state, enemy_md, player_md, log_box, selected, result_md]) | |
| new_btn.click(new_game, | |
| outputs=[game_state, enemy_md, player_md, log_box, selected, result_md]) | |
| demo.load(new_game, | |
| outputs=[game_state, enemy_md, player_md, log_box, selected, result_md]) | |
| return demo | |
| if __name__ == "__main__": | |
| build().launch(server_name="0.0.0.0", server_port=7860) | |