Spaces:
Running
Running
deploy parry
Browse files- Dockerfile +15 -0
- README.md +34 -7
- app.py +202 -4
- app_blocks.py +26 -0
- requirements.txt +3 -0
- static/assets/action_set-C6mnKwO1.js +1 -0
- static/assets/engine-DIUq88up.js +1 -0
- static/assets/golden-B6Ebeawj.js +3 -0
- static/assets/index-K8o-xskJ.js +0 -0
- static/assets/llm.worker-GbIGcnxV.js +0 -0
- static/assets/main-Cu1TzpLc.js +70 -0
- static/assets/pose.worker-BBF_KKBj.js +0 -0
- static/assets/spike-BPCTWSB0.js +5 -0
- static/grammar.hash +1 -0
- static/index.html +27 -0
- static/pose/group1-shard1of2.bin +3 -0
- static/pose/group1-shard2of2.bin +3 -0
- static/pose/model.json +0 -0
- static/pose/tfjs-backend-wasm-simd.wasm +3 -0
- static/pose/tfjs-backend-wasm-threaded-simd.wasm +3 -0
- static/pose/tfjs-backend-wasm.wasm +3 -0
- static/pose/thunder/group1-shard1of3.bin +3 -0
- static/pose/thunder/group1-shard2of3.bin +3 -0
- static/pose/thunder/group1-shard3of3.bin +3 -0
- static/pose/thunder/model.json +0 -0
- static/spike/latency.html +41 -0
- static/test/golden_browser.html +14 -0
Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HEDGE 1 (infra-level): same app.py under `sdk: docker`.
|
| 2 |
+
# Inert while README says `sdk: gradio`. Activate by swapping README YAML to:
|
| 3 |
+
# sdk: docker
|
| 4 |
+
# app_port: 7860
|
| 5 |
+
FROM python:3.12-slim
|
| 6 |
+
RUN useradd -m -u 1000 user
|
| 7 |
+
USER user
|
| 8 |
+
ENV HOME=/home/user PATH=/home/user/.local/bin:$PATH \
|
| 9 |
+
GRADIO_SERVER_NAME=0.0.0.0 GRADIO_SERVER_PORT=7860
|
| 10 |
+
WORKDIR $HOME/app
|
| 11 |
+
COPY --chown=user requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
COPY --chown=user . .
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,15 +1,42 @@
|
|
| 1 |
---
|
| 2 |
title: Parry
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
-
|
|
|
|
|
|
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Parry
|
| 3 |
+
emoji: ⚔️
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.10.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
+
models:
|
| 12 |
+
- Qwen/Qwen2.5-1.5B-Instruct
|
| 13 |
+
short_description: Duel a 1.5B model running live in your browser — 0ms network
|
| 14 |
---
|
| 15 |
|
| 16 |
+
# ⚔️ Parry
|
| 17 |
+
|
| 18 |
+
Duel a small language model that runs **entirely in your browser** (WebGPU), reads your
|
| 19 |
+
patterns mid-match, **tells you what it learned about you**, and adapts — inside a
|
| 20 |
+
real-time reaction loop no cloud API can physically serve.
|
| 21 |
+
|
| 22 |
+
- **🥊 Fight it with your body:** allow the camera and BOX it — punch to strike, both
|
| 23 |
+
hands up to parry, lean to advance/retreat. Pose tracking runs on your CPU
|
| 24 |
+
(the GPU belongs to the model). Keyboard always works too.
|
| 25 |
+
- **Local-first:** the opponent's brain is a Qwen2.5-1.5B decoding one grammar-constrained
|
| 26 |
+
intent token per decision on YOUR GPU. Pull your Wi-Fi out mid-match — nothing changes.
|
| 27 |
+
- **Observable adaptation:** the Analyst's live read of you is rendered on screen, and the
|
| 28 |
+
judge panel (B) lets you EDIT its read and watch the Tactician's play shift in real time.
|
| 29 |
+
> **⚠️ Browser note (for reviewers):** the opponent's brain runs **in your browser via
|
| 30 |
+
> WebGPU** — please use a WebGPU-enabled browser (Chrome or Edge on desktop). First load
|
| 31 |
+
> downloads ~900MB of model weights once, then they're cached. No WebGPU available?
|
| 32 |
+
> The demo video shows the full experience.
|
| 33 |
+
|
| 34 |
+
Controls: 🎥 camera = punch/guard/lean · ←/→ move · J strike · K feint · L parry ·
|
| 35 |
+
V toggle camera · B judge panel · 1–5 sparring bots · 7 the Masher · 8 the Executioner ·
|
| 36 |
+
9 the LLM · Enter rematch · M mute.
|
| 37 |
+
|
| 38 |
+
*(This Space is a `gradio.Server` app — Gradio's engine serves the custom canvas at `/`,
|
| 39 |
+
and the Analyst endpoint runs through Gradio's queue, callable with `gradio_client`.)*
|
| 40 |
+
|
| 41 |
+
NOTE deploy: copy the built client (`npm run build` → `dist/`) into `static/` before
|
| 42 |
+
pushing; `app.py` serves `static/index.html` at `/`.
|
app.py
CHANGED
|
@@ -1,7 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
demo.launch()
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PARRY — compliant Gradio hosting layer (C8). 100% LOCAL inference by design.
|
| 3 |
+
|
| 4 |
+
Primary posture (per the verified research): `gradio.Server` (Gradio 6.x) on
|
| 5 |
+
`sdk: gradio` — a FastAPI subclass running Gradio's engine, with our custom
|
| 6 |
+
canvas served at GET / (custom routes take priority over the default UI; this
|
| 7 |
+
is the official `ysharma/text-behind-image` pattern, and the custom-UI award
|
| 8 |
+
text says "gr.Server is your friend").
|
| 9 |
+
|
| 10 |
+
NO external inference calls: the model runs in the visitor's browser via
|
| 11 |
+
WebGPU. (Organizer guidance, June 10: external API fallbacks are hard to
|
| 12 |
+
review — "the video is the fallback". The earlier Modal relay was removed.)
|
| 13 |
+
|
| 14 |
+
Defensive: if the pinned Gradio somehow lacks `Server`, we degrade to
|
| 15 |
+
FastAPI + gr.mount_gradio_app so the Space still boots while we consult the
|
| 16 |
+
hedge ladder (docker swap / gr.Blocks+gr.HTML — see ../space/app_blocks.py).
|
| 17 |
+
|
| 18 |
+
Endpoints
|
| 19 |
+
GET / → static/index.html (the game)
|
| 20 |
+
GET /static/* → built Vite bundle (immutable assets)
|
| 21 |
+
api "trace_digest" → validates + summarizes an exported BrainTrace (Gradio queue; gradio_client-callable)
|
| 22 |
+
api "about" → build/model/grammar info via the queue
|
| 23 |
+
POST /funnel → enum-validated JSONL beacons (+ optional CommitScheduler → private HF Dataset)
|
| 24 |
+
GET /healthz → {ok, version, grammar_id}
|
| 25 |
+
|
| 26 |
+
Secrets (Space settings): HF_TOKEN (funnel dataset, optional),
|
| 27 |
+
FUNNEL_DATASET (e.g. "user/parry-funnel", optional).
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
|
| 32 |
+
import json
|
| 33 |
+
import os
|
| 34 |
+
import threading
|
| 35 |
+
import time
|
| 36 |
+
from pathlib import Path
|
| 37 |
+
|
| 38 |
import gradio as gr
|
| 39 |
+
from fastapi import Request
|
| 40 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 41 |
+
from fastapi.staticfiles import StaticFiles
|
| 42 |
+
|
| 43 |
+
HERE = Path(__file__).parent
|
| 44 |
+
STATIC = HERE / "static"
|
| 45 |
+
DATA = HERE / "data"
|
| 46 |
+
DATA.mkdir(exist_ok=True)
|
| 47 |
+
FUNNEL_PATH = DATA / "funnel.jsonl"
|
| 48 |
+
|
| 49 |
+
APP_VERSION = "parry-space-v1"
|
| 50 |
+
GRAMMAR_ID = os.environ.get("GRAMMAR_ID", "unset") # injected by CI from grammar/hash
|
| 51 |
+
|
| 52 |
+
FUNNEL_EVENTS = {
|
| 53 |
+
"page_load",
|
| 54 |
+
"webgpu_detected",
|
| 55 |
+
"webgpu_missing",
|
| 56 |
+
"tier_selected",
|
| 57 |
+
"model_download_started",
|
| 58 |
+
"model_download_complete",
|
| 59 |
+
"first_playable",
|
| 60 |
+
"match_started",
|
| 61 |
+
"match_completed",
|
| 62 |
+
"fell_back_to_server",
|
| 63 |
+
"bounced_during_download",
|
| 64 |
+
"debug_override_used",
|
| 65 |
+
"trace_exported",
|
| 66 |
+
"boss_reflexes_on",
|
| 67 |
+
"pose_mode_on",
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
_funnel_lock = threading.Lock()
|
| 71 |
+
|
| 72 |
+
# Optional: persist funnel JSONL to a private HF Dataset (ephemeral disk survival).
|
| 73 |
+
_scheduler = None
|
| 74 |
+
if os.environ.get("HF_TOKEN") and os.environ.get("FUNNEL_DATASET"):
|
| 75 |
+
try:
|
| 76 |
+
from huggingface_hub import CommitScheduler
|
| 77 |
+
|
| 78 |
+
_scheduler = CommitScheduler(
|
| 79 |
+
repo_id=os.environ["FUNNEL_DATASET"],
|
| 80 |
+
repo_type="dataset",
|
| 81 |
+
folder_path=str(DATA),
|
| 82 |
+
every=5, # minutes
|
| 83 |
+
private=True,
|
| 84 |
+
)
|
| 85 |
+
except Exception as e: # noqa: BLE001 — funnel persistence is best-effort
|
| 86 |
+
print(f"[funnel] CommitScheduler unavailable: {e}")
|
| 87 |
+
|
| 88 |
+
def _append_funnel(row: dict) -> None:
|
| 89 |
+
with _funnel_lock:
|
| 90 |
+
with open(FUNNEL_PATH, "a", encoding="utf-8") as f:
|
| 91 |
+
f.write(json.dumps(row, separators=(",", ":")) + "\n")
|
| 92 |
+
print(f"[funnel] {row.get('event')}")
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _about() -> dict:
|
| 96 |
+
return {
|
| 97 |
+
"app": "parry",
|
| 98 |
+
"version": APP_VERSION,
|
| 99 |
+
"grammar_id": GRAMMAR_ID,
|
| 100 |
+
"hero_model": "Qwen2.5-1.5B-Instruct (q4f16_1, in-browser WebGPU)",
|
| 101 |
+
"fallback": "none — inference is 100% in-browser by design",
|
| 102 |
+
"tuned_model": "Jainamshahhh/parry-tactician-1.5b-lora (published fine-tune)",
|
| 103 |
+
"thesis": "a sub-100ms reaction loop no network round-trip can serve",
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _trace_digest(trace_json: str) -> dict:
|
| 108 |
+
"""Pure-local BrainTrace summarizer (no model, no external calls): validates
|
| 109 |
+
an exported trace from the judge panel and returns its headline stats —
|
| 110 |
+
companion utility for the shared-trace (Sharing-is-Caring) flow."""
|
| 111 |
+
try:
|
| 112 |
+
rows = json.loads(trace_json)
|
| 113 |
+
assert isinstance(rows, list) and rows, "trace must be a non-empty JSON array"
|
| 114 |
+
except Exception as e: # noqa: BLE001
|
| 115 |
+
return {"valid": False, "error": str(e)[:200]}
|
| 116 |
+
intents: dict[str, int] = {}
|
| 117 |
+
plans: list[str] = []
|
| 118 |
+
for r in rows:
|
| 119 |
+
if isinstance(r, dict):
|
| 120 |
+
i = r.get("intent")
|
| 121 |
+
if isinstance(i, str):
|
| 122 |
+
intents[i] = intents.get(i, 0) + 1
|
| 123 |
+
p = r.get("planString")
|
| 124 |
+
if isinstance(p, str) and (not plans or plans[-1] != p):
|
| 125 |
+
plans.append(p)
|
| 126 |
+
return {
|
| 127 |
+
"valid": True,
|
| 128 |
+
"ticks": len(rows),
|
| 129 |
+
"intent_histogram": intents,
|
| 130 |
+
"distinct_plans": len(plans),
|
| 131 |
+
"plan_timeline": plans[:12],
|
| 132 |
+
"grammar_id": GRAMMAR_ID,
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
HAS_SERVER = hasattr(gr, "Server")
|
| 137 |
+
|
| 138 |
+
if HAS_SERVER:
|
| 139 |
+
app = gr.Server()
|
| 140 |
+
else: # degrade gracefully; primary fix is pinning sdk_version per README
|
| 141 |
+
print("[parry] WARNING: gradio.Server missing — booting FastAPI + mounted Blocks hedge")
|
| 142 |
+
from fastapi import FastAPI
|
| 143 |
+
|
| 144 |
+
app = FastAPI()
|
| 145 |
+
|
| 146 |
+
app.mount("/static", StaticFiles(directory=str(STATIC)), name="static")
|
| 147 |
+
# index.html is served at "/" with relative asset URLs → they resolve to /assets/*
|
| 148 |
+
app.mount("/assets", StaticFiles(directory=str(STATIC / "assets")), name="assets")
|
| 149 |
+
# pose-mode model + WASM are self-hosted under /pose (the worker fetches them
|
| 150 |
+
# by absolute origin URL — an unmounted path returns HTML 404s that die as
|
| 151 |
+
# "parse error" in TFJS, the same class of bug as the /assets mount above)
|
| 152 |
+
app.mount("/pose", StaticFiles(directory=str(STATIC / "pose")), name="pose")
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@app.get("/")
|
| 156 |
+
async def homepage() -> FileResponse:
|
| 157 |
+
# no-cache on the HTML only; hashed assets under /static are long-cached
|
| 158 |
+
return FileResponse(STATIC / "index.html", headers={"Cache-Control": "no-cache"})
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
@app.post("/funnel")
|
| 162 |
+
async def funnel(req: Request) -> JSONResponse:
|
| 163 |
+
try:
|
| 164 |
+
row = await req.json()
|
| 165 |
+
except Exception: # noqa: BLE001 — sendBeacon may post opaque bodies
|
| 166 |
+
return JSONResponse({"ok": False}, status_code=400)
|
| 167 |
+
if row.get("event") not in FUNNEL_EVENTS:
|
| 168 |
+
return JSONResponse({"ok": False, "error": "unknown event"}, status_code=400)
|
| 169 |
+
_append_funnel({k: row.get(k) for k in ("v", "event", "session", "ts", "props")})
|
| 170 |
+
return JSONResponse({"ok": True}, status_code=200)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
@app.get("/healthz")
|
| 174 |
+
async def healthz() -> JSONResponse:
|
| 175 |
+
return JSONResponse({"ok": True, "version": APP_VERSION, "grammar_id": GRAMMAR_ID, "inference": "in-browser"})
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
if HAS_SERVER:
|
| 179 |
+
# Gradio-queue endpoints: Gradio's engine genuinely doing work (queue, SSE),
|
| 180 |
+
# callable via gradio_client. Both are pure-local — no model, no external calls.
|
| 181 |
+
@app.api(name="trace_digest")
|
| 182 |
+
def trace_digest_api(trace_json: str) -> dict:
|
| 183 |
+
return _trace_digest(trace_json)
|
| 184 |
+
|
| 185 |
+
@app.api(name="about")
|
| 186 |
+
def about_api() -> dict:
|
| 187 |
+
return _about()
|
| 188 |
+
|
| 189 |
+
else:
|
| 190 |
+
# Hedge boot: mount a minimal Blocks app so Gradio is still in the loop.
|
| 191 |
+
with gr.Blocks() as demo:
|
| 192 |
+
gr.Markdown("# Parry backend (hedge boot)")
|
| 193 |
+
inp = gr.Textbox(label="exported BrainTrace JSON")
|
| 194 |
+
out = gr.JSON(label="trace digest")
|
| 195 |
+
gr.Button("digest").click(_trace_digest, inp, out)
|
| 196 |
+
app = gr.mount_gradio_app(app, demo, path="/gradio")
|
| 197 |
+
|
| 198 |
|
| 199 |
+
if __name__ == "__main__":
|
| 200 |
+
if HAS_SERVER:
|
| 201 |
+
app.launch(show_error=True)
|
| 202 |
+
else:
|
| 203 |
+
import uvicorn
|
| 204 |
|
| 205 |
+
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
|
|
|
app_blocks.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HEDGE 2 (rules-level): plain `gr.Blocks` + full-bleed `gr.HTML` wrapping the
|
| 3 |
+
IDENTICAL built bundle in a same-origin iframe served via the file route.
|
| 4 |
+
Activate by setting `app_file: app_blocks.py` in README.md — zero bundle changes.
|
| 5 |
+
|
| 6 |
+
Spike checks (§9.0 path C): WebGPU + module Worker + IndexedDB inside the
|
| 7 |
+
nested iframe; sizing; pointer/keyboard not captured by Gradio.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import gradio as gr
|
| 13 |
+
|
| 14 |
+
CSS = """
|
| 15 |
+
footer {display: none !important;}
|
| 16 |
+
.gradio-container {padding: 0 !important; max-width: 100% !important;}
|
| 17 |
+
#game-frame iframe {width: 100vw; height: 100dvh; border: 0; display: block;}
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
IFRAME = '<iframe src="/gradio_api/file=static/index.html" allow="autoplay" title="Parry"></iframe>'
|
| 21 |
+
|
| 22 |
+
with gr.Blocks(css=CSS, title="Parry") as demo:
|
| 23 |
+
gr.HTML(IFRAME, elem_id="game-frame")
|
| 24 |
+
|
| 25 |
+
if __name__ == "__main__":
|
| 26 |
+
demo.launch(allowed_paths=["static"], show_error=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==6.10.*
|
| 2 |
+
httpx>=0.27
|
| 3 |
+
huggingface_hub>=0.30
|
static/assets/action_set-C6mnKwO1.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))s(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const o of t.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function i(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function s(e){if(e.ep)return;e.ep=!0;const t=i(e);fetch(e.href,t)}})();const n={cls:"NEUTRAL",legal:["STRIKE","FEINT","PARRY","MOVE_L","MOVE_R","WAIT"]},l={cls:"LOCKED",legal:[]};export{n as A,l as L};
|
static/assets/engine-DIUq88up.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
import{A as F,L as V}from"./action_set-C6mnKwO1.js";const D="gs_v1",G=256;function h(e){return e*G|0}function T(e,r,n){return e<r?r:e>n?n:e}function d(e){return e<0?-e|0:e|0}function P(e){return e<0?-1:e>0?1:0}function K(e){return e*3>>2|0}const ae="engine_v5",W=60,p={startup:12,active:4,recovery:26,damage:15,chip:4},I={startup:8,recovery:10},m={startup:2,active:8,whiffRecovery:16},B=24,M=30,w=15,l=h(480),o=h(10),k=h(56),Y=h(84),g=h(3),H=h(160),X=h(12),j=h(6),Z=100,U=45*W,N=2,x=90,ne=k,ie=h(120);function O(e,r){e.verb=r,e.phase="STARTUP",e.hasHit=!1,e.framesLeft=r==="STRIKE"?p.startup:r==="FEINT"?I.startup:m.startup}function L(e,r,n){if(!(e.phase==="NEUTRAL"||e.framesLeft>0))switch(e.phase){case"STARTUP":e.verb==="STRIKE"?(e.phase="ACTIVE",e.framesLeft=p.active):e.verb==="PARRY"?(e.phase="ACTIVE",e.framesLeft=m.active):(e.phase="RECOVERY",e.framesLeft=I.recovery);break;case"ACTIVE":e.verb==="STRIKE"?(e.hasHit||n.push({t:"WHIFF",by:r}),e.phase="RECOVERY",e.framesLeft=p.recovery):(e.hasHit||n.push({t:"PARRY_WHIFF",by:r}),e.phase="RECOVERY",e.framesLeft=m.whiffRecovery);break;case"RECOVERY":case"BLOCKSTUN":case"STAGGER":e.phase="NEUTRAL",e.verb="NONE",e.framesLeft=0,e.hasHit=!1;break}}function _(e){e.phase!=="NEUTRAL"&&e.framesLeft>0&&(e.framesLeft=e.framesLeft-1|0)}function v(e){return{phase:e.phase,verb:e.verb,hasHit:e.hasHit}}function q(e,r){for(let c=0;c<=1;c=c+1){const s=e.players[c];s.phase==="NEUTRAL"&&r[c]!==0&&(s.x=s.x+r[c]*g|0),s.vx!==0&&(s.x=s.x+s.vx|0,s.vx=K(s.vx),d(s.vx)<8&&(s.vx=0)),s.x=T(s.x,o,l-o|0)}const n=e.players[0],t=e.players[1],i=t.x-n.x|0,a=2*o|0;if(d(i)<a){const c=a-d(i)|0,s=c>>1,u=c-s|0;i>=0?(n.x=T(n.x-s|0,o,l-o|0),t.x=T(t.x+u|0,o,l-o|0)):(n.x=T(n.x+s|0,o,l-o|0),t.x=T(t.x-u|0,o,l-o|0))}}function z(e){return d(e.players[1].x-e.players[0].x|0)<=k}function S(e,r){const n=P(e.x-r.x|0);return n===0?1:n}function J(e,r,n,t){if(!z(e))return;const i=[];for(let a=0;a<=1;a=a+1){const c=r[a];if(!(c.phase==="ACTIVE"&&c.verb==="STRIKE"&&!c.hasHit))continue;const s=1-a,u=r[s];if(u.phase==="ACTIVE"&&u.verb==="PARRY")i.push({kind:"parried",attacker:a,defender:s});else if(u.phase==="BLOCKSTUN"||u.phase==="STAGGER")e.players[a].hasHit=!0;else if(u.phase==="NEUTRAL"&&n[s]===S(e.players[s],e.players[a])){const R=S(e.players[s],e.players[a]),E=o,f=l-o|0,A=R===-1&&e.players[s].x<=E||R===1&&e.players[s].x>=f;i.push({kind:A?"hit":"blocked",attacker:a,defender:s})}else i.push({kind:"hit",attacker:a,defender:s})}for(const a of i){const c=e.players[a.attacker],s=e.players[a.defender];c.hasHit=!0,a.kind==="parried"?(t.push({t:"PARRY_SUCCESS",by:a.defender}),c.phase="STAGGER",c.verb="NONE",c.framesLeft=B,s.hasHit=!0,s.phase="NEUTRAL",s.verb="NONE",s.framesLeft=0):a.kind==="blocked"?(t.push({t:"BLOCK",by:a.attacker}),s.hp=Math.max(1,s.hp-p.chip|0),s.phase="BLOCKSTUN",s.verb="NONE",s.framesLeft=w,s.vx=s.vx+j*S(s,c)|0):(t.push({t:"HIT",by:a.attacker,dmg:p.damage}),s.hp=Math.max(0,s.hp-p.damage|0),s.phase="STAGGER",s.verb="NONE",s.framesLeft=M,s.vx=s.vx+X*S(s,c)|0)}}function Q(e,r){if(!(d(e.players[1].x-e.players[0].x|0)>Y))for(let t=0;t<=1;t=t+1){const i=e.players[t],a=e.players[1-t];i.verb==="FEINT"&&(i.phase==="STARTUP"||i.phase==="RECOVERY")&&!i.hasHit&&a.verb==="PARRY"&&a.phase==="STARTUP"&&a.framesLeft===m.startup&&(i.hasHit=!0,r.push({t:"FEINT_BAITED",by:t}))}}function y(e){return{x:e|0,vx:0,hp:Z,phase:"NEUTRAL",verb:"NONE",framesLeft:0,hasHit:!1}}function b(e){return{x:e.x,vx:e.vx,hp:e.hp,phase:e.phase,verb:e.verb,framesLeft:e.framesLeft,hasHit:e.hasHit}}function $(e){return{v:e.v,tick:e.tick,seed:e.seed,rng:e.rng,timer:e.timer,roundState:e.roundState,interFrames:e.interFrames,roundWins:[e.roundWins[0],e.roundWins[1]],players:[b(e.players[0]),b(e.players[1])]}}function ce(e){const r=l>>1,n=H>>1;return{v:D,tick:0,seed:e>>>0,rng:e>>>0,timer:U,roundState:"FIGHT",interFrames:0,roundWins:[0,0],players:[y(r-n|0),y(r+n|0)]}}function ee(e,r){return e.roundState==="FIGHT"&&e.players[r].phase==="NEUTRAL"}function oe(e,r){return ee(e,r)?F:V}function te(e){const r=l>>1,n=H>>1;e.players=[y(r-n|0),y(r+n|0)],e.timer=U,e.roundState="FIGHT",e.interFrames=0}function se(e){return e.roundWins[0]+e.roundWins[1]+1|0}function fe(e,r,n){const t=$(e),i=[];if(t.tick===0&&t.roundState==="FIGHT"&&i.push({t:"ROUND_START",round:1}),t.roundState==="MATCH_OVER")return t.tick=t.tick+1|0,{state:t,events:i};if(t.roundState==="ROUND_OVER"){if(t.interFrames=t.interFrames-1|0,t.interFrames<=0){const A=t.roundWins[0],C=t.roundWins[1];A>=N||C>=N?(t.roundState="MATCH_OVER",i.push({t:"MATCH_OVER",winner:A>=N?0:1})):(te(t),i.push({t:"ROUND_START",round:se(t)}))}return t.tick=t.tick+1|0,{state:t,events:i}}L(t.players[0],0,i),L(t.players[1],1,i);const a=[0,0],c=t.players[0];c.phase==="NEUTRAL"&&(r.action!==null?O(c,r.action):a[0]=r.dir|0);const s=t.players[1];if(n!==null&&s.phase==="NEUTRAL")switch(n){case"STRIKE":case"FEINT":case"PARRY":O(s,n);break;case"MOVE_L":a[1]=-1;break;case"MOVE_R":a[1]=1;break}q(t,a);const u=[v(t.players[0]),v(t.players[1])];J(t,u,a,i),Q(t,i);const R=t.players[0].hp,E=t.players[1].hp;let f=-1;return R<=0||E<=0?(R<=0&&E<=0?f=R===E?-1:R>E?0:1:f=R<=0?1:0,f!==-1&&i.push({t:"KO",winner:f}),t.roundState="ROUND_OVER",t.interFrames=x):(t.timer=t.timer-1|0,t.timer<=0&&(f=R===E?-1:R>E?0:1,t.roundState="ROUND_OVER",t.interFrames=x)),t.roundState==="ROUND_OVER"&&f!==-1&&(t.roundWins[f]=t.roundWins[f]+1|0,i.push({t:"ROUND_OVER",winner:f})),_(t.players[0]),_(t.players[1]),t.tick=t.tick+1|0,{state:t,events:i}}export{l as A,Y as B,ie as D,ae as E,Z as H,k as R,W as S,d as a,ne as b,o as c,h as f,ee as i,oe as l,ce as r,fe as s};
|
static/assets/golden-B6Ebeawj.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import"./action_set-C6mnKwO1.js";import{r,s as d}from"./engine-DIUq88up.js";async function f(){const i=document.getElementById("out");try{const t=await(await fetch("./fixtures/golden/index.json")).json();for(const o of t){const n=await(await fetch(`./fixtures/golden/${o}.json`)).json();let s=r(n.seed);for(let e=0;e<n.ticks;e++){const a=n.inputs[e];if(s=d(s,{dir:a.d,action:a.a},a.ai).state,JSON.stringify(s)!==n.states[e]){const c=`divergence in ${o} at tick ${e}`;i.textContent=`FAIL: ${c}`,window.__GOLDEN_RESULT={ok:!1,detail:c};return}}i.textContent+=`
|
| 2 |
+
${o}: OK (${n.ticks} ticks)`}window.__GOLDEN_RESULT={ok:!0,detail:`all ${t.length} fixtures byte-identical`},i.textContent+=`
|
| 3 |
+
ALL OK`}catch(t){window.__GOLDEN_RESULT={ok:!1,detail:String(t)},i.textContent=`ERROR: ${String(t)}`}}f();
|
static/assets/index-K8o-xskJ.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/assets/llm.worker-GbIGcnxV.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/assets/main-Cu1TzpLc.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import"./action_set-C6mnKwO1.js";import{H as j,A as z,S as tt,R,f as L,B as Rt,D as gt,a as bt,E as Et,i as C,s as yt,l as Mt,b as Pt,r as nt,c as G}from"./engine-DIUq88up.js";import{V as et,G as It,P as Lt,N as J,T as st,a as _t,b as it,f as Ot,A as Ct,H as wt,v as kt,g as at,p as K,C as Nt}from"./index-K8o-xskJ.js";const ot=120,Ft={KeyJ:"STRIKE",KeyZ:"STRIKE",KeyK:"FEINT",KeyX:"FEINT",KeyL:"PARRY",KeyC:"PARRY"};class $t{left=!1;right=!1;buffered=null;poseAdvance=0;facingSign=1;uiPresses=[];injectAction(t){this.buffered={action:t,at:performance.now()}}setPoseAdvance(t){this.poseAdvance=t}setFacingSign(t){this.facingSign=t}attach(t){t.addEventListener("keydown",e=>{if(e.repeat)return;const s=e.target?.tagName;if(!(s==="TEXTAREA"||s==="INPUT"||s==="SELECT"))switch(e.code){case"ArrowLeft":case"KeyA":this.left=!0;break;case"ArrowRight":case"KeyD":this.right=!0;break;default:{const i=Ft[e.code];i?(this.buffered={action:i,at:performance.now()},e.preventDefault()):this.uiPresses.push(e.code)}}}),t.addEventListener("keyup",e=>{(e.code==="ArrowLeft"||e.code==="KeyA")&&(this.left=!1),(e.code==="ArrowRight"||e.code==="KeyD")&&(this.right=!1)}),t.addEventListener("blur",()=>{this.left=!1,this.right=!1})}poll(){let t=null;this.buffered&&(performance.now()-this.buffered.at<=ot&&(t=this.buffered.action),(t||performance.now()-this.buffered.at>ot)&&(this.buffered=null));const e=this.left===this.right?0:this.left?-1:1;return{dir:e!==0?e:this.poseAdvance*this.facingSign,action:t}}drainUi(){const t=this.uiPresses;return this.uiPresses=[],t}}const V=10,Bt=.85;class Wt{hitStopFrames=0;timeScale=1;slowmoFrames=0;shakeMag=0;flash=0;hurt=0;chroma=0;koZoom=0;waves=[];onEvents(t,e){for(const s of t)switch(s.t){case"HIT":this.hitStopFrames=Math.max(this.hitStopFrames,7),this.shakeMag=Math.min(V,this.shakeMag+8),this.flash=Math.max(this.flash,.5),this.chroma=Math.max(this.chroma,.8),e&&this.spawnWave(e.x,e.y,"#c93a2e",5),s.by===1&&(this.hurt=1);break;case"PARRY_SUCCESS":this.hitStopFrames=Math.max(this.hitStopFrames,8),this.shakeMag=Math.min(V,this.shakeMag+5),this.flash=Math.max(this.flash,.85),e&&(this.spawnWave(e.x,e.y,"#4e87b8",4),this.spawnWave(e.x,e.y,"#dce9f5",2));break;case"BLOCK":this.hitStopFrames=Math.max(this.hitStopFrames,3),this.shakeMag=Math.min(V,this.shakeMag+3),e&&this.spawnWave(e.x,e.y,"#6e8aa6",2.5);break;case"KO":this.hitStopFrames=Math.max(this.hitStopFrames,16),this.shakeMag=V,this.flash=1,this.chroma=1,this.koZoom=1,this.slowmoFrames=55,e&&(this.spawnWave(e.x,e.y,"#2b2620",6),this.spawnWave(e.x,e.y,"#c93a2e",3));break;case"FEINT_BAITED":this.flash=Math.max(this.flash,.25),e&&this.spawnWave(e.x,e.y-30,"#b08d3e",2);break}}spawnWave(t,e,s,i){this.waves.push({x:t,y:e,r:6,life:1,color:s,width:i})}tick(){this.shakeMag*=Bt,this.shakeMag<.3&&(this.shakeMag=0),this.flash*=.82,this.flash<.02&&(this.flash=0),this.hurt*=.9,this.hurt<.02&&(this.hurt=0),this.chroma*=.85,this.chroma<.03&&(this.chroma=0),this.koZoom*=.96,this.koZoom<.02&&(this.koZoom=0),this.slowmoFrames>0?(this.slowmoFrames--,this.timeScale=.3):this.timeScale=1;for(let t=this.waves.length-1;t>=0;t--){const e=this.waves[t];e.r+=7+(1-e.life)*5,e.life-=.05,e.life<=0&&this.waves.splice(t,1)}}view(){const t=Math.random()*Math.PI*2;return{shakeX:Math.cos(t)*this.shakeMag,shakeY:Math.sin(t)*this.shakeMag*.6,flash:this.flash,hurt:this.hurt,chroma:this.chroma,koZoom:this.koZoom,shockwaves:this.waves}}}class Ut{pool=[];spawnSparks(t,e,s,i=14,r=7){for(let n=0;n<i;n++){const o=Math.random()*Math.PI*2,h=r*(.4+Math.random()*.8);this.pool.push({x:t,y:e,vx:Math.cos(o)*h,vy:Math.sin(o)*h-2,life:1,decay:.03+Math.random()*.03,size:2+Math.random()*3,color:s,kind:"spark"})}}spawnRing(t,e,s){this.pool.push({x:t,y:e,vx:0,vy:0,life:1,decay:.045,size:6,color:s,kind:"ring"})}spawnDust(t,e,s){for(let i=0;i<6;i++)this.pool.push({x:t-s*Math.random()*14,y:e-Math.random()*6,vx:-s*(1+Math.random()*2),vy:-Math.random()*1.2,life:.7,decay:.04,size:2+Math.random()*2,color:"rgba(90,78,58,0.5)",kind:"dust"})}update(){for(let t=this.pool.length-1;t>=0;t--){const e=this.pool[t];if(e.life-=e.decay,e.life<=0){this.pool.splice(t,1);continue}e.x+=e.vx,e.y+=e.vy,e.kind==="spark"&&(e.vy+=.35),e.kind==="ring"&&(e.size+=3.5)}}draw(t){for(const e of this.pool)t.globalAlpha=Math.max(0,e.life),e.kind==="ring"?(t.strokeStyle=e.color,t.lineWidth=2.5,t.beginPath(),t.arc(e.x,e.y,e.size,0,Math.PI*2),t.stroke()):(t.fillStyle=e.color,t.fillRect(e.x-e.size/2,e.y-e.size/2,e.size,e.size));t.globalAlpha=1}}class Dt{ctx=null;master=null;muted=!1;unlock(){if(!this.ctx)try{this.ctx=new AudioContext,this.master=this.ctx.createGain(),this.master.gain.value=.35,this.master.connect(this.ctx.destination)}catch{this.ctx=null}}noise(t,e,s,i="bandpass"){if(!this.ctx||!this.master||this.muted)return;const r=this.ctx,n=r.createBuffer(1,r.sampleRate*t,r.sampleRate),o=n.getChannelData(0);for(let d=0;d<o.length;d++)o[d]=Math.random()*2-1;const h=r.createBufferSource();h.buffer=n;const l=r.createBiquadFilter();l.type=i,l.frequency.value=e,l.Q.value=1.2;const c=r.createGain();c.gain.setValueAtTime(s,r.currentTime),c.gain.exponentialRampToValueAtTime(1e-4,r.currentTime+t),h.connect(l).connect(c).connect(this.master),h.start()}tone(t,e,s,i="sine",r){if(!this.ctx||!this.master||this.muted)return;const n=this.ctx,o=n.createOscillator();o.type=i,o.frequency.setValueAtTime(t,n.currentTime),r&&o.frequency.exponentialRampToValueAtTime(r,n.currentTime+e);const h=n.createGain();h.gain.setValueAtTime(s,n.currentTime),h.gain.exponentialRampToValueAtTime(1e-4,n.currentTime+e),o.connect(h).connect(this.master),o.start(),o.stop(n.currentTime+e)}gestureCue(t){t==="STRIKE"?this.noise(.07,2400,.28,"highpass"):this.tone(880,.05,.25,"triangle",1100)}onEvents(t){for(const e of t)switch(e.t){case"WHIFF":this.noise(.12,1800,.25,"highpass");break;case"HIT":this.tone(90,.16,.9,"sine",45),this.noise(.06,900,.5);break;case"BLOCK":this.tone(320,.07,.5,"square",240),this.noise(.04,1400,.3);break;case"PARRY_SUCCESS":this.tone(1100,.22,.5,"triangle",1650),this.tone(2200,.12,.2,"sine");break;case"PARRY_WHIFF":this.noise(.1,600,.25);break;case"FEINT_BAITED":this.tone(660,.06,.3,"square");break;case"KO":this.tone(60,.6,1,"sine",30),this.noise(.4,300,.6,"lowpass");break;case"ROUND_START":this.tone(523,.1,.3,"triangle"),this.tone(784,.18,.3,"triangle");break;case"MATCH_OVER":this.tone(392,.5,.4,"triangle",523);break}}}function Yt(a){switch(a.phase){case"NEUTRAL":return{body:"#2b2620",accent:"#52483a",glow:0,label:""};case"STARTUP":{if(a.verb==="PARRY")return{body:"#34465c",accent:"#3a5a78",glow:.5,label:""};const t=Math.max(0,1-a.framesLeft/12);return{body:"#6a4526",accent:`rgba(190,106,30,${.55+.45*t})`,glow:.3+.6*t,label:""}}case"ACTIVE":return a.verb==="PARRY"?{body:"#3a5a78",accent:"#4e87b8",glow:1,label:""}:{body:"#7a241c",accent:"#c93a2e",glow:1,label:""};case"RECOVERY":return{body:"#8a7c64",accent:"#a89878",glow:.1,label:""};case"BLOCKSTUN":return{body:"#52617a",accent:"#6e8aa6",glow:.25,label:""};case"STAGGER":return{body:"#7c4868",accent:"#a05c84",glow:.4,label:""}}}const I="#2b2620",q="#e0a878",Ht="#3d3a33";function A(a){return Math.round(a/3)*3}function $(a,t,e,s,i,r,n=!1){const o=Yt(i),h=i.phase==="NEUTRAL"?Math.round(Math.sin(r*.12)*2):0;let l=0,c=[19,-6],d=[-10,-3],f=10,p=-10;switch(i.phase){case"STARTUP":i.verb==="PARRY"?(c=[13,-18],d=[9,-15]):(l=-5,c=[-16,-15],d=[-7,-4]);break;case"ACTIVE":i.verb==="PARRY"?(c=[15,-20],d=[11,-16]):(l=11,c=[33,-10],d=[-13,3],f=19,p=-15);break;case"RECOVERY":l=5,c=[13,12],d=[-7,6];break;case"BLOCKSTUN":l=-7,c=[12,-16],d=[8,-13];break;case"STAGGER":l=-13,c=[-15,-18],d=[7,-19],f=15,p=-19;break}const m=At=>A(t+s*(At+l)),b=i.phase==="STARTUP"||i.phase==="ACTIVE",u=A(e-34+h),y=A(u-32),k=A(y-17),w=A(t+s*l);a.save(),o.glow>0&&!n&&(a.shadowColor=o.accent,a.shadowBlur=12*o.glow),n||(a.fillStyle="rgba(43,38,32,0.14)",a.fillRect(A(t-17),e-2,34,4)),a.fillStyle=Ht,a.fillRect(m(f)-4,u,8,e-u),a.fillRect(m(p)-4,u,8,e-u),a.fillStyle=I,a.fillRect(m(f)-5,e-7,10,7),a.fillRect(m(p)-5,e-7,10,7),a.fillStyle=o.body,a.fillRect(w-13,y,26,14),a.fillRect(w-9,y+14,18,u-y-12),a.lineWidth=2,a.strokeStyle=I,a.strokeRect(w-13,y,26,14),a.fillStyle=I,a.save(),a.translate(w,y+2),a.rotate(s*.5),a.fillRect(-15,0,30,4),a.restore(),a.fillStyle=o.accent,a.fillRect(w-9,u-5,18,5);const x=y+5,E=m(d[0]),M=A(x+d[1]+9);a.fillStyle=q,a.fillRect(Math.min(w,E)-3,M-3,Math.abs(E-w)+6,6),a.fillRect(E-4,M-4,8,8);const _=m(c[0]),P=A(x+c[1]+9);a.fillStyle=q,a.fillRect(Math.min(w,_)-3,P-3,Math.abs(_-w)+6,6),a.fillStyle=b?o.accent:q,a.fillRect(_-5,P-5,10,10),b&&!n&&(a.strokeStyle=I,a.strokeRect(_-5,P-5,10,10));const T=A(w+s*Math.round(l*.3));a.fillStyle=q,a.fillRect(T-7,k,14,14),a.fillStyle=I,a.fillRect(T-7,k-3,14,5),a.strokeStyle=I,a.strokeRect(T-7,k,14,14),a.fillStyle=o.accent,a.fillRect(T-8,k+2,16,4);const S=Math.round(Math.sin(r*.2)*2),F=i.phase==="ACTIVE"?16:11;a.fillRect(T-s*(8+F)+(s<0?0:-0),k+2+S,F,3),n||(a.fillStyle=I,a.fillRect(T+s*3,k+7,3,3)),i.verb==="PARRY"&&b&&(a.strokeStyle=o.accent,a.lineWidth=3,a.beginPath(),a.arc(t+s*20,x,20,-Math.PI/2.2,Math.PI/2.2),a.stroke()),a.restore()}const g={paperTop:"#f7f0e3",paperMid:"#f1e7d3",paperLow:"#e9dcc2",paperDeep:"#e0d0b1",cardBorder:"#cbbb98",ink:"#2b2620",inkSoft:"#52483a",inkFaint:"#8a7c64",inkGhost:"rgba(43,38,32,0.35)",inkWashFar:"rgba(43,38,32,0.08)",inkWashNear:"rgba(43,38,32,0.14)",vermillion:"#c93a2e",vermillionDeep:"#a32d23",indigo:"#3a5a78",gold:"#b08d3e",moss:"#6f7d54",mossBright:"#8a9b66",serif:'"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif'};class Gt{constructor(t){this.canvas=t,this.ctx=t.getContext("2d"),this.world=document.createElement("canvas"),this.wctx=this.world.getContext("2d"),this.g=this.wctx,this.resize(),window.addEventListener("resize",()=>this.resize());for(let e=0;e<26;e++)this.motes.push({x:Math.random(),y:Math.random(),v:.06+Math.random()*.22,r:Math.random()*1.6+.4})}ctx;world;wctx;g;w=0;h=0;dpr=1;displayedHp=[j,j];camX=0;frames=0;fps=0;lastFpsAt=performance.now();trail=[[],[]];motes=[];t=0;lite=!1;ridgeFar=null;ridgeNear=null;lanternSprite=null;px=3;resize(){this.px=this.lite?4:3,this.dpr=Math.min(this.lite?1.25:2,window.devicePixelRatio||1),this.w=window.innerWidth,this.h=window.innerHeight,this.canvas.width=this.w*this.dpr|0,this.canvas.height=this.h*this.dpr|0,this.canvas.style.width=`${this.w}px`,this.canvas.style.height=`${this.h}px`,this.world.width=Math.max(1,this.w/this.px|0),this.world.height=Math.max(1,this.h/this.px|0),this.buildStaticLayers()}buildStaticLayers(){const t=this.groundY(),e=(n,o,h,l,c,d)=>{const f=new Path2D;f.moveTo(-200,t-n);for(let p=-200;p<=this.w+200;p+=d){const m=p*l+c;f.lineTo(p,t-n-Math.abs(Math.sin(m)*o+Math.sin(m*2.7)*h))}return f.lineTo(this.w+200,t),f.lineTo(-200,t),f};this.ridgeFar=e(150,46,18,.012,2,40),this.ridgeNear=e(84,30,12,.02,9,30);const s=document.createElement("canvas");s.width=s.height=48;const i=s.getContext("2d"),r=i.createRadialGradient(24,24,2,24,24,24);r.addColorStop(0,"rgba(201,90,46,0.55)"),r.addColorStop(.3,"rgba(201,90,46,0.22)"),r.addColorStop(1,"rgba(201,90,46,0)"),i.fillStyle=r,i.fillRect(0,0,48,48),this.lanternSprite=s}sx(t){const s=t/256,i=z/256;return 70+s/i*(this.w-140)-this.camX}bandH(){return Math.min(this.h*.7,this.w/2.4)}bandTop(){return(this.h-this.bandH())/2}groundY(){return this.bandTop()+this.bandH()*.72}playerPx(t,e){return{x:this.sx(t.players[e].x),y:this.groundY()-60}}drawBackground(){const t=this.g,e=this.groundY(),s=this.bandTop(),i=this.bandH(),r=t.createLinearGradient(0,s,0,s+i);r.addColorStop(0,g.paperTop),r.addColorStop(.55,g.paperMid),r.addColorStop(.72,g.paperLow),r.addColorStop(1,g.paperDeep),t.fillStyle=r,t.fillRect(0,s,this.w,i),this.ridgeFar&&this.ridgeNear&&(t.save(),t.translate(-this.camX*.12,0),t.fillStyle=g.inkWashFar,t.fill(this.ridgeFar),t.restore(),t.save(),t.translate(-this.camX*.3,0),t.fillStyle=g.inkWashNear,t.fill(this.ridgeNear),t.restore());for(let h=0;h<5;h++){const l=this.w/6*(h+1)-this.camX*.35,c=s+48+h%2*22,d=.55+.45*Math.sin(this.t*.03+h*1.7);if(t.strokeStyle=g.inkGhost,t.beginPath(),t.moveTo(l,s),t.lineTo(l,c-8),t.stroke(),this.lanternSprite){t.globalAlpha=.4+.5*d;const f=30+14*d;t.drawImage(this.lanternSprite,l-f/2,c-f/2,f,f),t.globalAlpha=1}}if(!this.lite){t.fillStyle="rgba(90,78,58,0.14)";for(const h of this.motes)h.y-=h.v/this.h,h.x+=Math.sin(this.t*.01+h.r*9)*3e-4,h.y<0&&(h.y=1,h.x=Math.random()),t.beginPath(),t.arc(h.x*this.w,s+h.y*(e-s),h.r,0,Math.PI*2),t.fill()}t.strokeStyle=g.ink,t.lineWidth=2.5,t.beginPath(),t.moveTo(0,e),t.lineTo(this.w,e),t.stroke(),t.strokeStyle="rgba(43,38,32,0.07)",t.lineWidth=1;const n=s+i;for(let h=1;h<=6;h++){const l=e+h*h*4.5;if(l>n)break;t.beginPath(),t.moveTo(0,l),t.lineTo(this.w,l),t.stroke()}const o=this.sx(z/2);for(let h=-8;h<=8;h++)t.beginPath(),t.moveTo(o+h*70,e),t.lineTo(o+h*150,n),t.stroke()}drawFighters(t,e){const s=this.g,i=this.groundY(),r=this.sx(t.players[0].x),n=this.sx(t.players[1].x),o=r<=n?1:-1,h=n<r?1:-1;for(const l of[0,1]){const c=this.trail[l];c.push({p:{...t.players[l]},t:this.t}),c.length>16&&c.shift()}this.lite||(s.save(),s.globalAlpha=.1,s.translate(0,i*2),s.scale(1,-1),$(s,r,i,o,t.players[0],this.t,!0),$(s,n,i,h,t.players[1],this.t,!0),s.restore());for(const l of[0,1]){const c=t.players[l];if(c.phase==="ACTIVE"||c.phase==="STAGGER"||c.phase==="STARTUP"&&c.framesLeft<5){const d=this.trail[l];for(const f of this.lite?[4]:[10,6,3]){const p=d[d.length-f];p&&(s.save(),s.globalAlpha=f>=10?.07:f>=6?.13:.2,$(s,this.sx(p.p.x),i,l===0?o:h,p.p,p.t,!0),s.restore())}}}if($(s,r,i,o,t.players[0],this.t),$(s,n,i,h,t.players[1],this.t),e.scanActive){const l=Math.sin(this.t*.12)*46,c=r+l,d=s.createLinearGradient(c-14,0,c+14,0);d.addColorStop(0,"rgba(58,90,120,0)"),d.addColorStop(.5,"rgba(58,90,120,0.18)"),d.addColorStop(1,"rgba(58,90,120,0)"),s.fillStyle=d,s.fillRect(c-14,i-150,28,150),s.fillStyle="rgba(58,90,120,0.85)",s.font="12px ui-monospace, monospace",s.textAlign="center",s.fillText("R E A D I N G",r,i-158)}s.textAlign="center";for(const l of e.intents){const c=Math.max(0,1-l.age/50);c<=0||(s.globalAlpha=c,s.fillStyle=l.ch==="S"?g.vermillion:l.ch==="P"?g.indigo:l.ch==="F"?g.gold:g.inkFaint,s.font=`bold ${13+(1-c)*8}px ${g.serif}`,s.fillText(l.ch,n+(l.age%2===0?14:-10),i-142-l.age*.9))}s.globalAlpha=1}drawHud(t,e){const{ctx:s}=this,i=this.bandTop(),r=i+12;for(const o of[0,1]){this.displayedHp[o]+=(t.players[o].hp-this.displayedHp[o])*.15;const h=this.w*.36,l=o===0?24:this.w-24-h;s.fillStyle="rgba(43,38,32,0.10)",s.fillRect(l,r,h,14);const c=Math.max(0,this.displayedHp[o]/j),d=h*c,f=s.createLinearGradient(l,0,l+h,0);o===0?(f.addColorStop(0,g.moss),f.addColorStop(1,g.mossBright)):(f.addColorStop(0,g.vermillion),f.addColorStop(1,g.vermillionDeep)),s.fillStyle=f,s.fillRect(o===0?l+h-d:l,r,d,14),s.strokeStyle=g.ink,s.strokeRect(l,r,h,14);for(let p=0;p<2;p++)s.beginPath(),s.arc(o===0?l+10+p*18:l+h-10-p*18,r+28,5,0,Math.PI*2),s.fillStyle=p<t.roundWins[o]?o===0?g.moss:g.vermillion:"rgba(43,38,32,0.15)",s.fill()}if(s.fillStyle="#cfc2a4",s.font="bold 14px ui-monospace, monospace",s.textAlign="left",s.fillText("YOU",24,i-12),s.textAlign="right",s.fillText(e.botName,this.w-24,i-12),s.textAlign="center",s.fillStyle="#efe5cf",s.font=`bold 26px ${g.serif}`,s.fillText(`${Math.max(0,Math.ceil(t.timer/tt))}`,this.w/2,i-10),e.opsSpark.length>1){const l=this.w-18-130,c=this.h-72;s.strokeStyle="rgba(207,194,164,0.4)",s.strokeRect(l,c,130,26);const d=Math.max(...e.opsSpark,250);s.beginPath(),e.opsSpark.forEach((f,p)=>{const m=l+p/(e.opsSpark.length-1)*130,b=c+26-Math.min(1,f/d)*26;p===0?s.moveTo(m,b):s.lineTo(m,b)}),s.strokeStyle="#7fa6c8",s.stroke(),s.fillStyle="#a8997c",s.font="12px ui-monospace, monospace",s.textAlign="right",s.fillText(`decision ms (last ${e.opsSpark.length})`,l+130,c-4)}s.font="13px ui-monospace, monospace",s.fillStyle="#a8997c",s.textAlign="right",s.fillText(e.brainNote,this.w-16,this.h-36),s.textAlign="left",s.fillText("←→ move · J strike · K feint · L parry · V fight with your body · B judge panel · 6 TRAINED DUELIST · Enter rematch",16,this.h-16),this.frames++;const n=performance.now();n-this.lastFpsAt>=1e3&&(this.fps=this.frames,this.frames=0,this.lastFpsAt=n,!this.lite&&this.fps<45?(this.lite=!0,this.resize()):this.lite&&this.fps>57&&(this.lite=!1,this.resize())),s.textAlign="right",s.fillStyle=this.fps>=55?"#a8997c":"#e06a5a",s.fillText(`${this.fps} fps · draw ${this.drawMsEma.toFixed(1)}ms${this.lite?" · ⚡perf mode":""}`,this.w-16,this.h-16)}drawBanner(t){if(!t.msg)return;const{ctx:e}=this,s=t.msgAge,i=Math.min(1,s/10),r=1-Math.pow(1-i,3),n=(1-r)*-this.w*.25;e.save(),e.translate(n,0),e.globalAlpha=r;const o=this.bandTop()+this.bandH()*.36,h=e.createLinearGradient(0,o-44,0,o+18);h.addColorStop(0,"rgba(226,210,180,0)"),h.addColorStop(.5,"rgba(226,210,180,0.9)"),h.addColorStop(1,"rgba(226,210,180,0)"),e.fillStyle=h,e.fillRect(0,o-44,this.w,70),e.textAlign="center",e.fillStyle=g.ink,e.shadowColor="rgba(201,58,46,0.55)",e.shadowBlur=14*r,e.font=`bold 54px ${g.serif}`,e.fillText(t.msg,this.w/2,o),e.shadowBlur=0,t.sub&&(e.font="15px ui-monospace, monospace",e.fillStyle=g.inkSoft,e.fillText(t.sub,this.w/2,o+30)),e.restore()}drawRecap(t){const e=t.recap;if(!e)return;const{ctx:s}=this;s.fillStyle="rgba(244,236,221,0.96)",s.fillRect(0,0,this.w,this.h);const i=this.w/2;let r=this.h*.2;s.textAlign="center",s.fillStyle=e.winner==="you"?g.moss:g.vermillion,s.font=`bold 46px ${g.serif}`,s.fillText(e.winner==="you"?"YOU WIN":"THE AI WINS",i,r),s.fillStyle=g.inkFaint,s.font="13px ui-monospace, monospace",s.fillText(e.rounds,i,r+26),r+=72,s.fillStyle=g.indigo,s.font="bold 14px ui-monospace, monospace",s.fillText("— SCOUTING REPORT: what it learned about you —",i,r),r+=30,s.fillStyle=g.ink,s.font=`italic 19px ${g.serif}`;const n=e.finalRead.length>70?e.finalRead.slice(0,70)+"…":e.finalRead;s.fillText(`“${n}”`,i,r),r+=24,s.fillStyle=g.inkFaint,s.font="12px ui-monospace, monospace",s.fillText(`detected habit: ${e.habit}`,i,r),r+=44,s.font="14px ui-monospace, monospace";const o=[["hits landed",String(e.hitsYou),String(e.hitsAi)],["parries",String(e.parriesYou),String(e.parriesAi)],["times it baited you","",String(e.baitsAi)]];s.fillStyle=g.inkFaint,s.fillText("YOU · AI",i,r),r+=22;for(const[h,l,c]of o)s.fillStyle=g.inkSoft,s.fillText(`${l.padStart(3)} ${h} ${c.padEnd(3)}`,i,r),r+=22;r+=26,s.fillStyle=g.inkFaint,s.font="12px ui-monospace, monospace",s.fillText(e.modelLine,i,r),r+=30,s.fillStyle=g.indigo,s.font="13px ui-monospace, monospace",s.fillText("Enter — rematch · B — edit its brain (judge panel)",i,r)}drawOnboarding(t){if(!t.onboarding)return;const{ctx:e}=this,s=this.bandTop()+this.bandH()*.14;e.textAlign="center",e.fillStyle="rgba(250,245,233,0.92)",e.fillRect(this.w/2-310,s-18,620,86),e.strokeStyle=g.cardBorder,e.strokeRect(this.w/2-310,s-18,620,86),e.fillStyle=g.ink,e.font="13px ui-monospace, monospace",e.fillText("Strike beats idle · Parry beats strike · Feint baits parry",this.w/2,s+4),e.font="12px ui-monospace, monospace",e.fillStyle="#b06a2e",e.fillText("amber glow = attack winding up (real OR fake!) — parry it or step back",this.w/2,s+26),e.fillStyle=g.inkSoft,e.fillText("parrying NOTHING leaves you exposed · blocking fails in the corner",this.w/2,s+44),e.fillStyle=g.indigo,e.fillText("The AI reads your habits — its live read writes itself out at the bottom.",this.w/2,s+62)}drawMsEma=0;draw(t,e,s,i){const r=performance.now();this.t++;const n=this.wctx;this.g=n,n.setTransform(1/this.px,0,0,1/this.px,0,0);const o=(this.sx(t.players[0].x)+this.sx(t.players[1].x))/2+this.camX;this.camX+=(o-this.w/2)*.03,this.camX=Math.max(-40,Math.min(40,this.camX));const h=1+e.koZoom*.07;if(n.save(),n.translate(this.w/2,this.groundY()-40),n.scale(h,h),n.translate(-this.w/2,-(this.groundY()-40)),this.drawBackground(),i.cornerGlow!==0){const f=i.cornerGlow<0?0:this.w,p=n.createLinearGradient(f,0,f===0?120:this.w-120,0);p.addColorStop(0,"rgba(201,58,46,0.20)"),p.addColorStop(1,"rgba(201,58,46,0)"),n.fillStyle=p,n.fillRect(f===0?0:this.w-120,0,120,this.h)}n.save(),n.translate(e.shakeX,e.shakeY),this.drawFighters(t,i);for(const f of e.shockwaves)n.globalAlpha=f.life*.8,n.strokeStyle=f.color,n.lineWidth=f.width*f.life,n.beginPath(),n.arc(f.x,f.y,f.r,0,Math.PI*2),n.stroke();n.globalAlpha=1,s.draw(n),n.restore(),n.restore();const l=this.ctx;this.g=l,l.save(),l.setTransform(this.dpr,0,0,this.dpr,0,0),l.imageSmoothingEnabled=!1,l.drawImage(this.world,0,0,this.w,this.h);const c=this.bandTop(),d=c+this.bandH();if(l.fillStyle="#14110c",l.fillRect(0,0,this.w,c),l.fillRect(0,d,this.w,this.h-d),i.cornerGlow!==0){const f=i.cornerGlow<0?0:this.w;l.fillStyle="rgba(163,45,35,0.9)",l.font="bold 12px ui-monospace, monospace",l.textAlign=f===0?"left":"right",l.fillText("CORNERED — BLOCK FAILS HERE",f===0?14:this.w-14,this.groundY()+38)}if(this.drawHud(t,i),this.drawBanner(i),this.drawOnboarding(i),this.drawRecap(i),e.chroma>0&&(l.globalCompositeOperation="multiply",l.fillStyle=`rgba(201,58,46,${.05*e.chroma})`,l.fillRect(-4*e.chroma,0,this.w,this.h),l.fillStyle=`rgba(58,90,120,${.05*e.chroma})`,l.fillRect(4*e.chroma,0,this.w,this.h),l.globalCompositeOperation="source-over"),e.flash>0&&(l.fillStyle=`rgba(255,252,240,${e.flash*.55})`,l.fillRect(0,0,this.w,this.h)),e.hurt>0){const f=l.createRadialGradient(this.w/2,this.h/2,this.h*.3,this.w/2,this.h/2,this.h*.75);f.addColorStop(0,"rgba(201,58,46,0)"),f.addColorStop(1,`rgba(201,58,46,${e.hurt*.3})`),l.fillStyle=f,l.fillRect(0,0,this.w,this.h)}l.restore(),this.drawMsEma=this.drawMsEma*.92+(performance.now()-r)*.08}}function Kt(a){const t=a+1831565813|0;let e=t;return e=Math.imul(e^e>>>15,e|1),e=e+Math.imul(e^e>>>7,e|61)^e,{value:(e^e>>>14)>>>0,state:t>>>0}}function D(a){return bt(a.players[1].x-a.players[0].x|0)}function Y(a,t){const e=1-t;return a.players[e].x>a.players[t].x?"MOVE_R":"MOVE_L"}function St(a,t){const e=1-t;return a.players[e].x>a.players[t].x?"MOVE_L":"MOVE_R"}class H{state;constructor(t){this.state=t>>>0}f(){const t=Kt(this.state);return this.state=t.state,t.value/4294967296}}function Vt(){return{name:"spammer_v1",next(a,t){return a.players[t].phase!=="NEUTRAL"?"WAIT":D(a)<=R?"STRIKE":Y(a,t)}}}function qt(a){const t=vt(a);let e=!1;return{name:"wakeup_striker_v1",next(s,i){const r=s.players[i];return r.phase==="STAGGER"?(e=!0,"WAIT"):e&&r.phase==="NEUTRAL"?(e=!1,"STRIKE"):t.next(s,i)}}}function vt(a,t={}){const e=new H(a),s=t.range??R,i=t.pStrike??.55;return{name:`aggressive${t.pStrike!==void 0||t.range!==void 0?"_v2":"_v1"}`,next(r,n){const o=r.players[1-n],h=D(r);if((o.phase==="STAGGER"||o.phase==="RECOVERY")&&h<=R)return"STRIKE";if(h>s)return Y(r,n);const l=e.f();return l<i?"STRIKE":l<i+.1?"PARRY":"WAIT"}}}function Xt(a,t={}){const e=new H(a),s=t.prefer??L(72),i=t.pParry??.3;return{name:`defensive${t.prefer!==void 0||t.pParry!==void 0?"_v2":"_v1"}`,next(r,n){const o=r.players[1-n],h=D(r);return(o.phase==="RECOVERY"||o.phase==="STAGGER")&&h<=R?"STRIKE":o.phase==="STARTUP"&&h<=R&&e.f()<i?"PARRY":h<s?St(r,n):h>s+L(36)?Y(r,n):"WAIT"}}}function zt(a,t={}){const e=new H(a),s=t.pFeint??.4;return{name:`baiter${t.pFeint!==void 0?"_v2":"_v1"}`,next(i,r){const n=i.players[1-r],o=D(i);if((n.phase==="RECOVERY"||n.phase==="STAGGER")&&o<=R)return"STRIKE";if(o<=Rt){const h=e.f();return h<s?"FEINT":h<s+.3?St(i,r):h<s+.45?"STRIKE":"WAIT"}return Y(i,r)}}}function jt(a,t={}){const e=new H(a),s=t.delay??45,i=t.noise??.1,r=[];let n="NEUTRAL";const o=["STRIKE","FEINT","PARRY","MOVE_L","MOVE_R","WAIT"];return{name:"mirror",next(h,l){const c=h.players[1-l];if(c.phase==="STARTUP"&&n!=="STARTUP"&&c.verb!=="NONE"&&r.push({at:h.tick+s,verb:c.verb}),n=c.phase,e.f()<i)return o[e.f()*6|0];const d=r[0];return d&&d.at<=h.tick?(r.shift(),d.verb):D(h)>gt?Y(h,l):"WAIT"}}}function Zt(a){const t=new H(a),e=[{v:"STRIKE",w:.2},{v:"FEINT",w:.1},{v:"PARRY",w:.15},{v:"MOVE_L",w:.2},{v:"MOVE_R",w:.2},{v:"WAIT",w:.15}];return{name:"random",next(){let s=t.f();for(const i of e){if(s<i.w)return i.v;s-=i.w}return"WAIT"}}}const Jt=`
|
| 2 |
+
.parry-hud { position:absolute; inset:0; pointer-events:none; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
| 3 |
+
.parry-brain { position:absolute; left:50%; transform:translateX(-50%); bottom:46px; width:min(680px, 86vw);
|
| 4 |
+
background:#f8f0d8; border:3px solid #2b2620; border-radius:2px;
|
| 5 |
+
box-shadow: inset 0 0 0 2px #fffdf4, 4px 4px 0 rgba(43,38,32,.28);
|
| 6 |
+
padding:.55rem .8rem;
|
| 7 |
+
transition: border-color .3s; } /* NO backdrop-filter — it forced a re-blur on every text frame */
|
| 8 |
+
.parry-brain.fresh { border-color:#3a5a78; }
|
| 9 |
+
.parry-brain.override { border-color:#c93a2e; animation: parryOverride 1.4s infinite; }
|
| 10 |
+
@keyframes parryOverride { 50% { border-color:#7a2620; } }
|
| 11 |
+
.parry-brain .plan { color:#2b2620; font-size:13px; line-height:1.45; min-height:1.2em; letter-spacing:.02em; }
|
| 12 |
+
.parry-brain .plan .src-override { color:#c93a2e; font-weight:700; font-size:11px; }
|
| 13 |
+
.parry-brain .notes { margin-top:.35rem; padding-top:.3rem; border-top:1px dashed #cbbb98; color:#52483a; font-size:11px; line-height:1.5; }
|
| 14 |
+
.parry-brain .notes div::before { content:"▸ "; color:#3a5a78; }
|
| 15 |
+
.parry-brain .meta { display:flex; justify-content:space-between; gap:1rem; margin-top:.3rem; color:#8a7c64; font-size:11px; }
|
| 16 |
+
.parry-brain .meta b { color:#3a5a78; font-weight:600; }
|
| 17 |
+
.parry-brain .reading { color:#3a5a78; animation: parryPulse 1s infinite; }
|
| 18 |
+
@keyframes parryPulse { 50% { opacity:.35; } }
|
| 19 |
+
.parry-intent { position:absolute; right:18px; top:84px; text-align:right; color:#8a7c64; font-size:12px;
|
| 20 |
+
background:#f8f0d8; border:3px solid #2b2620; border-radius:2px; padding:.3rem .6rem;
|
| 21 |
+
box-shadow: inset 0 0 0 2px #fffdf4, 3px 3px 0 rgba(43,38,32,.28); }
|
| 22 |
+
.parry-intent .verb { font-size:26px; color:#3a5a78; font-weight:700; }
|
| 23 |
+
.parry-meter { height:6px; background:#d8ccae; border:1px solid #2b2620; overflow:hidden; margin-top:.3rem; }
|
| 24 |
+
.parry-meter > div { height:100%; background:#3a5a78; width:0%; transition:width .4s; }
|
| 25 |
+
`;class Qt{root;planSrc;planText;readingSlot;notesEl;metaL;metaR;intentEl;meterEl;constructor(t){const e=document.createElement("style");e.textContent=Jt,document.head.appendChild(e),this.root=document.createElement("div"),this.root.className="parry-hud",this.root.innerHTML=`
|
| 26 |
+
<div class="parry-brain">
|
| 27 |
+
<div class="plan"><span class="src"></span>🧠 “<span class="ptext"></span>”<span class="reading-slot"></span></div>
|
| 28 |
+
<div class="parry-meter"><div></div></div>
|
| 29 |
+
<div class="notes"></div>
|
| 30 |
+
<div class="meta"><span class="l"></span><span class="r"></span></div>
|
| 31 |
+
</div>
|
| 32 |
+
<div class="parry-intent"><div>AI intent</div><div class="verb">·</div></div>`,t.appendChild(this.root),this.planSrc=this.root.querySelector(".plan .src"),this.planText=this.root.querySelector(".plan .ptext"),this.readingSlot=this.root.querySelector(".plan .reading-slot"),this.notesEl=this.root.querySelector(".notes"),this.metaL=this.root.querySelector(".meta .l"),this.metaR=this.root.querySelector(".meta .r"),this.intentEl=this.root.querySelector(".parry-intent .verb"),this.meterEl=this.root.querySelector(".parry-meter > div"),this.root.style.display="none"}lastPlan="";reveal=0;freshTtl=0;render(t){if(!t){this.root.style.display="none";return}this.root.style.display="",t.planString!==this.lastPlan&&(this.lastPlan=t.planString,this.reveal=0,this.freshTtl=90),this.twPhase=(this.twPhase+1)%3,this.twPhase===0&&(this.reveal=Math.min(this.lastPlan.length,this.reveal+5)),this.freshTtl>0&&this.freshTtl--;const e=this.lastPlan.slice(0,this.reveal)+(this.reveal<this.lastPlan.length?"▋":""),s=this.root.querySelector(".parry-brain");s.classList.toggle("override",t.planSource==="override"),s.classList.toggle("fresh",this.freshTtl>0&&t.planSource!=="override"),e!==this.cache.plan&&(this.cache.plan=e,this.planText.textContent=e);const i=t.planSource==="override"?"[JUDGE OVERRIDE] ":"";i!==this.cache.src&&(this.cache.src=i,this.planSrc.textContent=i,this.planSrc.className="src"+(i?" src-override":""));const r=t.analystBusy?" · reading you…":"";r!==this.cache.reading&&(this.cache.reading=r,this.readingSlot.textContent=r,this.readingSlot.className="reading-slot"+(r?" reading":""));const n=`${Math.round(t.prediction.confidence*100)}%`;n!==this.cache.meter&&(this.cache.meter=n,this.meterEl.style.width=n);const o=t.scout.join(`
|
| 33 |
+
`);o!==this.cache.notes&&(this.cache.notes=o,this.notesEl.replaceChildren(...t.scout.map(p=>{const m=document.createElement("div");return m.textContent=p,m})));const h=`read: <b>${t.prediction.predictedHabit}</b> · conf ${(t.prediction.confidence*100).toFixed(0)}%`;h!==this.cache.metaL&&(this.cache.metaL=h,this.metaL.innerHTML=h);const c=`<b>${t.backend==="localWebGPU"?"0ms network · local":t.backend==="server"?"server fallback":"—"}</b> · ${t.medianOpMs.toFixed(0)}ms/decision · ${t.modelId.split("-q4")[0]}`;c!==this.cache.metaR&&(this.cache.metaR=c,this.metaR.innerHTML=c);const d=t.lastTick?.intent??null,f=d&&d!=="CONTINUE"?et[d]??"·":"·";f!==this.cache.intent&&(this.cache.intent=f,this.intentEl.textContent=f)}cache={plan:"",src:"",reading:"",meter:"",metaL:"",metaR:"",intent:"",notes:""};twPhase=0}const N={ms:0};function lt(){return Math.max(1,Math.round(N.ms/1e3*tt))}const te=64;class ee{frames=[];push(t,e,s){this.frames.push({tick:t,state:e,events:s}),this.frames.length>te&&this.frames.shift()}delayed(t){if(this.frames.length===0)return null;const e=t-lt();for(let s=this.frames.length-1;s>=0;s--)if(this.frames[s].tick<=e)return this.frames[s];return this.frames[0]}delayedWindow(t,e){const s=t-lt();return this.frames.filter(i=>i.tick<=s&&i.tick>s-e)}clear(){this.frames=[]}}const se="funnel_v1",ie="trace_v1",ae=Math.random().toString(36).slice(2,10);function ht(){return typeof window<"u"&&window.__PARRY_API_BASE||""}function v(a,t){try{if(!ht()&&/^(localhost|127\.0\.0\.1)$/.test(location.hostname))return;const e={v:se,event:a,session:ae,ts:Date.now(),...t?{props:t}:{}},s=new Blob([JSON.stringify(e)],{type:"application/json"});navigator.sendBeacon(`${ht()}/funnel`,s)}catch{}}function re(a,t){return{v:ie,createdAt:new Date().toISOString(),modelId:t.modelId,backend:t.backend,engineVersion:Et,promptVersion:Lt,grammarId:It,seed:t.seed,finalScore:t.finalScore,rows:a}}function ne(a){const t=new Blob([JSON.stringify(a,null,2)],{type:"application/json"}),e=document.createElement("a");e.href=URL.createObjectURL(t),e.download=`parry-brain-trace-${a.seed}-${Date.now()}.json`,e.click(),URL.revokeObjectURL(e.href),v("trace_exported")}const oe=`
|
| 34 |
+
.parry-debug { position:absolute; right:16px; top:120px; width:300px; background:#f8f0d8;
|
| 35 |
+
border:3px solid #2b2620; border-radius:2px; padding: .8rem;
|
| 36 |
+
font-family:ui-monospace,monospace; color:#2b2620; font-size:12px; pointer-events:auto; z-index:10;
|
| 37 |
+
box-shadow: inset 0 0 0 2px #fffdf4, 4px 4px 0 rgba(43,38,32,.28); }
|
| 38 |
+
.parry-debug h3 { margin:.1rem 0 .5rem; font-size:12px; color:#c93a2e; letter-spacing:.06em; }
|
| 39 |
+
.parry-debug textarea { width:100%; box-sizing:border-box; background:#fffdf4; color:#2b2620; border:2px solid #2b2620;
|
| 40 |
+
border-radius:0; font:12px ui-monospace,monospace; padding:.4rem; resize:vertical; min-height:54px; }
|
| 41 |
+
.parry-debug .row { display:flex; gap:.4rem; margin-top:.45rem; align-items:center; }
|
| 42 |
+
.parry-debug button { background:#efe5d0; color:#2b2620; border:2px solid #2b2620; border-radius:0; padding:.3rem .6rem;
|
| 43 |
+
font:inherit; font-weight:700; cursor:pointer; box-shadow:2px 2px 0 rgba(43,38,32,.3); }
|
| 44 |
+
.parry-debug button:hover { background:#3a5a78; color:#f8f0d8; }
|
| 45 |
+
.parry-debug button:active { box-shadow:none; transform:translate(2px,2px); }
|
| 46 |
+
.parry-debug .hint { color:#8a7c64; margin-top:.4rem; line-height:1.4; }
|
| 47 |
+
.parry-debug input[type=range] { flex:1; accent-color:#c93a2e; }
|
| 48 |
+
.parry-debug .val { color:#3a5a78; min-width:48px; text-align:right; }
|
| 49 |
+
`;class le{constructor(t,e,s){this.getBrain=e,this.getState=s;const i=document.createElement("style");i.textContent=oe,document.head.appendChild(i),this.root=document.createElement("div"),this.root.className="parry-debug",this.root.style.display="none",this.root.innerHTML=`
|
| 50 |
+
<h3>⚙ JUDGE PANEL — edit the AI's read</h3>
|
| 51 |
+
<textarea placeholder="Type a new read for the Tactician, e.g.:
|
| 52 |
+
They only parry — punish recovery with raw strikes."></textarea>
|
| 53 |
+
<div class="row">
|
| 54 |
+
<button data-act="apply">Apply override</button>
|
| 55 |
+
<button data-act="clear">Clear</button>
|
| 56 |
+
<button data-act="trace">Export trace</button>
|
| 57 |
+
</div>
|
| 58 |
+
<div class="row">
|
| 59 |
+
<span>reaction delay</span>
|
| 60 |
+
<input type="range" min="0" max="250" step="10" value="${N.ms}" />
|
| 61 |
+
<span class="val">${N.ms}ms</span>
|
| 62 |
+
<button data-act="boss" title="0ms reflexes — it sees your inputs the frame they happen, Tekken-boss style. Disclosed, not hidden.">🥊 BOSS</button>
|
| 63 |
+
<button data-act="fair" title="180ms human-like reaction window — the default, fair fight.">FAIR</button>
|
| 64 |
+
</div>
|
| 65 |
+
<div class="hint">The plan-string you type is injected into the Tactician's prompt verbatim —
|
| 66 |
+
the SAME path the Analyst writes through. If behavior flips, the coupling is real.</div>
|
| 67 |
+
<div class="hint info"></div>`,t.appendChild(this.root),this.overrideBox=this.root.querySelector("textarea"),this.delayVal=this.root.querySelector(".val"),this.info=this.root.querySelector(".info"),this.root.querySelector('[data-act="apply"]').addEventListener("click",()=>{const o=this.getBrain();o&&(o.setPlanOverride(this.overrideBox.value,this.getState().tick),v("debug_override_used"))}),this.root.querySelector('[data-act="clear"]').addEventListener("click",()=>{this.getBrain()?.setPlanOverride(null,this.getState().tick),this.overrideBox.value=""}),this.root.querySelector('[data-act="trace"]').addEventListener("click",()=>{const o=this.getBrain();if(!o)return;const h=this.getState();ne(re(o.exportTraceRows(),{modelId:o.telemetry().modelId,backend:o.telemetry().backend,seed:h.seed,finalScore:{player:h.roundWins[0],ai:h.roundWins[1]}}))});const r=this.root.querySelector('input[type="range"]'),n=o=>{N.ms=o,r.value=String(o),this.delayVal.textContent=o===0?"0ms 🥊":`${o}ms`};r.addEventListener("input",()=>n(Number(r.value))),this.root.querySelector('[data-act="boss"]').addEventListener("click",()=>{n(0),v("boss_reflexes_on")}),this.root.querySelector('[data-act="fair"]').addEventListener("click",()=>n(180))}root;visible=!1;overrideBox;delayVal;info;toggle(){if(this.visible=!this.visible,this.root.style.display=this.visible?"":"none",this.visible){const t=this.getBrain();this.info.textContent=t?`backend ${t.telemetry().backend} · ${t.telemetry().modelId} · seed ${this.getState().seed}`:"LLM brain not installed yet (still loading, or ?nollm=1)"}}isOpen(){return this.visible}}const he=2.5,ce=.003,pe=36;function ct(a,t,e,s){const i=t.filter(c=>c.p>=ce);if(i.length<2)return i[0]?.verb??null;const r=(e.match(/S/g)??[]).length,n=(e.match(/P/g)??[]).length,o=r>=2?"striker":n>=2?"parrier":"passive";let h=null,l=-1/0;for(const c of i){let d=structuredClone(a);const f=d.players[0].hp,p=d.players[1].hp,m=Math.abs(d.players[0].x-d.players[1].x);let b=c.verb,u=0;for(let M=0;M<pe;M++){const _=d.players[0].x<d.players[1].x?1:-1;let P={dir:0,action:null};if(o==="striker"?P={dir:_,action:C(d,0)?"STRIKE":null}:o==="parrier"&&(P={dir:0,action:C(d,0)&&M%18===0?"PARRY":null}),b===null&&C(d,1)){const S=d.players[0],F=Math.abs(d.players[0].x-d.players[1].x);((S.phase==="STAGGER"||S.phase==="RECOVERY")&&F<=R||S.phase==="NEUTRAL"&&F<=R&&M>=8&&M%12===0)&&(b="STRIKE")}const T=yt(d,P,b);d=T.state,b=null;for(const S of T.events)S.t==="PARRY_SUCCESS"&&S.by===1?u+=14:S.t==="PARRY_WHIFF"&&S.by===1?u-=6:S.t==="WHIFF"&&S.by===1?u-=4:S.t==="FEINT_BAITED"&&S.by===1&&(u+=6);if(d.roundState!=="FIGHT")break}const y=f-d.players[0].hp,k=p-d.players[1].hp,w=Math.abs(d.players[0].x-d.players[1].x),x=Math.max(-1.5,Math.min(1.5,(m-w)/R)),E=y*2.2-k*2.5+u+x*3+Math.log(Math.max(.001,c.p))*he;E>l&&(l=E,h=c.verb)}return h}class de{ring=[];lastSampleTick=-999;prevPhase="NEUTRAL";prevAiX=0;playerWasStaggered=!1;staggerEndTick=-999;strikes=0;feints=0;parries=0;wakeupStrikes=0;wakeups=0;parriesOnApproach=0;approaches=0;retreatTicks=0;observedTicks=0;observe(t){const e=t.state.players[0],s=t.state.players[1];if(t.tick-this.lastSampleTick>=6){this.lastSampleTick=t.tick;let i="W";e.phase==="STARTUP"||e.phase==="ACTIVE"?i=e.verb==="PARRY"?"P":"S":e.phase==="RECOVERY"&&e.verb==="FEINT"?i="F":e.x!==this.prevPlayerX&&(i=e.x>this.prevPlayerX?"R":"L"),this.ring.push(i),this.ring.length>8&&this.ring.shift()}this.prevPhase==="NEUTRAL"&&e.phase==="STARTUP"&&(e.verb==="PARRY"&&(this.parries++,s.x!==this.prevAiX&&Math.abs(s.x-e.x)<Math.abs(this.prevAiX-e.x)&&this.parriesOnApproach++),(e.verb==="STRIKE"||e.verb==="FEINT")&&(this.strikes++,this.playerWasStaggered&&t.tick-this.staggerEndTick<=18&&this.wakeupStrikes++)),this.prevPhase==="STARTUP"&&e.phase==="RECOVERY"&&e.verb==="FEINT"&&(this.strikes=Math.max(0,this.strikes-1),this.feints++),e.phase==="STAGGER"&&(this.playerWasStaggered=!0),this.prevPhase==="STAGGER"&&e.phase==="NEUTRAL"&&(this.staggerEndTick=t.tick,this.wakeups++),Math.abs(s.x-e.x)<Math.abs(this.prevAiX-e.x)&&this.approaches++,this.observedTicks++,e.phase==="NEUTRAL"&&e.x!==this.prevPlayerX&&e.x>this.prevPlayerX==e.x>s.x&&this.retreatTicks++,this.prevPhase=e.phase,this.prevAiX=s.x,this.prevPlayerX=e.x}prevPlayerX=0;recentChars(){return this.ring.join("").padStart(8,".")}detect(){const t=this.strikes+this.feints+this.parries;return t<3?{habit:"NONE",confidence:.2,line:"not enough data yet"}:this.wakeups>=2&&this.wakeupStrikes/Math.max(1,this.wakeups)>=.6?{habit:"WAKEUP_STRIKE",confidence:Math.min(.95,.5+.15*this.wakeupStrikes),line:`strikes on wake-up ${this.wakeupStrikes}/${this.wakeups}`}:this.approaches>=3&&this.parriesOnApproach/Math.max(1,this.approaches)>=.5?{habit:"PARRY_ON_DASH_IN",confidence:.7,line:`parries ${this.parriesOnApproach}/${this.approaches} of my approaches`}:this.feints>=2&&this.feints>=this.strikes?{habit:"FEINT_HAPPY",confidence:.65,line:`feints ${this.feints} vs strikes ${this.strikes}`}:this.strikes>=5&&this.strikes/Math.max(1,t)>=.7?{habit:"SPAM_STRIKE",confidence:.7,line:`${this.strikes} strikes of ${t} actions`}:this.retreatTicks/Math.max(1,this.observedTicks)>.3&&this.parries>=2?{habit:"TURTLE",confidence:.6,line:"retreats and parries, rarely commits"}:{habit:"RANDOM",confidence:.4,line:"no strong pattern yet"}}shadow={strikes:0,parries:0,feints:0,atTick:-1};trend="";scoutNotes(t){if(this.shadow.atTick<0)this.shadow={strikes:this.strikes,parries:this.parries,feints:this.feints,atTick:t};else if(t-this.shadow.atTick>600){const r=this.strikes-this.shadow.strikes,n=this.parries-this.shadow.parries,o=this.feints-this.shadow.feints;this.trend=n>r&&n>=2?"lately: parrying more ↑ — feints will open them up":r>=4?"lately: swinging freely ↑ — every whiff is mine":o>=2?"lately: feint-curious ↑ — I won't bite blind":r+n+o===0?"lately: gone quiet — watching for the next pattern":"",this.shadow={strikes:this.strikes,parries:this.parries,feints:this.feints,atTick:t}}const e=[];if(this.strikes+this.feints+this.parries<3)return e.push("watching… show me what you do under pressure"),e;if(e.push(`counted: ${this.strikes} strikes · ${this.parries} parries · ${this.feints} feints`),this.wakeups>=1){const r=this.wakeupStrikes/Math.max(1,this.wakeups);e.push(`wake-up: strikes ${this.wakeupStrikes}/${this.wakeups}${r>=.6?" — predictable, baiting it":""}`)}this.approaches>=3&&this.parriesOnApproach>0&&e.push(`parries ${this.parriesOnApproach}/${this.approaches} of my approaches`);const i=this.retreatTicks/Math.max(1,this.observedTicks);return i>.3&&e.push(`gives ground ${Math.round(i*100)}% of the time — corner is my friend`),this.trend&&e.push(this.trend),e.slice(0,4)}behaviorLog(){const t=this.detect();return`Recent actions (old->new): ${this.recentChars()}. Strikes ${this.strikes}, feints ${this.feints}, parries ${this.parries}. Wake-up strikes ${this.wakeupStrikes}/${this.wakeups}. Parries-on-my-approach ${this.parriesOnApproach}/${this.approaches}. Retreat-rate ${Math.round(100*this.retreatTicks/Math.max(1,this.observedTicks))}%. Detected: ${t.habit} (${t.line}).`}reset(){this.ring=[],this.strikes=this.feints=this.parries=0,this.wakeupStrikes=this.wakeups=0,this.parriesOnApproach=this.approaches=0,this.retreatTicks=this.observedTicks=0,this.playerWasStaggered=!1}}class fe{plan=J;source="neutral";override=null;prediction={predictedHabit:"NONE",confidence:.2};changedAtTick=0;current(){return this.override??this.plan}currentSource(){return this.override!==null?"override":this.source}currentPrediction(){return this.prediction}setFromAnalyst(t,e,s){this.plan=t,this.prediction=e,this.source="analyst",this.override===null&&(this.changedAtTick=s)}setOverride(t,e){this.override=t&&t.trim().length?t.trim():null,this.changedAtTick=e}hasOverride(){return this.override!==null}reset(t){this.plan=J,this.source="neutral",this.override=null,this.prediction={predictedHabit:"NONE",confidence:.2},this.changedAtTick=t}}function ue(a){if(a.phase==="NEUTRAL")return"NEUTRAL";const t=a.verb==="NONE"?a.phase:a.verb;return`${a.phase==="STARTUP"||a.phase==="ACTIVE"?t+" ":""}${a.phase} ${a.framesLeft}f`.trim()}function me(a){switch(a.phase){case"NEUTRAL":return"NEUTRAL";case"STARTUP":return a.verb==="PARRY"?`GUARD ${a.framesLeft}f`:`WINDUP ${a.framesLeft}f`;case"ACTIVE":return a.verb==="PARRY"?`PARRY-ACTIVE ${a.framesLeft}f`:`ATTACK ${a.framesLeft}f`;case"RECOVERY":return`RECOVERY ${a.framesLeft}f`;case"BLOCKSTUN":return`BLOCKED ${a.framesLeft}f`;case"STAGGER":return`DOWN ${a.framesLeft}f`}}function ge(a){const t=bt(a.players[1].x-a.players[0].x|0);return t<=Pt?"CLOSE":t<=gt?"MID":"FAR"}function be(a){const t=e=>e.phase==="NEUTRAL"?0:e.framesLeft;return t(a.players[0])-t(a.players[1])}function ye(a,t,e,s,i){const r=Mt(t,1).legal,n=(r.length?r:["WAIT"]).map(c=>et[c]).join(""),o=be(a),h=a.players[0].x<a.players[1].x?"L":"R",l=st.replace("{plan}",e.slice(0,_t)).replace("{me}",ue(a.players[1])).replace("{you}",me(a.players[0])).replace("{dist}",ge(a)).replace("{tw}",h).replace("{adv}",`${o>=0?"+":""}${o}`).replace("{recent}",s).replace("{meRecent}",i||"......");return{system:it,user:l,stateDigest:Ot(JSON.stringify(a)),legalChars:n}}const we=`You are the Analyst half of a dueling AI in PARRY (strike beats idle, parry beats strike, feint baits parry).
|
| 68 |
+
Read the opponent report. Output EXACTLY: <plan for the Tactician, imperative, MAX 12 words> | <HABIT> | <confidence 0-9>.
|
| 69 |
+
HABIT is one of: ${wt.join(", ")}. Be specific and a little cocky.`;function ke(a){const t=a.split("|").map(r=>r.trim());if(t.length<1||!t[0])return null;const e=t[0].slice(0,140);let s="NONE";if(t[1]){const r=t[1].toUpperCase().replace(/[^A-Z_]/g,"");wt.includes(r)&&(s=r)}let i=.5;if(t[2]){const r=parseInt(t[2].charAt(0),10);Number.isNaN(r)||(i=Math.min(1,Math.max(0,r/9)))}return{planString:e,prediction:{predictedHabit:s,confidence:i}}}async function Se(a,t,e=1500){const s=await a.generateOp(we,`Opponent report: ${t}
|
| 70 |
+
Your read:`,Ct,{maxTokens:24,budgetMs:e}),i=ke(s.text);return i?{...i,raw:s.text,interrupted:s.interrupted,ms:s.ms}:null}const ve=30,xe=1200,Te=18;class rt{constructor(t,e){this.backend=t,this.name=e}name;staged=null;busy=!1;seq=0;brainTicks=0;analystBusy=!1;lastAnalystBrainTick=-999;buffer=new ee;tracker=new de;plans=new fe;opsMs=[];lastTick=null;trace=[];safeWindowPending=!1;lastHabit="NONE";meRing=[];stickyVerb=null;stickyLeft=0;lastOpEndAt=0;static OP_GAP_MS=70;recentEvents=[];lastStrikeWasted=!1;observe(t,e){this.buffer.push(t.tick,t,e);for(const i of e)this.recentEvents.push(i.t),(i.t==="WHIFF"||i.t==="BLOCK")&&i.by===1&&(this.lastStrikeWasted=!0),i.t==="HIT"&&i.by===1&&(this.lastStrikeWasted=!1);this.recentEvents.length>12&&this.recentEvents.splice(0,this.recentEvents.length-12);const s=this.buffer.delayed(t.tick);s&&this.tracker.observe(s);for(const i of e)(i.t==="KO"||i.t==="ROUND_OVER"||i.t==="ROUND_START")&&(this.safeWindowPending=!0),i.t==="MATCH_OVER"&&(this.tracker.reset(),this.plans.reset(t.tick),this.buffer.clear())}lastTopk=null;lastTopkAt=0;request(t){if(this.maybeRunAnalyst(t),this.lastTopk&&performance.now()-this.lastTopkAt<2500&&C(t,1)){const l=N.ms>0?this.buffer.delayed(t.tick)?.state??t:t,c=l.players[0];if(c.phase==="STARTUP"||c.phase==="STAGGER"||c.phase==="RECOVERY"||t.tick%6===0){const f=ct(l,this.lastTopk,this.tracker.recentChars());f&&f!==this.staged&&(this.staged=f)}}if(this.busy||!C(t,1)||performance.now()-this.lastOpEndAt<rt.OP_GAP_MS)return;const e=this.buffer.delayed(t.tick)?.state??t,s=this.meRing.join("").padStart(6,"."),i=ye(e,t,this.plans.current(),this.tracker.recentChars(),s),r=this.lastStrikeWasted&&this.meRing.slice(-2).join("")==="SS",n=at("NEUTRAL",r);this.plans.currentSource();const o=++this.seq;this.busy=!0;const h=(t.seed^t.tick)>>>0;this.backend.tickOp(i.system,i.user,n,h).then(l=>{if(o!==this.seq)return;let c=kt(l.ch)??"WAIT";if(l.topk&&l.topk.length>1){this.lastTopk=l.topk,this.lastTopkAt=performance.now();const f=ct(t,l.topk,this.tracker.recentChars());f&&(c=f)}this.staged=c,this.meRing.push(c==="MOVE_L"?"L":c==="MOVE_R"?"R":c.charAt(0)),this.meRing.length>6&&this.meRing.shift(),this.opsMs.push(l.ms),this.opsMs.length>60&&this.opsMs.shift();const d={simTick:t.tick,opMs:Math.round(l.ms*10)/10,promptTokens:l.promptTokens,backend:this.backend.caps().label,intent:c,stale:!1,...l.topk?{topk:l.topk}:{}};this.lastTick=d,this.trace.push({tick:t.tick,stateDigest:i.stateDigest,planString:this.plans.current(),planSource:this.plans.currentSource(),prediction:this.plans.currentPrediction(),intent:c,opMs:d.opMs,events:this.recentEvents.splice(0)}),this.trace.length>5e3&&this.trace.shift()}).catch(l=>{console.warn(`[brain] tickOp failed: ${String(l).slice(0,140)}`)}).finally(()=>{this.busy=!1,this.lastOpEndAt=performance.now()}),this.brainTicks++}maybeRunAnalyst(t){if(this.analystBusy)return;const e=this.tracker.detect(),s=e.habit!=="NONE"&&e.habit!==this.lastHabit&&e.confidence>=.6;if(!(this.brainTicks-this.lastAnalystBrainTick>=ve||this.safeWindowPending||s))return;this.safeWindowPending=!1,this.lastAnalystBrainTick=this.brainTicks,this.analystBusy=!0;const r=this.tracker.behaviorLog();Se(this.backend,r,xe).then(n=>{if(!n)return;this.lastHabit=n.prediction.predictedHabit;const o=this.tracker.detect();if(o.habit==="SPAM_STRIKE"&&o.confidence>=.7){const h=this.tracker.strikes,l=[`Button masher — ${h} swings already. Punish every whiff.`,`Still swinging (${h} now). Stay on top — every miss costs 22.`,`${h} strikes and counting. No room to breathe, no free swings.`];this.plans.setFromAnalyst(l[Math.floor(h/5)%l.length],n.prediction,t.tick);return}this.plans.setFromAnalyst(n.planString,n.prediction,t.tick)}).catch(n=>console.warn(`[brain] analyst failed: ${String(n).slice(0,140)}`)).finally(()=>{this.analystBusy=!1})}consume(){const t=this.staged;return this.staged=null,t!==null?(t==="MOVE_L"||t==="MOVE_R"?(this.stickyVerb=t,this.stickyLeft=Te):(this.stickyVerb=null,this.stickyLeft=0),t):this.stickyVerb!==null&&this.stickyLeft>0?(this.stickyLeft--,this.stickyVerb):null}note(){const t=this.backend.caps(),e=this.medianMs();return`${t.label==="localWebGPU"?"0ms network · local GPU":"server fallback"} · ~${e.toFixed(0)}ms/decision · ${t.modelId} · reads you every ~2s`}medianMs(){if(!this.opsMs.length)return 0;const t=[...this.opsMs].sort((e,s)=>e-s);return t[t.length>>1]}opsWindow(){return[...this.opsMs]}telemetry(){const t=[...this.opsMs].sort((e,s)=>e-s);return{backend:this.backend.caps().label,modelId:this.backend.caps().modelId,medianOpMs:t.length?t[t.length>>1]:0,p95OpMs:t.length?t[Math.min(t.length-1,Math.ceil(t.length*.95)-1)]:0,lastTick:this.lastTick,planString:this.plans.current(),planSource:this.plans.currentSource(),prediction:this.plans.currentPrediction(),planChangedAtTick:this.plans.changedAtTick,analystBusy:this.analystBusy,scout:this.tracker.scoutNotes(this.lastTick?.simTick??0)}}exportTraceRows(){return[...this.trace]}setPlanOverride(t,e){this.plans.setOverride(t,e)}reactionDelayMs(){return N.ms}}const Q={hero15:"Qwen2.5-1.5B-Instruct-q4f16_1-MLC",low1:"Llama-3.2-1B-Instruct-q4f16_1-MLC",fast05:"Qwen2.5-0.5B-Instruct-q4f16_1-MLC",beefy3:"Qwen2.5-3B-Instruct-q4f16_1-MLC",tuned:"parry-tactician-1.5b-q4f16_1-MLC"},O={model_id:Q.tuned,model:"https://huggingface.co/Jainamshahhh/parry-tactician-1.5b-q4f16_1-MLC",baseForLib:Q.hero15,vram_required_MB:1700,low_resource_required:!0};class pt{engine=null;modelId="";grammarSupported=!0;logprobsAvailable=!1;logprobsProbed=!1;async init(t,e){this.modelId=Q[t];const s={initProgressCallback:i=>e({progress:i.progress,text:i.text})};if(t==="tuned"){const i=K.model_list.find(r=>r.model_id===O.baseForLib);if(!i)throw new Error(`base record ${O.baseForLib} missing from prebuilt list`);s.appConfig={...K,model_list:[...K.model_list,{model:O.model,model_id:O.model_id,model_lib:i.model_lib,vram_required_MB:O.vram_required_MB,low_resource_required:O.low_resource_required}]}}else K.model_list.some(i=>i.model_id===this.modelId)||console.warn(`[webllm] ${this.modelId} not in prebuilt list — check @mlc-ai/web-llm version pin`);this.engine=await Nt(new Worker(new URL(""+new URL("llm.worker-GbIGcnxV.js",import.meta.url).href,import.meta.url),{type:"module"}),this.modelId,s)}req(t,e,s,i){const r={messages:[{role:"system",content:t},{role:"user",content:e}],temperature:1,...i};return s&&this.grammarSupported&&(r.response_format={type:"grammar",grammar:s}),r}async tickOp(t,e,s,i){if(!this.engine)throw new Error("backend not initialized");const r=performance.now(),n=!this.logprobsProbed||this.logprobsAvailable;let o;try{o=await this.engine.chatCompletion(this.req(t,e,s,{stream:!1,max_tokens:1,seed:i,...n?{logprobs:!0,top_logprobs:5}:{}}))}catch(p){const m=String(p);if(/logprob/i.test(m))console.warn(`[webllm] logprobs rejected → disabled (${m.slice(0,100)})`),this.logprobsProbed=!0,this.logprobsAvailable=!1,o=await this.engine.chatCompletion(this.req(t,e,s,{stream:!1,max_tokens:2,seed:i}));else if(this.grammarSupported)console.warn(`[webllm] grammar response_format failed → unconstrained + post-map (${m.slice(0,120)})`),this.grammarSupported=!1,o=await this.engine.chatCompletion(this.req(t,e,null,{stream:!1,max_tokens:2,seed:i}));else throw p}const h=performance.now()-r,l=o.choices[0];this.logprobsProbed||(this.logprobsProbed=!0,this.logprobsAvailable=!!l?.logprobs?.content?.length);let c;const d=l?.logprobs?.content?.[0]?.top_logprobs;if(d?.length){c=[];for(const p of d){const m=kt(p.token??"");m&&c.push({verb:m,p:Math.exp(p.logprob??-99)})}}const f={ch:l?.message?.content??"",ms:h,promptTokens:o.usage?.prompt_tokens??null};return c&&c.length&&(f.topk=c),f}async generateOp(t,e,s,i){if(!this.engine)throw new Error("backend not initialized");const r=performance.now();let n="",o=!1;const h=setTimeout(()=>{o=!0;try{this.engine?.interruptGenerate()}catch{}},i.budgetMs);try{const l=await this.engine.chat.completions.create(this.req(t,e,s,{stream:!0,max_tokens:i.maxTokens,temperature:.8}));for await(const c of l)n+=c.choices[0]?.delta?.content??""}catch(l){if(s&&this.grammarSupported)return this.grammarSupported=!1,console.warn(`[webllm] analyst grammar failed → retry unconstrained (${String(l).slice(0,120)})`),clearTimeout(h),this.generateOp(t,e,null,i);if(!o)throw l}finally{clearTimeout(h)}return{text:n,interrupted:o,ms:performance.now()-r}}caps(){return{label:"localWebGPU",modelId:this.modelId,grammarSupported:this.grammarSupported,logprobsAvailable:this.logprobsAvailable}}dispose(){try{this.engine?.unload()}catch{}this.engine=null}}function Ae(){const a=st.replace("{plan}",J).replace("{me}","NEUTRAL").replace("{you}","NEUTRAL").replace("{dist}","MID").replace("{tw}","L").replace("{adv}","+0").replace("{recent}","WWWWWWWW").replace("{meRecent}","......");return{system:it,user:a}}async function dt(a,t){const{system:e,user:s}=Ae(),i=at("NEUTRAL");t?.("warming shaders (first decode)…");const r=await a.tickOp(e,s,i,1);t?.("warming long-generation path…");const n=await a.generateOp(e,"Say 'ready' and stop.",null,{maxTokens:8,budgetMs:4e3});t?.("settling…");const o=[];for(let h=0;h<3;h++){const l=await a.tickOp(e,s,i,2+h);o.push(Math.round(l.ms*10)/10)}return{coldFirstOpMs:Math.round(r.ms),settledOpsMs:o,generateMs:Math.round(n.ms)}}function Re(a){const t=[...a].sort((s,i)=>s-i),e=t.length>>1;return t.length%2?t[e]:(t[e-1]+t[e])/2}function Ee(){const a=st.replace("{plan}","Wake-up striker. Bait it with a feint, punish the whiff.").replace("{me}","NEUTRAL").replace("{you}","RECOVERY 12f").replace("{dist}","MID").replace("{tw}","L").replace("{adv}","+9").replace("{recent}","RRSWPSFS").replace("{meRecent}","LLSWFP");return{system:it,user:a}}async function ft(a,t=10){const{system:e,user:s}=Ee(),i=at("NEUTRAL"),r=[];for(let n=0;n<t;n++){const o=await a.tickOp(e,s,i,100+n);r.push(o.ms)}return Re(r)}function Me(){if(typeof location>"u")return{};const a=new URLSearchParams(location.search),t={},e=a.get("backend");e&&(t.backend=e);const s=a.get("tier");return s&&["hero15","low1","fast05","beefy3","tuned"].includes(s)&&(t.tier=s),t}async function Pe(a){const t=Me();if(t.backend==="server"||!("gpu"in navigator))throw new Error("WebGPU unavailable — Parry runs 100% in your browser (try Chrome/Edge desktop; the demo video shows the full experience)");try{if(!await navigator.gpu.requestAdapter())throw new Error("null adapter")}catch{throw new Error("WebGPU adapter unavailable — Parry runs 100% in your browser (try Chrome/Edge desktop; the demo video shows the full experience)")}let e=t.tier??"hero15",s=new pt;await s.init(e,a),await dt(s,n=>a({progress:.97,text:n}));let i=await ft(s);console.log(`[probe] ${e} warm median ${i.toFixed(1)}ms (caps: ${JSON.stringify(s.caps())})`),i>250&&!t.tier&&(a({progress:.5,text:`median ${i.toFixed(0)}ms — downshifting to 1B…`}),s.dispose(),s=new pt,e="low1",await s.init(e,a),await dt(s),i=await ft(s),console.log(`[probe] ${e} warm median ${i.toFixed(1)}ms`));const r=`local median ${i.toFixed(0)}ms/decision — ${e} tier`;return{backend:s,tier:e,medianOpMs:i,reason:r}}const Ie=.3,Le=1.9,_e=.22,Oe=.02,Ce=350,Ne=60,ut=.1,Fe=.06,$e=1200;function Z(a,t,e,s){const i=a-e,r=t-s;return Math.sqrt(i*i+r*r)}class Be{arms={L:{sx:0,sy:0,lastT:0,speed:0,cooldownUntil:0,armed:!0,hist:[]},R:{sx:0,sy:0,lastT:0,speed:0,cooldownUntil:0,armed:!0,hist:[]}};guardSince=0;guardFired=!1;leanState=0;calX=0;calN=0;calStart=0;calibrated=!1;recalibrate(){this.calX=0,this.calN=0,this.calStart=0,this.calibrated=!1}update(t,e){const s={strike:!1,strikeSide:null,parry:!1,lean:this.leanState,tracking:!1,debug:{speedL:0,speedR:0,travelL:0,travelR:0,guard:!1,leanOff:0,calibrated:this.calibrated}},i=new Map;for(const m of t)m.name&&(m.score??0)>=Ie&&i.set(m.name,m);const r=i.get("left_shoulder"),n=i.get("right_shoulder"),o=i.get("nose");if(!r||!n)return s;const h=Z(r.x,r.y,n.x,n.y);if(h<.001)return s;s.tracking=!0;const l=(r.x+n.x)/2,c=o?.6*o.x+.4*l:l;if(!this.calibrated)this.calStart===0&&(this.calStart=e),this.calX+=c,this.calN++,e-this.calStart>=$e&&(this.calX/=this.calN,this.calibrated=!0);else{const m=(c-this.calX)/h;s.debug.leanOff=m,this.leanState===0?m>ut?this.leanState=1:m<-ut&&(this.leanState=-1):Math.abs(m)<Fe?this.leanState=0:this.leanState=m>0?1:-1,s.lean=this.leanState}const d=i.get("left_wrist"),f=i.get("right_wrist");let p=!1;if(d&&f&&o){const m=(r.y+n.y)/2,b=u=>Math.abs(u.x-o.x)<1.3*h;p=d.y<m&&f.y<m&&b(d)&&b(f)}s.debug.guard=p,p?(this.guardSince===0&&(this.guardSince=e),!this.guardFired&&e-this.guardSince>=Ne&&(s.parry=!0,this.guardFired=!0)):(this.guardSince=0,this.guardFired=!1);for(const m of["L","R"]){const b=i.get(m==="L"?"left_wrist":"right_wrist"),u=this.arms[m];if(!b)continue;if(u.sx=b.x,u.sy=b.y,u.lastT>0&&e>u.lastT){const w=Math.max(Oe,(e-u.lastT)/1e3),x=u.hist[u.hist.length-1],E=x?Z(u.sx,u.sy,x.x,x.y)/h/w:0;u.speed=Math.max(E,u.speed*.55)}for(u.hist.push({x:u.sx,y:u.sy,t:e});u.hist.length>2&&e-u.hist[0].t>150;)u.hist.shift();u.lastT=e;const y=u.hist[0],k=Z(u.sx,u.sy,y.x,y.y)/h;m==="L"?(s.debug.speedL=u.speed,s.debug.travelL=k):(s.debug.speedR=u.speed,s.debug.travelR=k),!p&&e>=u.cooldownUntil&&u.speed>=Le&&k>=_e&&(s.strike=!0,s.strikeSide=m,u.cooldownUntil=e+Ce,u.speed=0,u.hist=[{x:u.sx,y:u.sy,t:e}])}return s}}const We=8,Ue=[[0,5],[0,6],[5,6],[5,7],[7,9],[6,8],[8,10],[5,11],[6,12],[11,12]],B=280,X=210;class De{constructor(t,e,s){this.input=t,this.hudHost=e,this.onGesture=s}worker=null;video=null;stream=null;engine=new Be;pumping=!1;inFlight=!1;pip=null;rafId=0;prevKps=[];lastKps=[];prevAt=0;lastAt=0;flash={text:"",color:"#fff",until:0};lastDebug={speedL:0,speedR:0,travelL:0,travelR:0,guard:!1,leanOff:0,calibrated:!1};peakSpd=0;peakTrv=0;peakAt=0;startedAt=0;variant="thunder";downgraded=!1;onWorkerMessage=()=>{};trailL=[];trailR=[];burst=null;lastParryInject=0;tracking=!1;fpsEma=0;active=!1;async start(){if(this.active)return;this.stream=await navigator.mediaDevices.getUserMedia({video:{width:320,height:240,facingMode:"user"},audio:!1}),this.video=document.createElement("video"),this.video.srcObject=this.stream,this.video.muted=!0,this.video.playsInline=!0,await this.video.play(),await this.spawnWorker(this.variant),this.onWorkerMessage=e=>{if(e.data.type==="error"){console.warn("[pose] worker error:",e.data.message),this.inFlight=!1,setTimeout(()=>this.pump(),100);return}if(e.data.type!=="pose")return;this.inFlight=!1;const s=performance.now();if(this.lastAt>0){const o=1e3/Math.max(1,s-this.lastAt);this.fpsEma=this.fpsEma===0?o:this.fpsEma*.9+o*.1}this.prevKps=this.lastKps,this.prevAt=this.lastAt,this.lastKps=e.data.keypoints,this.lastAt=s;const i=this.engine.update(this.lastKps,e.data.t);this.lastDebug=i.debug,this.tracking=i.tracking;const r=Math.max(i.debug.speedL,i.debug.speedR),n=Math.max(i.debug.travelL,i.debug.travelR);if((r>this.peakSpd||s-this.peakAt>1500)&&(this.peakSpd=r,this.peakTrv=n,this.peakAt=s),this.input.setPoseAdvance(i.tracking?-i.lean:0),i.strike){this.input.injectAction("STRIKE"),this.setFlash("STRIKE","#c93a2e"),this.onGesture?.("STRIKE");const o=i.strikeSide==="L"?9:10,h=this.lastKps[o];if(h){const l=this.video?.videoWidth||320,c=this.video?.videoHeight||240;this.burst={x:B*2-h.x/l*B*2,y:h.y/c*X*2,t:s}}}i.parry?(this.input.injectAction("PARRY"),this.setFlash("PARRY","#4e87b8"),this.onGesture?.("PARRY"),this.lastParryInject=s):i.debug.guard&&s-this.lastParryInject>260&&(this.input.injectAction("PARRY"),this.lastParryInject=s),setTimeout(()=>this.pump(),We)},this.makePip(),this.engine.recalibrate(),this.active=!0,this.pumping=!0,this.pump(),this.startedAt=performance.now();const t=()=>{if(this.active){try{this.drawPip()}catch(e){console.error("[pose] pip draw failed:",e)}this.pumping&&performance.now()-Math.max(this.lastAt,this.startedAt)>1500&&(this.inFlight=!1,this.lastAt=performance.now(),this.pump()),this.maybeDowngrade(),this.rafId=requestAnimationFrame(t)}};this.rafId=requestAnimationFrame(t),v("pose_mode_on")}stop(){this.pumping=!1,this.active=!1,cancelAnimationFrame(this.rafId),this.worker?.terminate(),this.worker=null,this.stream?.getTracks().forEach(t=>t.stop()),this.stream=null,this.video=null,this.input.setPoseAdvance(0),this.pip?.remove(),this.pip=null}async spawnWorker(t){this.worker?.terminate(),this.worker=new Worker(new URL(""+new URL("pose.worker-BBF_KKBj.js",import.meta.url).href,import.meta.url),{type:"module"}),await new Promise((e,s)=>{const i=setTimeout(()=>s(new Error("pose worker init timeout (25s)")),25e3);this.worker.onmessage=o=>{o.data.type==="ready"?(clearTimeout(i),e()):o.data.type==="error"&&(clearTimeout(i),s(new Error(o.data.message)))};const r=`${location.origin}/pose/`,n=t==="thunder"?`${r}thunder/model.json`:`${r}model.json`;this.worker.postMessage({type:"init",wasmBase:r,modelUrl:n,variant:t})}),this.worker.onmessage=e=>this.onWorkerMessage(e),this.variant=t,this.fpsEma=0,this.lastAt=0,this.inFlight=!1}maybeDowngrade(){this.downgraded||this.variant!=="thunder"||performance.now()-this.startedAt<8e3||this.fpsEma===0||this.fpsEma<14&&(this.downgraded=!0,console.warn(`[pose] thunder at ${this.fpsEma.toFixed(0)}fps — downgrading to lightning`),this.spawnWorker("lightning").then(()=>this.pump()))}pump(){!this.pumping||this.inFlight||(this.video&&this.worker&&this.video.readyState>=2?(this.inFlight=!0,createImageBitmap(this.video).then(t=>{this.worker?.postMessage({type:"frame",bitmap:t,t:performance.now()},[t])})):setTimeout(()=>this.pump(),50))}setFlash(t,e){this.flash={text:t,color:e,until:performance.now()+500}}makePip(){this.pip=document.createElement("canvas"),this.pip.width=B*2,this.pip.height=X*2,this.pip.style.cssText=`position:absolute;right:14px;bottom:14px;width:${B}px;height:${X}px;border:3px solid #2b2620;border-radius:2px;z-index:30;pointer-events:none;background:#0a0908;box-shadow: inset 0 0 0 2px #fffdf4, 4px 4px 0 rgba(43,38,32,.28);`,this.hudHost.appendChild(this.pip)}rendered=[];lerped(){if(!this.lastKps.length)return[];const t=this.prevKps.length>0&&this.lastAt>this.prevAt,e=t?this.lastAt-this.prevAt:1,s=t?Math.min(35,performance.now()-this.lastAt)/e:0;return this.lastKps.map((i,r)=>{const n=this.prevKps[r],o=n?i.x+(i.x-n.x)*s:i.x,h=n?i.y+(i.y-n.y)*s:i.y,l=this.rendered[r]??={x:o,y:h};return l.x+=(o-l.x)*.55,l.y+=(h-l.y)*.55,{...i,x:l.x,y:l.y}})}drawPip(){if(!this.pip||!this.video)return;const t=this.pip.getContext("2d"),e=B*2,s=X*2;t.save(),t.scale(-1,1),t.drawImage(this.video,-e,0,e,s),t.restore(),t.fillStyle="rgba(10,9,8,0.45)",t.fillRect(0,0,e,s);const i=this.video.videoWidth||320,r=this.video.videoHeight||240,n=this.lerped(),o=p=>e-p.x/i*e,h=p=>p.y/r*s,l=p=>!!p&&(p.score??0)>=.3,c=this.lastDebug.guard,d=c?"rgba(78,135,184,":"rgba(212,176,110,",f=c?"#dce9f5":"#fff8ec";for(const[p,m]of[[this.trailL,9],[this.trailR,10]]){const b=n[m];if(l(b)?(p.push({x:o(b),y:h(b)}),p.length>14&&p.shift()):p.length&&p.shift(),p.length>2)for(let u=1;u<p.length;u++){const y=u/p.length;t.strokeStyle=`rgba(201,90,46,${y*.5})`,t.lineWidth=2+y*7,t.lineCap="round",t.beginPath(),t.moveTo(p[u-1].x,p[u-1].y),t.lineTo(p[u].x,p[u].y),t.stroke()}}t.save(),t.lineCap="round";for(const p of[{width:14,style:`${d}0.22)`},{width:7,style:`${d}0.45)`},{width:3,style:f}]){t.lineWidth=p.width,t.strokeStyle=p.style;for(const[m,b]of Ue){const u=n[m],y=n[b];!l(u)||!l(y)||(t.beginPath(),t.moveTo(o(u),h(u)),t.lineTo(o(y),h(y)),t.stroke())}}for(let p=0;p<n.length;p++){const m=n[p];if(!l(m))continue;const b=p===9||p===10,u=o(m),y=h(m),k=b?8:p===0?7:4.5,w=t.createRadialGradient(u,y,0,u,y,k*3);w.addColorStop(0,b?"rgba(201,58,46,0.55)":`${d}0.5)`),w.addColorStop(1,"rgba(0,0,0,0)"),t.fillStyle=w,t.beginPath(),t.arc(u,y,k*3,0,Math.PI*2),t.fill(),t.fillStyle=b?"#e8604e":f,t.beginPath(),t.arc(u,y,k,0,Math.PI*2),t.fill()}if(c){const p=n[5],m=n[6];if(l(p)&&l(m)){const b=(o(p)+o(m))/2,u=(h(p)+h(m))/2,y=56+Math.sin(performance.now()*.008)*7;t.strokeStyle="rgba(78,135,184,0.65)",t.lineWidth=4,t.beginPath(),t.arc(b,u,y,0,Math.PI*2),t.stroke()}}if(this.burst&&performance.now()-this.burst.t<350){const p=1-(performance.now()-this.burst.t)/350;t.strokeStyle=`rgba(201,58,46,${p})`,t.lineWidth=3+5*p,t.beginPath(),t.arc(this.burst.x,this.burst.y,14+(1-p)*52,0,Math.PI*2),t.stroke()}if(t.restore(),t.font="12px ui-monospace,monospace",t.textAlign="right",t.fillStyle="rgba(241,231,211,0.5)",t.fillText("v6",e-10,22),t.font="bold 18px ui-monospace,monospace",t.textAlign="left",t.fillStyle=this.tracking?"#9ef0a0":"#ff8a7a",t.fillText(this.tracking?this.lastDebug.calibrated?`● LIVE · ${this.fpsEma.toFixed(0)}fps · ${this.variant}`:"◐ calibrating — stand still…":"○ step back — can't see you",12,26),this.tracking&&(t.font="15px ui-monospace,monospace",t.fillStyle=this.peakSpd>=2.2&&this.peakTrv>=.28?"#9ef0a0":"#e8d9a8",t.fillText(`peak spd ${this.peakSpd.toFixed(1)} (≥2.2) · trv ${this.peakTrv.toFixed(2)} (≥0.28)`,12,48)),this.lastDebug.calibrated&&this.tracking){const p=-this.lastDebug.leanOff;t.textAlign="center",t.font="bold 20px ui-monospace,monospace";const m=Math.abs(p)>.1;t.fillStyle=m?"#f1e7d3":"rgba(241,231,211,0.35)",t.fillText(p>.1?"FWD ▶":p<-.1?"◀ BACK":`lean ${p>=0?"+":""}${p.toFixed(2)}`,e/2,s-14)}if(performance.now()<this.flash.until){const p=(this.flash.until-performance.now())/500;t.globalAlpha=Math.min(1,p*2),t.font="bold 44px ui-monospace,monospace",t.textAlign="center",t.shadowColor=this.flash.color,t.shadowBlur=22,t.fillStyle=this.flash.color,t.fillText(this.flash.text,e/2,s/2+14),t.shadowBlur=0,t.globalAlpha=1}}}const Ye=6;class He{constructor(t){this.bot=t,this.name=t.name}name;staged=null;request(t){C(t,1)&&(this.staged=this.bot.next(t,1))}consume(){const t=this.staged;return this.staged=null,t}note(){return"opponent: scripted bot · sim 60Hz"}}const mt=1e3/tt;class Ge{state;input=new $t;fx=new Wt;particles=new Ut;sfx=new Dt;renderer;opponent;acc=0;last=performance.now();msg="";sub="";msgTtl=0;msgAge=0;seedCounter=1;hud;debugPanel;maybeBrain=null;lastBrain=null;poseInput;firstPlayableSent=!1;stats={hitsYou:0,hitsAi:0,parriesYou:0,parriesAi:0,baitsAi:0};intents=[];lastDecisionTick=-1;recap=null;onboardingTtl=660;constructor(t,e){this.renderer=new Gt(t),this.state=nt(this.nextSeed()),this.opponent=this.makeBot(3),this.input.attach(window),this.poseInput=new De(this.input,e,s=>{this.sfx.gestureCue(s);const i=this.renderer.playerPx(this.state,0);this.fx.spawnWave(i.x,i.y-18,s==="STRIKE"?"#c93a2e":"#4e87b8",2.5)}),this.hud=new Qt(e),this.debugPanel=new le(e,()=>this.maybeBrain,()=>this.state),window.addEventListener("keydown",()=>this.sfx.unlock(),{once:!0}),requestAnimationFrame(s=>this.frame(s))}startPose(t){this.poseInput.active||(t||this.setMsg("SUMMONING YOUR BODY…","allow the camera — everything stays in your browser",90),this.poseInput.start().then(()=>{this.setMsg("FIGHT WITH YOUR BODY","👊 punch = strike · 🛡 both hands up = parry · lean right = advance, left = retreat · (V toggles, keyboard always works)",200)}).catch(e=>{t?this.setMsg("KEYBOARD MODE","no camera — ←→ move · J strike · K feint · L parry (V retries the camera)",120):this.setMsg("CAMERA UNAVAILABLE",String(e).slice(0,70),90)}))}setOpponent(t){this.opponent=t,this.maybeBrain="telemetry"in t&&"setPlanOverride"in t?t:null,this.maybeBrain&&(this.lastBrain=this.maybeBrain)}brainHint(){return this.maybeBrain?"":this.lastBrain?" · press 9 to face the LLM":" · (the LLM brain is loading)"}nextSeed(){return(Date.now()^this.seedCounter++<<16)>>>0}makeBot(t){const e=1e3+t,s=t===1?vt(e):t===2?Xt(e):t===3?zt(e):t===4?jt(e):t===7?Vt():t===8?qt(e):Zt(e);return new He(s)}setMsg(t,e="",s=70){this.msg=t,this.sub=e,this.msgTtl=s,this.msgAge=0}resetMatch(){this.state=nt(this.nextSeed()),this.stats={hitsYou:0,hitsAi:0,parriesYou:0,parriesAi:0,baitsAi:0},this.recap=null,this.msg="",this.sub=""}buildRecap(t){const e=this.maybeBrain?.telemetry();this.recap={winner:t===0?"you":"the AI",rounds:`${this.state.roundWins[0]} – ${this.state.roundWins[1]}`,hitsYou:this.stats.hitsYou,hitsAi:this.stats.hitsAi,parriesYou:this.stats.parriesYou,parriesAi:this.stats.parriesAi,baitsAi:this.stats.baitsAi,finalRead:e?e.planString:"(scripted bot — no model in this match)",habit:e?e.prediction.predictedHabit:"—",modelLine:this.opponent.note()}}simTick(){if(this.fx.hitStopFrames>0){this.fx.hitStopFrames--;return}this.input.setFacingSign(this.state.players[1].x>=this.state.players[0].x?1:-1);const t=this.input.poll(),e=this.opponent.consume(),s=yt(this.state,t,e);if(this.state=s.state,this.opponent.observe?.(this.state,s.events),s.events.length){const i=this.renderer.playerPx(this.state,0),r=this.renderer.playerPx(this.state,1),n={x:(i.x+r.x)/2,y:(i.y+r.y)/2};this.fx.onEvents(s.events,n),this.sfx.onEvents(s.events);for(const o of s.events)switch(o.t){case"HIT":o.by===0?this.stats.hitsYou++:this.stats.hitsAi++,this.particles.spawnSparks(n.x,n.y,"#ff5d52",18,8);break;case"BLOCK":this.particles.spawnSparks(n.x,n.y,"#7fa6e8",8,5);break;case"PARRY_SUCCESS":o.by===0?this.stats.parriesYou++:this.stats.parriesAi++,this.particles.spawnRing(n.x,n.y-10,"#19e3ff"),this.particles.spawnSparks(n.x,n.y,"#bfffff",12,6),this.setMsg("PARRY!","",30);break;case"FEINT_BAITED":o.by===1&&this.stats.baitsAi++,this.particles.spawnRing(o.by===1?r.x:i.x,n.y-30,"#ffd27f"),this.setMsg("BAITED!","",26);break;case"WHIFF":this.particles.spawnDust(o.by===0?i.x:r.x,(o.by===0?i.y:r.y)+50,o.by===0?1:-1);break;case"PARRY_WHIFF":o.by===0&&this.setMsg("PARRY WHIFFED!","you guarded thin air — exposed for 16 frames",40);break;case"KO":this.particles.spawnSparks(n.x,n.y,"#ffffff",30,10),this.setMsg("K.O.","",80);break;case"ROUND_START":this.setMsg(`ROUND ${o.round}`,"fight!",60),o.round===1&&v("match_started",{opponent:this.opponent.name});break;case"MATCH_OVER":this.buildRecap(o.winner),this.msg="",v("match_completed",{winner:o.winner===0?"human":"ai"});break}}this.state.tick%Ye===0&&this.state.roundState==="FIGHT"&&this.opponent.request(this.state),this.msgTtl>0&&--this.msgTtl===0&&(this.msg="",this.sub="")}handleUiKeys(){for(const t of this.input.drainUi())if(t==="Enter")this.resetMatch();else if(t.startsWith("Digit")){const e=Number(t.slice(5));e>=1&&e<=5||e===7||e===8?(this.opponent=this.makeBot(e),this.maybeBrain=null,this.setMsg(e===7?"THE MASHER":e===8?"THE EXECUTIONER":this.opponent.name.toUpperCase(),e===8?"frame-perfect wake-up meaties — the gauntlet's apex (scripted)":"opponent switched (scripted)",e>=7?80:50)):e===9&&this.lastBrain&&(this.setOpponent(this.lastBrain),this.setMsg("THE MIND READER RETURNS","browser LLM re-engaged — it remembers your habits",80))}else if(t==="Digit6"){this.setMsg("LOCAL-ONLY BUILD","the trained duelist stars in the demo video — this Space is 100% in-browser",80);continue}else t==="KeyM"?this.sfx.muted=!this.sfx.muted:t==="KeyB"?this.debugPanel.toggle():t==="KeyV"&&(this.poseInput.active?(this.poseInput.stop(),this.setMsg("POSE MODE OFF","back to the keyboard",60)):this.startPose(!1))}cornerGlow(){const t=this.state.players[0],e=this.state.players[1];return Math.abs(e.x-t.x)<L(95)?t.x<=G+L(4)||e.x<=G+L(4)?-1:t.x>=z-G-L(4)||e.x>=z-G-L(4)?1:0:0}frame(t){for(this.acc+=Math.min(t-this.last,250)*this.fx.timeScale,this.last=t;this.acc>=mt;)this.acc-=mt,this.simTick();this.handleUiKeys(),this.fx.tick(),this.particles.update(),this.msgAge++,this.onboardingTtl>0&&this.onboardingTtl--;const e=this.maybeBrain?.telemetry().lastTick??null;e&&e.simTick!==this.lastDecisionTick&&e.intent!=="CONTINUE"&&(this.lastDecisionTick=e.simTick,this.intents.push({ch:et[e.intent]??"·",age:0}));for(const r of this.intents)r.age++;this.intents=this.intents.filter(r=>r.age<50);const s=this.maybeBrain?.telemetry()??null,i={msg:this.msg,sub:this.sub,msgAge:this.msgAge,botName:this.opponent.name.toUpperCase(),brainNote:this.opponent.note()+this.brainHint(),intents:this.intents,scanActive:!!s?.analystBusy,opsSpark:this.maybeBrain?this.maybeBrain.opsWindow():[],cornerGlow:this.cornerGlow(),recap:this.recap,onboarding:this.onboardingTtl>0&&!this.recap};this.renderer.draw(this.state,this.fx.view(),this.particles,i),this.hud.render(s),this.firstPlayableSent||(this.firstPlayableSent=!0,v("first_playable")),requestAnimationFrame(r=>this.frame(r))}}function Ke(a,t){return new Ge(a,t)}const Ve=document.getElementById("game"),xt=document.getElementById("hud"),Tt=Ke(Ve,xt),U=document.createElement("div");U.style.cssText="position:absolute;top:64px;left:50%;transform:translateX(-50%);background:#f8f0d8;border:3px solid #2b2620;border-radius:2px;padding:.5rem .9rem;font:12px ui-monospace,monospace;color:#3a5a78;max-width:70vw;text-align:center;pointer-events:none;display:none;box-shadow:inset 0 0 0 2px #fffdf4, 3px 3px 0 rgba(43,38,32,.28);";xt.appendChild(U);function W(a,t=0){U.style.display="",U.textContent=a,t>0&&setTimeout(()=>U.style.display="none",t)}async function qe(){if(v("page_load"),new URLSearchParams(location.search).get("nollm")==="1"){W("LLM disabled (?nollm=1) — scripted opponents only",4e3);return}v("gpu"in navigator?"webgpu_detected":"webgpu_missing"),W("🧠 loading the AI's brain in the background — warm up against the scripted bot…"),v("model_download_started");try{const a=await Pe(e=>{const s=e.progress>0&&e.progress<=1?` ${(e.progress*100).toFixed(0)}%`:"";W(`🧠 ${e.text}${s}`)});v("model_download_complete"),v("tier_selected",{tier:a.tier,medianMs:Math.round(a.medianOpMs),reason:a.reason}),a.backend.caps().label==="server"&&v("fell_back_to_server");const t=new rt(a.backend,`LLM · ${a.tier}`);Tt.setOpponent(t),W(`🧠 LLM opponent live — ${a.reason}. Press B for the judge panel. It is reading you now.`,6e3),window.__parryBrain=t}catch(a){console.error("[parry] brain boot failed:",a),W(`brain failed to load (${String(a).slice(0,90)}) — scripted opponent stays. Try ?backend=server`,8e3)}}qe();Tt.startPose(!0);console.log("[parry] boot — scripted play immediately; LLM brain installs when ready. B = judge panel.");
|
static/assets/pose.worker-BBF_KKBj.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/assets/spike-BPCTWSB0.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import"./action_set-C6mnKwO1.js";import{G as v,f as E,T as S,b as _,N as I,p as G,C as F,A as N,V as B,g as U}from"./index-K8o-xskJ.js";const W=["Qwen2.5-1.5B-Instruct-q4f16_1-MLC","Llama-3.2-1B-Instruct-q4f16_1-MLC","Qwen2.5-0.5B-Instruct-q4f16_1-MLC","Qwen2.5-3B-Instruct-q4f16_1-MLC"],m=e=>document.getElementById(e),o=e=>{m("log").textContent+=`
|
| 2 |
+
${e}`,console.log(`[spike] ${e}`)},M=e=>{m("log").textContent=e};function A(e){const n=[...e].sort((r,c)=>r-c),a=n.length>>1;return n.length%2?n[a]:(n[a-1]+n[a])/2}function j(e){const n=[...e].sort((a,r)=>a-r);return n[Math.min(n.length-1,Math.ceil(n.length*.95)-1)]}const i=e=>Math.round(e*10)/10;function O(e){const n=S.replace("{plan}",e.slice(0,80)).replace("{me}","NEUTRAL").replace("{you}","RECOVERY 12f").replace("{dist}","MID").replace("{tw}","L").replace("{adv}","+9").replace("{recent}","RRSWPSFS").replace("{meRecent}","LLSWFP");return{system:_,user:n}}const C="Wake-up striker. Bait it with a feint, punish the whiff.";let d=null,t={};function L(e,n={}){const{system:a,user:r}=O(C),c={stream:!1,messages:[{role:"system",content:a},{role:"user",content:r}],max_tokens:2,temperature:1,seed:42,...n};return e&&(c.response_format={type:"grammar",grammar:e}),c}async function u(e,n={}){const a=performance.now(),r=await d.chatCompletion(L(e,n)),c=performance.now()-a,p=r.choices[0];return{ms:c,content:p?.message?.content??"",promptTokens:r.usage?.prompt_tokens??null,logprobs:p?.logprobs??null}}async function q(){const e=m("model").value,n=G.model_list.some(c=>c.model_id===e);if(t={ua:navigator.userAgent,modelId:e,grammarId:v,inPrebuiltList:n},M(`model: ${e}
|
| 3 |
+
in prebuilt list: ${n} ${n?"":"⚠️ version drift — check @mlc-ai/web-llm pin!"}`),!("gpu"in navigator)){o("❌ navigator.gpu is undefined — no WebGPU in this browser. Spike cannot run locally.");return}const a=m("dl");a.style.display="block";const r=performance.now();d=await F(new Worker(new URL(""+new URL("llm.worker-GbIGcnxV.js",import.meta.url).href,import.meta.url),{type:"module"}),e,{initProgressCallback:c=>{a.value=c.progress,M(`loading: ${c.text}`)}}),t.loadMs=Math.round(performance.now()-r),a.style.display="none",M(`loaded ${e} in ${(t.loadMs/1e3).toFixed(1)}s (cached loads are much faster)`),m("run").disabled=!1}async function D(){if(!d)return;const e=U("NEUTRAL"),n=[];o("A · cold first op (shader compile)…");let a=!0,r;try{r=await u(e)}catch(s){a=!1,o(` grammar response_format threw (${String(s).slice(0,120)}) → falling back to unconstrained + post-map`),r=await u(null)}t.grammarSupported=a,t.coldFirstOpMs=i(r.ms),t.promptTokens=r.promptTokens,n.push(r.content),o(` cold: ${i(r.ms)}ms · prompt_tokens=${r.promptTokens??"?"} · out="${r.content}"`),(r.promptTokens??0)>160&&o(` ⚠️ prompt_tokens ${r.promptTokens} > 160 cap — shrink template!`),o("warm-up · 3 tickOps + 1 short generate…");const c=[];for(let s=0;s<3;s++){const l=await u(a?e:null);c.push(i(l.ms)),n.push(l.content)}t.warmupOpsMs=c,o(` warm-up ops: ${c.join(", ")}ms`),o("B · 20 warm tickOps (production prompt, grammar on)…");const p=[];for(let s=0;s<20;s++){const l=await u(a?e:null,{seed:42+s});p.push(i(l.ms)),n.push(l.content)}t.tickOpsMs=p,t.warmMedianMs=i(A(p)),t.warmP95Ms=i(j(p)),o(` ops: ${p.join(", ")}`),o(` warm median ${t.warmMedianMs}ms · p95 ${t.warmP95Ms}ms`),o("C · Analyst interleave (streamed 48-tok generate on the same engine)…");const{system:T}=O(C),R=performance.now();let w="";try{const s=await d.chat.completions.create({stream:!0,messages:[{role:"system",content:T},{role:"user",content:"Opponent log: struck on wake-up 3 times in a row; parried twice after retreating. Output: <plan> | <HABIT> | <0-9 confidence>."}],max_tokens:48,temperature:.8,...a?{response_format:{type:"grammar",grammar:N}}:{}});for await(const l of s)w+=l.choices[0]?.delta?.content??""}catch(s){o(` analyst grammar failed (${String(s).slice(0,100)}) → retrying unconstrained`);const l=await d.chat.completions.create({stream:!0,messages:[{role:"user",content:"One short sentence about parrying."}],max_tokens:48});for await(const g of l)w+=g.choices[0]?.delta?.content??""}t.analystGenMs=i(performance.now()-R);const P=await u(a?e:null);t.postAnalystFirstOpMs=i(P.ms),o(` analyst gen ${t.analystGenMs}ms → "${w.slice(0,80)}"`),o(` first tickOp after analyst: ${t.postAnalystFirstOpMs}ms`),o("D · grammar OFF A/B (mask overhead)…");const y=[];for(let s=0;s<10;s++){const l=await u(null,{seed:142+s});y.push(i(l.ms))}t.grammarOffMedianMs=i(A(y)),o(` grammar-off median ${t.grammarOffMedianMs}ms (overhead ≈ ${i((t.warmMedianMs??0)-t.grammarOffMedianMs)}ms)`),o("E · logprobs/top_logprobs under constrained decoding…");try{const s=await u(a?e:null,{logprobs:!0,top_logprobs:5}),l=s.logprobs!==null&&s.logprobs!==void 0;t.logprobsAvailable=!!l,o(` logprobs available: ${t.logprobsAvailable}${l?"":" → HUD confidence falls back to Analyst confidence field"}`)}catch{t.logprobsAvailable=!1,o(" logprobs threw → unavailable; HUD uses Analyst confidence")}o("F · interruptGenerate() on a streaming call (issue-#447 guard)…");try{const s=(async()=>{const g=await d.chat.completions.create({stream:!0,messages:[{role:"user",content:"Write 200 words about fencing footwork."}],max_tokens:200});let $=0;for await(const x of g)$+=(x.choices[0]?.delta?.content??"").length;return $})();await new Promise(g=>setTimeout(g,200)),d.interruptGenerate(),await s.catch(()=>0);const l=await u(a?e:null);t.interruptSafe=l.content.trim().length>0,o(` post-interrupt tickOp out="${l.content}" (${i(l.ms)}ms) → interruptSafe=${t.interruptSafe}`)}catch(s){t.interruptSafe=!1,o(` interrupt check failed: ${String(s).slice(0,120)}`)}o("G · verb single-token check (per-letter grammar, max_tokens 1)…");const h={};for(const s of Object.values(B))try{const g=((await d.chatCompletion(L(a?`root ::= "${s}"
|
| 4 |
+
`:null,{max_tokens:1}))).choices[0]?.message?.content??"").trim();h[s]=g===s}catch{h[s]=!1}t.verbSingleToken=h,o(` ${JSON.stringify(h)}`),t.outputsSeen=n.slice(0,10);const f={warmMedian:(t.warmMedianMs??999)<=25,p95:(t.warmP95Ms??999)<=40,postAnalyst:(t.postAnalystFirstOpMs??999)<=60,overall:!1};f.overall=f.warmMedian&&f.p95&&f.postAnalyst,t.pass=f;const k=m("verdict");k.style.display="block",k.className=f.overall?"pass":"fail",k.textContent=f.overall?`✅ G1 PASS — median ${t.warmMedianMs}ms · p95 ${t.warmP95Ms}ms · post-analyst ${t.postAnalystFirstOpMs}ms`:`❌ G1 FAIL — median ${t.warmMedianMs}ms (≤25?) · p95 ${t.warmP95Ms}ms (≤40?) · post-analyst ${t.postAnalystFirstOpMs}ms (≤60?) — levers: shrink prompt, smaller tier, chunked analyst`;const b=m("json");b.style.display="block",b.textContent=JSON.stringify(t,null,2),m("copy").disabled=!1,o(`
|
| 5 |
+
done — copy the JSON block and commit it to spike/results/ as the G1 record.`)}function H(){const e=m("model");for(const n of W){const a=document.createElement("option");a.value=n,a.textContent=n,e.appendChild(a)}m("load").onclick=()=>void q().catch(n=>o(`load failed: ${String(n)}`)),m("run").onclick=()=>void D().catch(n=>o(`spike failed: ${String(n)}`)),m("copy").onclick=()=>void navigator.clipboard.writeText(JSON.stringify(t,null,2)),o(`grammar_id=${v} · template_hash=${E(S+_+I)}`)}H();
|
static/grammar.hash
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
582fb167
|
static/index.html
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<link rel="icon" href="data:," />
|
| 7 |
+
<title>PARRY — duel a small model running in your browser</title>
|
| 8 |
+
<style>
|
| 9 |
+
/* MORNING DOJO shell — warm washi paper, ink type. */
|
| 10 |
+
html, body { margin: 0; padding: 0; background: #ece1cb; color: #2b2620; height: 100%; overflow: hidden; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
| 11 |
+
#stage { position: relative; width: 100vw; height: 100vh; }
|
| 12 |
+
/* RETRO: the world renders in a low-res buffer and upscales nearest-neighbor */
|
| 13 |
+
canvas { display: block; width: 100%; height: 100%; image-rendering: pixelated; }
|
| 14 |
+
#hud { position: absolute; inset: 0; pointer-events: none; }
|
| 15 |
+
</style>
|
| 16 |
+
<script type="module" crossorigin src="./assets/main-Cu1TzpLc.js"></script>
|
| 17 |
+
<link rel="modulepreload" crossorigin href="./assets/action_set-C6mnKwO1.js">
|
| 18 |
+
<link rel="modulepreload" crossorigin href="./assets/engine-DIUq88up.js">
|
| 19 |
+
<link rel="modulepreload" crossorigin href="./assets/index-K8o-xskJ.js">
|
| 20 |
+
</head>
|
| 21 |
+
<body>
|
| 22 |
+
<div id="stage">
|
| 23 |
+
<canvas id="game"></canvas>
|
| 24 |
+
<div id="hud"></div>
|
| 25 |
+
</div>
|
| 26 |
+
</body>
|
| 27 |
+
</html>
|
static/pose/group1-shard1of2.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b42c3232bf13b0efc691d3f0693dd3fc74404f709b1deee0a271828af6dbbea2
|
| 3 |
+
size 4194304
|
static/pose/group1-shard2of2.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8253ab965aa3122c08f331777ca395ce9ca19bb9e7cb278b99de1c46dbdeb6dc
|
| 3 |
+
size 455912
|
static/pose/model.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/pose/tfjs-backend-wasm-simd.wasm
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:77ebb28a6d34f371dbbf2086b7f2de8994acd8ea5a3cf1fa24d2c26c840cac7b
|
| 3 |
+
size 424594
|
static/pose/tfjs-backend-wasm-threaded-simd.wasm
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c052228d4bef185c27bbe59a9e029570c78bbb9f08b3cb46b597851650373de2
|
| 3 |
+
size 435643
|
static/pose/tfjs-backend-wasm.wasm
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:70a5d516060464e5269f01c74bac1772d6b8ab6cb612acf16b5cdaf61f78d892
|
| 3 |
+
size 311123
|
static/pose/thunder/group1-shard1of3.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:58dd47fd600a4849c342ce14e2ec32102744ca3d5f7fd9e6888d284f7f922cba
|
| 3 |
+
size 4194304
|
static/pose/thunder/group1-shard2of3.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:08da980bc00886854f29b49b9c14528cd41626c0498c67b450de394713f1d0bc
|
| 3 |
+
size 4194304
|
static/pose/thunder/group1-shard3of3.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c1c6b712e33456aa95aa556b6425528d04fe8730ce9b92de38ec58e947fede34
|
| 3 |
+
size 4088504
|
static/pose/thunder/model.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/spike/latency.html
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>PARRY — §9.1 latency spike</title>
|
| 7 |
+
<style>
|
| 8 |
+
body { background: #0b0e14; color: #d8dee9; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; margin: 2rem; max-width: 900px; }
|
| 9 |
+
h1 { font-size: 1.1rem; color: #88c0d0; }
|
| 10 |
+
select, button { background: #1b222e; color: #d8dee9; border: 1px solid #3b4252; padding: .5rem .8rem; font: inherit; border-radius: 6px; margin-right: .5rem; }
|
| 11 |
+
button:disabled { opacity: .4; }
|
| 12 |
+
#log { white-space: pre-wrap; background: #11151d; border: 1px solid #2a3140; border-radius: 8px; padding: 1rem; margin-top: 1rem; min-height: 8rem; font-size: .85rem; }
|
| 13 |
+
#verdict { font-weight: bold; padding: .8rem 1rem; border-radius: 8px; margin-top: 1rem; display: none; }
|
| 14 |
+
.pass { background: #14331f; color: #a3be8c; border: 1px solid #a3be8c; }
|
| 15 |
+
.fail { background: #33141a; color: #bf616a; border: 1px solid #bf616a; }
|
| 16 |
+
#json { white-space: pre-wrap; background: #11151d; border: 1px dashed #2a3140; border-radius: 8px; padding: 1rem; margin-top: 1rem; font-size: .75rem; max-height: 18rem; overflow: auto; }
|
| 17 |
+
progress { width: 100%; }
|
| 18 |
+
</style>
|
| 19 |
+
<script type="module" crossorigin src="../assets/spike-BPCTWSB0.js"></script>
|
| 20 |
+
<link rel="modulepreload" crossorigin href="../assets/action_set-C6mnKwO1.js">
|
| 21 |
+
<link rel="modulepreload" crossorigin href="../assets/index-K8o-xskJ.js">
|
| 22 |
+
</head>
|
| 23 |
+
<body>
|
| 24 |
+
<h1>PARRY latency spike — gate G1 (§9.1): can the Tactician tick inside the budget?</h1>
|
| 25 |
+
<p>
|
| 26 |
+
Measures the full <b>prefill+decode</b> op on a production-length prompt (plan-string included),
|
| 27 |
+
grammar-constrained, end-to-end through the Web Worker. PASS on this machine:
|
| 28 |
+
<b>warm median ≤ 25ms · p95 ≤ 40ms · first-op-after-Analyst ≤ 60ms</b>.
|
| 29 |
+
</p>
|
| 30 |
+
<div>
|
| 31 |
+
<select id="model"></select>
|
| 32 |
+
<button id="load">1 · Load model</button>
|
| 33 |
+
<button id="run" disabled>2 · Run spike</button>
|
| 34 |
+
<button id="copy" disabled>Copy results JSON</button>
|
| 35 |
+
</div>
|
| 36 |
+
<progress id="dl" value="0" max="1" style="display:none"></progress>
|
| 37 |
+
<div id="verdict"></div>
|
| 38 |
+
<div id="log">idle.</div>
|
| 39 |
+
<div id="json" style="display:none"></div>
|
| 40 |
+
</body>
|
| 41 |
+
</html>
|
static/test/golden_browser.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<title>Parry golden-trace browser harness</title>
|
| 6 |
+
<script type="module" crossorigin src="../assets/golden-B6Ebeawj.js"></script>
|
| 7 |
+
<link rel="modulepreload" crossorigin href="../assets/action_set-C6mnKwO1.js">
|
| 8 |
+
<link rel="modulepreload" crossorigin href="../assets/engine-DIUq88up.js">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<h1>Golden-trace browser replay</h1>
|
| 12 |
+
<pre id="out">running…</pre>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|