Jev vs Jev
September 17, 2026 · View on GitHub
Two virtual chess players, both powered by TypeSafe's System One model Jev. No engine, no generated text: each move is a typed decision that the model returns with a full probability distribution, and the code does the rest. Optionally, a Monte Carlo Tree Search uses those same distributions as policy and value, AlphaZero-style, to play noticeably better.

Why this exists
Most "AI plays a game" demos prompt a chat model, parse whatever text comes back and hope it is a legal move. TypeSafe turns that around. You keep the rules, the state and the workflow in ordinary code, and ask the model a narrow, typed question whenever your code needs judgment instead of computation.
That is what makes System One models feel like a new programming primitive:
| What you send | What you get back |
|---|---|
| The current game state as JSON | Nothing you have to parse |
| A Choice question with one option per legal move | The chosen move plus a probability for every option and a confidence value |
| A Score question with ordered levels | A calibrated position evaluation, useful for a UI or a threshold |
| A Noul (yes/no) question | The probability that the position is tactically sharp |
All three questions ride along in one request and are answered in parallel. In fast mode a move takes about 350 ms end to end.
How it works
chess.js (rules) ──► legal moves ──► one Choice option per move ──► Jev ──► move + probabilities
state (board, material, history, style) ─────┘ score + noul
- Code owns the rules.
chess.jsgenerates the legal moves. The model can only ever pick one of them, so an illegal move is impossible by construction. - Code computes the facts. For each candidate move the server works out what a rules engine knows for sure: the piece, captures, checks, castling, promotion, and whether the moved piece can be captured on arrival and whether that square is defended. Those facts become the option's description.
- Jev supplies the judgment. The model sees the board, the material balance, the recent moves, the player's style and the annotated options, and returns a probability distribution over the moves.
- Code acts. The server plays the top choice, records the distribution, score and sharpness for the UI, and hands the turn to the other Jev.
White is told to play actively, Black to play solidly. Same model, two personalities, both expressed as plain text in the state.
MCTS: Jev as policy and value network
The same Jev call that picks a move in fast mode is exactly what an AlphaZero-style search needs
from a neural network: a policy (prior probability for every legal move, from the Choice
answer) and a value (how good the position is for the side to move, from the Score answer,
mapped to −1…+1). mcts.js wires that into a PUCT tree search:
- Every new tree node costs one Jev call. There are no random rollouts. The budget is counted in API calls; transpositions are cached for the whole game.
- Selection follows PUCT:
Q + c · P · √N_parent / (1 + N_child), so Jev's prior steers the search towards its favourite moves while values from deeper positions can overrule it. - Leaves are evaluated concurrently (virtual loss), so 16 evaluations take a few seconds.
- The final move is ranked by Q among candidates with comparable coverage, because with a small budget visit counts mostly mirror the prior.
- The UI shows visits, prior and Q for the top candidates and says whether the search confirmed or overruled Jev's first instinct.
What the first benchmark taught us. The initial version lost to the single-call player.
Dissecting a lost position with scripts/mcts-inspect.mjs showed why: Jev's Score is an absolute
judgment, and once one side is materially ahead it says "clearly better" for every child
position, so the tree had nothing to rank on and its choices were noise. The tree mechanics were
fine (verified with an injected perfect-information evaluator in scripts/mcts-selftest.mjs and
against an alpha-beta oracle in scripts/mcts-oracle.mjs, plus an independent code review).
The fix follows the TypeSafe rule of thumb, keep what code can compute in code
(tactics.js):
- Exact facts in the option descriptions. Each candidate move now carries an exchange estimate from a capture-only lookahead ("wins about 3 after forced recaptures") and a flag if it allows a mate in one. This sharpens Jev's policy in both modes.
- Hybrid leaf value. In the search, Jev's value is blended with the change in settled material relative to the root and with mate-in-one detection. Jev keeps the positional judgment, code supplies the tactical delta that a coarse rubric cannot express.
- Exact tactics at the root. An immediate mate is always played; moves that allow a mate in one are excluded when an alternative exists.
Does it help now? Yes, and it is measured per move against Stockfish rather than by game results;
see "Measuring strength with Stockfish" below. A 2-game match (npm run match -- 16 6 2) also went
2–0 for the search, at about 47k tokens and 3 seconds per move versus 2.6k tokens and 0.4 seconds
for the single-call player.
Run it
Requires Node.js 20+ and a TypeSafe API key (get one at typesafe.ai).
git clone https://github.com/TholeG/typesafe-chess.git
cd typesafe-chess
npm install
export TYPESAFE_API_KEY=your_key_here # or put it in .env and source it
npm start # builds the React client, then serves it
# open http://localhost:3000
For UI development run the API and the Vite dev server side by side:
npm run build && node server.js # API + built client on :3000
npm run dev # hot-reloading client on :5173, proxies /api
To benchmark MCTS against the single-call player:
npm run match -- 16 6 2 # simulations, parallel evaluations, games
Click One move for a single move or ▶ Autoplay to let the two Jevs play a full game. Switch between MCTS and Fast in the players card and set the number of simulations and parallel evaluations. The board draws arrows for the top candidates (the played move in blue), the bar next to it shows the position score from White's view, and the analysis card lists the candidates (probabilities in fast mode, visits / prior / Q in MCTS mode), the sharpness estimate, latency and token usage per move. Click any move in the list to review that position and its analysis.
Cost guide: a fast move uses roughly 2–3k tokens. An MCTS move with 16 evaluations uses roughly
40–50k tokens and 3 seconds. Defaults can be set with MODE, MCTS_SIMULATIONS and
MCTS_CONCURRENCY environment variables.
The API key stays on the server. The browser only talks to three local endpoints:
GET /api/state, POST /api/new, POST /api/step, POST /api/settings.
Measuring strength with Stockfish
Two Jev players beating each other says little. The repo therefore includes a small UCI
wrapper (stockfish-uci.js) around the stockfish npm package (WASM,
runs in Node) and three scripts that use it as an external, deterministic reference:
npm run regret -- 10 16 12 # positions, MCTS evaluations, Stockfish depth
npm run match -- 16 6 2 --vs stockfish --elo 1500 --depth 6
npm run elo -- --player mcts --levels 1320,1600,1900,2200 --games 2
- regret scores every legal move in a set of middlegame positions with Stockfish, then reports the centipawn loss of Jev's fast move and of Jev's MCTS move on the same positions. This is the cheapest and most direct comparison: per move, on identical inputs, against a fixed yardstick.
- match can play either Jev player against Stockfish limited by
UCI_Elo. - elo plays a ladder of Elo levels and fits a rating by maximum likelihood, with a 95 % interval. The scale caveat is real: Stockfish's limiter is calibrated for the full engine at tournament time controls, so numbers from the lite WASM build at fixed depth are relative, not FIDE. Precision is the bigger issue: eight games give roughly ±200 Elo, and each MCTS game costs about 2M tokens.
Tuning loop, measured. npm run sweep scores fixed positions once with Stockfish and then runs
several MCTS configurations on them, so differences are differences in the search. Results live in
docs/sweep-*.json. Two rounds so far, 20 tactical positions each, oracle depth 12, 16 evaluations:
| Configuration | Avg loss | Median | Best move | ≥100 cp |
|---|---|---|---|---|
| Fast (single call) | 168–190 cp | 82 | 5/20 | 6 |
| MCTS, first version | 119 cp | 19 | 7/20 | 5 |
| MCTS, visits-only selection | 156 cp | 43 | 7/20 | 6 |
| MCTS, 32 evaluations | 123 cp | 22 | 6/20 | 5 |
| MCTS + forcing-reply facts at the root | 25 cp | 9 | 9/20 | 1 |
| same, root prior temperature 1.5 | 46 cp | 9 | 9/20 | 3 |
The decisive change came out of a code review of the first sweep (by a second model reading the
JSON and replaying the positions with chess.js): the capture-only lookahead cannot see quiet
threats, so in one position every rook move scored the same while one of them allowed a check with
a single legal reply. The fix is again a code fact, opponent_forcing_replies in
tactics.js: for each candidate move, the opponent's checks that cannot be answered
by capturing the checker (with the list of evasions) and captures without recapture. It is added
only to the root call, so it costs a few hundred tokens per move, not per node. The same review also
found that the root evaluation was eating one unit of the search budget and that moves Jev omits
from its distribution never got a tree edge. The now-default configuration is the bold row.
Beware of one Node quirk: the Stockfish WASM loader sets the global fetch to null, which
silently breaks any HTTP client in the same process. The wrapper restores it.
What to look at
- Confidence is not strength. In quiet openings the confidence is often below 0.2 because several moves are genuinely fine. That is calibration, not indecision. Once a capture or a mate is on the board the distribution collapses onto one move.
- Probabilities are reusable data. The UI ranks candidates from the same response that produced the move. No second call needed for "what else was considered".
- Speculative questions are cheap. Score and Noul add a few hundred tokens and no latency, because all questions are evaluated in parallel over the same state.
- Facts beat prose. The single biggest quality lever is what the code puts into each option description. Better facts (say, the material balance after a short exchange) would make Jev a better player without changing a single prompt sentence.
Jev is not a chess engine and does not calculate variations. It judges the options it is given. Expect creative openings, sound development, and the occasional blunder. Expect it to be fun to watch.
Files
| File | Purpose |
|---|---|
jev-player.js | Builds the state and questions, calls the TypeSafe SDK, returns policy and value |
mcts.js | PUCT tree search using Jev's policy and hybrid value, with concurrency and a transposition cache |
tactics.js | Exact code-side helpers: material, capture-only lookahead, mate in one |
server.js | Express server, game state, settings and the JSON endpoints; serves the built client |
client/ | React UI (Vite + react-chessboard): board, arrows, eval bar, analysis, move review |
stockfish-uci.js | UCI wrapper around the Stockfish WASM package: best move, score all moves, strength limit |
scripts/match.mjs | Benchmark games: Jev vs Jev or Jev vs Stockfish, alternating colours |
scripts/regret.mjs, scripts/elo.mjs | Centipawn loss against Stockfish; Elo estimate from a ladder of games |
scripts/mcts-selftest.mjs, mcts-oracle.mjs, mcts-inspect.mjs | Mechanics tests without API calls, and a tree dump for any position |
Learn more about TypeSafe
- Documentation and the quick start
- How to build with System One
- The three primitives: Choice, Score, Noul
- Confidence and the speculative fan-out pattern used here
- SDKs for JavaScript and Python
- Coding agent? Install the TypeSafe skill and it will read the live docs before writing an integration. This repo was built that way.
MIT licensed. Built in a day with Claude Code and the TypeSafe agent skill.