omm7's picture
Upload folder using huggingface_hub
b0620f3 verified
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
"""
FastAPI application for the Toxic Royale Env Environment.
This module creates an HTTP server that exposes the ToxicRoyaleEnvironment
over HTTP and WebSocket endpoints, compatible with EnvClient.
Endpoints:
- POST /reset: Reset the environment
- POST /step: Execute an action
- GET /state: Get current environment state
- GET /schema: Get action/observation schemas
- WS /ws: WebSocket endpoint for persistent sessions
Usage:
# Development (with auto-reload):
uvicorn toxic_royale_env.server.app:app --reload --host 0.0.0.0 --port 8000
# Production:
uvicorn toxic_royale_env.server.app:app --host 0.0.0.0 --port 8000 --workers 4
# Or run directly:
python -m toxic_royale_env.server.app
"""
try:
from openenv.core.env_server.http_server import create_app
except Exception as e: # pragma: no cover
raise ImportError(
"openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
) from e
import json
import time
from collections import deque
from threading import Lock
from fastapi.responses import HTMLResponse, StreamingResponse
from toxic_royale_env.models import ToxicRoyaleAction, ToxicRoyaleObservation
from toxic_royale_env.policy_models import FramePacket, PolicyActResponse, PolicyAction
from toxic_royale_env.policy_utils import append_jsonl, now_ms, rule_policy, should_gate
from toxic_royale_env.server.toxic_royale_env_environment import ToxicRoyaleEnvironment
# Create the app with web interface and README integration
app = create_app(
ToxicRoyaleEnvironment,
ToxicRoyaleAction,
ToxicRoyaleObservation,
env_name="toxic_royale_env",
max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
)
# --- Live viewer memory (in-RAM ring buffer) ---
_LIVE_LOCK = Lock()
_LIVE_EVENTS: deque[dict] = deque(maxlen=2000) # last ~2000 ticks across matches
def _live_push(obj: dict) -> None:
with _LIVE_LOCK:
_LIVE_EVENTS.append(obj)
def _live_snapshot(match_id: str | None = None) -> list[dict]:
with _LIVE_LOCK:
if match_id:
return [e for e in list(_LIVE_EVENTS) if e.get("match_id") == match_id]
return list(_LIVE_EVENTS)
@app.post("/policy/act", response_model=PolicyActResponse)
def policy_act(packet: FramePacket) -> PolicyActResponse:
"""
Stateless policy endpoint for the Windows live pipeline.
Input: structured FramePacket (no images)
Output: one action (play/wait) for this tick
"""
t0 = now_ms()
tick_id = int(packet.meta.tick_id or 0)
if should_gate(packet):
action = PolicyAction(kind="wait", emote="yawn", reasoning="low_detection_quality")
resp = PolicyActResponse(tick_id=tick_id, action=action, source="gated")
else:
action = rule_policy(packet)
resp = PolicyActResponse(tick_id=tick_id, action=action, source="rules")
latency_ms = now_ms() - t0
live_obj = {
"ts_ms": now_ms(),
"match_id": packet.meta.match_id,
"tick_id": tick_id,
"time_left_s": (packet.ui.time_left_s if packet.ui else None),
"overall_q": (
packet.debug.detections_quality.overall
if packet.debug and packet.debug.detections_quality
else None
),
"my_elixir": packet.player.elixir if packet.player else None,
"hand": [c.model_dump() for c in (packet.player.hand if packet.player else [])],
"action": resp.action.model_dump(),
"source": resp.source,
"latency_ms": latency_ms,
}
_live_push(live_obj)
append_jsonl(
# stored under toxic_royale_env/outputs/policy_logs/
__import__("pathlib").Path(__file__).resolve().parents[1] / "outputs" / "policy_logs" / "policy_act.jsonl",
live_obj,
)
return resp
@app.get("/live", response_class=HTMLResponse)
def live_page() -> str:
return """
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>ToxicRoyale Live Match</title>
<style>
body { font-family: ui-sans-serif, system-ui, -apple-system; margin: 16px; }
.row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; }
.card { border: 1px solid #ddd; border-radius: 12px; padding: 10px 12px; min-width: 210px; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas; font-size: 12px; white-space: pre-wrap; }
table { border-collapse: collapse; width: 100%; }
th, td { border-bottom: 1px solid #eee; text-align: left; padding: 6px 8px; font-size: 13px; }
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; background: #f3f4f6; }
</style>
</head>
<body>
<h2>Live Match Viewer</h2>
<div class="row">
<div class="card"><b>Match</b><div id="match">-</div></div>
<div class="card"><b>Tick</b><div id="tick">-</div></div>
<div class="card"><b>Time left</b><div id="time">-</div></div>
<div class="card"><b>Elixir</b><div id="elixir">-</div></div>
<div class="card"><b>Quality</b><div id="q">-</div></div>
<div class="card"><b>Chosen action</b><div id="action">-</div><div id="src" class="pill">-</div></div>
</div>
<h3>Hand</h3>
<table>
<thead><tr><th>slot</th><th>card</th><th>cost</th><th>playable</th><th>conf</th></tr></thead>
<tbody id="hand"></tbody>
</table>
<h3>Raw event JSON</h3>
<div id="raw" class="mono"></div>
<script>
const handEl = document.getElementById("hand");
const rawEl = document.getElementById("raw");
const params = new URLSearchParams(window.location.search);
const match = params.get("match_id");
const url = match ? `/live/stream?match_id=${encodeURIComponent(match)}` : `/live/stream`;
const ev = new EventSource(url);
function td(v){ return `<td>${v ?? ""}</td>`; }
ev.onmessage = (e) => {
const obj = JSON.parse(e.data);
document.getElementById("match").innerText = obj.match_id ?? "-";
document.getElementById("tick").innerText = obj.tick_id ?? "-";
document.getElementById("time").innerText = (obj.time_left_s == null) ? "-" : obj.time_left_s.toFixed(1) + "s";
document.getElementById("elixir").innerText = (obj.my_elixir == null) ? "-" : obj.my_elixir.toFixed(2);
document.getElementById("q").innerText = (obj.overall_q == null) ? "-" : obj.overall_q.toFixed(2);
document.getElementById("action").innerText = JSON.stringify(obj.action);
document.getElementById("src").innerText = obj.source;
const hand = obj.hand ?? [];
handEl.innerHTML = hand.map(c => `<tr>${td(c.slot)}${td(c.card)}${td(c.cost)}${td(c.is_playable)}${td(c.conf)}</tr>`).join("");
rawEl.innerText = JSON.stringify(obj, null, 2);
};
</script>
</body>
</html>
"""
@app.get("/live/stream")
def live_stream(match_id: str | None = None):
def gen():
last_ts = 0
while True:
events = _live_snapshot(match_id)
if events:
ev = events[-1]
if int(ev.get("ts_ms") or 0) != last_ts:
last_ts = int(ev.get("ts_ms") or 0)
yield f"data: {json.dumps(ev, ensure_ascii=False)}\\n\\n"
time.sleep(0.2)
return StreamingResponse(gen(), media_type="text/event-stream")
def main(host: str = "0.0.0.0", port: int = 8000):
"""
Entry point for direct execution via uv run or python -m.
This function enables running the server without Docker:
uv run --project . server
uv run --project . server --port 8001
python -m toxic_royale_env.server.app
Args:
host: Host address to bind to (default: "0.0.0.0")
port: Port number to listen on (default: 8000)
For production deployments, consider using uvicorn directly with
multiple workers:
uvicorn toxic_royale_env.server.app:app --workers 4
"""
import uvicorn
uvicorn.run(app, host=host, port=port)
if __name__ == "__main__":
main()