pencilmark 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pencilmark-1.0.0/CHANGELOG.md +130 -0
- pencilmark-1.0.0/LICENSE +21 -0
- pencilmark-1.0.0/MANIFEST.in +6 -0
- pencilmark-1.0.0/PKG-INFO +236 -0
- pencilmark-1.0.0/README.md +202 -0
- pencilmark-1.0.0/csrc/bindings.cpp +996 -0
- pencilmark-1.0.0/csrc/canon.cpp +182 -0
- pencilmark-1.0.0/csrc/chains.cpp +767 -0
- pencilmark-1.0.0/csrc/colour.cpp +1636 -0
- pencilmark-1.0.0/csrc/deadline.hpp +25 -0
- pencilmark-1.0.0/csrc/desc.cpp +355 -0
- pencilmark-1.0.0/csrc/explain.cpp +297 -0
- pencilmark-1.0.0/csrc/extra.cpp +2297 -0
- pencilmark-1.0.0/csrc/forcing.cpp +434 -0
- pencilmark-1.0.0/csrc/human.cpp +1529 -0
- pencilmark-1.0.0/csrc/mental.cpp +933 -0
- pencilmark-1.0.0/csrc/model.cpp +1270 -0
- pencilmark-1.0.0/csrc/model.hpp +171 -0
- pencilmark-1.0.0/csrc/nary.cpp +1096 -0
- pencilmark-1.0.0/csrc/nary.hpp +123 -0
- pencilmark-1.0.0/csrc/portable.hpp +46 -0
- pencilmark-1.0.0/csrc/pycompat.hpp +225 -0
- pencilmark-1.0.0/csrc/refute.cpp +468 -0
- pencilmark-1.0.0/csrc/rich.cpp +523 -0
- pencilmark-1.0.0/csrc/rollplace.cpp +199 -0
- pencilmark-1.0.0/csrc/solver.cpp +3663 -0
- pencilmark-1.0.0/csrc/solver.hpp +764 -0
- pencilmark-1.0.0/csrc/sudokuwiki.cpp +631 -0
- pencilmark-1.0.0/docs/BENCHMARK.md +115 -0
- pencilmark-1.0.0/docs/ENGINE.md +68 -0
- pencilmark-1.0.0/docs/FORMAT.md +790 -0
- pencilmark-1.0.0/docs/GRADE.md +120 -0
- pencilmark-1.0.0/docs/INTEROP.md +112 -0
- pencilmark-1.0.0/docs/NAMING.md +83 -0
- pencilmark-1.0.0/docs/PHILOSOPHY.md +125 -0
- pencilmark-1.0.0/docs/SPEC.md +104 -0
- pencilmark-1.0.0/docs/STYLE.md +67 -0
- pencilmark-1.0.0/docs/USAGE.md +268 -0
- pencilmark-1.0.0/docs/api.md +161 -0
- pencilmark-1.0.0/docs/examples/specs/README.md +17 -0
- pencilmark-1.0.0/docs/examples/specs/giant_16x16_32cages.json +34 -0
- pencilmark-1.0.0/docs/examples/specs/killer_whispers_2givens.json +56 -0
- pencilmark-1.0.0/docs/examples/specs/nine_variants_9x9.json +103 -0
- pencilmark-1.0.0/docs/examples/specs/reserved/displaced_jigsaw.json +36 -0
- pencilmark-1.0.0/docs/examples/specs/reserved/hardest_0clue_6x6_kropki.json +39 -0
- pencilmark-1.0.0/docs/examples/specs/reserved/one_xwing_classic.json +49 -0
- pencilmark-1.0.0/docs/examples/specs/reserved/picture_mask_heart.json +116 -0
- pencilmark-1.0.0/docs/examples/specs/reserved/super_samurai_variants.json +391 -0
- pencilmark-1.0.0/docs/examples/specs/zero_clue_6x6_kropki_today.json +41 -0
- pencilmark-1.0.0/docs/palette.json +92 -0
- pencilmark-1.0.0/docs/sudoku-grade.schema.json +353 -0
- pencilmark-1.0.0/docs/sudoku-puzzle.schema.json +1602 -0
- pencilmark-1.0.0/pyproject.toml +66 -0
- pencilmark-1.0.0/setup.cfg +4 -0
- pencilmark-1.0.0/setup.py +62 -0
- pencilmark-1.0.0/src/pencilmark/__init__.py +11 -0
- pencilmark-1.0.0/src/pencilmark/__main__.py +265 -0
- pencilmark-1.0.0/src/pencilmark/_data.py +38 -0
- pencilmark-1.0.0/src/pencilmark/api.py +285 -0
- pencilmark-1.0.0/src/pencilmark/band_path.py +695 -0
- pencilmark-1.0.0/src/pencilmark/card.py +242 -0
- pencilmark-1.0.0/src/pencilmark/card_score.py +254 -0
- pencilmark-1.0.0/src/pencilmark/cbackend.py +1000 -0
- pencilmark-1.0.0/src/pencilmark/cpsat_verify.py +339 -0
- pencilmark-1.0.0/src/pencilmark/data/card_calibration.json +1 -0
- pencilmark-1.0.0/src/pencilmark/data/card_score.json +27 -0
- pencilmark-1.0.0/src/pencilmark/data/explain_links.json +372 -0
- pencilmark-1.0.0/src/pencilmark/data/layouts_sudoku_one.json +510 -0
- pencilmark-1.0.0/src/pencilmark/data/params.json +227 -0
- pencilmark-1.0.0/src/pencilmark/data/price_params.json +227 -0
- pencilmark-1.0.0/src/pencilmark/data/technique_bands.csv +88 -0
- pencilmark-1.0.0/src/pencilmark/data/tiers.json +115 -0
- pencilmark-1.0.0/src/pencilmark/data/time_model.json +26 -0
- pencilmark-1.0.0/src/pencilmark/exchange.py +2103 -0
- pencilmark-1.0.0/src/pencilmark/explain.py +5850 -0
- pencilmark-1.0.0/src/pencilmark/export.py +687 -0
- pencilmark-1.0.0/src/pencilmark/gen/__init__.py +16 -0
- pencilmark-1.0.0/src/pencilmark/gen/api.py +549 -0
- pencilmark-1.0.0/src/pencilmark/gen/board.py +103 -0
- pencilmark-1.0.0/src/pencilmark/gen/checks.py +102 -0
- pencilmark-1.0.0/src/pencilmark/gen/elements.py +278 -0
- pencilmark-1.0.0/src/pencilmark/gen/search.py +302 -0
- pencilmark-1.0.0/src/pencilmark/interop.py +645 -0
- pencilmark-1.0.0/src/pencilmark/metrics.py +417 -0
- pencilmark-1.0.0/src/pencilmark/model.py +474 -0
- pencilmark-1.0.0/src/pencilmark/publish_flags.py +137 -0
- pencilmark-1.0.0/src/pencilmark/puzzle_json.py +576 -0
- pencilmark-1.0.0/src/pencilmark/scoring.py +534 -0
- pencilmark-1.0.0/src/pencilmark/se.py +37 -0
- pencilmark-1.0.0/src/pencilmark/solver.py +1227 -0
- pencilmark-1.0.0/src/pencilmark/sources/__init__.py +141 -0
- pencilmark-1.0.0/src/pencilmark/sources/_common.py +136 -0
- pencilmark-1.0.0/src/pencilmark/sources/_fpuz_translate.py +366 -0
- pencilmark-1.0.0/src/pencilmark/sources/_sp_translate.py +1174 -0
- pencilmark-1.0.0/src/pencilmark/sources/fpuzzles.py +115 -0
- pencilmark-1.0.0/src/pencilmark/sources/lmd.py +187 -0
- pencilmark-1.0.0/src/pencilmark/sources/penpa.py +471 -0
- pencilmark-1.0.0/src/pencilmark/sources/sudokupad.py +278 -0
- pencilmark-1.0.0/src/pencilmark/spec_format.py +313 -0
- pencilmark-1.0.0/src/pencilmark/usage.py +201 -0
- pencilmark-1.0.0/src/pencilmark/variants.py +566 -0
- pencilmark-1.0.0/src/pencilmark.egg-info/PKG-INFO +236 -0
- pencilmark-1.0.0/src/pencilmark.egg-info/SOURCES.txt +113 -0
- pencilmark-1.0.0/src/pencilmark.egg-info/dependency_links.txt +1 -0
- pencilmark-1.0.0/src/pencilmark.egg-info/entry_points.txt +2 -0
- pencilmark-1.0.0/src/pencilmark.egg-info/requires.txt +8 -0
- pencilmark-1.0.0/src/pencilmark.egg-info/top_level.txt +1 -0
- pencilmark-1.0.0/tests/conftest.py +13 -0
- pencilmark-1.0.0/tests/fixtures/variants.json +7 -0
- pencilmark-1.0.0/tests/smoke.py +20 -0
- pencilmark-1.0.0/tests/test_api.py +252 -0
- pencilmark-1.0.0/tests/test_cli.py +55 -0
- pencilmark-1.0.0/tests/test_format.py +569 -0
- pencilmark-1.0.0/tests/test_generate.py +127 -0
- pencilmark-1.0.0/tests/test_sources.py +74 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
First stable release: the `sudoku-puzzle`, `sudoku-grade` and `sudoku-spec` formats are format_version 1, and the card's
|
|
6
|
+
meaning is set out in docs/PHILOSOPHY.md. Changes since 1.0.0rc2:
|
|
7
|
+
|
|
8
|
+
- Grade card simplified (one meaning per key, docs/PHILOSOPHY.md): `contains_complete` removed (it was band != Unknown;
|
|
9
|
+
`stopped_at_step` still marks where human logic ran out); `time_dist.median` removed (it repeated `time`); steps use
|
|
10
|
+
the same keys as `contains` (`code`, `name`, `band`; `technique` renamed `name`); `alternatives` is `null` when the
|
|
11
|
+
search was skipped (`alternatives=False`) and omitted only when there are none.
|
|
12
|
+
- More text formats: text grids with separators (Simple Sudoku / SadMan / forum style; `to_string(puzzle, "grid")`
|
|
13
|
+
writes them), a trailing `# comment` after a plain string, pencil-mark grids and 729-character candidate strings
|
|
14
|
+
(Sukaku). `rate()` and every entry point accept them as one string.
|
|
15
|
+
- ipuz import: `from_ipuz` reads ipuz 1 / 2 sudoku (boxes, jigsaw zones, diagonals, windows, cages); `to_ipuz` now refuses
|
|
16
|
+
given candidates instead of dropping them.
|
|
17
|
+
- `max_seconds` is now also checked inside the forcing-chain fallback (Easter Monster at `max_seconds=2` returned after
|
|
18
|
+
4.3 s, now 2.0 s); grades without a limit are unchanged.
|
|
19
|
+
- Windows / macOS: `pencilmark rate -j N` hung under the spawn start method (the workers looked for their function in
|
|
20
|
+
pencilmark's `__main__`, which spawn does not re-import); it now passes them from the importable module and reads all
|
|
21
|
+
input first and reads / writes UTF-8 on every platform; the test suite runs on Windows and macOS too.
|
|
22
|
+
- HoDoKu library lines (`:0901:4:<puzzle>:<removed candidates>:...`) are read by `from_string`.
|
|
23
|
+
- `band_index` is no longer a default field (ask for it with `fields=[...]`); `se_diuf` removed (`se` gives the classic
|
|
24
|
+
Sudoku Explainer scale).
|
|
25
|
+
- Rules keep the file's order: `to_export` writes a puzzle file's rules back in the order given, with their `id`, `label`
|
|
26
|
+
and `style` (it used to reorder them and drop those keys), and `issues` count rules in the graded file (`rules`), plus
|
|
27
|
+
their `ids` where present.
|
|
28
|
+
- Quality checks: every entry of `quality` is an object, `{"limit": n, "severity": "reject" | "warning" | "off"}`
|
|
29
|
+
(CLI `--quality trivial.limit=0.5,trial.severity=reject`), so a check's severity can be changed as well as its limit;
|
|
30
|
+
bare numbers and `true` / `false` are no longer accepted. New optional field `warning` (true when any issue is a
|
|
31
|
+
warning), next to `reject`.
|
|
32
|
+
- Format: `ext` removed; data for one program goes in `meta` under a reverse-DNS key, next to the suggested common keys
|
|
33
|
+
(the generator's own knobs too). Reserved items (`"type": "custom"`, `digits` as an object) are refused with a
|
|
34
|
+
message naming them as future work. `neighbours` kinds are documented as exact shortcuts for an operator on a pair
|
|
35
|
+
pattern.
|
|
36
|
+
- Pair operators take their parameter under its own key, as the rest of the format: `{"op": "difference", "value": 3}`,
|
|
37
|
+
`{"op": "sum", "sum": 10}`, `any_of` a list of objects; positional `params` lists are refused. Writing an
|
|
38
|
+
`any_of` relation back no longer crashes.
|
|
39
|
+
- `"type": "table"` and the `allowed` operator are reserved: explicit digit tables made files large and could not be
|
|
40
|
+
drawn; a custom rule type is planned to replace them. Mechanics without a rule family cannot be written in
|
|
41
|
+
format_version 1. The Rossini test puzzle now uses `rossini` rules and `negative` instead of edges and tables.
|
|
42
|
+
- Generator: one vocabulary. `pencilmark.generate(spec)` (also `pencilmark.gen.generate`) takes a `sudoku-spec` request and
|
|
43
|
+
returns a `sudoku-puzzle` (with `meta.generated_from`), honouring the spec's `fields`; the internal Spec dicts
|
|
44
|
+
(`target_band`, `max_givens`, ...) and `spec_from_gen` / `spec_to_gen` are no longer public. `SPEC.md` version 1
|
|
45
|
+
describes exactly what the generator runs; the rest is listed as reserved and refused with a message naming it.
|
|
46
|
+
Example specs that use reserved parts moved to `docs/examples/specs/reserved/`.
|
|
47
|
+
- **Seven bands**: Very Easy (scanning: full house, hidden singles) / Easy (naked singles, pointing / claiming, one rule's
|
|
48
|
+
combinations) / Medium (pairs, triples, quads; variant implications) / Hard (the first learned patterns: X-Wing,
|
|
49
|
+
Skyscraper, 2-String Kite, Turbot Fish, simple colouring) / Very Hard (Empty Rectangle, finned and larger fish, wings,
|
|
50
|
+
uniqueness) / **Expert** (new: XY-Chain, AIC, Medusa, ALS) / Extreme (forcing, trial). The steps common curricula
|
|
51
|
+
take (docs/PHILOSOPHY.md). `band_index` runs 0..6. Difficulty and time are unchanged
|
|
52
|
+
(all 7,427 graded puzzles of the frozen benchmark identical).
|
|
53
|
+
- Step explanations at three levels: `notation` (the solvers' shared form, `X-Wing: 3 r58 c68 => r679c6<>3`), `short`,
|
|
54
|
+
`teach`; `pencilmark rate --report -v / -vv / -vvv` lists the steps at those levels. The report shows time percentiles
|
|
55
|
+
only when asked for (`--fields ...,time_p80`) and names alternatives in full.
|
|
56
|
+
- Singles never carry `uses`; alternatives inside a chip are ordered deterministically (the order depended on Python's
|
|
57
|
+
per-process hashing); `max_seconds` also checked in the generalised fish search.
|
|
58
|
+
- Difficulty below 2: the score map turns gentler there (about the square root of effort, the same value and slope at 2)
|
|
59
|
+
instead of bending into 0, so very easy puzzles keep apart (an easy 4x4 about 0.1-0.4, a nearly finished grid about
|
|
60
|
+
0.2; most used to read 0.0). Scores of 2 and above, every ranking and every time are unchanged.
|
|
61
|
+
- Docs: philosophy page (goals, exact meaning of band / difficulty / time, format rules); README known issues.
|
|
62
|
+
|
|
63
|
+
## 1.0.0rc2
|
|
64
|
+
|
|
65
|
+
- `max_seconds` bounds the whole `rate()` call (uniqueness proof, card, technique list). The band and difficulty route
|
|
66
|
+
runs first, the technique list in the time left. If the limit passes in the route, the card says band `Unknown` with
|
|
67
|
+
`difficulty` / `time` null and the techniques found so far; if uniqueness is not proven in time, `rate` raises
|
|
68
|
+
`Undecided`. New `completed` field whenever `max_seconds` is set.
|
|
69
|
+
- `contains_truncated` removed: `completed: false` already says the limit passed (when it passed only while the technique
|
|
70
|
+
list was being minimised, the grade stands and the list may not be minimal).
|
|
71
|
+
- Optional `elapsed` field (wall-clock seconds for the call); `solve(puzzle, info=True)` returns the solution with its
|
|
72
|
+
seconds; the generator's `info` has `seconds` and `attempts`.
|
|
73
|
+
- Optional `time_dist` field (`{"type": "lognormal", "median", "sigma_players", "sigma_model"}`); `time_p50` accepted.
|
|
74
|
+
- Three variant techniques from how people solve on video: clue facts (`CFX`, Very Easy: what a clue rules out on an
|
|
75
|
+
empty grid, e.g. no 5 on a whisper), clue shape (`CSH`, Easy: a line's shape inside one house, e.g. a whisper 4
|
|
76
|
+
between two 9s) and high/low & odd/even colouring (`CCL`, Medium). Accuracy on human data is unchanged on average
|
|
77
|
+
(kropki ranks better); solving routes are shorter and closer to human ones.
|
|
78
|
+
- Time spread: `time_mean` and any percentile `time_pNN` (e.g. `time_p20`, `time_p80`) on request, over today's players
|
|
79
|
+
(log-normal; sigma 0.79 for classics and multi-grid layouts, 0.62 with any other rule, from two sources' per-player times). `time_p80` was a one-player spread (1.53x) and is now
|
|
80
|
+
the population one (about 1.85x). `time` is the median player.
|
|
81
|
+
- Generator: element varieties (kropki colour, XV letter, odd/even) with counts, ranges and caps; the information-budget
|
|
82
|
+
check uses per-class maximums.
|
|
83
|
+
- `spike` check: a Medium+ puzzle whose 3 hardest steps carry more than 60% of its effort (adjustable, `quality`), as a
|
|
84
|
+
warning; replaces the old "few band moves in an all-singles solve" rule.
|
|
85
|
+
- Cards without `contains` / `issues` skip the technique-list search: about 3x faster for the same band, difficulty and
|
|
86
|
+
time; the generator screens candidates this way.
|
|
87
|
+
- `alternatives=False` / `--no-alternatives`: skip the search for interchangeable techniques (faster; band, difficulty and
|
|
88
|
+
time unchanged). The generator screens candidates this way.
|
|
89
|
+
- Faster cards: the search-effort profile stops trials past the shortest refutation and skips unchanged rules; the
|
|
90
|
+
solution is found once per card.
|
|
91
|
+
- Publishing checks (`issues`, `reject`) are no longer default card fields; ask for them with `fields=[...]`.
|
|
92
|
+
- Docs: the puzzle format (`FORMAT.md`), grade card (`GRADE.md`) and generator spec (`SPEC.md`) are separate
|
|
93
|
+
documents, each saying how to implement a subset; drawing conventions moved out of the format into `STYLE.md`, variant
|
|
94
|
+
naming into `NAMING.md`.
|
|
95
|
+
- Drawing: `docs/palette.json` with the default sizes, light and dark themes and the community line colours; reference
|
|
96
|
+
SVG renderer `tools/render_svg.py`.
|
|
97
|
+
|
|
98
|
+
## 1.0.0rc1
|
|
99
|
+
|
|
100
|
+
First public release.
|
|
101
|
+
|
|
102
|
+
- `rate`, `rate_many`, `solve`, `to_export`, `load`: the "sudoku-grade" card (difficulty, time, band, contents, issues /
|
|
103
|
+
reject; on request time_p80, steps with two explanation levels, Sudoku Explainer-style ratings, describe, symmetry;
|
|
104
|
+
`fields=[...]` computes only the requested fields) for classic and variant sudoku.
|
|
105
|
+
- Publishing checks (`issues`) with adjustable limits: `quality={"trivial": 0.5, "trial": False}` / `--quality`, the same
|
|
106
|
+
object the generator spec's `quality` takes.
|
|
107
|
+
- Command line: `pencilmark rate | solve | json | export | fingerprint | describe` (also `python -m pencilmark`).
|
|
108
|
+
- Exchange format: `sudoku-puzzle` format_version 1, read and written, specified in `docs/FORMAT.md` with a JSON Schema (`docs/sudoku-puzzle.schema.json`):
|
|
109
|
+
every constraint is a rule (houses as `grid` / `box_grid` / `box`, explicit `row` / `column` for bent ones; nothing
|
|
110
|
+
implied), cells named `r1c1`, a rule
|
|
111
|
+
vocabulary of families and kinds with exact semantics and default drawings, outside clues anchored by `side`,
|
|
112
|
+
explicit `negative` rules, strict reading (unknown keys or rule types are refused).
|
|
113
|
+
- `sudoku-spec` generator requests (givens, locked rules and templates, symmetry, `where` card targets, quality,
|
|
114
|
+
limits); `spec_to_gen` runs what the generator supports and refuses the rest with the reason; showcase specs in
|
|
115
|
+
`docs/examples/specs/`.
|
|
116
|
+
- Format functions: `dumps` (canonical text), `fingerprint` (content hash), `validate`, `describe` / `rules_text` (variant
|
|
117
|
+
name and rules from a registry of placement variants and rule families), optional `to_ipuz`, `from_string` / `to_string`
|
|
118
|
+
(plain and sudokuwiki.org strings), `spec_from_gen` / `spec_to_gen` (the `sudoku-spec` generator request).
|
|
119
|
+
- Cards carry `"format_version": 1` and `"grader": {"name": "pencilmark", "version": ...}`.
|
|
120
|
+
- One engine form per mechanic: renban / between / zipper / region-sum constraints as lines, jigsaw pieces as boxes,
|
|
121
|
+
numbered pair clues (difference / sum / product / ratio, mathrax) as 2-cell tables, duplicated houses merged, global entropy and quadruple clues as named rules for
|
|
122
|
+
every importer (docs/ENGINE.md).
|
|
123
|
+
- Puzzle generator (`pencilmark.gen`) with band, difficulty, technique and rule-element targets.
|
|
124
|
+
- Puzzle importers (`pencilmark.sources`) for SudokuPad, f-puzzles, penpa-edit and Logic Masters links.
|
|
125
|
+
- C++ engine built with pybind11; OR-Tools is optional (`pencilmark[verify]`).
|
|
126
|
+
- No guessing by default: where no technique applies, the engine proves an elimination by full search (Trial and error,
|
|
127
|
+
band Extreme), so every uniquely solvable puzzle gets a band; `allow_guessing=True` / `--allow-guessing` guesses there
|
|
128
|
+
instead (band Unknown).
|
|
129
|
+
- Bands: forcing chains / nets, Nishio, pattern overlay and trial and error are Extreme; bounded case splits (either
|
|
130
|
+
way, what if, short trial) and ALS chains / Death Blossom are Very Hard.
|
pencilmark-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pencilmark contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pencilmark
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Human-like difficulty grader, solver and generator for classic and variant sudoku
|
|
5
|
+
Author: pencilmark contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/silly-puppyy/pencilmark
|
|
8
|
+
Project-URL: Documentation, https://silly-puppyy.github.io/pencilmark/
|
|
9
|
+
Project-URL: Source, https://github.com/silly-puppyy/pencilmark
|
|
10
|
+
Project-URL: Issues, https://github.com/silly-puppyy/pencilmark/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/silly-puppyy/pencilmark/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: sudoku,puzzle,difficulty,grader,solver,generator
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: C++
|
|
20
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
21
|
+
Classifier: Operating System :: MacOS
|
|
22
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
23
|
+
Classifier: Topic :: Games/Entertainment :: Puzzle Games
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: lzstring>=1.0.4
|
|
28
|
+
Provides-Extra: verify
|
|
29
|
+
Requires-Dist: ortools>=9.8; extra == "verify"
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
32
|
+
Requires-Dist: jsonschema>=4.18; extra == "test"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# pencilmark
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/pencilmark/)
|
|
38
|
+
[](https://pypi.org/project/pencilmark/)
|
|
39
|
+
[](https://github.com/silly-puppyy/pencilmark/actions/workflows/wheels.yml)
|
|
40
|
+
[](https://github.com/silly-puppyy/pencilmark/blob/main/LICENSE)
|
|
41
|
+
[](https://silly-puppyy.github.io/pencilmark/)
|
|
42
|
+
|
|
43
|
+
pencilmark rates sudoku puzzles the way a human solver experiences them. It solves a puzzle step by step with human
|
|
44
|
+
techniques, easiest first, and reports how hard it is, how long it takes, which band it belongs to and which techniques
|
|
45
|
+
you need. It works for classic sudoku of any standard size and for many variants (killer, thermo, arrow, kropki, XV,
|
|
46
|
+
sandwich, multi-grid boards and more). It also solves puzzles, checks uniqueness, imports puzzles from [SudokuPad](https://sudokupad.app/),
|
|
47
|
+
[f-puzzles](https://www.f-puzzles.com/), [penpa-edit](https://swaroopg92.github.io/penpa-edit/) and [Logic Masters](https://logic-masters.de/) links, and generates new puzzles to a target band or difficulty.
|
|
48
|
+
|
|
49
|
+
The engine is written in C++ ([pybind11](https://github.com/pybind/pybind11)). Results are deterministic: the same puzzle always gets the same card.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
pip install pencilmark
|
|
55
|
+
pip install "pencilmark[verify]" # optional: OR-Tools CP-SAT as a fallback uniqueness checker
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Python 3.10 or newer. Building from source needs a C++17 compiler.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import pencilmark
|
|
64
|
+
|
|
65
|
+
card = pencilmark.rate("....4.8.3.6...8..29.86.5..1235.........7..2.........96..2......8.12..7...9.......")
|
|
66
|
+
card["band"], card["difficulty"], card["time"] # ('Hard', 5.9, 10.7)
|
|
67
|
+
|
|
68
|
+
card = pencilmark.rate(puzzle, steps=True) # add the step-by-step solve with explanations
|
|
69
|
+
cards = pencilmark.rate_many(puzzles, jobs=4) # several puzzles in parallel, input order kept
|
|
70
|
+
sol = pencilmark.solve(puzzle) # list of digits; raises NotUnique / NoSolution
|
|
71
|
+
exp = pencilmark.to_export(puzzle, title="My puzzle") # "sudoku-puzzle" exchange format (docs/FORMAT.md)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A puzzle can be a classic string (`.` or `0` for blanks), a `sudoku-puzzle` dict, a puzzle JSON dict or string, or a
|
|
75
|
+
path to a `.json` file.
|
|
76
|
+
|
|
77
|
+
Command line:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
$ echo '....4.8.3.6...8..29.86.5..1235.........7..2.........96..2......8.12..7...9.......' | pencilmark rate - --report
|
|
81
|
+
#1 Hard - difficulty 5.9 - time 10.7 min
|
|
82
|
+
contains: Naked Pair (or Hidden Pair), X-Wing (or 2-String Kite / Simple Colouring / Short chain in the head)
|
|
83
|
+
|
|
84
|
+
pencilmark rate puzzles.txt --json # full card per line (python -m pencilmark works too)
|
|
85
|
+
pencilmark solve puzzles.txt # the solution, or 'not unique' / 'no solution'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
More in [docs/USAGE.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/USAGE.md).
|
|
89
|
+
|
|
90
|
+
## The card
|
|
91
|
+
|
|
92
|
+
`rate` returns a dict (format `"sudoku-grade"`) with these fields by default (`grader`, `format`, `format_version` are
|
|
93
|
+
always there); `rate(puzzle, fields=[...])` / `--fields` asks for others by their key names, and only requested fields
|
|
94
|
+
are computed ([docs/GRADE.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/GRADE.md)):
|
|
95
|
+
|
|
96
|
+
| Field | Meaning |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `difficulty` | how hard each step feels, continuous, above 0 (a nearly finished grid is about 0.2), no ceiling; a typical newspaper medium is about 5 |
|
|
99
|
+
| `time` | expected minutes for the median player (on request `time_mean` or any percentile `time_pNN`, e.g. `time_p80`: 80% of players finish within it) |
|
|
100
|
+
| `band` | Very Easy, Easy, Medium, Hard, Very Hard, Expert, Extreme; `"Unknown"` when guessing was allowed and used, or `max_seconds` passed (on request `band_index` 0..6) |
|
|
101
|
+
| `contains` | techniques met going easy to hard: `code`, `chip`, `name`, `band`, `uses`, `alternatives`; with `stopped_at_step` when human logic ran out |
|
|
102
|
+
| on request | `issues` / `reject` (publishing checks, below), `time_mean` / `time_pNN`, `steps` (technique, placement `r2c8=3` or eliminations `r4c5#7`; `explain="notation"` / `"short"` / `"teach"` adds explanations, from `X-Wing: 3 r58 c68 => r679c6<>3` to a short lesson), `se`, `band_index`, `describe`, `symmetry` |
|
|
103
|
+
|
|
104
|
+
A chip like `XWG/KTE/SCL` means any one of those techniques gets you through at that point.
|
|
105
|
+
|
|
106
|
+
Publishing checks are off unless you ask for them. They flag puzzles a publisher would usually not pick: a difficulty
|
|
107
|
+
spike, a rule that is never used, too many givens, ... (`code`, `severity` reject / warning, `text`, and `rules`: the
|
|
108
|
+
offending rules' indices in the exported puzzle); `reject` is true when any issue is a reject. Each check's limit is
|
|
109
|
+
adjustable:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
card = pencilmark.rate(puzzle, fields=["difficulty", "band", "issues", "reject"], quality={"trivial": {"limit": 0.4}, "trial": {"severity": "off"}})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Guessing is off by default: where no technique applies, the engine proves an elimination by full search (Trial and
|
|
116
|
+
error, band Extreme), so every uniquely solvable puzzle gets a band. `allow_guessing=True` (CLI `--allow-guessing`) lets it
|
|
117
|
+
guess there instead, which is faster on the hardest puzzles; the card then says Unknown, with
|
|
118
|
+
`stopped_at_step`. With `max_seconds` set, the whole call is capped and the card
|
|
119
|
+
says `completed: false` if the limit passed. A puzzle without exactly one solution has no card: `rate` raises `NotUnique` /
|
|
120
|
+
`NoSolution`.
|
|
121
|
+
|
|
122
|
+
## Exchange format
|
|
123
|
+
|
|
124
|
+
`to_export` writes, and every entry point reads, the `"sudoku-puzzle"` format, **format_version 1**, specified in
|
|
125
|
+
[docs/FORMAT.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/FORMAT.md) (JSON Schema: [docs/sudoku-puzzle.schema.json](https://github.com/silly-puppyy/pencilmark/blob/main/docs/sudoku-puzzle.schema.json)):
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{"format": "sudoku-puzzle", "format_version": 1,
|
|
129
|
+
"meta": {"title": "Heartstring", "author": "...", "source": "example.org", "type": "Killer Sudoku"},
|
|
130
|
+
"board": {"rows": 9, "cols": 9},
|
|
131
|
+
"digits": 9,
|
|
132
|
+
"givens": [0, 0, 5, 4, 0, ...],
|
|
133
|
+
"solution": [6, 1, 5, 4, 2, ...],
|
|
134
|
+
"rules": [
|
|
135
|
+
{"type": "box_grid", "origin": "r1c1", "box": [3, 3]},
|
|
136
|
+
{"type": "cage", "sum": 15, "cells": ["r1c4", "r1c5", "r2c4"]},
|
|
137
|
+
{"type": "edge", "kind": "kropki", "color": "white", "cells": ["r5c5", "r5c6"]}]}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- Every constraint is a rule, houses included: a `grid` gives a sudoku grid's rows and columns, a `box_grid` adds its
|
|
141
|
+
regular boxes, so a classic is one `box_grid` and a jigsaw is a `grid` plus `box` rules. Nothing is implied. Cells are
|
|
142
|
+
named `r{row}c{col}`, 1-based.
|
|
143
|
+
`givens`: `0` = empty, `null` = no cell (multi-grid gaps).
|
|
144
|
+
- Each rule has one exact meaning and a default drawing, so a renderer needs nothing else; `style` and `decorations`
|
|
145
|
+
are cosmetic only. `meta` is free-form information that never changes solving.
|
|
146
|
+
- Strict reading: an unknown key or rule type makes the reader refuse the file (`FormatError`) instead of dropping a
|
|
147
|
+
rule.
|
|
148
|
+
- `pencilmark.dumps` writes the canonical text (one grid row per line, one rule per line); `pencilmark.fingerprint`
|
|
149
|
+
gives a content hash that ignores metadata and styling; `pencilmark.describe` names the variant and lists its rules.
|
|
150
|
+
|
|
151
|
+
### Other formats
|
|
152
|
+
|
|
153
|
+
For convenience pencilmark also reads and writes [sudokuwiki.org](https://www.sudokuwiki.org/) packed strings, text grids and [ipuz](http://www.ipuz.org/), reads pencil-mark grids, and loads SudokuPad,
|
|
154
|
+
f-puzzles, [Penpa](https://swaroopg92.github.io/penpa-edit/)+ and Logic Masters links. None of these is needed to use the `sudoku-puzzle` format; see
|
|
155
|
+
[docs/INTEROP.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/INTEROP.md). How rules map onto the engine, and how to add one, is in
|
|
156
|
+
[docs/ENGINE.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/ENGINE.md).
|
|
157
|
+
|
|
158
|
+
## Supported rules
|
|
159
|
+
|
|
160
|
+
Classic sudoku from 4x4 to 25x25, irregular (jigsaw) regions, diagonals and extra regions, multi-grid layouts (samurai,
|
|
161
|
+
twodoku, butterfly, windmill and more), killer cages, little killers, sandwich, X-sums, skyscrapers, frame sums,
|
|
162
|
+
arrows, thermometers (normal and slow), German and Dutch whispers, renban, region-sum, palindrome, zipper, between,
|
|
163
|
+
entropic, modular and parity lines, kropki, XV, greater-than, difference / ratio dots, anti-knight, anti-king,
|
|
164
|
+
non-consecutive, odd / even, low / high, Rossini, Vudoku, equal sums, counting rules, and given candidates (Sukaku).
|
|
165
|
+
Rules that cannot be translated exactly are refused, never dropped silently.
|
|
166
|
+
|
|
167
|
+
## Generator
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
r = pencilmark.generate({"format": "sudoku-spec", "format_version": 1,
|
|
171
|
+
"board": {"rows": 9, "cols": 9}, "digits": 9,
|
|
172
|
+
"rules": [{"type": "box_grid", "origin": "r1c1", "box": [3, 3]}],
|
|
173
|
+
"where": {"band": "Hard"}})
|
|
174
|
+
r["puzzle"], r["card"], r["info"] # sudoku-puzzle, its card, run details
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The request is a `sudoku-spec` ([docs/SPEC.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/SPEC.md)).
|
|
178
|
+
|
|
179
|
+
Targets: band, difficulty range, required techniques, symmetry, given counts, and rule elements (cages, thermos,
|
|
180
|
+
whispers, renban, between lines, arrows, kropki, XV, parity). Every result is checked for uniqueness. Multi-grid boards
|
|
181
|
+
are not generated.
|
|
182
|
+
|
|
183
|
+
## Puzzle sources
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from pencilmark import sources
|
|
187
|
+
pj = sources.load("https://sudokupad.app/...") # SudokuPad, f-puzzles, penpa-edit, Logic Masters, short links
|
|
188
|
+
card = pencilmark.rate(pj)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Loaders that fetch pages identify themselves, obey robots.txt and pause between requests. Override the user agent with
|
|
192
|
+
the `PENCILMARK_UA` environment variable.
|
|
193
|
+
|
|
194
|
+
## Limitations
|
|
195
|
+
|
|
196
|
+
- Bands and difficulty measure different things. The band is the hardest technique a puzzle needs; difficulty is how
|
|
197
|
+
hard it is overall. A puzzle that needs one X-Wing (Hard) is often easier than a long Medium one, and solvers agree
|
|
198
|
+
(docs/PHILOSOPHY.md). This is by design, not an error.
|
|
199
|
+
- Difficulty and time are models fitted to human solving data. Differences under one point of difficulty are not
|
|
200
|
+
meaningful, and a single solve can vary a lot from the predicted time.
|
|
201
|
+
- Bands depend on the technique catalogue; a puzzle that needs a technique the engine does not have is solved with
|
|
202
|
+
Trial and error at that point and rated Extreme.
|
|
203
|
+
- Very hard puzzles with many-cell rules (killers, arrows, palindromes, thermos) can take minutes while the contents list
|
|
204
|
+
is minimised; use `max_seconds` to cap it (the card then says `completed: false`).
|
|
205
|
+
- The Sudoku Explainer-style ratings (`se=True`) are approximate and meaningful for classic sudoku only.
|
|
206
|
+
|
|
207
|
+
## Benchmark
|
|
208
|
+
|
|
209
|
+
On a frozen held-out set of 7,623 puzzles with real human solve data from seven sources, pencilmark's difficulty ranks
|
|
210
|
+
puzzles by human solve time at Spearman 0.61 to 0.80 within each group, closer than four established graders on the same
|
|
211
|
+
classics. Details: [docs/BENCHMARK.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/BENCHMARK.md).
|
|
212
|
+
|
|
213
|
+
## Known issues
|
|
214
|
+
|
|
215
|
+
Being worked on after 1.0:
|
|
216
|
+
|
|
217
|
+
- **Speed.** Grading takes a median 0.5 s per puzzle, but the slowest few percent take 10 s to minutes. On the
|
|
218
|
+
benchmark's classics: pencilmark 0.04 s, Rangsk's solver 0.09 s, Sudoku Explainer and HoDoKu under a millisecond (they do
|
|
219
|
+
not model human solving); on variants: pencilmark 0.49 s, Rangsk's solver 0.13 s. Use `max_seconds` to bound a call.
|
|
220
|
+
- **Generator speed and range.** About 20-70 s per puzzle that meets a spec (HoDoKu: 0.05-1 s), and classic Extreme puzzles
|
|
221
|
+
are not reached within the default 120 s. A maximum on the givens count is not always enforced.
|
|
222
|
+
- **Variants on one scale.** On classics, difficulty matches established ratings of the same puzzles; across variant
|
|
223
|
+
types the same difficulty does not yet mean the same experience (puzzle types differ by about 2 points on average, and
|
|
224
|
+
variants often score above the classic range of their band). Time is most off for the same reason (about 1.4x per
|
|
225
|
+
puzzle type).
|
|
226
|
+
- **The ends of the scale.** Between about 2 and 8, each point of difficulty means about 20-40% more solving time per
|
|
227
|
+
cell. Above about 8 a point is worth less time (5-15% on three sources). Below 2 the scale is gentler (about the square root of
|
|
228
|
+
effort, so it never goes negative); there it predicts solve times about 1.5% worse than a pure log scale would.
|
|
229
|
+
- **Uniqueness on some killers.** A few large killers can raise `Undecided` without OR-Tools (`pip install
|
|
230
|
+
"pencilmark[verify]"`) although human logic solves them.
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT, see [LICENSE](https://github.com/silly-puppyy/pencilmark/blob/main/LICENSE).
|
|
235
|
+
|
|
236
|
+
pencilmark was written with [Claude](https://claude.ai).
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# pencilmark
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pencilmark/)
|
|
4
|
+
[](https://pypi.org/project/pencilmark/)
|
|
5
|
+
[](https://github.com/silly-puppyy/pencilmark/actions/workflows/wheels.yml)
|
|
6
|
+
[](https://github.com/silly-puppyy/pencilmark/blob/main/LICENSE)
|
|
7
|
+
[](https://silly-puppyy.github.io/pencilmark/)
|
|
8
|
+
|
|
9
|
+
pencilmark rates sudoku puzzles the way a human solver experiences them. It solves a puzzle step by step with human
|
|
10
|
+
techniques, easiest first, and reports how hard it is, how long it takes, which band it belongs to and which techniques
|
|
11
|
+
you need. It works for classic sudoku of any standard size and for many variants (killer, thermo, arrow, kropki, XV,
|
|
12
|
+
sandwich, multi-grid boards and more). It also solves puzzles, checks uniqueness, imports puzzles from [SudokuPad](https://sudokupad.app/),
|
|
13
|
+
[f-puzzles](https://www.f-puzzles.com/), [penpa-edit](https://swaroopg92.github.io/penpa-edit/) and [Logic Masters](https://logic-masters.de/) links, and generates new puzzles to a target band or difficulty.
|
|
14
|
+
|
|
15
|
+
The engine is written in C++ ([pybind11](https://github.com/pybind/pybind11)). Results are deterministic: the same puzzle always gets the same card.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pip install pencilmark
|
|
21
|
+
pip install "pencilmark[verify]" # optional: OR-Tools CP-SAT as a fallback uniqueness checker
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Python 3.10 or newer. Building from source needs a C++17 compiler.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import pencilmark
|
|
30
|
+
|
|
31
|
+
card = pencilmark.rate("....4.8.3.6...8..29.86.5..1235.........7..2.........96..2......8.12..7...9.......")
|
|
32
|
+
card["band"], card["difficulty"], card["time"] # ('Hard', 5.9, 10.7)
|
|
33
|
+
|
|
34
|
+
card = pencilmark.rate(puzzle, steps=True) # add the step-by-step solve with explanations
|
|
35
|
+
cards = pencilmark.rate_many(puzzles, jobs=4) # several puzzles in parallel, input order kept
|
|
36
|
+
sol = pencilmark.solve(puzzle) # list of digits; raises NotUnique / NoSolution
|
|
37
|
+
exp = pencilmark.to_export(puzzle, title="My puzzle") # "sudoku-puzzle" exchange format (docs/FORMAT.md)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A puzzle can be a classic string (`.` or `0` for blanks), a `sudoku-puzzle` dict, a puzzle JSON dict or string, or a
|
|
41
|
+
path to a `.json` file.
|
|
42
|
+
|
|
43
|
+
Command line:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
$ echo '....4.8.3.6...8..29.86.5..1235.........7..2.........96..2......8.12..7...9.......' | pencilmark rate - --report
|
|
47
|
+
#1 Hard - difficulty 5.9 - time 10.7 min
|
|
48
|
+
contains: Naked Pair (or Hidden Pair), X-Wing (or 2-String Kite / Simple Colouring / Short chain in the head)
|
|
49
|
+
|
|
50
|
+
pencilmark rate puzzles.txt --json # full card per line (python -m pencilmark works too)
|
|
51
|
+
pencilmark solve puzzles.txt # the solution, or 'not unique' / 'no solution'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
More in [docs/USAGE.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/USAGE.md).
|
|
55
|
+
|
|
56
|
+
## The card
|
|
57
|
+
|
|
58
|
+
`rate` returns a dict (format `"sudoku-grade"`) with these fields by default (`grader`, `format`, `format_version` are
|
|
59
|
+
always there); `rate(puzzle, fields=[...])` / `--fields` asks for others by their key names, and only requested fields
|
|
60
|
+
are computed ([docs/GRADE.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/GRADE.md)):
|
|
61
|
+
|
|
62
|
+
| Field | Meaning |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `difficulty` | how hard each step feels, continuous, above 0 (a nearly finished grid is about 0.2), no ceiling; a typical newspaper medium is about 5 |
|
|
65
|
+
| `time` | expected minutes for the median player (on request `time_mean` or any percentile `time_pNN`, e.g. `time_p80`: 80% of players finish within it) |
|
|
66
|
+
| `band` | Very Easy, Easy, Medium, Hard, Very Hard, Expert, Extreme; `"Unknown"` when guessing was allowed and used, or `max_seconds` passed (on request `band_index` 0..6) |
|
|
67
|
+
| `contains` | techniques met going easy to hard: `code`, `chip`, `name`, `band`, `uses`, `alternatives`; with `stopped_at_step` when human logic ran out |
|
|
68
|
+
| on request | `issues` / `reject` (publishing checks, below), `time_mean` / `time_pNN`, `steps` (technique, placement `r2c8=3` or eliminations `r4c5#7`; `explain="notation"` / `"short"` / `"teach"` adds explanations, from `X-Wing: 3 r58 c68 => r679c6<>3` to a short lesson), `se`, `band_index`, `describe`, `symmetry` |
|
|
69
|
+
|
|
70
|
+
A chip like `XWG/KTE/SCL` means any one of those techniques gets you through at that point.
|
|
71
|
+
|
|
72
|
+
Publishing checks are off unless you ask for them. They flag puzzles a publisher would usually not pick: a difficulty
|
|
73
|
+
spike, a rule that is never used, too many givens, ... (`code`, `severity` reject / warning, `text`, and `rules`: the
|
|
74
|
+
offending rules' indices in the exported puzzle); `reject` is true when any issue is a reject. Each check's limit is
|
|
75
|
+
adjustable:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
card = pencilmark.rate(puzzle, fields=["difficulty", "band", "issues", "reject"], quality={"trivial": {"limit": 0.4}, "trial": {"severity": "off"}})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Guessing is off by default: where no technique applies, the engine proves an elimination by full search (Trial and
|
|
82
|
+
error, band Extreme), so every uniquely solvable puzzle gets a band. `allow_guessing=True` (CLI `--allow-guessing`) lets it
|
|
83
|
+
guess there instead, which is faster on the hardest puzzles; the card then says Unknown, with
|
|
84
|
+
`stopped_at_step`. With `max_seconds` set, the whole call is capped and the card
|
|
85
|
+
says `completed: false` if the limit passed. A puzzle without exactly one solution has no card: `rate` raises `NotUnique` /
|
|
86
|
+
`NoSolution`.
|
|
87
|
+
|
|
88
|
+
## Exchange format
|
|
89
|
+
|
|
90
|
+
`to_export` writes, and every entry point reads, the `"sudoku-puzzle"` format, **format_version 1**, specified in
|
|
91
|
+
[docs/FORMAT.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/FORMAT.md) (JSON Schema: [docs/sudoku-puzzle.schema.json](https://github.com/silly-puppyy/pencilmark/blob/main/docs/sudoku-puzzle.schema.json)):
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{"format": "sudoku-puzzle", "format_version": 1,
|
|
95
|
+
"meta": {"title": "Heartstring", "author": "...", "source": "example.org", "type": "Killer Sudoku"},
|
|
96
|
+
"board": {"rows": 9, "cols": 9},
|
|
97
|
+
"digits": 9,
|
|
98
|
+
"givens": [0, 0, 5, 4, 0, ...],
|
|
99
|
+
"solution": [6, 1, 5, 4, 2, ...],
|
|
100
|
+
"rules": [
|
|
101
|
+
{"type": "box_grid", "origin": "r1c1", "box": [3, 3]},
|
|
102
|
+
{"type": "cage", "sum": 15, "cells": ["r1c4", "r1c5", "r2c4"]},
|
|
103
|
+
{"type": "edge", "kind": "kropki", "color": "white", "cells": ["r5c5", "r5c6"]}]}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- Every constraint is a rule, houses included: a `grid` gives a sudoku grid's rows and columns, a `box_grid` adds its
|
|
107
|
+
regular boxes, so a classic is one `box_grid` and a jigsaw is a `grid` plus `box` rules. Nothing is implied. Cells are
|
|
108
|
+
named `r{row}c{col}`, 1-based.
|
|
109
|
+
`givens`: `0` = empty, `null` = no cell (multi-grid gaps).
|
|
110
|
+
- Each rule has one exact meaning and a default drawing, so a renderer needs nothing else; `style` and `decorations`
|
|
111
|
+
are cosmetic only. `meta` is free-form information that never changes solving.
|
|
112
|
+
- Strict reading: an unknown key or rule type makes the reader refuse the file (`FormatError`) instead of dropping a
|
|
113
|
+
rule.
|
|
114
|
+
- `pencilmark.dumps` writes the canonical text (one grid row per line, one rule per line); `pencilmark.fingerprint`
|
|
115
|
+
gives a content hash that ignores metadata and styling; `pencilmark.describe` names the variant and lists its rules.
|
|
116
|
+
|
|
117
|
+
### Other formats
|
|
118
|
+
|
|
119
|
+
For convenience pencilmark also reads and writes [sudokuwiki.org](https://www.sudokuwiki.org/) packed strings, text grids and [ipuz](http://www.ipuz.org/), reads pencil-mark grids, and loads SudokuPad,
|
|
120
|
+
f-puzzles, [Penpa](https://swaroopg92.github.io/penpa-edit/)+ and Logic Masters links. None of these is needed to use the `sudoku-puzzle` format; see
|
|
121
|
+
[docs/INTEROP.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/INTEROP.md). How rules map onto the engine, and how to add one, is in
|
|
122
|
+
[docs/ENGINE.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/ENGINE.md).
|
|
123
|
+
|
|
124
|
+
## Supported rules
|
|
125
|
+
|
|
126
|
+
Classic sudoku from 4x4 to 25x25, irregular (jigsaw) regions, diagonals and extra regions, multi-grid layouts (samurai,
|
|
127
|
+
twodoku, butterfly, windmill and more), killer cages, little killers, sandwich, X-sums, skyscrapers, frame sums,
|
|
128
|
+
arrows, thermometers (normal and slow), German and Dutch whispers, renban, region-sum, palindrome, zipper, between,
|
|
129
|
+
entropic, modular and parity lines, kropki, XV, greater-than, difference / ratio dots, anti-knight, anti-king,
|
|
130
|
+
non-consecutive, odd / even, low / high, Rossini, Vudoku, equal sums, counting rules, and given candidates (Sukaku).
|
|
131
|
+
Rules that cannot be translated exactly are refused, never dropped silently.
|
|
132
|
+
|
|
133
|
+
## Generator
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
r = pencilmark.generate({"format": "sudoku-spec", "format_version": 1,
|
|
137
|
+
"board": {"rows": 9, "cols": 9}, "digits": 9,
|
|
138
|
+
"rules": [{"type": "box_grid", "origin": "r1c1", "box": [3, 3]}],
|
|
139
|
+
"where": {"band": "Hard"}})
|
|
140
|
+
r["puzzle"], r["card"], r["info"] # sudoku-puzzle, its card, run details
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The request is a `sudoku-spec` ([docs/SPEC.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/SPEC.md)).
|
|
144
|
+
|
|
145
|
+
Targets: band, difficulty range, required techniques, symmetry, given counts, and rule elements (cages, thermos,
|
|
146
|
+
whispers, renban, between lines, arrows, kropki, XV, parity). Every result is checked for uniqueness. Multi-grid boards
|
|
147
|
+
are not generated.
|
|
148
|
+
|
|
149
|
+
## Puzzle sources
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from pencilmark import sources
|
|
153
|
+
pj = sources.load("https://sudokupad.app/...") # SudokuPad, f-puzzles, penpa-edit, Logic Masters, short links
|
|
154
|
+
card = pencilmark.rate(pj)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Loaders that fetch pages identify themselves, obey robots.txt and pause between requests. Override the user agent with
|
|
158
|
+
the `PENCILMARK_UA` environment variable.
|
|
159
|
+
|
|
160
|
+
## Limitations
|
|
161
|
+
|
|
162
|
+
- Bands and difficulty measure different things. The band is the hardest technique a puzzle needs; difficulty is how
|
|
163
|
+
hard it is overall. A puzzle that needs one X-Wing (Hard) is often easier than a long Medium one, and solvers agree
|
|
164
|
+
(docs/PHILOSOPHY.md). This is by design, not an error.
|
|
165
|
+
- Difficulty and time are models fitted to human solving data. Differences under one point of difficulty are not
|
|
166
|
+
meaningful, and a single solve can vary a lot from the predicted time.
|
|
167
|
+
- Bands depend on the technique catalogue; a puzzle that needs a technique the engine does not have is solved with
|
|
168
|
+
Trial and error at that point and rated Extreme.
|
|
169
|
+
- Very hard puzzles with many-cell rules (killers, arrows, palindromes, thermos) can take minutes while the contents list
|
|
170
|
+
is minimised; use `max_seconds` to cap it (the card then says `completed: false`).
|
|
171
|
+
- The Sudoku Explainer-style ratings (`se=True`) are approximate and meaningful for classic sudoku only.
|
|
172
|
+
|
|
173
|
+
## Benchmark
|
|
174
|
+
|
|
175
|
+
On a frozen held-out set of 7,623 puzzles with real human solve data from seven sources, pencilmark's difficulty ranks
|
|
176
|
+
puzzles by human solve time at Spearman 0.61 to 0.80 within each group, closer than four established graders on the same
|
|
177
|
+
classics. Details: [docs/BENCHMARK.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/BENCHMARK.md).
|
|
178
|
+
|
|
179
|
+
## Known issues
|
|
180
|
+
|
|
181
|
+
Being worked on after 1.0:
|
|
182
|
+
|
|
183
|
+
- **Speed.** Grading takes a median 0.5 s per puzzle, but the slowest few percent take 10 s to minutes. On the
|
|
184
|
+
benchmark's classics: pencilmark 0.04 s, Rangsk's solver 0.09 s, Sudoku Explainer and HoDoKu under a millisecond (they do
|
|
185
|
+
not model human solving); on variants: pencilmark 0.49 s, Rangsk's solver 0.13 s. Use `max_seconds` to bound a call.
|
|
186
|
+
- **Generator speed and range.** About 20-70 s per puzzle that meets a spec (HoDoKu: 0.05-1 s), and classic Extreme puzzles
|
|
187
|
+
are not reached within the default 120 s. A maximum on the givens count is not always enforced.
|
|
188
|
+
- **Variants on one scale.** On classics, difficulty matches established ratings of the same puzzles; across variant
|
|
189
|
+
types the same difficulty does not yet mean the same experience (puzzle types differ by about 2 points on average, and
|
|
190
|
+
variants often score above the classic range of their band). Time is most off for the same reason (about 1.4x per
|
|
191
|
+
puzzle type).
|
|
192
|
+
- **The ends of the scale.** Between about 2 and 8, each point of difficulty means about 20-40% more solving time per
|
|
193
|
+
cell. Above about 8 a point is worth less time (5-15% on three sources). Below 2 the scale is gentler (about the square root of
|
|
194
|
+
effort, so it never goes negative); there it predicts solve times about 1.5% worse than a pure log scale would.
|
|
195
|
+
- **Uniqueness on some killers.** A few large killers can raise `Undecided` without OR-Tools (`pip install
|
|
196
|
+
"pencilmark[verify]"`) although human logic solves them.
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT, see [LICENSE](https://github.com/silly-puppyy/pencilmark/blob/main/LICENSE).
|
|
201
|
+
|
|
202
|
+
pencilmark was written with [Claude](https://claude.ai).
|