Skip to content
apscotPublic

About

Two fighters, one deadline: a typed-decision model(JEV) races a general LLM in real time. Same state, same clock — whoever answers first acts.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

⚔️ THE DUEL

A typed-decision model vs. a general LLM. Same state. Same deadline. Every beat.

TypeScript Vite Local only Real API calls

The Duel — Jev (cyan) attacking an LLM (magenta) frozen mid-hesitation, with live request/response panels and a moveset legend

Jev decided in 1.0s and landed a clean hit. The LLM was still composing its reasoning when the deadline hit — frozen, exposed, -30%.


What this is

Two silhouette fighters, one fight. Every beat, Fighter A's move comes from a real Jev typed-decision call. Fighter B's move comes from a real general-purpose LLM (Claude Haiku) prompted for prose reasoning and a parsed final move. Both get the exact same state and the exact same deadline. Whoever doesn't answer in time hesitates — visibly frozen, open to a hit.

That gap, measured live on screen, is the entire point. Nothing here is scripted or pre-baked — every number on the HUD comes from a real network call, every beat. Full spec: DUEL_ARCHITECTURE.md.

Local-only by design. This isn't meant to be deployed publicly — clone it, run it locally with your own API key, and record your own footage. This repo is what gets shared, not a hosted URL.

Why Jev wins

Not an assertion — measured, from a real recorded session against the real proxies. Your own numbers will vary (that's the point — the HUD measures live), but the shape of the gap is structural, not a fluke:

🩵 Jev 💗 The LLM (Claude Haiku)
How it decides Typed answer — { choice, score, noul } Prose reasoning, then a parsed MOVE: line
Avg. latency ~1.0 – 1.2s ~2.7 – 3.0s
Missed deadlines (one session) 0 15
Cost (one 15-min session) $0.00227 $0.035 — ~15× more
Tokens used (same session) 61K 25K — fewer tokens, still costlier

The last row is the tell: Jev processed more tokens and still cost 15× less, because a typed answer has no output tokens to pay for. Every word the LLM writes — including the reasoning you never see truncated in the HUD — is billed. Structured output isn't a trick to make the LLM look bad; it's just architecturally cheaper and faster for this kind of decision.

How it works

flowchart TD
  A["Beat clock — ~3.8s cadence"] --> B["Shared combat state"]
  B --> C["Jev proxy — /api/jev<br/>(OpenRouter alpha decisions)"]
  B --> D["LLM proxy — /api/llm<br/>(OpenRouter chat completions)"]
  C -->|"typed answer, ~1.0-1.2s"| E["Deadline gate — 3000ms"]
  D -->|"prose + parsed move, ~2.4-3s+"| E
  E -->|"on time → apply move"| F["Animation state machine<br/>(pure time-based tween)"]
  E -->|"late → hesitate"| F
  F --> G["Canvas render + damage popups"]
  E --> H["Telemetry: latency, timeouts"]
  H --> I["HUD: big latency numbers,<br/>misses counter, wire panels"]
Loading

Both proxies are real serverless functions (api/jev.ts, api/llm.ts) — no mocks, no local fallback decision-maker standing in for either side. If a key isn't configured, both fighters just hesitate, honestly, every beat.

Status

All 6 implementation phases from the architecture doc are in:

  1. Fixed-state combat scene — two silhouettes, a pure time-based animation state machine, no physics (src/combat/stateMachine.ts, src/scene/renderer.ts).
  2. api/jev.ts — real Jev integration via OpenRouter's alpha decisions endpoint.
  3. api/llm.ts — real Claude Haiku integration via OpenRouter's normal chat completions endpoint: prose reasoning, a parsed MOVE: <choice> line, no schema, no shortcut — the LLM does what an LLM normally does.
  4. Telemetry HUD — live latency average and timeout count per side, round tally, a headline "misses from timeout" counter, and per-fighter wire panels showing the literal request/response each beat (src/telemetry/).
  5. Round/match structure: best-of-5, first to 3 round wins, auto-restarts (src/combat/match.ts, src/combat/beatClock.ts).
  6. Local dev via vercel dev, .env-based secrets, bundle checked for leaked keys.

Not done, because there's nothing to do: this project is explicitly not deployed anywhere. There's no hosting-platform step to complete.

Combat rules (not specified by the architecture doc — designed here)

Five moves: attackHigh, attackLow, block, dodge, feint.

Move Effect
attackHigh 30% damage on a clean hit
attackLow 25% damage on a clean hit
block Negates any attack aimed at you · costs 12% stamina
dodge Negates any attack aimed at you · costs 8% stamina
feint No damage, no defense — a bait or a breather, regenerates stamina
hesitate Not a chosen move — the call missed the shared deadline. Exposed exactly like a feint, but forced, not picked
  • A clean hit only lands on an undefended opponent (feinting or hesitating).
  • Two attacks in the same beat trade — both connect.
  • feint provides zero defense — mechanically identical exposure to hesitating, which is what makes bluffing genuinely risky.
  • Stamina drains per move and regenerates passively each beat; attacking while nearly empty halves that attack's damage.
  • A round ends when a fighter's health hits 0. A match is best-of-5 — first fighter to 3 round wins takes the match, then it auto-restarts.

See src/combat/resolver.ts for the exact numbers — the on-screen legend reads its numbers from the same constants, so it can't drift out of sync.

Local development

npm install
npm run dev

Opens the frontend only — /api/jev and /api/llm aren't served, so every beat 404s and both fighters hesitate every time. That's the honest default: there's no local fallback decision-maker standing in for either side, because the entire point of this app is measuring real timing, not looking alive without it.

To exercise the real proxies, with a .env (copied from .env.example) holding a real OPENROUTER_API_KEY:

npm run dev:vercel   # runs `vercel dev` via the local devDependency

First run prompts an interactive Vercel login/link in your own terminal. vercel dev runs everything locally — no deployment happens.

Real API access

Both fighters are reached through one OpenRouter key:

  • Jev — ~typesafe/jev-latest via OpenRouter's alpha /decisions endpoint. (TypeSafe AI isn't taking new direct registrations right now — OpenRouter is the door in.)
  • The LLM — ~anthropic/claude-haiku-latest via OpenRouter's normal /chat/completions endpoint, prompted for one short sentence of reasoning plus a parsed MOVE: line.

Cost note

Each beat is just one Jev call and one Haiku call — cost per session is low, but it's still real API spend for real, unbounded usage. Don't leave it running unattended.

Secrets

Both proxies read OPENROUTER_API_KEY from the environment (api/jev.ts, api/llm.ts) — never referenced from src/, so it can't end up in the client bundle. Verified: npm run build && grep -r OPENROUTER dist/ comes back empty.


Built by Arpan — typed decisions vs. prose reasoning, raced on a clock.

About

Two fighters, one deadline: a typed-decision model(JEV) races a general LLM in real time. Same state, same clock — whoever answers first acts.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages