"""Micro RPG Engine โ€” Gradio app. A text RPG generated live by a small (1B-4B) language model. The model imagines; Python keeps the books. See README.md for the architecture. Run: python app.py Test: MICRORPG_BACKEND=mock python app.py (no weights, no network) """ from __future__ import annotations import os import gradio as gr from engine import GameEngine, GameState, build_backend from engine.parser import TurnResult BACKEND_KIND = os.environ.get("MICRORPG_BACKEND", "transformers") MODEL_ID = os.environ.get("MICRORPG_MODEL", "Qwen/Qwen3-4B-Instruct-2507") # Build the backend once at startup (loading weights can be slow). _backend = build_backend(BACKEND_KIND, MODEL_ID) # --------------------------------------------------------------------------- # # rendering helpers # --------------------------------------------------------------------------- # def render_story(history: list[TurnResult]) -> str: """Render the running story as markdown.""" if not history: return "_Press **Begin Adventure** to enter the world..._" parts = [] for i, turn in enumerate(history): if turn.narrative: parts.append(turn.narrative) if turn.applied: parts.append("> " + " ยท ".join(turn.applied)) return "\n\n".join(parts) def render_stats(state: GameState) -> str: hp_pct = int(100 * state.hp / max(1, state.max_hp)) bar = ( f'
' ) lines = [ "### โš”๏ธ Hero", f"**HP** {state.hp}/{state.max_hp}", bar, f"**Level** {state.level}   (XP {state.xp}/{state.level*10})", f"**Gold** {state.gold} ๐Ÿช™", f"**Location** {state.location}", "", "**Inventory**", ] lines += [f"- {it}" for it in state.inventory] or ["- (empty)"] if state.enemy and state.enemy.alive: e = state.enemy lines += ["", f"### ๐Ÿ—ก๏ธ Combat", f"**{e.name}** โ€” {e.hp}/{e.max_hp} HP"] if state.npcs: lines += ["", "**Known characters**"] lines += [f"- {n.name} ({n.role or n.disposition})" for n in state.npcs.values()] if state.game_over: lines += ["", "### ๐Ÿ’€ **GAME OVER**"] return "\n".join(lines) def choice_updates(choices: list[str]): """Map up to 3 model-proposed choices onto the three choice buttons.""" updates = [] for i in range(3): if i < len(choices): updates.append(gr.update(value=choices[i], visible=True)) else: updates.append(gr.update(visible=False)) return updates # --------------------------------------------------------------------------- # # event handlers (engine lives in gr.State so each browser session is isolated) # --------------------------------------------------------------------------- # def new_game(): engine = GameEngine(_backend) turn = engine.start() return ( engine, render_story(engine.history), render_stats(engine.state), *choice_updates(turn.choices), "", # clear the textbox ) def take_action(engine: GameEngine, action: str): if engine is None: engine = GameEngine(_backend) engine.start() if action and action.strip(): turn = engine.act(action) else: turn = engine.history[-1] if engine.history else engine.start() return ( engine, render_story(engine.history), render_stats(engine.state), *choice_updates(turn.choices), "", ) # --------------------------------------------------------------------------- # # UI # --------------------------------------------------------------------------- # def build_ui(): css_path = os.path.join(os.path.dirname(__file__), "style.css") css = open(css_path, encoding="utf-8").read() if os.path.exists(css_path) else "" with gr.Blocks(css=css, title="Micro RPG Engine", theme=gr.themes.Base()) as demo: engine_state = gr.State(None) gr.Markdown( f"# ๐Ÿ„ Micro RPG Engine\n" f"*A world dreamed up live by a small model โ€” `{MODEL_ID}` " f"({BACKEND_KIND}). No AI, no game.*", elem_id="title-md", ) with gr.Row(): with gr.Column(scale=3): story = gr.Markdown(render_story([]), elem_id="story") with gr.Column(scale=1): stats = gr.Markdown("", elem_id="stats") with gr.Row(): c1 = gr.Button("...", visible=False, variant="secondary") c2 = gr.Button("...", visible=False, variant="secondary") c3 = gr.Button("...", visible=False, variant="secondary") with gr.Row(): action = gr.Textbox( placeholder="...or type your own action and press Enter", show_label=False, elem_id="action-input", scale=4, ) send = gr.Button("Act", variant="primary", scale=1) with gr.Row(): begin = gr.Button("๐ŸŽฒ Begin / Restart Adventure", variant="primary") outputs = [engine_state, story, stats, c1, c2, c3, action] begin.click(new_game, outputs=outputs) send.click(take_action, inputs=[engine_state, action], outputs=outputs) action.submit(take_action, inputs=[engine_state, action], outputs=outputs) # Clicking a choice button sends that choice text as the action. for btn in (c1, c2, c3): btn.click(take_action, inputs=[engine_state, btn], outputs=outputs) return demo if __name__ == "__main__": build_ui().launch()