People look at one digit at a time
The human method written in solve.md goes like this. Read the board and pick one digit. Cross out the rows, columns and boxes where that digit already is, and a box or line with only one place left appears. That cell is 100% the answer. Fill it in and read the board again.
So we reduced the VLM's work to two tasks.
- Reading the board: read the digits of all 81 cells as they are.
- Pick one digit and list every cell where that digit cannot go.
We never ask which cell is the answer. The code counts whether only one place remains after crossing out the cells where the digit cannot go.

The 8B before training
We switched the model to Qwen3-VL-8B-Instruct. Running 20 problems without LoRA, it got the coordinates 100% right, but only 20% of boards had all 81 cells read correctly (83% per cell). For the cells where digit 1 cannot go, it was 91% right per cell, but not a single board was fully correct.
Step-by-step training
The two tasks were trained separately. Each cycle runs a test, adds the problems it got wrong and trains again.
- Reading the board: 2B went from 17.5% to 100% in 4 cycles, and 8B from 25.2% to 100% in 2 cycles.
- Cells where digit 1 cannot go: 2B rose to 69.5% over 9 cycles, and 8B to 97.6% in 15 cycles. Per cell, that is 99.98%.
Unlike in post 2, it improved the more it trained. That is because the basis for the answer lies within the board.

The actual code for this step
This is the prompt given to the VLM. It picks one digit and asks only for the cells where it cannot go.
vlm_prompt.py
def build_digit_illegal_prompt(
board: list[list[int]],
given_mask: list[list[bool]],
digit: int,
*,
retry_hints: list[str] | None = None,
) -> str:
"""digit 의 비합법수(둘 수 없는 모든 칸) 나열."""
board_txt = board_to_text(board, given_mask)
placed = sum(1 for r in range(9) for c in range(9) if board[r][c] == digit)
prompt = (
"You are solving Sudoku like a human (solve.md digit tracking).\n"
f"{_COORD_HEADER}"
f"{_HUMAN_RULES}"
"The board has been read. Now track ONE digit on the number pad.\n"
f"TARGET DIGIT: {digit} (already placed {placed} times on the board).\n\n"
f"Task: list EVERY cell where digit {digit} CANNOT be placed.\n"
"A cell is ILLEGAL for this digit if:\n"
f"- it is filled with a digit other than {digit}, OR\n"
f"- it is EMPTY but its row, column, or 3×3 box already contains {digit}.\n"
f"Do NOT list cells that already contain {digit} — those are legal placements.\n"
"You MUST include ALL illegal cells. Do not omit any.\n"
"Use compact JSON (no spaces) to fit all cells.\n\n"
f"Text grid:\n{board_txt}\n\n"
f'Return ONLY JSON: {{"digit":{digit},"illegal":[[row,col],...]}}\n'
f'Example: {{"digit":{digit},"illegal":[[0,3],[1,5],[2,8]]}}\n'
)
return _append_retry_hints(prompt, retry_hints)This is the function that builds the training answers. They are decided only by whether that digit is already in the row, column or box.
vlm_board.py
def illegal_cells_for_digit(
board: list[list[int]], digit: int
) -> list[tuple[int, int]]:
"""digit 을 둘 수 없는 모든 칸 (이미 digit 인 칸은 제외)."""
from sudoku_human_solver import cell_candidates
out: list[tuple[int, int]] = []
for row in range(9):
for col in range(9):
v = board[row][col]
if v == digit:
continue
if v != 0:
out.append((row, col))
continue
if digit not in cell_candidates(board, row, col):
out.append((row, col))
return outThe next post is "4. Comparing solves before and after training".
COMMENTS · CHAPTER 03
Turning human solving into rules 0 comments
No comments yet. Leave a question or anything you would like to add.
To post a comment, continue with Google and register a nickname and email address.
Continue with Google