luizbarbedo's picture
Upload folder using huggingface_hub
7fe39f3 verified
Raw
History Blame Contribute Delete
5.68 kB
"""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'<div class="hpbar"><div class="hpfill" style="width:{hp_pct}%"></div></div>'
)
lines = [
"### βš”οΈ Hero",
f"**HP** {state.hp}/{state.max_hp}",
bar,
f"**Level** {state.level} &nbsp; (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()