Jainamshahhh commited on
Commit
2ff4f6d
·
verified ·
1 Parent(s): 11a841d

deploy parry

Browse files
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,13 +1,35 @@
1
  ---
2
  title: Parry
3
- emoji: 🐢
4
- colorFrom: purple
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.17.3
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ sub-100ms reaction loop no cloud API can physically serve.
21
+
22
+ - **Local-first:** the opponent's brain is a Qwen2.5-1.5B decoding one grammar-constrained
23
+ intent token every ~100ms on YOUR GPU. Pull your Wi-Fi out mid-match — nothing changes.
24
+ - **Observable adaptation:** the Analyst's live read of you is rendered on screen, and the
25
+ debug panel lets you EDIT its read and watch the Tactician's play flip in real time.
26
+ - **No WebGPU? You still play:** transparent server fallback (same model, same grammar)
27
+ served via Modal.
28
+
29
+ Controls: ←/→ move · J strike · K feint · L parry · 1–5 switch opponent · Enter rematch.
30
+
31
+ *(This Space is a `gradio.Server` app — Gradio's engine serves the custom canvas at `/`,
32
+ and the Analyst endpoint runs through Gradio's queue, callable with `gradio_client`.)*
33
+
34
+ NOTE deploy: copy the built client (`npm run build` → `dist/`) into `static/` before
35
+ pushing; `app.py` serves `static/index.html` at `/`.
app.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PARRY — compliant Gradio hosting layer (C8) + server-side inference relay.
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
+ Defensive: if the pinned Gradio somehow lacks `Server`, we degrade to
11
+ FastAPI + gr.mount_gradio_app so the Space still boots while we consult the
12
+ hedge ladder (docker swap / gr.Blocks+gr.HTML — see ../space/app_blocks.py).
13
+
14
+ Endpoints
15
+ GET / → static/index.html (the game)
16
+ GET /static/* → built Vite bundle (immutable assets)
17
+ POST /infer → stateless Tactician relay → Modal vLLM {prompt, grammar_id, tier} → {token, t_ms}
18
+ POST /analyst → stateless Analyst relay (raw fallback path)
19
+ api "analyst" → same, through Gradio's queue (compliance centerpiece; gradio_client-callable)
20
+ api "about" → build/model/grammar info via the queue
21
+ POST /funnel → enum-validated JSONL beacons (+ optional CommitScheduler → private HF Dataset)
22
+ GET /healthz → {ok, modal, version, grammar_id}
23
+
24
+ Secrets (Space settings): MODAL_INFER_URL, MODAL_INFER_KEY, HF_TOKEN (funnel dataset, optional),
25
+ FUNNEL_DATASET (e.g. "user/parry-funnel", optional).
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import os
32
+ import threading
33
+ import time
34
+ from pathlib import Path
35
+
36
+ import gradio as gr
37
+ import httpx
38
+ from fastapi import Request
39
+ from fastapi.responses import FileResponse, JSONResponse
40
+ from fastapi.staticfiles import StaticFiles
41
+
42
+ HERE = Path(__file__).parent
43
+ STATIC = HERE / "static"
44
+ DATA = HERE / "data"
45
+ DATA.mkdir(exist_ok=True)
46
+ FUNNEL_PATH = DATA / "funnel.jsonl"
47
+
48
+ APP_VERSION = "parry-space-v1"
49
+ GRAMMAR_ID = os.environ.get("GRAMMAR_ID", "unset") # injected by CI from grammar/hash
50
+ MODAL_INFER_URL = os.environ.get("MODAL_INFER_URL", "").rstrip("/")
51
+ MODAL_INFER_KEY = os.environ.get("MODAL_INFER_KEY", "")
52
+
53
+ FUNNEL_EVENTS = {
54
+ "page_load",
55
+ "webgpu_detected",
56
+ "webgpu_missing",
57
+ "tier_selected",
58
+ "model_download_started",
59
+ "model_download_complete",
60
+ "first_playable",
61
+ "match_started",
62
+ "match_completed",
63
+ "fell_back_to_server",
64
+ "bounced_during_download",
65
+ "debug_override_used",
66
+ "trace_exported",
67
+ }
68
+
69
+ _funnel_lock = threading.Lock()
70
+
71
+ # Optional: persist funnel JSONL to a private HF Dataset (ephemeral disk survival).
72
+ _scheduler = None
73
+ if os.environ.get("HF_TOKEN") and os.environ.get("FUNNEL_DATASET"):
74
+ try:
75
+ from huggingface_hub import CommitScheduler
76
+
77
+ _scheduler = CommitScheduler(
78
+ repo_id=os.environ["FUNNEL_DATASET"],
79
+ repo_type="dataset",
80
+ folder_path=str(DATA),
81
+ every=5, # minutes
82
+ private=True,
83
+ )
84
+ except Exception as e: # noqa: BLE001 — funnel persistence is best-effort
85
+ print(f"[funnel] CommitScheduler unavailable: {e}")
86
+
87
+ # Shared keep-alive pool to Modal (amortizes TLS); created lazily.
88
+ _client = httpx.AsyncClient(timeout=2.5)
89
+
90
+
91
+ async def _relay(path: str, payload: dict) -> tuple[int, dict]:
92
+ """Stateless relay to the Modal vLLM endpoint. The browser holds ALL state."""
93
+ if not MODAL_INFER_URL:
94
+ return 503, {"state": "no_server_configured"}
95
+ t0 = time.perf_counter()
96
+ try:
97
+ r = await _client.post(
98
+ f"{MODAL_INFER_URL}{path}",
99
+ json=payload,
100
+ headers={"Authorization": f"Bearer {MODAL_INFER_KEY}"} if MODAL_INFER_KEY else {},
101
+ )
102
+ body = r.json()
103
+ body["t_ms"] = round((time.perf_counter() - t0) * 1000, 1)
104
+ return r.status_code, body
105
+ except httpx.TimeoutException:
106
+ return 503, {"state": "warming", "retry_after_s": 10}
107
+ except Exception as e: # noqa: BLE001
108
+ return 502, {"state": "relay_error", "detail": str(e)[:200]}
109
+
110
+
111
+ def _append_funnel(row: dict) -> None:
112
+ with _funnel_lock:
113
+ with open(FUNNEL_PATH, "a", encoding="utf-8") as f:
114
+ f.write(json.dumps(row, separators=(",", ":")) + "\n")
115
+ print(f"[funnel] {row.get('event')}")
116
+
117
+
118
+ def _about() -> dict:
119
+ return {
120
+ "app": "parry",
121
+ "version": APP_VERSION,
122
+ "grammar_id": GRAMMAR_ID,
123
+ "hero_model": "Qwen2.5-1.5B-Instruct (q4f16_1, in-browser WebGPU)",
124
+ "fallback": "same grammar via Modal vLLM" if MODAL_INFER_URL else "not configured",
125
+ "thesis": "a sub-100ms reaction loop no network round-trip can serve",
126
+ }
127
+
128
+
129
+ def _analyst_via_queue(behavior_log: str, grammar_id: str) -> dict:
130
+ """Sync wrapper used by the Gradio-queue endpoint (runs in Gradio's worker)."""
131
+ import asyncio
132
+
133
+ status, body = asyncio.run(_relay("/analyst", {"behaviorLog": behavior_log, "grammar_id": grammar_id}))
134
+ return body if status == 200 else {"error": body}
135
+
136
+
137
+ HAS_SERVER = hasattr(gr, "Server")
138
+
139
+ if HAS_SERVER:
140
+ app = gr.Server()
141
+ else: # degrade gracefully; primary fix is pinning sdk_version per README
142
+ print("[parry] WARNING: gradio.Server missing — booting FastAPI + mounted Blocks hedge")
143
+ from fastapi import FastAPI
144
+
145
+ app = FastAPI()
146
+
147
+ app.mount("/static", StaticFiles(directory=str(STATIC)), name="static")
148
+
149
+
150
+ @app.get("/")
151
+ async def homepage() -> FileResponse:
152
+ # no-cache on the HTML only; hashed assets under /static are long-cached
153
+ return FileResponse(STATIC / "index.html", headers={"Cache-Control": "no-cache"})
154
+
155
+
156
+ @app.post("/infer")
157
+ async def infer(req: Request) -> JSONResponse:
158
+ payload = await req.json()
159
+ if not isinstance(payload.get("prompt"), str) or len(payload["prompt"]) > 4000:
160
+ return JSONResponse({"error": "bad prompt"}, status_code=400)
161
+ status, body = await _relay("/infer", payload)
162
+ return JSONResponse(body, status_code=status, headers={"Retry-After": "10"} if status == 503 else {})
163
+
164
+
165
+ @app.post("/analyst")
166
+ async def analyst_raw(req: Request) -> JSONResponse:
167
+ payload = await req.json()
168
+ status, body = await _relay("/analyst", payload)
169
+ return JSONResponse(body, status_code=status)
170
+
171
+
172
+ @app.post("/funnel")
173
+ async def funnel(req: Request) -> JSONResponse:
174
+ try:
175
+ row = await req.json()
176
+ except Exception: # noqa: BLE001 — sendBeacon may post opaque bodies
177
+ return JSONResponse({"ok": False}, status_code=400)
178
+ if row.get("event") not in FUNNEL_EVENTS:
179
+ return JSONResponse({"ok": False, "error": "unknown event"}, status_code=400)
180
+ _append_funnel({k: row.get(k) for k in ("v", "event", "session", "ts", "props")})
181
+ return JSONResponse({"ok": True}, status_code=200)
182
+
183
+
184
+ @app.get("/healthz")
185
+ async def healthz() -> JSONResponse:
186
+ modal_state = "unconfigured"
187
+ if MODAL_INFER_URL:
188
+ try:
189
+ r = await _client.get(f"{MODAL_INFER_URL}/healthz")
190
+ modal_state = "warm" if r.status_code == 200 else f"status_{r.status_code}"
191
+ except Exception: # noqa: BLE001
192
+ modal_state = "cold_or_down"
193
+ return JSONResponse({"ok": True, "version": APP_VERSION, "grammar_id": GRAMMAR_ID, "modal": modal_state})
194
+
195
+
196
+ if HAS_SERVER:
197
+ # Gradio-queue endpoints — the compliance centerpiece: Gradio's engine is
198
+ # genuinely doing work (queue, SSE), callable via gradio_client.
199
+ @app.api(name="analyst")
200
+ def analyst_api(behavior_log: str, grammar_id: str = "") -> dict:
201
+ return _analyst_via_queue(behavior_log, grammar_id or GRAMMAR_ID)
202
+
203
+ @app.api(name="about")
204
+ def about_api() -> dict:
205
+ return _about()
206
+
207
+ else:
208
+ # Hedge boot: mount a minimal Blocks app so Gradio is still in the loop.
209
+ with gr.Blocks() as demo:
210
+ gr.Markdown("# Parry backend (hedge boot)")
211
+ inp = gr.Textbox(label="behavior log")
212
+ out = gr.JSON(label="analyst read")
213
+ gr.Button("analyze").click(lambda b: _analyst_via_queue(b, GRAMMAR_ID), inp, out)
214
+ app = gr.mount_gradio_app(app, demo, path="/gradio")
215
+
216
+
217
+ if __name__ == "__main__":
218
+ if HAS_SERVER:
219
+ app.launch(show_error=True)
220
+ else:
221
+ import uvicorn
222
+
223
+ 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-BCs5oIKV.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,s,n){return e<s?s: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_v2",W=60,p={startup:12,active:4,recovery:18,damage:34,chip:8},I={startup:8,recovery:10},m={startup:2,active:8,whiffRecovery:16},B=24,M=30,w=15,l=h(480),c=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,s){e.verb=s,e.phase="STARTUP",e.hasHit=!1,e.framesLeft=s==="STRIKE"?p.startup:s==="FEINT"?I.startup:m.startup}function L(e,s,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:s}),e.phase="RECOVERY",e.framesLeft=p.recovery):(e.hasHit||n.push({t:"PARRY_WHIFF",by:s}),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,s){for(let o=0;o<=1;o=o+1){const r=e.players[o];r.phase==="NEUTRAL"&&s[o]!==0&&(r.x=r.x+s[o]*g|0),r.vx!==0&&(r.x=r.x+r.vx|0,r.vx=K(r.vx),d(r.vx)<8&&(r.vx=0)),r.x=T(r.x,c,l-c|0)}const n=e.players[0],t=e.players[1],i=t.x-n.x|0,a=2*c|0;if(d(i)<a){const o=a-d(i)|0,r=o>>1,u=o-r|0;i>=0?(n.x=T(n.x-r|0,c,l-c|0),t.x=T(t.x+u|0,c,l-c|0)):(n.x=T(n.x+r|0,c,l-c|0),t.x=T(t.x-u|0,c,l-c|0))}}function z(e){return d(e.players[1].x-e.players[0].x|0)<=k}function S(e,s){const n=P(e.x-s.x|0);return n===0?1:n}function J(e,s,n,t){if(!z(e))return;const i=[];for(let a=0;a<=1;a=a+1){const o=s[a];if(!(o.phase==="ACTIVE"&&o.verb==="STRIKE"&&!o.hasHit))continue;const r=1-a,u=s[r];if(u.phase==="ACTIVE"&&u.verb==="PARRY")i.push({kind:"parried",attacker:a,defender:r});else if(u.phase==="BLOCKSTUN"||u.phase==="STAGGER")e.players[a].hasHit=!0;else if(u.phase==="NEUTRAL"&&n[r]===S(e.players[r],e.players[a])){const R=S(e.players[r],e.players[a]),E=c,f=l-c|0,A=R===-1&&e.players[r].x<=E||R===1&&e.players[r].x>=f;i.push({kind:A?"hit":"blocked",attacker:a,defender:r})}else i.push({kind:"hit",attacker:a,defender:r})}for(const a of i){const o=e.players[a.attacker],r=e.players[a.defender];o.hasHit=!0,a.kind==="parried"?(t.push({t:"PARRY_SUCCESS",by:a.defender}),o.phase="STAGGER",o.verb="NONE",o.framesLeft=B,r.hasHit=!0,r.phase="NEUTRAL",r.verb="NONE",r.framesLeft=0):a.kind==="blocked"?(t.push({t:"BLOCK",by:a.attacker}),r.hp=Math.max(1,r.hp-p.chip|0),r.phase="BLOCKSTUN",r.verb="NONE",r.framesLeft=w,r.vx=r.vx+j*S(r,o)|0):(t.push({t:"HIT",by:a.attacker,dmg:p.damage}),r.hp=Math.max(0,r.hp-p.damage|0),r.phase="STAGGER",r.verb="NONE",r.framesLeft=M,r.vx=r.vx+X*S(r,o)|0)}}function Q(e,s){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,s.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 oe(e){const s=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(s-n|0),y(s+n|0)]}}function ee(e,s){return e.roundState==="FIGHT"&&e.players[s].phase==="NEUTRAL"}function ce(e,s){return ee(e,s)?F:V}function te(e){const s=l>>1,n=H>>1;e.players=[y(s-n|0),y(s+n|0)],e.timer=U,e.roundState="FIGHT",e.interFrames=0}function re(e){return e.roundWins[0]+e.roundWins[1]+1|0}function fe(e,s,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:re(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],o=t.players[0];o.phase==="NEUTRAL"&&(s.action!==null?O(o,s.action):a[0]=s.dir|0);const r=t.players[1];if(n!==null&&r.phase==="NEUTRAL")switch(n){case"STRIKE":case"FEINT":case"PARRY":O(r,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,h as f,ee as i,ce as l,oe as r,fe as s};
static/assets/golden-C-3W91Cw.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import"./action_set-C6mnKwO1.js";import{r,s as d}from"./engine-BCs5oIKV.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-CqxAeE_s.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-C1_lv4Q8.js ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import"./action_set-C6mnKwO1.js";import{H as _,A as P,S as $,R as A,f as V,B as ne,D as J,a as Q,E as oe,r as W,s as le,i as Z,l as ce,b as he}from"./engine-BCs5oIKV.js";import{V as ee,G as O,P as pe,p as R,C as de,v as te,g as B,T as U,a as F,N as C,b as fe,f as ue,A as me,H as se}from"./index-CqxAeE_s.js";const Y=120,be={KeyJ:"STRIKE",KeyZ:"STRIKE",KeyK:"FEINT",KeyX:"FEINT",KeyL:"PARRY",KeyC:"PARRY"};class ge{left=!1;right=!1;buffered=null;uiPresses=[];attach(e){e.addEventListener("keydown",t=>{if(t.repeat)return;const r=t.target?.tagName;if(!(r==="TEXTAREA"||r==="INPUT"||r==="SELECT"))switch(t.code){case"ArrowLeft":case"KeyA":this.left=!0;break;case"ArrowRight":case"KeyD":this.right=!0;break;default:{const a=be[t.code];a?(this.buffered={action:a,at:performance.now()},t.preventDefault()):this.uiPresses.push(t.code)}}}),e.addEventListener("keyup",t=>{(t.code==="ArrowLeft"||t.code==="KeyA")&&(this.left=!1),(t.code==="ArrowRight"||t.code==="KeyD")&&(this.right=!1)}),e.addEventListener("blur",()=>{this.left=!1,this.right=!1})}poll(){let e=null;return this.buffered&&(performance.now()-this.buffered.at<=Y&&(e=this.buffered.action),(e||performance.now()-this.buffered.at>Y)&&(this.buffered=null)),{dir:this.left===this.right?0:this.left?-1:1,action:e}}drainUi(){const e=this.uiPresses;return this.uiPresses=[],e}}const E=8,ye=.85;class ke{hitStopFrames=0;shakeMag=0;flash=0;hurt=0;onEvents(e){for(const t of e)switch(t.t){case"HIT":this.hitStopFrames=Math.max(this.hitStopFrames,6),this.shakeMag=Math.min(E,this.shakeMag+7),this.flash=Math.max(this.flash,.55),t.by===1&&(this.hurt=1);break;case"PARRY_SUCCESS":this.hitStopFrames=Math.max(this.hitStopFrames,6),this.shakeMag=Math.min(E,this.shakeMag+5),this.flash=Math.max(this.flash,.8);break;case"BLOCK":this.hitStopFrames=Math.max(this.hitStopFrames,3),this.shakeMag=Math.min(E,this.shakeMag+3);break;case"KO":this.hitStopFrames=Math.max(this.hitStopFrames,14),this.shakeMag=E,this.flash=1;break;case"FEINT_BAITED":this.flash=Math.max(this.flash,.25);break}}tick(){this.shakeMag*=ye,this.shakeMag<.3&&(this.shakeMag=0),this.flash*=.82,this.flash<.02&&(this.flash=0),this.hurt*=.9,this.hurt<.02&&(this.hurt=0)}view(){const e=Math.random()*Math.PI*2;return{shakeX:Math.cos(e)*this.shakeMag,shakeY:Math.sin(e)*this.shakeMag*.6,flash:this.flash,hurt:this.hurt}}}class we{pool=[];spawnSparks(e,t,r,a=14,i=7){for(let n=0;n<a;n++){const l=Math.random()*Math.PI*2,o=i*(.4+Math.random()*.8);this.pool.push({x:e,y:t,vx:Math.cos(l)*o,vy:Math.sin(l)*o-2,life:1,decay:.03+Math.random()*.03,size:2+Math.random()*3,color:r,kind:"spark"})}}spawnRing(e,t,r){this.pool.push({x:e,y:t,vx:0,vy:0,life:1,decay:.045,size:6,color:r,kind:"ring"})}spawnDust(e,t,r){for(let a=0;a<6;a++)this.pool.push({x:e-r*Math.random()*14,y:t-Math.random()*6,vx:-r*(1+Math.random()*2),vy:-Math.random()*1.2,life:.7,decay:.04,size:2+Math.random()*2,color:"rgba(160,160,170,0.5)",kind:"dust"})}update(){for(let e=this.pool.length-1;e>=0;e--){const t=this.pool[e];if(t.life-=t.decay,t.life<=0){this.pool.splice(e,1);continue}t.x+=t.vx,t.y+=t.vy,t.kind==="spark"&&(t.vy+=.35),t.kind==="ring"&&(t.size+=3.5)}}draw(e){for(const t of this.pool)e.globalAlpha=Math.max(0,t.life),t.kind==="ring"?(e.strokeStyle=t.color,e.lineWidth=2.5,e.beginPath(),e.arc(t.x,t.y,t.size,0,Math.PI*2),e.stroke()):(e.fillStyle=t.color,e.fillRect(t.x-t.size/2,t.y-t.size/2,t.size,t.size));e.globalAlpha=1}}class ve{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(e,t,r,a="bandpass"){if(!this.ctx||!this.master||this.muted)return;const i=this.ctx,n=i.createBuffer(1,i.sampleRate*e,i.sampleRate),l=n.getChannelData(0);for(let d=0;d<l.length;d++)l[d]=Math.random()*2-1;const o=i.createBufferSource();o.buffer=n;const c=i.createBiquadFilter();c.type=a,c.frequency.value=t,c.Q.value=1.2;const h=i.createGain();h.gain.setValueAtTime(r,i.currentTime),h.gain.exponentialRampToValueAtTime(1e-4,i.currentTime+e),o.connect(c).connect(h).connect(this.master),o.start()}tone(e,t,r,a="sine",i){if(!this.ctx||!this.master||this.muted)return;const n=this.ctx,l=n.createOscillator();l.type=a,l.frequency.setValueAtTime(e,n.currentTime),i&&l.frequency.exponentialRampToValueAtTime(i,n.currentTime+t);const o=n.createGain();o.gain.setValueAtTime(r,n.currentTime),o.gain.exponentialRampToValueAtTime(1e-4,n.currentTime+t),l.connect(o).connect(this.master),l.start(),l.stop(n.currentTime+t)}onEvents(e){for(const t of e)switch(t.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 Te(s){switch(s.phase){case"NEUTRAL":return{body:"#cfd8e3",accent:"#8fa1b8",glow:0,label:""};case"STARTUP":{if(s.verb==="PARRY")return{body:"#bfe8f5",accent:"#43c6e8",glow:.5,label:""};const e=Math.max(0,1-s.framesLeft/12);return{body:"#ffd9a8",accent:`rgba(255,140,40,${.5+.5*e})`,glow:.3+.6*e,label:""}}case"ACTIVE":return s.verb==="PARRY"?{body:"#cfffff",accent:"#19e3ff",glow:1,label:""}:{body:"#ffb3a8",accent:"#ff3b2f",glow:1,label:""};case"RECOVERY":return{body:"#9aa7bd",accent:"#5b6a82",glow:.1,label:""};case"BLOCKSTUN":return{body:"#b7c4e0",accent:"#6e87c8",glow:.25,label:""};case"STAGGER":return{body:"#d8b6f0",accent:"#a05ce8",glow:.4,label:""}}}const D=120;function q(s,e,t,r,a,i){const n=Te(a),l=a.phase==="NEUTRAL"?Math.sin(i*.12)*2.2:0,o=t-D+14+l,c=o+12,h=t-D*.42+l;let d=0,p=[26,-18],f=[-14,-10],u=[16,0],w=[-16,0];switch(a.phase){case"STARTUP":a.verb==="PARRY"?(p=[18,-26],f=[14,-22]):(d=-6,p=[-20,-24],f=[-10,-6]);break;case"ACTIVE":a.verb==="PARRY"?(p=[22,-30],f=[18,-24]):(d=14,p=[44,-14],f=[-18,4],u=[26,0],w=[-22,0]);break;case"RECOVERY":d=6,p=[18,16],f=[-10,8];break;case"BLOCKSTUN":d=-10,p=[16,-24],f=[12,-18];break;case"STAGGER":d=-18,p=[-22,-26],f=[10,-28],u=[22,0],w=[-26,0];break}const g=ae=>e+r*(ae+d),m=c+6;s.save(),s.lineWidth=5,s.lineCap="round",s.strokeStyle=n.body,n.glow>0&&(s.shadowColor=n.accent,s.shadowBlur=18*n.glow),s.beginPath(),s.moveTo(g(0),h),s.lineTo(g(u[0]),t),s.moveTo(g(0),h),s.lineTo(g(w[0]),t),s.stroke(),s.beginPath(),s.moveTo(g(0),h),s.lineTo(e+r*d*1.2,c),s.stroke(),s.beginPath(),s.moveTo(e+r*d*1.2,m),s.lineTo(g(f[0]),m+f[1]*-1*0+f[1]),s.stroke(),s.strokeStyle=a.phase==="STARTUP"||a.phase==="ACTIVE"?n.accent:n.body,s.beginPath(),s.moveTo(e+r*d*1.2,m),s.lineTo(g(p[0]),m+p[1]),s.stroke(),s.strokeStyle=n.body,s.beginPath(),s.arc(e+r*d*1.4,o,10,0,Math.PI*2),s.stroke(),a.verb==="PARRY"&&(a.phase==="STARTUP"||a.phase==="ACTIVE")&&(s.strokeStyle=n.accent,s.lineWidth=3,s.beginPath(),s.arc(e+r*26,m-6,24,-Math.PI/2.2,Math.PI/2.2),s.stroke()),s.restore()}class Ae{constructor(e){this.canvas=e,this.ctx=e.getContext("2d"),this.resize(),window.addEventListener("resize",()=>this.resize())}ctx;w=0;h=0;dpr=1;displayedHp=[_,_];camX=0;frames=0;fps=0;lastFpsAt=performance.now();resize(){this.dpr=Math.min(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`}sx(e){const r=e/256,a=P/256;return 70+r/a*(this.w-140)-this.camX}groundY(){return this.h*.72}playerPx(e,t){return{x:this.sx(e.players[t].x),y:this.groundY()-60}}draw(e,t,r,a){const{ctx:i}=this;i.save(),i.scale(this.dpr,this.dpr);const n=(this.sx(e.players[0].x)+this.sx(e.players[1].x))/2+this.camX;this.camX+=(n-this.w/2)*.03,this.camX=Math.max(-40,Math.min(40,this.camX));const l=i.createLinearGradient(0,0,0,this.h);l.addColorStop(0,"#0b0e14"),l.addColorStop(.75,"#121826"),l.addColorStop(1,"#0d111a"),i.fillStyle=l,i.fillRect(0,0,this.w,this.h),i.save(),i.translate(t.shakeX,t.shakeY);const o=this.groundY();i.strokeStyle="#2a3346",i.lineWidth=2,i.beginPath(),i.moveTo(0,o),i.lineTo(this.w,o),i.stroke(),i.strokeStyle="#1d2536",i.beginPath(),i.moveTo(this.sx(P/2),o),i.lineTo(this.sx(P/2),o+10),i.stroke();const c=this.sx(e.players[0].x),h=this.sx(e.players[1].x);q(i,c,o,c<=h?1:-1,e.players[0],e.tick),q(i,h,o,h<c?1:-1,e.players[1],e.tick),r.draw(i),i.restore();for(const p of[0,1]){this.displayedHp[p]+=(e.players[p].hp-this.displayedHp[p])*.15;const f=this.w*.36,u=p===0?24:this.w-24-f;i.fillStyle="#1a2233",i.fillRect(u,22,f,14);const w=Math.max(0,this.displayedHp[p]/_);i.fillStyle=p===0?"#7fd1b9":"#e0857f";const g=f*w;i.fillRect(p===0?u+f-g:u,22,g,14),i.strokeStyle="#3b4860",i.strokeRect(u,22,f,14);for(let m=0;m<2;m++)i.beginPath(),i.arc(p===0?u+10+m*18:u+f-10-m*18,52,5,0,Math.PI*2),i.fillStyle=m<e.roundWins[p]?p===0?"#7fd1b9":"#e0857f":"#243049",i.fill()}i.fillStyle="#8fa1b8",i.font="12px ui-monospace, monospace",i.textAlign="left",i.fillText("YOU",24,16),i.textAlign="right",i.fillText(a.botName,this.w-24,16),i.textAlign="center",i.fillStyle="#cfd8e3",i.font="bold 22px ui-monospace, monospace",i.fillText(`${Math.max(0,Math.ceil(e.timer/$))}`,this.w/2,40),i.font="11px ui-monospace, monospace",i.fillStyle="#5b6a82",i.textAlign="right",i.fillText(a.brainNote,this.w-16,this.h-34),i.textAlign="left",i.fillText("←→ move · J strike · K feint · L parry · 1-5 bot · Enter rematch · M mute",16,this.h-16),this.frames++;const d=performance.now();if(d-this.lastFpsAt>=1e3&&(this.fps=this.frames,this.frames=0,this.lastFpsAt=d),i.textAlign="right",i.fillStyle=this.fps>=55?"#5b6a82":"#bf616a",i.fillText(`${this.fps} fps`,this.w-16,this.h-16),a.msg&&(i.textAlign="center",i.fillStyle="#e6edf5",i.font="bold 54px ui-monospace, monospace",i.fillText(a.msg,this.w/2,this.h*.38),a.sub&&(i.font="16px ui-monospace, monospace",i.fillStyle="#8fa1b8",i.fillText(a.sub,this.w/2,this.h*.38+34))),t.flash>0&&(i.fillStyle=`rgba(255,255,255,${t.flash*.5})`,i.fillRect(0,0,this.w,this.h)),t.hurt>0){const p=i.createRadialGradient(this.w/2,this.h/2,this.h*.3,this.w/2,this.h/2,this.h*.75);p.addColorStop(0,"rgba(255,40,40,0)"),p.addColorStop(1,`rgba(255,40,40,${t.hurt*.35})`),i.fillStyle=p,i.fillRect(0,0,this.w,this.h)}i.restore()}}function Se(s){const e=s+1831565813|0;let t=e;return t=Math.imul(t^t>>>15,t|1),t=t+Math.imul(t^t>>>7,t|61)^t,{value:(t^t>>>14)>>>0,state:e>>>0}}function x(s){return Q(s.players[1].x-s.players[0].x|0)}function M(s,e){const t=1-e;return s.players[t].x>s.players[e].x?"MOVE_R":"MOVE_L"}function ie(s,e){const t=1-e;return s.players[t].x>s.players[e].x?"MOVE_L":"MOVE_R"}class S{state;constructor(e){this.state=e>>>0}f(){const e=Se(this.state);return this.state=e.state,e.value/4294967296}}function Re(s,e={}){const t=new S(s),r=e.range??A,a=e.pStrike??.55;return{name:`aggressive${e.pStrike!==void 0||e.range!==void 0?"_v2":"_v1"}`,next(i,n){const l=i.players[1-n],o=x(i);if((l.phase==="STAGGER"||l.phase==="RECOVERY")&&o<=A)return"STRIKE";if(o>r)return M(i,n);const c=t.f();return c<a?"STRIKE":c<a+.1?"PARRY":"WAIT"}}}function Ee(s,e={}){const t=new S(s),r=e.prefer??V(72),a=e.pParry??.3;return{name:`defensive${e.prefer!==void 0||e.pParry!==void 0?"_v2":"_v1"}`,next(i,n){const l=i.players[1-n],o=x(i);return(l.phase==="RECOVERY"||l.phase==="STAGGER")&&o<=A?"STRIKE":l.phase==="STARTUP"&&o<=A&&t.f()<a?"PARRY":o<r?ie(i,n):o>r+V(36)?M(i,n):"WAIT"}}}function xe(s,e={}){const t=new S(s),r=e.pFeint??.4;return{name:`baiter${e.pFeint!==void 0?"_v2":"_v1"}`,next(a,i){const n=a.players[1-i],l=x(a);if((n.phase==="RECOVERY"||n.phase==="STAGGER")&&l<=A)return"STRIKE";if(l<=ne){const o=t.f();return o<r?"FEINT":o<r+.3?ie(a,i):o<r+.45?"STRIKE":"WAIT"}return M(a,i)}}}function Me(s,e={}){const t=new S(s),r=e.delay??45,a=e.noise??.1,i=[];let n="NEUTRAL";const l=["STRIKE","FEINT","PARRY","MOVE_L","MOVE_R","WAIT"];return{name:"mirror",next(o,c){const h=o.players[1-c];if(h.phase==="STARTUP"&&n!=="STARTUP"&&h.verb!=="NONE"&&i.push({at:o.tick+r,verb:h.verb}),n=h.phase,t.f()<a)return l[t.f()*6|0];const d=i[0];return d&&d.at<=o.tick?(i.shift(),d.verb):x(o)>J?M(o,c):"WAIT"}}}function _e(s){const e=new S(s),t=[{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 r=e.f();for(const a of t){if(r<a.w)return a.v;r-=a.w}return"WAIT"}}}const Pe=`
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:rgba(13,17,26,.82); border:1px solid #2a3346; border-radius:10px; padding:.55rem .8rem; }
5
+ .parry-brain .plan { color:#e8d9a8; font-size:13px; line-height:1.35; min-height:1.2em; }
6
+ .parry-brain .plan .src-override { color:#ff9d7a; }
7
+ .parry-brain .meta { display:flex; justify-content:space-between; gap:1rem; margin-top:.3rem; color:#5b6a82; font-size:11px; }
8
+ .parry-brain .meta b { color:#88c0d0; font-weight:600; }
9
+ .parry-brain .reading { color:#88c0d0; animation: parryPulse 1s infinite; }
10
+ @keyframes parryPulse { 50% { opacity:.35; } }
11
+ .parry-intent { position:absolute; right:18px; top:84px; text-align:right; color:#8fa1b8; font-size:12px; }
12
+ .parry-intent .verb { font-size:26px; color:#cfe8ff; font-weight:700; }
13
+ .parry-meter { height:5px; background:#1a2233; border-radius:3px; overflow:hidden; margin-top:.3rem; }
14
+ .parry-meter > div { height:100%; background:linear-gradient(90deg,#43c6e8,#e8d9a8); width:0%; transition:width .4s; }
15
+ `;class Ie{root;planEl;metaL;metaR;intentEl;meterEl;constructor(e){const t=document.createElement("style");t.textContent=Pe,document.head.appendChild(t),this.root=document.createElement("div"),this.root.className="parry-hud",this.root.innerHTML=`
16
+ <div class="parry-brain">
17
+ <div class="plan"></div>
18
+ <div class="parry-meter"><div></div></div>
19
+ <div class="meta"><span class="l"></span><span class="r"></span></div>
20
+ </div>
21
+ <div class="parry-intent"><div>AI intent</div><div class="verb">·</div></div>`,e.appendChild(this.root),this.planEl=this.root.querySelector(".plan"),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"}render(e){if(!e){this.root.style.display="none";return}this.root.style.display="";const t=e.planSource==="override"?'<span class="src-override">[JUDGE OVERRIDE] </span>':(e.planSource==="neutral","");this.planEl.innerHTML=`${t}🧠 “${Le(e.planString)}”${e.analystBusy?' <span class="reading">· reading you…</span>':""}`,this.meterEl.style.width=`${Math.round(e.prediction.confidence*100)}%`,this.metaL.innerHTML=`read: <b>${e.prediction.predictedHabit}</b> · conf ${(e.prediction.confidence*100).toFixed(0)}%`;const r=e.backend==="localWebGPU"?"0ms network · local":e.backend==="server"?"server fallback":"—";this.metaR.innerHTML=`<b>${r}</b> · ${e.medianOpMs.toFixed(0)}ms/decision · ${e.modelId.split("-q4")[0]}`;const a=e.lastTick?.intent??null;this.intentEl.textContent=a&&a!=="CONTINUE"?ee[a]??"·":"·"}}function Le(s){return s.replace(/[&<>"']/g,e=>`&#${e.charCodeAt(0)};`)}const k={ms:180};function H(){return Math.max(1,Math.round(k.ms/1e3*$))}const Oe=64;class Ce{frames=[];push(e,t,r){this.frames.push({tick:e,state:t,events:r}),this.frames.length>Oe&&this.frames.shift()}delayed(e){if(this.frames.length===0)return null;const t=e-H();for(let r=this.frames.length-1;r>=0;r--)if(this.frames[r].tick<=t)return this.frames[r];return this.frames[0]}delayedWindow(e,t){const r=e-H();return this.frames.filter(a=>a.tick<=r&&a.tick>r-t)}clear(){this.frames=[]}}const Ne="funnel_v1",$e="trace_v1",Be=Math.random().toString(36).slice(2,10);function K(){return typeof window<"u"&&window.__PARRY_API_BASE||""}function b(s,e){try{if(!K()&&/^(localhost|127\.0\.0\.1)$/.test(location.hostname))return;const t={v:Ne,event:s,session:Be,ts:Date.now(),...e?{props:e}:{}},r=new Blob([JSON.stringify(t)],{type:"application/json"});navigator.sendBeacon(`${K()}/funnel`,r)}catch{}}function Ue(s,e){return{v:$e,createdAt:new Date().toISOString(),modelId:e.modelId,backend:e.backend,engineVersion:oe,promptVersion:pe,grammarId:O,seed:e.seed,finalScore:e.finalScore,rows:s}}function Fe(s){const e=new Blob([JSON.stringify(s,null,2)],{type:"application/json"}),t=document.createElement("a");t.href=URL.createObjectURL(e),t.download=`parry-brain-trace-${s.seed}-${Date.now()}.json`,t.click(),URL.revokeObjectURL(t.href),b("trace_exported")}const Ve=`
22
+ .parry-debug { position:absolute; right:16px; top:120px; width:300px; background:rgba(13,17,26,.95);
23
+ border:1px solid #3b4860; border-radius:10px; padding: .8rem; font-family:ui-monospace,monospace;
24
+ color:#cfd8e3; font-size:12px; pointer-events:auto; z-index:10; }
25
+ .parry-debug h3 { margin:.1rem 0 .5rem; font-size:12px; color:#88c0d0; letter-spacing:.06em; }
26
+ .parry-debug textarea { width:100%; box-sizing:border-box; background:#0b0e14; color:#e8d9a8; border:1px solid #2a3346;
27
+ border-radius:6px; font:inherit; padding:.4rem; resize:vertical; min-height:54px; }
28
+ .parry-debug .row { display:flex; gap:.4rem; margin-top:.45rem; align-items:center; }
29
+ .parry-debug button { background:#1b222e; color:#cfd8e3; border:1px solid #3b4860; border-radius:6px; padding:.3rem .6rem;
30
+ font:inherit; cursor:pointer; }
31
+ .parry-debug button:hover { border-color:#88c0d0; }
32
+ .parry-debug .hint { color:#5b6a82; margin-top:.4rem; line-height:1.4; }
33
+ .parry-debug input[type=range] { flex:1; }
34
+ .parry-debug .val { color:#88c0d0; min-width:48px; text-align:right; }
35
+ `;class We{constructor(e,t,r){this.getBrain=t,this.getState=r;const a=document.createElement("style");a.textContent=Ve,document.head.appendChild(a),this.root=document.createElement("div"),this.root.className="parry-debug",this.root.style.display="none",this.root.innerHTML=`
36
+ <h3>⚙ JUDGE PANEL — edit the AI's read</h3>
37
+ <textarea placeholder="Type a new read for the Tactician, e.g.:
38
+ They only parry — punish recovery with raw strikes."></textarea>
39
+ <div class="row">
40
+ <button data-act="apply">Apply override</button>
41
+ <button data-act="clear">Clear</button>
42
+ <button data-act="trace">Export trace</button>
43
+ </div>
44
+ <div class="row">
45
+ <span>reaction delay</span>
46
+ <input type="range" min="150" max="250" step="10" value="${k.ms}" />
47
+ <span class="val">${k.ms}ms</span>
48
+ </div>
49
+ <div class="hint">The plan-string you type is injected into the Tactician's prompt verbatim —
50
+ the SAME path the Analyst writes through. If behavior flips, the coupling is real.</div>
51
+ <div class="hint info"></div>`,e.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 n=this.getBrain();n&&(n.setPlanOverride(this.overrideBox.value,this.getState().tick),b("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 n=this.getBrain();if(!n)return;const l=this.getState();Fe(Ue(n.exportTraceRows(),{modelId:n.telemetry().modelId,backend:n.telemetry().backend,seed:l.seed,finalScore:{player:l.roundWins[0],ai:l.roundWins[1]}}))});const i=this.root.querySelector('input[type="range"]');i.addEventListener("input",()=>{k.ms=Number(i.value),this.delayVal.textContent=`${k.ms}ms`})}root;visible=!1;overrideBox;delayVal;info;toggle(){if(this.visible=!this.visible,this.root.style.display=this.visible?"":"none",this.visible){const e=this.getBrain();this.info.textContent=e?`backend ${e.telemetry().backend} · ${e.telemetry().modelId} · seed ${this.getState().seed}`:"LLM brain not installed yet (still loading, or ?nollm=1)"}}isOpen(){return this.visible}}const Ye=6;class De{constructor(e){this.bot=e,this.name=e.name}name;staged=null;request(e){Z(e,1)&&(this.staged=this.bot.next(e,1))}consume(){const e=this.staged;return this.staged=null,e}note(){return"opponent: scripted bot (LLM brain lands in M4) · sim 60Hz · brain 10Hz"}}const G=1e3/$;class qe{state;input=new ge;fx=new ke;particles=new we;sfx=new ve;renderer;opponent;acc=0;last=performance.now();msg="";sub="";msgTtl=0;seedCounter=1;hud;debugPanel;maybeBrain=null;firstPlayableSent=!1;constructor(e,t){this.renderer=new Ae(e),this.state=W(this.nextSeed()),this.opponent=this.makeBot(3),this.input.attach(window),this.hud=new Ie(t),this.debugPanel=new We(t,()=>this.maybeBrain,()=>this.state),window.addEventListener("keydown",()=>this.sfx.unlock(),{once:!0}),requestAnimationFrame(r=>this.frame(r))}setOpponent(e){this.opponent=e,this.maybeBrain="telemetry"in e&&"setPlanOverride"in e?e:null}nextSeed(){return(Date.now()^this.seedCounter++<<16)>>>0}makeBot(e){const t=1e3+e,r=e===1?Re(t):e===2?Ee(t):e===3?xe(t):e===4?Me(t):_e(t);return new De(r)}setMsg(e,t="",r=70){this.msg=e,this.sub=t,this.msgTtl=r}simTick(){if(this.fx.hitStopFrames>0){this.fx.hitStopFrames--;return}const e=this.input.poll(),t=this.opponent.consume(),r=le(this.state,e,t),a=this.state;if(this.state=r.state,this.opponent.observe?.(this.state,r.events),r.events.length){this.fx.onEvents(r.events),this.sfx.onEvents(r.events);for(const i of r.events){const n=this.renderer.playerPx(this.state,0),l=this.renderer.playerPx(this.state,1),o={x:(n.x+l.x)/2,y:(n.y+l.y)/2};switch(i.t){case"HIT":this.particles.spawnSparks(o.x,o.y,"#ff5d52",18,8);break;case"BLOCK":this.particles.spawnSparks(o.x,o.y,"#7fa6e8",8,5);break;case"PARRY_SUCCESS":this.particles.spawnRing(o.x,o.y-10,"#19e3ff"),this.particles.spawnSparks(o.x,o.y,"#bfffff",12,6),this.setMsg("PARRY!","",30);break;case"FEINT_BAITED":this.particles.spawnRing(i.by===1?l.x:n.x,o.y-30,"#ffd27f"),this.setMsg("BAITED!","",26);break;case"WHIFF":this.particles.spawnDust(i.by===0?n.x:l.x,(i.by===0?n.y:l.y)+50,i.by===0?1:-1);break;case"KO":this.particles.spawnSparks(o.x,o.y,"#ffffff",30,10),this.setMsg("K.O.","",80);break;case"ROUND_START":this.setMsg(`ROUND ${i.round}`,"fight!",60),i.round===1&&b("match_started",{opponent:this.opponent.name});break;case"MATCH_OVER":this.setMsg(i.winner===0?"YOU WIN":"AI WINS","Enter — rematch · 1-5 — switch opponent",1e5),b("match_completed",{winner:i.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&&a.roundState!=="MATCH_OVER"&&(this.msg="",this.sub="")}handleUiKeys(){for(const e of this.input.drainUi())if(e==="Enter")this.state=W(this.nextSeed()),this.msg="",this.sub="";else if(e.startsWith("Digit")){const t=Number(e.slice(5));t>=1&&t<=5&&(this.opponent=this.makeBot(t),this.setMsg(this.opponent.name.toUpperCase(),"opponent switched",50))}else e==="KeyM"?this.sfx.muted=!this.sfx.muted:e==="KeyB"&&this.debugPanel.toggle()}frame(e){for(this.acc+=Math.min(e-this.last,250),this.last=e;this.acc>=G;)this.acc-=G,this.simTick();this.handleUiKeys(),this.fx.tick(),this.particles.update();const t={msg:this.msg,sub:this.sub,botName:this.opponent.name.toUpperCase(),brainNote:this.opponent.note()};this.renderer.draw(this.state,this.fx.view(),this.particles,t),this.hud.render(this.maybeBrain?this.maybeBrain.telemetry():null),this.firstPlayableSent||(this.firstPlayableSent=!0,b("first_playable")),requestAnimationFrame(r=>this.frame(r))}}function He(s,e){return new qe(s,e)}const N={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"},y={model_id:N.tuned,model:"https://huggingface.co/Jainamshahhh/parry-tactician-1.5b-q4f16_1-MLC",baseForLib:N.hero15,vram_required_MB:1700,low_resource_required:!0};class X{engine=null;modelId="";grammarSupported=!0;logprobsAvailable=!1;logprobsProbed=!1;async init(e,t){this.modelId=N[e];const r={initProgressCallback:a=>t({progress:a.progress,text:a.text})};if(e==="tuned"){const a=R.model_list.find(i=>i.model_id===y.baseForLib);if(!a)throw new Error(`base record ${y.baseForLib} missing from prebuilt list`);r.appConfig={...R,model_list:[...R.model_list,{model:y.model,model_id:y.model_id,model_lib:a.model_lib,vram_required_MB:y.vram_required_MB,low_resource_required:y.low_resource_required}]}}else R.model_list.some(a=>a.model_id===this.modelId)||console.warn(`[webllm] ${this.modelId} not in prebuilt list — check @mlc-ai/web-llm version pin`);this.engine=await de(new Worker(new URL(""+new URL("llm.worker-GbIGcnxV.js",import.meta.url).href,import.meta.url),{type:"module"}),this.modelId,r)}req(e,t,r,a){const i={messages:[{role:"system",content:e},{role:"user",content:t}],temperature:1,...a};return r&&this.grammarSupported&&(i.response_format={type:"grammar",grammar:r}),i}async tickOp(e,t,r,a){if(!this.engine)throw new Error("backend not initialized");const i=performance.now(),n=!1;let l;try{l=await this.engine.chatCompletion(this.req(e,t,r,{stream:!1,max_tokens:1,seed:a,...n?{logprobs:!0,top_logprobs:5}:{}}))}catch(f){const u=String(f);if(/logprob/i.test(u))console.warn(`[webllm] logprobs rejected → disabled (${u.slice(0,100)})`),this.logprobsProbed=!0,this.logprobsAvailable=!1,l=await this.engine.chatCompletion(this.req(e,t,r,{stream:!1,max_tokens:2,seed:a}));else if(this.grammarSupported)console.warn(`[webllm] grammar response_format failed → unconstrained + post-map (${u.slice(0,120)})`),this.grammarSupported=!1,l=await this.engine.chatCompletion(this.req(e,t,null,{stream:!1,max_tokens:2,seed:a}));else throw f}const o=performance.now()-i,c=l.choices[0];this.logprobsProbed||(this.logprobsProbed=!0,this.logprobsAvailable=!!c?.logprobs?.content?.length);let h;const d=c?.logprobs?.content?.[0]?.top_logprobs;if(d?.length){h=[];for(const f of d){const u=te(f.token??"");u&&h.push({verb:u,p:Math.exp(f.logprob??-99)})}}const p={ch:c?.message?.content??"",ms:o,promptTokens:l.usage?.prompt_tokens??null};return h&&h.length&&(p.topk=h),p}async generateOp(e,t,r,a){if(!this.engine)throw new Error("backend not initialized");const i=performance.now();let n="",l=!1;const o=setTimeout(()=>{l=!0;try{this.engine?.interruptGenerate()}catch{}},a.budgetMs);try{const c=await this.engine.chat.completions.create(this.req(e,t,r,{stream:!0,max_tokens:a.maxTokens,temperature:.8}));for await(const h of c)n+=h.choices[0]?.delta?.content??""}catch(c){if(r&&this.grammarSupported)return this.grammarSupported=!1,console.warn(`[webllm] analyst grammar failed → retry unconstrained (${String(c).slice(0,120)})`),clearTimeout(o),this.generateOp(e,t,null,a);if(!l)throw c}finally{clearTimeout(o)}return{text:n,interrupted:l,ms:performance.now()-i}}caps(){return{label:"localWebGPU",modelId:this.modelId,grammarSupported:this.grammarSupported,logprobsAvailable:this.logprobsAvailable}}dispose(){try{this.engine?.unload()}catch{}this.engine=null}}function I(){return typeof window<"u"&&window.__PARRY_API_BASE||""}class L{tier="hero15";async init(e,t){this.tier=e,t({progress:.5,text:"connecting to server fallback…"});const r=await fetch(`${I()}/healthz`).catch(()=>null);if(!r||!r.ok)throw new Error("server fallback unreachable (no /healthz) — not installing a dead brain");const a=await r.json();t({progress:1,text:`server fallback ready (modal: ${a.modal??"?"})`})}async tickOp(e,t,r,a){const i=performance.now(),n=await fetch(`${I()}/infer`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({system:e,user:t,prompt:`${e}
52
+ ${t}`,grammar_id:O,seed:a,tier:this.tier})}),l=performance.now()-i;return n.ok?{ch:(await n.json()).token??"",ms:l,promptTokens:null}:{ch:"",ms:l,promptTokens:null}}async generateOp(e,t,r,a){const i=performance.now();try{const n=new AbortController,l=setTimeout(()=>n.abort(),a.budgetMs+1500),o=await fetch(`${I()}/analyst`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({system:e,user:t,behaviorLog:t,grammar_id:O,max_tokens:a.maxTokens}),signal:n.signal});if(clearTimeout(l),!o.ok)return{text:"",interrupted:!0,ms:performance.now()-i};const c=await o.json();return{text:c.text??c.planString??"",interrupted:!1,ms:performance.now()-i}}catch{return{text:"",interrupted:!0,ms:performance.now()-i}}}caps(){return{label:"server",modelId:`server:${this.tier}`,grammarSupported:!0,logprobsAvailable:!1}}dispose(){}}function Ke(){const s=F.replace("{plan}",C).replace("{me}","NEUTRAL").replace("{you}","NEUTRAL").replace("{dist}","MID").replace("{tw}","L").replace("{adv}","+0").replace("{recent}","WWWWWWWW").replace("{meRecent}","......");return{system:U,user:s}}async function z(s,e){const{system:t,user:r}=Ke(),a=B("NEUTRAL");e?.("warming shaders (first decode)…");const i=await s.tickOp(t,r,a,1);e?.("warming long-generation path…");const n=await s.generateOp(t,"Say 'ready' and stop.",null,{maxTokens:8,budgetMs:4e3});e?.("settling…");const l=[];for(let o=0;o<3;o++){const c=await s.tickOp(t,r,a,2+o);l.push(Math.round(c.ms*10)/10)}return{coldFirstOpMs:Math.round(i.ms),settledOpsMs:l,generateMs:Math.round(n.ms)}}function Ge(s){const e=[...s].sort((r,a)=>r-a),t=e.length>>1;return e.length%2?e[t]:(e[t-1]+e[t])/2}function Xe(){const s=F.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:U,user:s}}async function j(s,e=10){const{system:t,user:r}=Xe(),a=B("NEUTRAL"),i=[];for(let n=0;n<e;n++){const l=await s.tickOp(t,r,a,100+n);i.push(l.ms)}return Ge(i)}function ze(){if(typeof location>"u")return{};const s=new URLSearchParams(location.search),e={},t=s.get("backend");t&&(e.backend=t);const r=s.get("tier");return r&&["hero15","low1","fast05","beefy3","tuned"].includes(r)&&(e.tier=r),e}async function je(s){const e=ze();if(e.backend==="server"||!("gpu"in navigator)){const n=new L;return await n.init(e.tier??"hero15",s),{backend:n,tier:e.tier??"hero15",medianOpMs:-1,reason:e.backend==="server"?"forced via ?backend=server":"WebGPU unavailable in this browser"}}try{if(!await navigator.gpu.requestAdapter())throw new Error("null adapter")}catch{const n=new L;return await n.init("hero15",s),{backend:n,tier:"hero15",medianOpMs:-1,reason:"WebGPU adapter unavailable"}}let t=e.tier??"hero15",r=new X;await r.init(t,s),await z(r,n=>s({progress:.97,text:n}));let a=await j(r);if(console.log(`[probe] ${t} warm median ${a.toFixed(1)}ms (caps: ${JSON.stringify(r.caps())})`),a>250&&!e.tier&&(s({progress:.5,text:`median ${a.toFixed(0)}ms — downshifting to 1B…`}),r.dispose(),r=new X,t="low1",await r.init(t,s),await z(r),a=await j(r),console.log(`[probe] ${t} warm median ${a.toFixed(1)}ms`)),a>400){s({progress:.9,text:`local slow (${a.toFixed(0)}ms) — checking server fallback…`});try{const n=new L;return await n.init("hero15",s),r.dispose(),{backend:n,tier:"hero15",medianOpMs:a,reason:`local median ${a.toFixed(0)}ms > 400ms → server`}}catch(n){return console.warn(`[probe] server fallback unavailable (${String(n).slice(0,80)}) — keeping slow local`),{backend:r,tier:t,medianOpMs:a,reason:`local ${a.toFixed(0)}ms/decision (slow tick — AI acts less often, still fair)`}}}const i=`local median ${a.toFixed(0)}ms/decision — ${t} tier`;return{backend:r,tier:t,medianOpMs:a,reason:i}}class Je{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(e){const t=e.state.players[0],r=e.state.players[1];if(e.tick-this.lastSampleTick>=6){this.lastSampleTick=e.tick;let a="W";t.phase==="STARTUP"||t.phase==="ACTIVE"?a=t.verb==="PARRY"?"P":"S":t.phase==="RECOVERY"&&t.verb==="FEINT"?a="F":t.x!==this.prevPlayerX&&(a=t.x>this.prevPlayerX?"R":"L"),this.ring.push(a),this.ring.length>8&&this.ring.shift()}this.prevPhase==="NEUTRAL"&&t.phase==="STARTUP"&&(t.verb==="PARRY"&&(this.parries++,r.x!==this.prevAiX&&Math.abs(r.x-t.x)<Math.abs(this.prevAiX-t.x)&&this.parriesOnApproach++),(t.verb==="STRIKE"||t.verb==="FEINT")&&(this.strikes++,this.playerWasStaggered&&e.tick-this.staggerEndTick<=18&&this.wakeupStrikes++)),this.prevPhase==="STARTUP"&&t.phase==="RECOVERY"&&t.verb==="FEINT"&&(this.strikes=Math.max(0,this.strikes-1),this.feints++),t.phase==="STAGGER"&&(this.playerWasStaggered=!0),this.prevPhase==="STAGGER"&&t.phase==="NEUTRAL"&&(this.staggerEndTick=e.tick,this.wakeups++),Math.abs(r.x-t.x)<Math.abs(this.prevAiX-t.x)&&this.approaches++,this.observedTicks++,t.phase==="NEUTRAL"&&t.x!==this.prevPlayerX&&t.x>this.prevPlayerX==t.x>r.x&&this.retreatTicks++,this.prevPhase=t.phase,this.prevAiX=r.x,this.prevPlayerX=t.x}prevPlayerX=0;recentChars(){return this.ring.join("").padStart(8,".")}detect(){const e=this.strikes+this.feints+this.parries;return e<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,e)>=.7?{habit:"SPAM_STRIKE",confidence:.7,line:`${this.strikes} strikes of ${e} 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"}}behaviorLog(){const e=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: ${e.habit} (${e.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 Qe{plan=C;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(e,t,r){this.plan=e,this.prediction=t,this.source="analyst",this.override===null&&(this.changedAtTick=r)}setOverride(e,t){this.override=e&&e.trim().length?e.trim():null,this.changedAtTick=t}hasOverride(){return this.override!==null}reset(e){this.plan=C,this.source="neutral",this.override=null,this.prediction={predictedHabit:"NONE",confidence:.2},this.changedAtTick=e}}function Ze(s){if(s.phase==="NEUTRAL")return"NEUTRAL";const e=s.verb==="NONE"?s.phase:s.verb;return`${s.phase==="STARTUP"||s.phase==="ACTIVE"?e+" ":""}${s.phase} ${s.framesLeft}f`.trim()}function et(s){switch(s.phase){case"NEUTRAL":return"NEUTRAL";case"STARTUP":return s.verb==="PARRY"?`GUARD ${s.framesLeft}f`:`WINDUP ${s.framesLeft}f`;case"ACTIVE":return s.verb==="PARRY"?`PARRY-ACTIVE ${s.framesLeft}f`:`ATTACK ${s.framesLeft}f`;case"RECOVERY":return`RECOVERY ${s.framesLeft}f`;case"BLOCKSTUN":return`BLOCKED ${s.framesLeft}f`;case"STAGGER":return`DOWN ${s.framesLeft}f`}}function tt(s){const e=Q(s.players[1].x-s.players[0].x|0);return e<=he?"CLOSE":e<=J?"MID":"FAR"}function st(s){const e=t=>t.phase==="NEUTRAL"?0:t.framesLeft;return e(s.players[0])-e(s.players[1])}function it(s,e,t,r,a){const i=ce(e,1).legal,n=(i.length?i:["WAIT"]).map(h=>ee[h]).join(""),l=st(s),o=s.players[0].x<s.players[1].x?"L":"R",c=F.replace("{plan}",t.slice(0,fe)).replace("{me}",Ze(s.players[1])).replace("{you}",et(s.players[0])).replace("{dist}",tt(s)).replace("{tw}",o).replace("{adv}",`${l>=0?"+":""}${l}`).replace("{recent}",r).replace("{meRecent}",a||"......");return{system:U,user:c,stateDigest:ue(JSON.stringify(s)),legalChars:n}}const rt=`You are the Analyst half of a dueling AI in PARRY (strike beats idle, parry beats strike, feint baits parry).
53
+ Read the opponent report. Output EXACTLY: <plan for the Tactician, imperative, MAX 12 words> | <HABIT> | <confidence 0-9>.
54
+ HABIT is one of: ${se.join(", ")}. Be specific and a little cocky.`;function at(s){const e=s.split("|").map(i=>i.trim());if(e.length<1||!e[0])return null;const t=e[0].slice(0,140);let r="NONE";if(e[1]){const i=e[1].toUpperCase().replace(/[^A-Z_]/g,"");se.includes(i)&&(r=i)}let a=.5;if(e[2]){const i=parseInt(e[2].charAt(0),10);Number.isNaN(i)||(a=Math.min(1,Math.max(0,i/9)))}return{planString:t,prediction:{predictedHabit:r,confidence:a}}}async function nt(s,e,t=1500){const r=await s.generateOp(rt,`Opponent report: ${e}
55
+ Your read:`,me,{maxTokens:32,budgetMs:t}),a=at(r.text);return a?{...a,raw:r.text,interrupted:r.interrupted,ms:r.ms}:null}const ot=30,lt=1500,ct=18;class ht{constructor(e,t){this.backend=e,this.name=t}name;staged=null;busy=!1;seq=0;brainTicks=0;analystBusy=!1;lastAnalystBrainTick=-999;buffer=new Ce;tracker=new Je;plans=new Qe;opsMs=[];lastTick=null;trace=[];safeWindowPending=!1;lastHabit="NONE";meRing=[];stickyVerb=null;stickyLeft=0;recentEvents=[];observe(e,t){this.buffer.push(e.tick,e,t);for(const a of t)this.recentEvents.push(a.t);this.recentEvents.length>12&&this.recentEvents.splice(0,this.recentEvents.length-12);const r=this.buffer.delayed(e.tick);r&&this.tracker.observe(r);for(const a of t)(a.t==="KO"||a.t==="ROUND_OVER"||a.t==="ROUND_START")&&(this.safeWindowPending=!0),a.t==="MATCH_OVER"&&(this.tracker.reset(),this.plans.reset(e.tick),this.buffer.clear())}request(e){if(this.maybeRunAnalyst(e),this.busy||!Z(e,1))return;const t=this.buffer.delayed(e.tick)?.state??e,r=this.meRing.join("").padStart(6,"."),a=it(t,e,this.plans.current(),this.tracker.recentChars(),r),i=B("NEUTRAL"),n=++this.seq;this.busy=!0;const l=(e.seed^e.tick)>>>0;this.backend.tickOp(a.system,a.user,i,l).then(o=>{if(n!==this.seq)return;const c=te(o.ch)??"WAIT";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(o.ms),this.opsMs.length>60&&this.opsMs.shift();const h={simTick:e.tick,opMs:Math.round(o.ms*10)/10,promptTokens:o.promptTokens,backend:this.backend.caps().label,intent:c,stale:!1,...o.topk?{topk:o.topk}:{}};this.lastTick=h,this.trace.push({tick:e.tick,stateDigest:a.stateDigest,planString:this.plans.current(),planSource:this.plans.currentSource(),prediction:this.plans.currentPrediction(),intent:c,opMs:h.opMs,events:this.recentEvents.splice(0)}),this.trace.length>5e3&&this.trace.shift()}).catch(o=>{console.warn(`[brain] tickOp failed: ${String(o).slice(0,140)}`)}).finally(()=>{this.busy=!1}),this.brainTicks++}maybeRunAnalyst(e){if(this.analystBusy)return;const t=this.tracker.detect(),r=t.habit!=="NONE"&&t.habit!==this.lastHabit&&t.confidence>=.6;if(!(this.brainTicks-this.lastAnalystBrainTick>=ot||this.safeWindowPending||r))return;this.safeWindowPending=!1,this.lastAnalystBrainTick=this.brainTicks,this.analystBusy=!0;const i=this.tracker.behaviorLog();nt(this.backend,i,lt).then(n=>{n&&(this.lastHabit=n.prediction.predictedHabit,this.plans.setFromAnalyst(n.planString,n.prediction,e.tick))}).catch(n=>console.warn(`[brain] analyst failed: ${String(n).slice(0,140)}`)).finally(()=>{this.analystBusy=!1})}consume(){const e=this.staged;return this.staged=null,e!==null?(e==="MOVE_L"||e==="MOVE_R"?(this.stickyVerb=e,this.stickyLeft=ct):(this.stickyVerb=null,this.stickyLeft=0),e):this.stickyVerb!==null&&this.stickyLeft>0?(this.stickyLeft--,this.stickyVerb):null}note(){const e=this.backend.caps(),t=this.medianMs();return`${e.label==="localWebGPU"?"0ms network · local GPU":"server fallback"} · ~${t.toFixed(0)}ms/decision · ${e.modelId} · reads you every ~2s`}medianMs(){if(!this.opsMs.length)return 0;const e=[...this.opsMs].sort((t,r)=>t-r);return e[e.length>>1]}telemetry(){const e=[...this.opsMs].sort((t,r)=>t-r);return{backend:this.backend.caps().label,modelId:this.backend.caps().modelId,medianOpMs:e.length?e[e.length>>1]:0,p95OpMs:e.length?e[Math.min(e.length-1,Math.ceil(e.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}}exportTraceRows(){return[...this.trace]}setPlanOverride(e,t){this.plans.setOverride(e,t)}reactionDelayMs(){return k.ms}}const pt=document.getElementById("game"),re=document.getElementById("hud"),dt=He(pt,re),T=document.createElement("div");T.style.cssText="position:absolute;top:64px;left:50%;transform:translateX(-50%);background:#11151dcc;border:1px solid #2a3140;border-radius:8px;padding:.5rem .9rem;font:12px ui-monospace,monospace;color:#88c0d0;max-width:70vw;text-align:center;pointer-events:none;display:none;";re.appendChild(T);function v(s,e=0){T.style.display="",T.textContent=s,e>0&&setTimeout(()=>T.style.display="none",e)}async function ft(){if(b("page_load"),new URLSearchParams(location.search).get("nollm")==="1"){v("LLM disabled (?nollm=1) — scripted opponents only",4e3);return}b("gpu"in navigator?"webgpu_detected":"webgpu_missing"),v("🧠 loading the AI's brain in the background — warm up against the scripted bot…"),b("model_download_started");try{const s=await je(t=>{const r=t.progress>0&&t.progress<=1?` ${(t.progress*100).toFixed(0)}%`:"";v(`🧠 ${t.text}${r}`)});b("model_download_complete"),b("tier_selected",{tier:s.tier,medianMs:Math.round(s.medianOpMs),reason:s.reason}),s.backend.caps().label==="server"&&b("fell_back_to_server");const e=new ht(s.backend,`LLM · ${s.tier}`);dt.setOpponent(e),v(`🧠 LLM opponent live — ${s.reason}. Press B for the judge panel. It is reading you now.`,6e3),window.__parryBrain=e}catch(s){console.error("[parry] brain boot failed:",s),v(`brain failed to load (${String(s).slice(0,90)}) — scripted opponent stays. Try ?backend=server`,8e3)}}ft();console.log("[parry] boot — scripted play immediately; LLM brain installs when ready. B = judge panel.");
static/assets/spike-9jhQYq_1.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import"./action_set-C6mnKwO1.js";import{G as v,f as E,a as S,T as _,N as I,p as G,C as F,A as N,V as B,g as U}from"./index-CqxAeE_s.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 b=0;for await(const x of g)b+=(x.choices[0]?.delta?.content??"").length;return b})();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 $=m("json");$.style.display="block",$.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
+ 633ea0c7
static/index.html ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ /* Minimal shell — the real presentation layer lands in M2 (render/juice/hud). */
10
+ html, body { margin: 0; padding: 0; background: #0b0e14; color: #e6e6e6; height: 100%; overflow: hidden; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
11
+ #stage { position: relative; width: 100vw; height: 100vh; }
12
+ canvas { display: block; width: 100%; height: 100%; }
13
+ #hud { position: absolute; inset: 0; pointer-events: none; }
14
+ </style>
15
+ <script type="module" crossorigin src="./assets/main-C1_lv4Q8.js"></script>
16
+ <link rel="modulepreload" crossorigin href="./assets/action_set-C6mnKwO1.js">
17
+ <link rel="modulepreload" crossorigin href="./assets/engine-BCs5oIKV.js">
18
+ <link rel="modulepreload" crossorigin href="./assets/index-CqxAeE_s.js">
19
+ </head>
20
+ <body>
21
+ <div id="stage">
22
+ <canvas id="game"></canvas>
23
+ <div id="hud"></div>
24
+ </div>
25
+ </body>
26
+ </html>
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-9jhQYq_1.js"></script>
20
+ <link rel="modulepreload" crossorigin href="../assets/action_set-C6mnKwO1.js">
21
+ <link rel="modulepreload" crossorigin href="../assets/index-CqxAeE_s.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-C-3W91Cw.js"></script>
7
+ <link rel="modulepreload" crossorigin href="../assets/action_set-C6mnKwO1.js">
8
+ <link rel="modulepreload" crossorigin href="../assets/engine-BCs5oIKV.js">
9
+ </head>
10
+ <body>
11
+ <h1>Golden-trace browser replay</h1>
12
+ <pre id="out">running…</pre>
13
+ </body>
14
+ </html>