The first solve we saw

The scene described in the [About](/en/about/) page is this model. The sudoku-solving AI filled in answers one by one, row by row, starting from the first cell. It was like a young child who already knew the answers.

Training data made from the answer sheet

The first training data was made with dataset_sudoku.py. The engine generates a puzzle together with its solution, and writing the correct digit into the frontmost empty cell (row by row from the top left) is saved as one problem. That cell is filled, then it moves on to the next empty cell, repeating until the board is full.

300 easy, medium and hard boards produced 13,497 problems, which were used to train Qwen3-VL-2B-Instruct with LoRA (lora_stoku).

The prompt also included the list of possible moves. But the answer was always the first empty cell. Even if both 5 and 9 could go in (0,3), if the answer sheet said 9, then 9 was the answer. Why it was 9 appeared nowhere in the data.

The order in which one hard puzzle was filled. Red cells could not be determined from the board alone at that moment
The order in which one hard puzzle was filled. Red cells could not be determined from the board alone at that moment

Could the cell be solved at that moment?

We rechecked the answer cells of the 13,497 problems using only the board at that moment.

  • Cells with only one candidate digit: 9,502 (70%)
  • Cells that were the only place for that digit within a row, column or box: 2,127 (16%)
  • Cells that neither method could decide and that required the answer sheet: 1,868 (14%)

Cells that required the answer sheet were about 21% in hard, 12% in medium and 5% in easy. A person would skip such cells and fill the certain ones first. The model learned to write the answer sheet's digit in the same spot.

Whether the answer cell could be determined at that moment, by difficulty
Whether the answer cell could be determined at that moment, by difficulty

The actual code for this step

This is the function that builds the first training data. empties[0] is the first empty cell counted from the top left, and the value comes from engine.solution.

dataset_sudoku.pypython · 42 lines
def generate_game_samples(
    engine: SudokuEngine,
    difficulty: str,
    game_id: int,
    image_dir: Path,
) -> list[dict]:
    engine.new_game(difficulty)
    board = [row[:] for row in engine.puzzle]
    given_mask = [[engine.is_given(r, c) for c in range(9)] for r in range(9)]
    samples: list[dict] = []
    step = 0

    while True:
        empties = [(r, c) for r in range(9) for c in range(9) if board[r][c] == 0]
        if not empties:
            break

        r, c = empties[0]
        value = engine.solution[r][c]

        img = board_to_image(board, given_mask)
        img_name = f"g{game_id:05d}_s{step:02d}.png"
        img_path = image_dir / img_name
        img.save(img_path)

        answer = json.dumps({"row": r, "col": c, "value": value}, ensure_ascii=False)
        prompt = build_sudoku_prompt(board, given_mask)

        samples.append(
            {
                "id": f"g{game_id:05d}_s{step:02d}",
                "image_path": str(img_path.resolve()),
                "prompt": prompt,
                "answer": answer,
                "difficulty": difficulty,
            }
        )

        board[r][c] = value
        step += 1

    return samples

The next post is "2. It could not learn by trial and error".