sudoku-jev · how the loop works

Jev never solves the sudoku. It picks one legal placement from a list, over and over.

Jev is a decision model. It takes a state and a set of questions with fixed options, and returns one option per question with a probability for every option. The app turns each turn of the game into exactly that shape: the board is the state, every legal placement is an option, and the browser judges the answer.

01 · the loop

One round trip per placed digit

Every digit costs one websocket round trip and one Jev request. The backend has no memory between rounds; the browser carries the whole state.
02 · what Jev sees

The state: the board as nine text rows

The state field is structured JSON. The board is nine strings with a dot for an empty cell. The goal sentence is the only rule text. No solution, no hint about the right move.

···6·5···
·1···7·62
···821··9
··897·4··
4··21····
1·6·34·2·
·2··63·17
34·1·9··6
·817··3·4

Real puzzle from a captured request. The orange cell is the move Jev chose in the answer below.

// request body, state part (captured)
{
  "model": "typesafe/jev-1.13",
  "state": {
    "game": "sudoku",
    "goal": "Fill every empty cell (.) so that each row,
             each column, and each 3x3 box holds the
             digits 1 to 9 once.",
    "rows": [
      ". . . 6 . . . . .",
      ". 1 . . . 7 . 6 2",
      ". . . 8 2 1 . . 9",
      ". . 8 9 7 . 4 . .",
      "4 . . 2 1 . . . .",
      "1 . 6 . 3 4 . 2 .",
      ". 2 . . 6 3 . 1 7",
      "3 4 . 1 . 9 . . 6",
      ". 8 1 7 . . 3 . 4"
    ],
    "empty_cells": 45
  },
  "questions": { ... see below ... }
}
03 · the choices

The question: every legal placement is one option

The backend computes the candidates. Only placements that break no row, column, or box rule become options, minus the ones the browser already rejected. This puzzle had 135 legal placements, so one question. An option id reads like a sentence, and its description is a small object: the position, the digit, and every digit that still fits in that cell.

"questions": {
  "move_batch_1": {
    "type": "choice",
    "instructions": "Which placement is certainly correct for this sudoku?
                     Prefer a cell whose cell_candidates has one value.
                     Otherwise pick the placement that the row, column,
                     and box rules force.",
    "criteria": { // 135 entries, one per legal placement
      "row_1_col_1_value_2": { "row": 1, "column": 1, "value": 2, "cell_candidates": [2,5,7,8,9] },
      "row_1_col_1_value_5": { "row": 1, "column": 1, "value": 5, "cell_candidates": [2,5,7,8,9] },
      ...
      "row_1_col_6_value_5": { "row": 1, "column": 6, "value": 5, "cell_candidates": [5] },
      ...
    }
  }
}

Same options as cards

row_1_col_1_value_2row 1 · column 1 · value 2
cell_candidates [2,5,7,8,9]
row_1_col_1_value_5row 1 · column 1 · value 5
cell_candidates [2,5,7,8,9]
row_1_col_1_value_7row 1 · column 1 · value 7
cell_candidates [2,5,7,8,9]
row_1_col_6_value_5row 1 · column 6 · value 5
cell_candidates [5]
row_9_col_5_value_5row 9 · column 5 · value 5
cell_candidates [2,5]
+ 130 more

Why 255 matters

Jev accepts at most 255 options per choice question. An empty board has 729 placements. The backend slices the list into move_batch_1, move_batch_2, ... and sends all batches in one request. Jev answers them in parallel.

Why cell_candidates

Jev does not count or calculate. The code computes which digits fit in each cell and puts the list in every option. A single-digit list is the visible signal that the instructions ask it to prefer.

Why an id like a sentence

row_1_col_6_value_5 is the option key Jev returns. The backend parses it back into {row: 0, col: 5, value: 5} with a regex. No free text ever comes back.

04 · what Jev answers

A probability for every option, one choice per question

row_1_col_6_value_5
0.51
row_9_col_5_value_5
0.19
row_6_col_4_value_5
0.16
row_4_col_6_value_5
0.02
row_9_col_1_value_6
0.01
row_9_col_6_value_2
0.01
129 others
≈0.10

Jev put half its mass on the one cell with a single candidate. The next three are the same digit 5 in other cells, so it also reads the row and column pattern, not just the list length.

// response body (captured, trimmed)
{
  "model": "typesafe/jev-1.13-20260917",
  "answers": {
    "move_batch_1": {
      "type": "choice",
      "choice": "row_1_col_6_value_5",
      "confidence": 0.5,
      "probabilities": {
        "row_1_col_6_value_5": 0.51,
        "row_9_col_5_value_5": 0.19,
        "row_6_col_4_value_5": 0.16,
        ... 132 more ...
      }
    }
  },
  "usage": { "input_tokens": 7251, "output_tokens": 2053,
             "cost": 0.000304 }
}

// backend → browser
{ "type": "decision", "move": { "row": 0, "col": 5, "value": 5 },
  "probability": 0.51, "confidence": 0.5,
  "options_considered": 135, "questions_asked": 1, "latency_ms": ~350 }

With several batches, each question names its own winner. The backend ignores the choice fields and takes the single highest probability across all batches, then reports the confidence of the batch it came from.

05 · who judges

The browser owns the solution. Jev is never told if it was right.

Correct value

The move matches the hidden solution. The cell fills in blue and the next state goes out after a short delay so the board stays readable.

Wrong value

Cell stays empty, shown red with a strike. The move joins rejected, so the backend never offers it again. Mistakes count goes up.

Won

No empty cell left. Status becomes won, the loop stops, and the Solve button disables until New game.

Lost

Three mistakes. Status becomes lost. The server also declares a loss when no legal move remains after the rejections.

Feedback reaches Jev only indirectly: a wrong option disappears from the next list, and a right one shows up as a filled digit in the next state rows.

06 · numbers from this capture

What one move costs

EndpointPOST https://openrouter.ai/api/alpha/decisions. The chat completions endpoint rejects the model with a 400.
Model idtypesafe/jev-1.13, served as jev-1.13-20260917
Options in this request135 legal placements, 1 question (limit 255 per question)
Tokens7,251 input, 2,053 output. Output tokens are free on this model.
Cost per move$0.0003. A 45-cell puzzle solved without mistakes costs about $0.014.
Latency300 to 700 ms per decision, measured in the backend around the fetch call
Where the logic livesbackend/src/jev.ts builds the request and picks the best option. shared/src/sudoku.ts computes legal moves. frontend/src/game.ts judges the move.