prime-rush-env / app.py
IndusHomie's picture
Add a Findings write-up: what we ran, what happened, and what broke
fbd4c33 verified
Raw History Blame Contribute Delete
26.4 kB
"""Hugging Face Space: watch an agent play Prime Rush through MCP tools.
Honest about what it is. The six tools, their schemas, the scoring and the
outcome file are the real ones - this Space imports the same ``indus_mcp``
package a lab would run. What it is NOT is the real game: a Space has no
Unity server, so the tools here drive ``tests/fake_game.py``, the stand-in
that speaks the same bridge protocol and reproduces the real game's response
characteristics, so behaviour here matches behaviour there.
The map is drawn from the tool responses alone - position, circle centre,
circle radius - which is the point: everything the viewer sees is everything
the agent was told. Nothing is rendered that the agent did not have.
"""
from __future__ import annotations
import html
import json
import math
import os
import sys
import tempfile
from pathlib import Path
import gradio as gr
from findings import FINDINGS, FINDINGS_CSS
sys.path.insert(0, str(Path(__file__).resolve().parent))
BUDGET = 100.0
INK, INK3 = "#15161a", "#8a8d94"
BLUE, ORANGE = "#2a78d6", "#eb6834"
GRID, SURF = "#e8e6e1", "#ffffff"
# ---------------------------------------------------------------- the map
def render_map(path: list[tuple[float, float]], sit: dict | None) -> str:
"""Top-down view, drawn only from what the tools reported."""
W = H = 420
if not sit:
return (f'<div class="pr-map"><svg viewBox="0 0 {W} {H}">'
f'<rect width="{W}" height="{H}" fill="{SURF}"/>'
f'<text x="{W/2}" y="{H/2}" text-anchor="middle" fill="{INK3}"'
f' font-size="13" font-family="ui-sans-serif,system-ui">'
f'press play</text></svg></div>')
cx, cz = sit["zone_center_x"], sit["zone_center_z"]
r = max(sit["zone_radius"], 1.0)
nr = max(sit.get("next_zone_radius") or r, 1.0)
px, pz = sit["position_x"], sit["position_z"]
# Frame the circle, the next circle and the whole path walked so far.
xs = [cx - r, cx + r, px] + [p[0] for p in path]
zs = [cz - r, cz + r, pz] + [p[1] for p in path]
pad = max(40.0, 0.08 * max(max(xs) - min(xs), max(zs) - min(zs)))
x0, x1 = min(xs) - pad, max(xs) + pad
z0, z1 = min(zs) - pad, max(zs) + pad
span = max(x1 - x0, z1 - z0, 1.0)
x0 = (x0 + x1) / 2 - span / 2
z0 = (z0 + z1) / 2 - span / 2
k = W / span
def sx(x: float) -> float: return (x - x0) * k
def sy(z: float) -> float: return H - (z - z0) * k # +z is up
inside = bool(sit.get("inside_safe_zone"))
trail = " ".join(f"{sx(a):.1f},{sy(b):.1f}" for a, b in path)
ncx = sit.get("next_zone_center_x", cx)
ncz = sit.get("next_zone_center_z", cz)
bar = 100.0
while bar * k < 48:
bar *= 2
grid = "".join(
f'<line x1="{i*W/6:.0f}" y1="0" x2="{i*W/6:.0f}" y2="{H}"/>'
f'<line x1="0" y1="{i*H/6:.0f}" x2="{W}" y2="{i*H/6:.0f}"/>'
for i in range(1, 6))
return f"""<div class="pr-map"><svg viewBox="0 0 {W} {H}" role="img"
aria-label="top-down view of the agent and the safe circle">
<defs><mask id="outside">
<rect width="{W}" height="{H}" fill="white"/>
<circle cx="{sx(cx):.1f}" cy="{sy(cz):.1f}" r="{r*k:.1f}" fill="black"/>
</mask></defs>
<rect width="{W}" height="{H}" fill="{SURF}"/>
<g stroke="{GRID}" stroke-width="1">{grid}</g>
<rect width="{W}" height="{H}" fill="{ORANGE}" opacity=".07" mask="url(#outside)"/>
<circle cx="{sx(ncx):.1f}" cy="{sy(ncz):.1f}" r="{nr*k:.1f}"
fill="none" stroke="{INK3}" stroke-width="1.2" stroke-dasharray="4 4"/>
<circle cx="{sx(cx):.1f}" cy="{sy(cz):.1f}" r="{r*k:.1f}"
fill="{BLUE}" fill-opacity=".05" stroke="{ORANGE}" stroke-width="2"/>
<circle cx="{sx(cx):.1f}" cy="{sy(cz):.1f}" r="3" fill="{ORANGE}"/>
<polyline points="{trail}" fill="none" stroke="{BLUE}" stroke-width="2"
stroke-linejoin="round" stroke-linecap="round" opacity=".5"/>
<circle cx="{sx(px):.1f}" cy="{sy(pz):.1f}" r="6"
fill="{BLUE if inside else ORANGE}" stroke="{SURF}" stroke-width="2.5"/>
<g font-family="ui-sans-serif,system-ui" font-size="10" fill="{INK3}">
<line x1="14" y1="{H-16}" x2="{14+bar*k:.0f}" y2="{H-16}" stroke="{INK3}" stroke-width="1.5"/>
<line x1="14" y1="{H-20}" x2="14" y2="{H-12}" stroke="{INK3}" stroke-width="1.5"/>
<line x1="{14+bar*k:.0f}" y1="{H-20}" x2="{14+bar*k:.0f}" y2="{H-12}" stroke="{INK3}" stroke-width="1.5"/>
<text x="14" y="{H-24}">{bar:.0f} units</text>
<text x="{W-14}" y="20" text-anchor="end">+z &#8593;</text>
</g>
<g font-family="ui-sans-serif,system-ui" font-size="11">
<text x="14" y="22" fill="{INK}" font-weight="600">{
'INSIDE the circle' if inside else 'OUTSIDE &#8212; taking storm damage'}</text>
<text x="14" y="38" fill="{INK3}">radius {r:.0f} &#183; {
sit.get('seconds_remaining', 0):.0f}s left &#183; health {
(sit.get('health') or 0)*100:.0f}%</text>
</g>
</svg></div>"""
def render_calls(items: list[dict]) -> str:
if not items:
return '<div class="pr-feed"><p class="pr-dim">tool calls appear here</p></div>'
rows = []
for it in items[-16:]:
args = ", ".join(f"{k}={v}" for k, v in (it["args"] or {}).items())
cls = "pr-call pr-free" if it["free"] else "pr-call"
rows.append(
f'<div class="{cls}"><div class="pr-name">{html.escape(it["name"])}'
f'<span class="pr-args">({html.escape(args)})</span></div>'
f'<div class="pr-out">{html.escape(it["text"])}</div></div>')
return '<div class="pr-feed">' + "".join(rows) + "</div>"
def render_stats(o: dict | None) -> str:
o = o or {}
def tile(k: str, v) -> str:
return f'<div class="pr-tile"><span class="k">{k}</span><span class="v">{v}</span></div>'
status = "alive" if o.get("alive") else ("dead" if o.get("finished") else "&#8212;")
return ('<div class="pr-tiles">'
+ tile("inside the circle", f"{o.get('frac_inside_zone', 0)*100:.0f}%")
+ tile("game time scored", f"{o.get('seconds_scored', 0):.0f}s")
+ tile("decisions", o.get("tool_calls_that_advanced_time", 0))
+ tile("status", status)
+ "</div>")
# ---------------------------------------------------------------- the agent
def _fresh_session(port: int, seed: int):
"""Point the MCP session at a new match. The session is a singleton by
design - one server, one match - so a Space that plays repeatedly has to
retire it between runs."""
import indus_mcp.session as S
outcome = Path(tempfile.gettempdir()) / f"space-outcome-{seed}.json"
os.environ.update(
INDUS_BRIDGE_HOST="127.0.0.1", INDUS_BRIDGE_PORT=str(port),
INDUS_TASK="zone_survival", INDUS_BUDGET_SECONDS=str(BUDGET),
INDUS_FF_RADIUS="350", INDUS_OUTCOME_PATH=str(outcome),
)
S._SESSION = None
return S.session()
def _start_fake(seed: int, port: int):
import random as _r
from tests.fake_game import FakeGame
rng = _r.Random(seed)
return FakeGame(port=port,
zone_center=(rng.uniform(-400, 400), rng.uniform(-400, 400)),
radius0=2000.0, shrink_per_tick=0.3).start()
def scripted_agent(sit: dict) -> list[tuple[str, dict]]:
"""Work out the bearing, face it, run. The whole policy, and the control
every trained policy here is measured against."""
dx = sit["zone_center_x"] - sit["position_x"]
dz = sit["zone_center_z"] - sit["position_z"]
bearing = math.degrees(math.atan2(dx, dz)) % 360
if sit["inside_safe_zone"]:
return [("hold_position", {"seconds": 10})]
return [("turn_to_bearing", {"bearing": round(bearing, 1)}),
("move", {"seconds": 12, "forward": 1.0, "run": True})]
async def play(seed: int):
from fastmcp import Client
from indus_mcp.main import mcp
seed = int(seed)
port = 9100 + (seed % 200)
game = _start_fake(seed, port)
session = _fresh_session(port, seed)
feed: list[dict] = []
path: list[tuple[float, float]] = []
sit: dict | None = None
def frame():
return render_map(path, sit), render_calls(feed), render_stats(session.outcome())
try:
async with Client(mcp) as c:
for _ in range(40):
r = await c.call_tool("get_situation", {})
sit = r.structured_content
path.append((sit["position_x"], sit["position_z"]))
feed.append({"name": "get_situation", "args": {}, "free": True,
"text": (r.content[0].text if r.content else "").strip()})
yield frame()
if session.finished:
break
for name, args in scripted_agent(sit):
r = await c.call_tool(name, args)
d = r.structured_content or {}
if d.get("position_x") is not None:
path.append((d["position_x"], d["position_z"]))
sit = _merge(sit, d, False)
feed.append({"name": name, "args": args, "free": False,
"text": (r.content[0].text if r.content else "").strip()})
yield frame()
if session.finished:
break
if session.finished:
break
if not session.finished:
session.finish("out of turns")
yield frame()
finally:
game.stop()
# ---------------------------------------------------------------- leaderboard
RESULTS = Path(__file__).resolve().parent / "results" / "summary.json"
def _board_table(d: dict) -> str:
"""One task's table: agents across the top, measures down the side.
Reading a column tells you about one agent, reading a row compares them
on one thing, and the best in each row is bold. The footnotes say what
was held constant, because a number without them is not a result.
"""
rows = sorted(d.get("rows", []), key=lambda r: -r.get("score", 0))
if not rows:
return ""
def label(r):
n = r["model"].split(":", 1)[-1].split("/")[-1]
return f"{n}<sup>1</sup>" if r.get("reference") else n
measures = [
("Score", d.get("measure", "Task score"), lambda r: f'{r["score"]*100:.1f}', True),
("95% interval", "Bootstrap over the scored zones",
lambda r: (f'{r["ci95"][0]*100:.1f} &ndash; {r["ci95"][1]*100:.1f}'
if r.get("ci95") else "&mdash;"), False),
("Median", "Per-zone median", lambda r: f'{r.get("median", 0)*100:.1f}', True),
("Scored above 50%", "Zones", lambda r: str(r.get("reached", "&mdash;")), True),
("Decisions", "Tool calls per episode",
lambda r: (f'{r["tool_calls_mean"]:.1f}' if r.get("tool_calls_mean")
else "&mdash;"), False),
]
head = "<tr><th></th>" + "".join(f"<th class=r>{label(r)}</th>" for r in rows) + "</tr>"
body = []
for name, gloss, fmt, mark in measures:
vals = [fmt(r) for r in rows]
best = -1
if mark:
try:
best = [float(v.split("/")[0]) for v in vals].index(
max(float(v.split("/")[0]) for v in vals))
except ValueError:
best = -1
cells = "".join(f'<td class=r>{"<b>" + v + "</b>" if i == best else v}</td>'
for i, v in enumerate(vals))
body.append(f'<tr><td class="m"><b>{name}</b><span>{gloss}</span></td>{cells}</tr>')
title = d.get("task", "task").replace("_", " ")
meta = html.escape(d.get("note", ""))
gloss = d.get("describe", "")
return (f'<h3 class="pr-task">{title}</h3>'
+ (f'<p class="pr-dim pr-gloss">{gloss}</p>' if gloss else "")
+ f'<table class="pr-board">{head}{"".join(body)}</table>'
+ (f'<p class="pr-dim pr-meta">{meta}</p>' if meta else ""))
def render_board() -> str:
"""Read whatever has been published into results/summary.json.
The file is absent until there are numbers worth standing behind, and the
page says so rather than showing an empty table. Publishing is a commit
of one file, not a change to this app.
"""
if not RESULTS.exists():
return ('<div class="pr-board-empty">'
'<p>No scores published yet.</p>'
'<p class="pr-dim">A ranking is only worth reading once every zone is '
'winnable. The numbers go here when they are.</p></div>')
d = json.loads(RESULTS.read_text(encoding="utf-8"))
boards = d if isinstance(d, list) else d.get("tasks") or [d]
notes = [
"The best result in each row is shown in bold, where a higher number is "
"better. &mdash; indicates not applicable: the reference policies do not "
"call tools.",
"Every agent is driven by one tool-calling loop; the only per-provider "
"difference is the endpoint.",
"Episodes are truncated at the action budget rather than at match end, so a "
"score is not a win rate.",
"Intervals are wide at these sample sizes. Rows whose intervals overlap are "
"not separated by this data, whatever their order in the table.",
]
return ("".join(_board_table(b) for b in boards)
+ '<ol class="pr-notes">' + "".join(f"<li>{n}</li>" for n in notes) + "</ol>"
+ '<p class="pr-dim"><sup>1</sup> Reference policies, not models: a '
"twelve-line hand-written policy, and uniform random actions as the "
"floor. A benchmark whose floor is high is not measuring anything.</p>")
BOARD_INTRO = """
<div class="pr-lab">Leaderboard</div>
Every row plays the same zones through the same six tools and is scored by the same loop.
Rows marked as references are not models: a random policy, a short hand-written one, and a
policy trained in the environment, included so the model scores have a floor and a
comparison.
"""
# ---------------------------------------------------------------- replays
REPLAY_DIR = Path(__file__).resolve().parent / "replays"
# Only get_situation reports the circle. Movement tools report the agent and a
# couple of derived fields, so merging everything from every call leaves a
# stale next-circle sitting beside a fresh current one - which draws a "next"
# ring larger than the one it is shrinking towards.
ZONE_FIELDS = ("zone_center_x", "zone_center_z", "zone_radius", "zone_round",
"next_zone_center_x", "next_zone_center_z", "next_zone_radius")
def _merge(sit: dict | None, result: dict, is_situation: bool) -> dict:
keep = {k: v for k, v in result.items()
if v is not None and (is_situation or k not in ZONE_FIELDS)}
return {**(sit or {}), **keep}
def _replays() -> dict[str, Path]:
"""Recorded episodes, newest naming first. Each file is the trajectory a
real run wrote: the model, the zone, every tool call and its response."""
out = {}
for f in sorted(REPLAY_DIR.glob("*.json")) if REPLAY_DIR.exists() else []:
try:
d = json.loads(f.read_text(encoding="utf-8"))
except ValueError:
continue
o = d.get("outcome") or {}
out[f"{d.get('model', '?')} — zone {d.get('seed', '?')} — "
f"{o.get('frac_inside_zone', 0)*100:.0f}% inside"] = f
return out
async def play_replay(label: str):
"""Step through a recorded episode at reading speed.
This is a real run against a real game server, not the stand-in: the
positions and the tool responses are what the model actually got.
"""
import asyncio
files = _replays()
f = files.get(label)
if f is None:
yield render_map([], None), render_calls([]), render_stats(None)
return
d = json.loads(f.read_text(encoding="utf-8"))
calls, path, sit, feed = d.get("calls", []), [], None, []
o = d.get("outcome") or {}
for c in calls:
r = c.get("result") or {}
if r.get("position_x") is not None:
path.append((r["position_x"], r["position_z"]))
sit = _merge(sit, r, c.get("tool") == "get_situation")
feed.append({"name": c.get("tool", "?"), "args": c.get("input") or {},
"free": c.get("tool") == "get_situation",
"text": _summarise(c)})
if sit and sit.get("zone_center_x") is not None:
yield render_map(path, sit), render_calls(feed), render_stats({})
await asyncio.sleep(0.45)
yield render_map(path, sit), render_calls(feed), render_stats(o)
def _summarise(call: dict) -> str:
"""The transcripts store structured results, not the text block, so
rebuild a readable line rather than dumping json at the viewer."""
r = call.get("result") or {}
bits = []
if r.get("position_x") is not None:
bits.append(f"at x={r['position_x']}, z={r['position_z']}")
if r.get("facing_yaw") is not None:
bits.append(f"facing {r['facing_yaw']}")
if r.get("distance_travelled") is not None:
bits.append(f"moved {r['distance_travelled']} units")
if r.get("degrees_off") is not None:
bits.append(f"{r['degrees_off']} off the bearing asked for")
if r.get("inside_safe_zone") is not None:
bits.append("inside the circle" if r["inside_safe_zone"] else "outside the circle")
if r.get("zone_radius") is not None:
bits.append(f"radius {r['zone_radius']}")
if r.get("seconds_remaining") is not None:
bits.append(f"{r['seconds_remaining']}s left")
return " · ".join(bits) or "(no fields)"
WATCH_INTRO = """
<div class="pr-lab">Recorded runs</div>
Episodes from real matches on a real game server. The positions and the tool responses are
what the model actually received. Pick one and it plays back a step at a time.
"""
NO_REPLAYS = """
<div class="pr-board-empty"><p>No recordings published yet.</p>
<p class="pr-dim">Runs against the real game go here. The tab beside this one drives the
same tools against a stand-in, which needs no game server.</p></div>
"""
# ---------------------------------------------------------------- the page
CSS = FINDINGS_CSS + """
.gradio-container { max-width: 1120px !important; }
#pr-hero { border-bottom: 1px solid var(--border-color-primary); padding-bottom: 18px; }
#pr-hero h1 { font-size: 30px; line-height: 1.15; margin: 0 0 6px; letter-spacing: -.02em; font-weight: 700; }
#pr-hero .pr-sub { font-size: 15px; color: var(--body-text-color-subdued); max-width: 64ch; margin: 0 0 14px; }
#pr-hero .pr-chips { display: flex; flex-wrap: wrap; gap: 8px; }
#pr-hero .pr-chip { font-size: 11px; letter-spacing: .05em; text-transform: uppercase;
border: 1px solid var(--border-color-primary); border-radius: 999px; padding: 4px 11px;
color: var(--body-text-color-subdued); }
#pr-hero .pr-chip.on { border-color: #2a78d6; color: #2a78d6; font-weight: 600; }
.pr-lab { font-size: 11px; letter-spacing: .14em; text-transform: uppercase; font-weight: 700;
color: #9a7400; margin: 4px 0 8px; }
.pr-map svg { width: 100%; height: auto; border: 1px solid var(--border-color-primary);
border-radius: 12px; display: block; }
.pr-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 12px; }
.pr-tile { border: 1px solid var(--border-color-primary); border-radius: 10px; padding: 9px 11px; }
.pr-tile .k { display: block; font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
color: var(--body-text-color-subdued); }
.pr-tile .v { display: block; font-size: 19px; font-variant-numeric: tabular-nums; margin-top: 2px; }
.pr-feed { height: 522px; overflow-y: auto; border: 1px solid var(--border-color-primary);
border-radius: 12px; padding: 12px; }
.pr-call { border-left: 2px solid #2a78d6; padding: 6px 0 6px 10px; margin-bottom: 9px; }
.pr-call.pr-free { border-left-color: var(--border-color-primary); }
.pr-name { font-family: ui-monospace, monospace; font-size: 12.5px; font-weight: 600; }
.pr-args { font-weight: 400; color: var(--body-text-color-subdued); }
.pr-out { font-size: 12.5px; color: var(--body-text-color-subdued); white-space: pre-wrap;
margin-top: 3px; line-height: 1.45; }
.pr-dim { color: var(--body-text-color-subdued); font-size: 13px; }
table.pr-board { width: 100%; border-collapse: collapse; font-size: 14px; }
table.pr-board th { text-align: left; font-size: 10.5px; letter-spacing: .08em;
text-transform: uppercase; color: var(--body-text-color-subdued); font-weight: 700;
border-bottom: 1px solid var(--border-color-primary); padding: 6px 8px 6px 0; }
table.pr-board td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--border-color-primary);
font-variant-numeric: tabular-nums; }
table.pr-board .r { text-align: right; white-space: nowrap; }
table.pr-board td.m { width: 34%; }
table.pr-board td.m span { display: block; font-size: 11.5px; font-weight: 400;
color: var(--body-text-color-subdued); margin-top: 1px; }
table.pr-board th.r { font-family: ui-monospace, monospace; text-transform: none;
letter-spacing: 0; font-size: 11.5px; text-align: right; }
.pr-notes { font-size: 12.5px; color: var(--body-text-color-subdued);
padding-left: 18px; margin: 10px 0; }
.pr-notes li { margin-bottom: 4px; }
.pr-meta { margin-top: 8px; font-size: 12.5px; }
h3.pr-task { font-size: 17px; margin: 26px 0 2px; text-transform: capitalize; }
h3.pr-task:first-of-type { margin-top: 4px; }
.pr-gloss { max-width: 74ch; margin: 0 0 10px; font-size: 13.5px; }
table.pr-board tr.pr-ref td { color: var(--body-text-color-subdued); }
.pr-board-empty { border: 1px solid var(--border-color-primary); border-radius: 12px;
padding: 18px; max-width: 60ch; }
.pr-board-empty p { margin: 0 0 8px; }
#pr-foot { border-top: 1px solid var(--border-color-primary); margin-top: 24px; padding-top: 14px;
font-size: 13px; color: var(--body-text-color-subdued); }
"""
HERO = """
<div id="pr-hero">
<h1>Prime Rush as an MCP environment</h1>
<p class="pr-sub">An agent plays a battle-royale match through six tools. A safe circle
shrinks and the agent has to be inside it. It is given its own coordinates and the
circle&rsquo;s, and has to work out the direction itself.</p>
<div class="pr-chips">
<span class="pr-chip">six tools</span>
<span class="pr-chip">runs under Archipelago</span>
<span class="pr-chip">Apache-2.0</span>
</div>
</div>
"""
WHY = """
<div class="pr-lab">Timing</div>
The match only advances inside a tool call, by the amount that call asks for. A model can
take as long as it likes to decide and the game waits. The same seed and the same calls
give the same match.
<div class="pr-lab" style="margin-top:20px">What the agent gets</div>
`get_situation` returns coordinates: where it is, which way it is facing, where the circle
is and how big. There is no `move_to` and no bearing. Movement is relative to the direction
it is facing, so it has to turn first.
"""
NOTE = """
<div class="pr-lab">Scoring</div>
Each call that advances the match writes `outcome.json` from the game state. The score is
read from that file, not from the transcript, and a run that stops early is still scored on
what it did. The task pack and a reference grader are in the Files tab.
<div class="pr-lab" style="margin-top:20px">What this page runs</div>
The tools, their schemas and the scoring are the ones a lab would run. The game is not: a
Space has no Unity server, so the tools drive a stand-in that speaks the same protocol. The
map shows only what the tools returned.
"""
FOOT = """
<div id="pr-foot">
SuperGaming &nbsp;&middot;&nbsp; code, task pack and tests in the Files tab
&nbsp;&middot;&nbsp; Apache-2.0
</div>
"""
with gr.Blocks(title="Prime Rush — a battle royale as an MCP environment") as demo:
gr.HTML(HERO)
_reps = _replays()
with gr.Tab("Watch a model play"):
gr.Markdown(WATCH_INTRO)
if _reps:
with gr.Row():
with gr.Column(scale=3):
rmap = gr.HTML(render_map([], None))
rstats = gr.HTML(render_stats(None))
with gr.Row():
pick = gr.Dropdown(list(_reps), value=list(_reps)[0],
label="Episode", scale=3)
rgo = gr.Button("Play", variant="primary", scale=0)
with gr.Column(scale=2):
gr.HTML('<div class="pr-lab">What the model called</div>')
rfeed = gr.HTML(render_calls([]))
rgo.click(play_replay, inputs=[pick], outputs=[rmap, rfeed, rstats])
else:
gr.HTML(NO_REPLAYS)
with gr.Tab("Try the tools"):
with gr.Row():
with gr.Column(scale=3):
gr.HTML('<div class="pr-lab">The match</div>')
map_out = gr.HTML(render_map([], None))
stats_out = gr.HTML(render_stats(None))
with gr.Row():
seed = gr.Slider(2000, 2029, value=2004, step=1, label="Zone layout (seed)")
go = gr.Button("Play an episode", variant="primary", scale=0)
with gr.Column(scale=2):
gr.HTML('<div class="pr-lab">What the agent called</div>')
feed_out = gr.HTML(render_calls([]))
with gr.Row():
with gr.Column():
gr.Markdown(WHY)
with gr.Column():
gr.Markdown(NOTE)
with gr.Tab("Findings"):
gr.HTML(FINDINGS)
with gr.Tab("Leaderboard"):
gr.Markdown(BOARD_INTRO)
gr.HTML(render_board())
gr.HTML(FOOT)
go.click(play, inputs=[seed], outputs=[map_out, feed_out, stats_out])
if __name__ == "__main__":
demo.launch(css=CSS, theme=gr.themes.Soft(
primary_hue="blue", neutral_hue="slate",
font=["ui-sans-serif", "system-ui", "sans-serif"]))