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.
Files changed (115) hide show
  1. pencilmark-1.0.0/CHANGELOG.md +130 -0
  2. pencilmark-1.0.0/LICENSE +21 -0
  3. pencilmark-1.0.0/MANIFEST.in +6 -0
  4. pencilmark-1.0.0/PKG-INFO +236 -0
  5. pencilmark-1.0.0/README.md +202 -0
  6. pencilmark-1.0.0/csrc/bindings.cpp +996 -0
  7. pencilmark-1.0.0/csrc/canon.cpp +182 -0
  8. pencilmark-1.0.0/csrc/chains.cpp +767 -0
  9. pencilmark-1.0.0/csrc/colour.cpp +1636 -0
  10. pencilmark-1.0.0/csrc/deadline.hpp +25 -0
  11. pencilmark-1.0.0/csrc/desc.cpp +355 -0
  12. pencilmark-1.0.0/csrc/explain.cpp +297 -0
  13. pencilmark-1.0.0/csrc/extra.cpp +2297 -0
  14. pencilmark-1.0.0/csrc/forcing.cpp +434 -0
  15. pencilmark-1.0.0/csrc/human.cpp +1529 -0
  16. pencilmark-1.0.0/csrc/mental.cpp +933 -0
  17. pencilmark-1.0.0/csrc/model.cpp +1270 -0
  18. pencilmark-1.0.0/csrc/model.hpp +171 -0
  19. pencilmark-1.0.0/csrc/nary.cpp +1096 -0
  20. pencilmark-1.0.0/csrc/nary.hpp +123 -0
  21. pencilmark-1.0.0/csrc/portable.hpp +46 -0
  22. pencilmark-1.0.0/csrc/pycompat.hpp +225 -0
  23. pencilmark-1.0.0/csrc/refute.cpp +468 -0
  24. pencilmark-1.0.0/csrc/rich.cpp +523 -0
  25. pencilmark-1.0.0/csrc/rollplace.cpp +199 -0
  26. pencilmark-1.0.0/csrc/solver.cpp +3663 -0
  27. pencilmark-1.0.0/csrc/solver.hpp +764 -0
  28. pencilmark-1.0.0/csrc/sudokuwiki.cpp +631 -0
  29. pencilmark-1.0.0/docs/BENCHMARK.md +115 -0
  30. pencilmark-1.0.0/docs/ENGINE.md +68 -0
  31. pencilmark-1.0.0/docs/FORMAT.md +790 -0
  32. pencilmark-1.0.0/docs/GRADE.md +120 -0
  33. pencilmark-1.0.0/docs/INTEROP.md +112 -0
  34. pencilmark-1.0.0/docs/NAMING.md +83 -0
  35. pencilmark-1.0.0/docs/PHILOSOPHY.md +125 -0
  36. pencilmark-1.0.0/docs/SPEC.md +104 -0
  37. pencilmark-1.0.0/docs/STYLE.md +67 -0
  38. pencilmark-1.0.0/docs/USAGE.md +268 -0
  39. pencilmark-1.0.0/docs/api.md +161 -0
  40. pencilmark-1.0.0/docs/examples/specs/README.md +17 -0
  41. pencilmark-1.0.0/docs/examples/specs/giant_16x16_32cages.json +34 -0
  42. pencilmark-1.0.0/docs/examples/specs/killer_whispers_2givens.json +56 -0
  43. pencilmark-1.0.0/docs/examples/specs/nine_variants_9x9.json +103 -0
  44. pencilmark-1.0.0/docs/examples/specs/reserved/displaced_jigsaw.json +36 -0
  45. pencilmark-1.0.0/docs/examples/specs/reserved/hardest_0clue_6x6_kropki.json +39 -0
  46. pencilmark-1.0.0/docs/examples/specs/reserved/one_xwing_classic.json +49 -0
  47. pencilmark-1.0.0/docs/examples/specs/reserved/picture_mask_heart.json +116 -0
  48. pencilmark-1.0.0/docs/examples/specs/reserved/super_samurai_variants.json +391 -0
  49. pencilmark-1.0.0/docs/examples/specs/zero_clue_6x6_kropki_today.json +41 -0
  50. pencilmark-1.0.0/docs/palette.json +92 -0
  51. pencilmark-1.0.0/docs/sudoku-grade.schema.json +353 -0
  52. pencilmark-1.0.0/docs/sudoku-puzzle.schema.json +1602 -0
  53. pencilmark-1.0.0/pyproject.toml +66 -0
  54. pencilmark-1.0.0/setup.cfg +4 -0
  55. pencilmark-1.0.0/setup.py +62 -0
  56. pencilmark-1.0.0/src/pencilmark/__init__.py +11 -0
  57. pencilmark-1.0.0/src/pencilmark/__main__.py +265 -0
  58. pencilmark-1.0.0/src/pencilmark/_data.py +38 -0
  59. pencilmark-1.0.0/src/pencilmark/api.py +285 -0
  60. pencilmark-1.0.0/src/pencilmark/band_path.py +695 -0
  61. pencilmark-1.0.0/src/pencilmark/card.py +242 -0
  62. pencilmark-1.0.0/src/pencilmark/card_score.py +254 -0
  63. pencilmark-1.0.0/src/pencilmark/cbackend.py +1000 -0
  64. pencilmark-1.0.0/src/pencilmark/cpsat_verify.py +339 -0
  65. pencilmark-1.0.0/src/pencilmark/data/card_calibration.json +1 -0
  66. pencilmark-1.0.0/src/pencilmark/data/card_score.json +27 -0
  67. pencilmark-1.0.0/src/pencilmark/data/explain_links.json +372 -0
  68. pencilmark-1.0.0/src/pencilmark/data/layouts_sudoku_one.json +510 -0
  69. pencilmark-1.0.0/src/pencilmark/data/params.json +227 -0
  70. pencilmark-1.0.0/src/pencilmark/data/price_params.json +227 -0
  71. pencilmark-1.0.0/src/pencilmark/data/technique_bands.csv +88 -0
  72. pencilmark-1.0.0/src/pencilmark/data/tiers.json +115 -0
  73. pencilmark-1.0.0/src/pencilmark/data/time_model.json +26 -0
  74. pencilmark-1.0.0/src/pencilmark/exchange.py +2103 -0
  75. pencilmark-1.0.0/src/pencilmark/explain.py +5850 -0
  76. pencilmark-1.0.0/src/pencilmark/export.py +687 -0
  77. pencilmark-1.0.0/src/pencilmark/gen/__init__.py +16 -0
  78. pencilmark-1.0.0/src/pencilmark/gen/api.py +549 -0
  79. pencilmark-1.0.0/src/pencilmark/gen/board.py +103 -0
  80. pencilmark-1.0.0/src/pencilmark/gen/checks.py +102 -0
  81. pencilmark-1.0.0/src/pencilmark/gen/elements.py +278 -0
  82. pencilmark-1.0.0/src/pencilmark/gen/search.py +302 -0
  83. pencilmark-1.0.0/src/pencilmark/interop.py +645 -0
  84. pencilmark-1.0.0/src/pencilmark/metrics.py +417 -0
  85. pencilmark-1.0.0/src/pencilmark/model.py +474 -0
  86. pencilmark-1.0.0/src/pencilmark/publish_flags.py +137 -0
  87. pencilmark-1.0.0/src/pencilmark/puzzle_json.py +576 -0
  88. pencilmark-1.0.0/src/pencilmark/scoring.py +534 -0
  89. pencilmark-1.0.0/src/pencilmark/se.py +37 -0
  90. pencilmark-1.0.0/src/pencilmark/solver.py +1227 -0
  91. pencilmark-1.0.0/src/pencilmark/sources/__init__.py +141 -0
  92. pencilmark-1.0.0/src/pencilmark/sources/_common.py +136 -0
  93. pencilmark-1.0.0/src/pencilmark/sources/_fpuz_translate.py +366 -0
  94. pencilmark-1.0.0/src/pencilmark/sources/_sp_translate.py +1174 -0
  95. pencilmark-1.0.0/src/pencilmark/sources/fpuzzles.py +115 -0
  96. pencilmark-1.0.0/src/pencilmark/sources/lmd.py +187 -0
  97. pencilmark-1.0.0/src/pencilmark/sources/penpa.py +471 -0
  98. pencilmark-1.0.0/src/pencilmark/sources/sudokupad.py +278 -0
  99. pencilmark-1.0.0/src/pencilmark/spec_format.py +313 -0
  100. pencilmark-1.0.0/src/pencilmark/usage.py +201 -0
  101. pencilmark-1.0.0/src/pencilmark/variants.py +566 -0
  102. pencilmark-1.0.0/src/pencilmark.egg-info/PKG-INFO +236 -0
  103. pencilmark-1.0.0/src/pencilmark.egg-info/SOURCES.txt +113 -0
  104. pencilmark-1.0.0/src/pencilmark.egg-info/dependency_links.txt +1 -0
  105. pencilmark-1.0.0/src/pencilmark.egg-info/entry_points.txt +2 -0
  106. pencilmark-1.0.0/src/pencilmark.egg-info/requires.txt +8 -0
  107. pencilmark-1.0.0/src/pencilmark.egg-info/top_level.txt +1 -0
  108. pencilmark-1.0.0/tests/conftest.py +13 -0
  109. pencilmark-1.0.0/tests/fixtures/variants.json +7 -0
  110. pencilmark-1.0.0/tests/smoke.py +20 -0
  111. pencilmark-1.0.0/tests/test_api.py +252 -0
  112. pencilmark-1.0.0/tests/test_cli.py +55 -0
  113. pencilmark-1.0.0/tests/test_format.py +569 -0
  114. pencilmark-1.0.0/tests/test_generate.py +127 -0
  115. 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.
