Puzzle format
The sudoku-puzzle format, version 1: the transport format for sudoku puzzles, classic and variant. It says what a file means, not how to
draw it or how hard the puzzle is: those are separate documents (Drawing and style, Grade card
and Generator spec). The JSON Schema is
sudoku-puzzle.schema.json; pencilmark is the
reference implementation.
The key words MUST, MUST NOT, SHOULD and MAY are used as in RFC 2119.
Contents
- Overview
- Envelope
- Cells, outside clues and ordering
- Strict reading (must-understand)
- Rules: common shape and pair operators
- Rule families
- Cosmetic layers: style and decorations
- Canonical form, fingerprint, compression
- Examples
1. Overview
A sudoku-puzzle file describes one puzzle completely: the board, the givens, and every rule, houses included.
It is written so that:
- a solver or grader can rely on every rule having one exact meaning (section 6), with no defaults that depend on the board size;
- a renderer can draw the puzzle from the rules alone (how is up to the renderer; Drawing and style lists common practice);
- a reader that meets something it does not understand refuses the file instead of silently dropping a rule (section 4);
- every puzzle has one canonical representation (section 8).
The file is JSON (RFC 8259), UTF-8, conventionally named *.json or
*.sudoku.json, media type application/json. A file is recognised by its "format" member, not by its name.
Several puzzles go in a JSON Lines file (*.jsonl): one compact sudoku-puzzle object per
line.
1.1 What each reader needs
Meaning and drawing are kept apart, so different programs read different parts of a file:
- a renderer needs the board, the givens, and each rule's family, kind and cells (plus any number it shows, such as
a cage sum); it never needs to know what a rule means arithmetically. It MAY use
styleanddecorations. - a solver or grader needs the board, the givens and the rules; it MUST NOT depend on
meta,styleordecorations, which never change a puzzle's meaning.
1.2 Implementing a subset
Nothing obliges a reader to support every rule family. A reader MAY implement any subset of section 6, as long as it
refuses a file that uses a family or kind outside it (section 4) instead of reading it without that rule. The
smallest useful reader understands the envelope, grid and box_grid: that is every classic puzzle. Each further
family can be added on its own; the families do not depend on each other except through the shared shapes of
section 5. A writer SHOULD use only the families the puzzle needs. Sections 7 (cosmetic layers) and 8 (canonical form)
are optional for readers: cosmetic layers may be ignored, and only a writer that produces fingerprints needs section 8.
2. Envelope
{"format": "sudoku-puzzle",
"format_version": 1,
"$schema": "https://silly-puppyy.github.io/pencilmark/schema/sudoku-puzzle-1.json",
"meta": {"title": "…", "author": "…"},
"board": {"rows": 9, "cols": 9},
"digits": 9,
"givens": [ …R*C entries… ],
"solution": [ …R*C entries… ],
"candidates": {"r1c1": [1, 3, 7]},
"rules": [ … ],
"decorations": [ … ],
"style": { … }}
| Key | Required | Meaning |
|---|---|---|
format | yes | Always "sudoku-puzzle". |
format_version | yes | Integer, 1. Bumped only on a breaking change. A reader MUST refuse a file whose format_version is higher than it supports. |
$schema | no | URL of the JSON Schema. Ignored by readers. |
meta | no | Information about the puzzle (2.1). Never changes how it is solved or graded. |
board | yes | {"rows": R, "cols": C}: the bounding rectangle of the board. No other keys. |
digits | yes | Integer D ≥ 1: cells hold the digits 1..D. (Other digit sets and display symbols are reserved for a later version; see Reserved names (6.17)). |
givens | yes | Flat list of R×C entries in reading order (row by row). 0 = empty cell, null = no cell at that position (the gaps of multi-grid boards), otherwise the given digit as a number (so 10..D work). This is the only representation: no text rows, no digit strings, no per-cell objects. |
solution | no | Same shape as givens, every existing cell filled, null exactly where givens has null. |
candidates | no | Given pencil marks (Sukaku): {"r1c1": [1, 3, 7], …}, ascending integer lists. Semantic: each listed cell's digit is one of its list; unlisted cells are unrestricted. Graphical cell clues (odd, even, …) are cell rules, never candidates. |
rules | yes | Every constraint, houses included (section 6). Nothing is implied: a board without house rules has no houses. |
decorations | no | Cosmetic drawing primitives (section 7). Never semantic. |
style | no | Cosmetic, puzzle-wide drawing hints (section 7). Never semantic. |
There is no uniqueness flag: whether a puzzle has one solution is a property a tool checks, not a field.
2.1 meta
Everything a program wants to carry about the puzzle goes here: every key is optional and free-form, and tools keep
keys they do not know, so adding "author" is all attribution needs. Text is plain (no HTML, no language maps). Data
for one program goes under a reverse-DNS key, such as "com.example.app": {…}. The suggested keys, so that programs
agree on the common ones:
| Key | Meaning |
|---|---|
title | Title. |
author | Setter's name. |
source | The website's domain, such as "logic-masters.de". |
id | That site's own id. |
url | Link to the puzzle. |
date | ISO 8601 date. |
rules_text | The setter's own wording of the rules. Tools never overwrite it. |
type | Display name of the variant, such as "Killer Sudoku". Variant names live only here; they never change the rules. describe() (How variants are named) fills it when it is empty. |
license | Licence of the puzzle. |
tags | List of strings. |
generated_from | Fingerprint of the sudoku-spec that produced the puzzle (Generator spec). |
3. Cells, outside clues and ordering
3.1 Cell names
A cell is named "r{row}c{col}", 1-based on the board, lowercase, no leading zeros: "r1c1" is the top-left
position, "r9c9" the bottom-right of a classic, "r21c21" the bottom-right of a samurai. A cell name anywhere in a
rule or in candidates MUST refer to an existing cell, one whose givens entry is not null. There are no virtual
or outside cell names, and names are always lowercase.
Reading order is by row, then by column: r1c1 < r1c2 < … < r1c9 < r2c1.
3.2 Outside clues: side
A clue drawn outside the grid (sandwich, x-sum, skyscraper, frame, little killer, rossini, …) has
"side": "top" | "bottom" | "left" | "right" and ordered cells that start at the cell next to the clue.
The clue attaches to cells[0], on side. Where exactly it is drawn (spacing, a gap that is too narrow, two
clues competing for one gap) is the renderer's business; the semantics never depend on it.
- Row and column clues: the cells run straight away from the side (
top: down the column,left: along the row to the right, and so on). - Diagonal clues (little killer, diagonal distinct count): the cells run diagonally away from the side; the
direction is
cells[0] → cells[1].sidesays which edge ofcells[0]the clue is drawn against (top or left ofr1c1). A 1-cell diagonal clue points straight at its cell. - The position next to
cells[0]onsideMUST NOT be an existing cell: it is beyond the board or a gap (null). - Multi-grid boards:
cells[0]need not be on the board's edge. A clue on the left of board row 10 of a samurai (the 4th row of the centre grid) is "side": "left" withcells[0] = "r10c7"; it attaches to the gap at r10c6.
3.3 Ordered lists and sets
| Ordered (the order carries meaning) | Sets (readers ignore the order; writers SHOULD use reading order) |
|---|---|
line cells (thermo bulb first; a whisper's order is its path) | box, row, column, region, cage cells; sum kinds group / corner |
arrow bulb (a pill reads as a number in bulb order) and each shaft in lines (away from the bulb) | cell, contains, appears cells, count, distinct_count without side |
outside clues (sandwich, xsum, skyscraper, rossini, first_seen, sum kinds little_killer / frame, distinct_count with side): from the cell next to the clue outwards | equal_sum segments (each a set; the segment list is unordered) |
clone / isomorphic regions: corresponding positions | edge pairs except greater, relation pairs of symmetric operators |
edge kind greater (larger digit first) and relation pairs of less / less_equal / any_of: [a, b] in operator order | area cell lists, except pairs |
vudoku cells (vertex first); corner cells TL, TR, BL, BR (reading order; any other order is refused) |
The order of the rules array carries no meaning. Writers SHOULD keep the order they were given (the
fingerprint sorts, section 8.2).
3.4 Geometry derived from cells
Edge and corner positions are never named directly. They follow from the cells:
- Edge clue: exactly 2 orthogonally adjacent cells. The clue sits at the midpoint of the shared side.
- Corner clue: exactly 4 cells forming a 2×2 block, written TL, TR, BL, BR. The clue sits at the shared corner.
- Outside clue:
sideplus orderedcellsalong a straight line (row, column or diagonal), contiguous (3.2). - Line: centre-to-centre segments between consecutive cells; consecutive cells MUST touch (side or corner). A segment between diagonal neighbours passes straight through their shared corner.
4. Strict reading (must-understand)
A reader MUST refuse a file, raising an error that names the offending key or rule and its position, when any of these holds:
formatis not"sudoku-puzzle", orformat_versionis missing, not an integer, or higher than supported.- A key it does not know appears anywhere: at the top level, in
board, or inside a rule. The only exceptions are these, which are always safe to ignore:meta,style(top level and per rule),decorations, and a rule'sidandlabel. - A rule
typeit does not know, or akindit does not know for a known type. - A vendor rule type (
"com.example:foo": reverse-DNS namespace, colon, name) it does not implement. Core type names never contain a colon. - "type": "custom" (reserved, Reserved names (6.17)).
- A structural check fails:
len(givens) != R*C;solutionorcandidatesdisagree in shape; a cell name refers to a missing cell; an edge pair is not orthogonally adjacent; a corner set is not a 2×2 block; an outside clue's cells are not collinear and contiguous, or the position next tocells[0]onsideis a cell; an arrow shaft does not start next to a bulb cell; a digit lies outside 1..D; a required parameter is missing (whisperwithoutvaluewhen D ≠ 9); agrid/box_gridarea is not fully made of existing cells; abox_grid'sboxdoes not hold D cells.
pencilmark raises FormatError (a ValueError) for these. A file can also be valid and still use something the
pencilmark engine cannot build (a modular line with modulus 4); then it raises NotSupportedError (also a
ValueError) naming the rule and the reason, never a partial puzzle.
A program that only draws MAY draw what it understands and show a warning, but MUST NOT present the puzzle as
complete. Ignorable containers never carry rules: no constraint may be derived from meta, style or
decorations.
5. Rules: common shape and pair operators
Every rule is an object with a type (the mechanic family); many families also have a kind (a flavour with its own
semantics and drawing). Every rule MAY carry:
| Key | Meaning |
|---|---|
id | String. Ignorable. |
label | Short plain text. Ignorable. Optional everywhere; for vendor rules it is what a renderer shows. |
style | Cosmetic drawing hints for this rule (section 7). Ignorable. |
Areas. A rule that applies to part of the board says where with area: the origin of a grid / box_grid rule
in the file (that grid's D × D cells), or a list of cells. The same key means the same thing everywhere it appears
(neighbours, negative, and the generator spec, Generator spec).
Number keys: sum when the clue is a sum of digits (cage, every sum kind, sandwich, x-sum, edge kind sum),
value for every other number (difference, ratio, product, whisper gap, skyscraper count, …). Parameters with a
stated default are omitted by canonical writers when they equal the default.
5.1 Pair operators
relation and neighbours use these operator names, with the operator's parameter under its own key, as everywhere
else in the format (value for a number, sum for a sum):
{"type": "relation", "op": "difference", "value": 3, "pairs": [["r1c1", "r3c3"]]}
{"type": "relation", "op": "any_of", "any_of": [{"op": "sum", "sum": 10}, {"op": "difference", "value": 1}], "pairs": [["r1c1", "r2c3"]]}
op | Parameter | The pair [a, b] satisfies it when |
|---|---|---|
different | – | a ≠ b |
equal | – | a = b |
less | – | a < b |
less_equal | – | a ≤ b |
difference | value k | |a − b| = k |
not_difference | value k | |a − b| ≠ k |
not_consecutive | – | |a − b| ≠ 1 |
difference_at_least | value k | |a − b| ≥ k |
difference_at_most | value k | |a − b| ≤ k |
ratio | value k | a = k·b or b = k·a |
not_ratio | value k | neither |
sum | sum s | a + b = s |
not_sum | sum s | a + b ≠ s |
parity_differs | – | one is odd, the other even |
same_third / different_third | – | a and b are / are not in the same third of 1..9 (entropic) |
same_mod3 / different_mod3 | – | a ≡ b / a ≢ b (mod 3) |
any_of | any_of [{op, parameter}, …] | at least one of the listed operators holds |
less, less_equal and any_of are directional (pair order matters); the others are symmetric.
6. Rule families
Each rule family has its own page; the quick reference lists every type and kind with a link.
- Houses and regions: 6.1 houses, 6.2
region - Cages and sums: 6.3
cage, 6.8sum - Lines and arrows: 6.4
line, 6.5arrow - Edges and corners: 6.6
edge, 6.7corner - Outside clues: 6.9
sandwich,xsum,skyscraper,rossini,first_seen - Whole-board rules: 6.10
neighbours, 6.11relation, 6.14negative - Cell clues: 6.12
cell - Other rules: 6.13 other families, 6.15 vendor rules, 6.16 what is not a rule, 6.17 reserved
- Quick reference: every
typeandkindon one page
"Repeats" in the family tables says whether digits may repeat among a rule's cells because of that rule; houses still apply.
7. Cosmetic layers: style and decorations
Per-rule style (optional, never semantic):
{"color": "#rrggbb" | palette index, "fill": "#rrggbb" | palette index, "opacity": 0..1, "width": cell units, "shape": "circle" | "square" | "diamond", "label_cell": "r1c1", "dash": true}. The text a rule shows is its
label (section 5); label_cell only says where to draw it.
Top-level style: puzzle-wide hints, such as {"palette": [...], "cell_size": 64}. Free-form.
decorations: artwork that carries no rule.
{"kind": "line" | "arrow" | "rect" | "circle" | "text",
"points": [[y, x], …], "center": [y, x], "size": [h, w], "text": "…", "style": { … }}
Coordinates are cell units [row, col] with the origin at the top-left corner of the board, so the centre of
rRcC is [R − 0.5, C − 0.5] (SudokuPad's convention shifted to the outer corner).
8. Canonical form, fingerprint, compression
8.1 Canonical writer (pencilmark.dumps)
- Top-level key order:
format,format_version,$schema,meta,board,digits,givens,solution,candidates,rules,decorations,style. Insideboard:rows, thencols. - Rule key order:
type,kind,of, then the parameters (op,any_of,color,letter,value,sum,split,values,digits,groups,each,counts,arrow,size,repeat,box,where,within,area,closed), thenside, then the cell keys (origin,source,bulb,lines,cells,regions,segments,pairs,except), thenid,label,style. - Houses as described in 6.1. Rules in the order given (a grader's tie-breaks can depend on it). Candidate lists ascending;
candidateskeys in reading order. Cells of sets as the source listed them (the fingerprint sorts them). - Defaults omitted (
closed: false; whispervalue5, entropicgroups, modularvalue3, lockoutvalue4 on 9×9; they are written when D ≠ 9). - Pretty form:
givensandsolutionone grid row per line; one rule per line; LF line endings; UTF-8; no trailing spaces. Compact form: no whitespace.
import pencilmark
messy = {"rules": [{"cells": ["r1c2", "r1c1"], "sum": 3, "type": "cage"},
{"box": [2, 2], "type": "box_grid", "origin": "r1c1"}],
"digits": 4, "board": {"cols": 4, "rows": 4}, "format_version": 1, "format": "sudoku-puzzle",
"givens": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4]}
print(pencilmark.dumps(messy))
{"format": "sudoku-puzzle",
"format_version": 1,
"board": {"rows": 4, "cols": 4},
"digits": 4,
"givens": [
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 4],
"rules": [
{"type": "cage", "sum": 3, "cells": ["r1c2", "r1c1"]},
{"type": "box_grid", "box": [2, 2], "origin": "r1c1"}]}
The keys are reordered and the grid laid out row by row; the rules and the cage's cells keep their order.
dumps(doc, pretty=False) gives the compact form, on one line.
8.2 Fingerprint
pencilmark.fingerprint(puzzle) -> "sha256:<64 hex>":
- Read the puzzle and write it canonically (8.1).
- Keep only
format,format_version,board,digits,givens,candidates,rules; in every rule dropid,label,style. - Replace all house rules (
grid,box_grid,box,row,column) by the houses they state, one {"type": "row" | "column" | "box", "cells": [...]} per distinct house, so every spelling of the same houses (abox_grid, agridplusboxrules, single rules, in any order) gives the same result. - Sort the cells of set-like lists (3.3: region, cage, cell, contains, appears, count, sum group / corner,
edge clues other than
greater, scope andexceptlists, the pairs of symmetricrelationoperators,equal_sumsegments) in reading order. - Sort
rulesby their compact canonical JSON text. - Serialise with RFC 8785 (JSON Canonicalization Scheme) and hash with SHA-256.
same = dict(messy, rules=[{"type": "box_grid", "origin": "r1c1", "box": [2, 2]},
{"type": "cage", "sum": 3, "cells": ["r1c1", "r1c2"]}])
pencilmark.fingerprint(messy) == pencilmark.fingerprint(same) # True: rule order and cell order do not matter
solution, meta, style and decorations never affect it, and neither does the order of rules or of the
engine's houses, so the same puzzle from two importers gets the same fingerprint. It is not symmetry-invariant: a
rotated or relabelled copy has another fingerprint.
8.3 Compression for links
For a share link, compress the compact canonical JSON with lz-string compressToEncodedURIComponent (as f-puzzles and
SudokuPad do), or gzip + base64url. Suggested URL parameter: ?pm=<data>. Compression is transport only: the
decompressed text is a normal sudoku-puzzle file.
9. Examples
Complete example files are on the examples page.