Add a rule kind
Each rule family with kinds keeps one registry in src/pencilmark/rules/: line.py (LINE), edge.py (EDGE) and
cell.py (CELL). The reader, the writer, describe(), the drawing and the generator all consult it, so a new kind
starts there. The quick suite then names most of what is still missing: the JSON Schema, the default colour and the
rule page are checked against the registry. This page follows a new line kind, the case with the most steps; edge
and cell kinds follow the same order (section 6).
1. The registry entry
Add a LineKind to LINE in src/pencilmark/rules/line.py:
LineKind("nabner", "Nabner", "No two digits on a nabner line are equal or consecutive."),
The fields after the one-sentence rule are optional: params (keys such as value, required when the board is not
9×9), closable, width and ends (how it is drawn), name_fn / a function as text when the words depend on the
rule's values, and generated once the generator can place it. The dataclass's comments say what each takes.
2. What the engine sees
The engine knows no public kinds: the reader translates each rule. Choose the cheapest of three routes.
- Existing engine rules only. If the kind can be written with rules the engine already has, translate it in
line_to_engine(and back inline_from_engine) inrules/line.py, asdouble_arrowbecomes an equal sum. Nothing else changes in the engine. The writer then sees only the engine rules, so the kind may come back under another name with the same meaning (a four-cell double arrow is written as a zipper). - A new engine line kind built from existing constraints. Most lines are this. Append the name to
PM_LINE_KINDSincsrc/nary.hpp(the ids are frozen: append, never insert) and toLINE_KINDSinsrc/pencilmark/model.py, in the same order (tests/test_registry.pychecks it). Then compile it inPuzzle.finalizeinmodel.py: into pair relations fromRELATIONS(thermo steps arelt, palindrome mirrorseq, and a nabner line would benotdiff1 andneqvalon every pair of its cells; list the kind inPAIRWISE_LINE_KINDStoo), or into an n-ary constraint the engine already solves (set,between,equal_sumortable) carrying "line_kind": kind, as renban, between, zipper and lockout do. The writer drops what the line compiled to and writes the line back. Rebuild the engine (pip install -e .) after the C++ change. - A new kind of constraint. Only when no combination of the above can say the rule: a new
NT_type incsrc/nary.hpp, its enumeration incsrc/nary.cpp, and techniques that use it (Add a technique). Start a discussion in an issue first.
3. The format and the drawing
- JSON Schema. Add the kind to the
kindenum ofrule_lineindocs/sudoku-puzzle.schema.json, and any new parameter to its properties. - Default colour. Add it to
linesindocs/palette.json. Use the colour most setters use for it, if there is one; Drawing and style says where the current defaults come from. - Width in Drawing and style. If
widthis a number rather than a palette size name, state it in the kind's row ofdocs/library/drawing-style.md(tests/test_format.pychecks the two agree). - Swatch. Add a
line-<kind>entry toSWATCHESintools/gen_swatches.py, with digits along the path that show the rule.
4. The docs
Add a row for the kind to the table in docs/formats/rules/lines-and-arrows.mdx: the kind, its parameters, the rule in a
sentence, a link to a page that explains it to solvers, and <Swatch name="line-<kind>" />. Follow the
writing style. The Palette page and the drawings are regenerated by the docs build.
5. Explanations and readers
- Words in explanations. Name the line in
LINE_NAMESinsrc/pencilmark/explain/base.py("nabner line"). If it needs its own lesson, map the engine kind to a catalogue key in_LINE_KEY, add the entry toCATALOGand a sentence to_VARIANT_LESSONSinexplain/catalog.py. Without these, explanations fall back to "the nabner line". - Imported puzzles. If SudokuPad or f-puzzles puzzles use the rule, teach the readers in
sources/to recognise it:_sp_translate.pymatches rule text and line colours,_fpuz_translate.pythe f-puzzles constraint names.
6. Edge and cell kinds
An edge kind is an EdgeKind in EDGE (rules/edge.py). Its meaning is a pair relation between the two cells:
add the case to edge_relation (public rule to relation) and to edge_family (relation back to the clue, for the
writer), and, if the kind can be negated ("all clues shown"), to negative_relation. Then the rule_edge part of the schema, docs/formats/rules/edges-and-corners.mdx and
an edge-<kind> swatch.
A cell kind is a CellKind in CELL (rules/cell.py): its variant name, its parameter, whether the negative is
defined, and its mark. Its meaning is a domain restriction on the cell; follow how odd and low reach the engine in
exchange/read.py. If it is drawn shaded, give it a colour of its own name in both themes of docs/palette.json (the
renderer reads the colour by kind). Then the schema, docs/formats/rules/cell-clues.mdx and a swatch.
7. Check and record
python tests/test_line_matrix.py --write # the snapshot of every line kind gains the new one's cases
python tools/tasks.py check # quick suite, doc examples, golden corpus
python tools/tasks.py serve # the rule page, its swatch and the Palette page
The snapshot files (tests/fixtures/line_matrix.jsonl, edge_matrix.jsonl, cell_matrix.jsonl) record every kind's
reading, writing and description; rewrite them for the new kind only, and read the new lines before committing them.
A new kind should change no golden record. Add a line under "Unreleased" in CHANGELOG.md, and an example puzzle that
uses the kind to tests/golden/inputs.jsonl (then python tools/golden.py write, in its own commit) so the corpus
grades it.