@@ -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,6 @@
1
+ include LICENSE README.md CHANGELOG.md setup.py
2
+ recursive-include csrc *.cpp *.hpp
3
+ recursive-include src/pencilmark/data *.json *.csv
4
+ recursive-include docs *.md *.json
5
+ recursive-include tests *.py *.json
6
+ global-exclude __pycache__ *.py[cod] *.so
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/pencilmark?include_prereleases)](https://pypi.org/project/pencilmark/)
38
+ [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.14-blue)](https://pypi.org/project/pencilmark/)
39
+ [![Wheels](https://github.com/silly-puppyy/pencilmark/actions/workflows/wheels.yml/badge.svg)](https://github.com/silly-puppyy/pencilmark/actions/workflows/wheels.yml)
40
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/silly-puppyy/pencilmark/blob/main/LICENSE)
41
+ [![Docs](https://img.shields.io/badge/docs-silly--puppyy.github.io-blue)](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
+ [![PyPI](https://img.shields.io/pypi/v/pencilmark?include_prereleases)](https://pypi.org/project/pencilmark/)
4
+ [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.14-blue)](https://pypi.org/project/pencilmark/)
5
+ [![Wheels](https://github.com/silly-puppyy/pencilmark/actions/workflows/wheels.yml/badge.svg)](https://github.com/silly-puppyy/pencilmark/actions/workflows/wheels.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/silly-puppyy/pencilmark/blob/main/LICENSE)
7
+ [![Docs](https://img.shields.io/badge/docs-silly--puppyy.github.io-blue)](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).