pencilmark 1.0.0rc1__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 (107) hide show
  1. pencilmark-1.0.0rc1/CHANGELOG.md +35 -0
  2. pencilmark-1.0.0rc1/LICENSE +21 -0
  3. pencilmark-1.0.0rc1/MANIFEST.in +6 -0
  4. pencilmark-1.0.0rc1/PKG-INFO +196 -0
  5. pencilmark-1.0.0rc1/README.md +162 -0
  6. pencilmark-1.0.0rc1/csrc/bindings.cpp +993 -0
  7. pencilmark-1.0.0rc1/csrc/canon.cpp +182 -0
  8. pencilmark-1.0.0rc1/csrc/chains.cpp +763 -0
  9. pencilmark-1.0.0rc1/csrc/colour.cpp +1636 -0
  10. pencilmark-1.0.0rc1/csrc/desc.cpp +355 -0
  11. pencilmark-1.0.0rc1/csrc/explain.cpp +297 -0
  12. pencilmark-1.0.0rc1/csrc/extra.cpp +2297 -0
  13. pencilmark-1.0.0rc1/csrc/forcing.cpp +433 -0
  14. pencilmark-1.0.0rc1/csrc/human.cpp +1281 -0
  15. pencilmark-1.0.0rc1/csrc/mental.cpp +933 -0
  16. pencilmark-1.0.0rc1/csrc/model.cpp +1270 -0
  17. pencilmark-1.0.0rc1/csrc/model.hpp +171 -0
  18. pencilmark-1.0.0rc1/csrc/nary.cpp +1095 -0
  19. pencilmark-1.0.0rc1/csrc/nary.hpp +123 -0
  20. pencilmark-1.0.0rc1/csrc/portable.hpp +46 -0
  21. pencilmark-1.0.0rc1/csrc/pycompat.hpp +225 -0
  22. pencilmark-1.0.0rc1/csrc/refute.cpp +439 -0
  23. pencilmark-1.0.0rc1/csrc/rich.cpp +523 -0
  24. pencilmark-1.0.0rc1/csrc/rollplace.cpp +199 -0
  25. pencilmark-1.0.0rc1/csrc/solver.cpp +3651 -0
  26. pencilmark-1.0.0rc1/csrc/solver.hpp +754 -0
  27. pencilmark-1.0.0rc1/csrc/sudokuwiki.cpp +631 -0
  28. pencilmark-1.0.0rc1/docs/ENGINE.md +67 -0
  29. pencilmark-1.0.0rc1/docs/FORMAT.md +1062 -0
  30. pencilmark-1.0.0rc1/docs/INTEROP.md +72 -0
  31. pencilmark-1.0.0rc1/docs/USAGE.md +269 -0
  32. pencilmark-1.0.0rc1/docs/api.md +134 -0
  33. pencilmark-1.0.0rc1/docs/examples/specs/README.md +18 -0
  34. pencilmark-1.0.0rc1/docs/examples/specs/displaced_jigsaw.json +36 -0
  35. pencilmark-1.0.0rc1/docs/examples/specs/giant_16x16_32cages.json +34 -0
  36. pencilmark-1.0.0rc1/docs/examples/specs/hardest_0clue_6x6_kropki.json +39 -0
  37. pencilmark-1.0.0rc1/docs/examples/specs/killer_whispers_2givens.json +56 -0
  38. pencilmark-1.0.0rc1/docs/examples/specs/nine_variants_9x9.json +103 -0
  39. pencilmark-1.0.0rc1/docs/examples/specs/one_xwing_classic.json +49 -0
  40. pencilmark-1.0.0rc1/docs/examples/specs/picture_mask_heart.json +116 -0
  41. pencilmark-1.0.0rc1/docs/examples/specs/super_samurai_variants.json +391 -0
  42. pencilmark-1.0.0rc1/docs/examples/specs/zero_clue_6x6_kropki_today.json +41 -0
  43. pencilmark-1.0.0rc1/docs/sudoku-grade.schema.json +356 -0
  44. pencilmark-1.0.0rc1/docs/sudoku-puzzle.schema.json +1568 -0
  45. pencilmark-1.0.0rc1/pyproject.toml +66 -0
  46. pencilmark-1.0.0rc1/setup.cfg +4 -0
  47. pencilmark-1.0.0rc1/setup.py +62 -0
  48. pencilmark-1.0.0rc1/src/pencilmark/__init__.py +11 -0
  49. pencilmark-1.0.0rc1/src/pencilmark/__main__.py +234 -0
  50. pencilmark-1.0.0rc1/src/pencilmark/_data.py +38 -0
  51. pencilmark-1.0.0rc1/src/pencilmark/api.py +237 -0
  52. pencilmark-1.0.0rc1/src/pencilmark/band_path.py +611 -0
  53. pencilmark-1.0.0rc1/src/pencilmark/card.py +240 -0
  54. pencilmark-1.0.0rc1/src/pencilmark/card_score.py +227 -0
  55. pencilmark-1.0.0rc1/src/pencilmark/cbackend.py +994 -0
  56. pencilmark-1.0.0rc1/src/pencilmark/cpsat_verify.py +339 -0
  57. pencilmark-1.0.0rc1/src/pencilmark/data/card_calibration.json +1 -0
  58. pencilmark-1.0.0rc1/src/pencilmark/data/card_score.json +27 -0
  59. pencilmark-1.0.0rc1/src/pencilmark/data/explain_links.json +372 -0
  60. pencilmark-1.0.0rc1/src/pencilmark/data/layouts_sudoku_one.json +510 -0
  61. pencilmark-1.0.0rc1/src/pencilmark/data/params.json +227 -0
  62. pencilmark-1.0.0rc1/src/pencilmark/data/price_params.json +227 -0
  63. pencilmark-1.0.0rc1/src/pencilmark/data/technique_bands.csv +88 -0
  64. pencilmark-1.0.0rc1/src/pencilmark/data/tiers.json +115 -0
  65. pencilmark-1.0.0rc1/src/pencilmark/data/time_model.json +21 -0
  66. pencilmark-1.0.0rc1/src/pencilmark/exchange.py +2006 -0
  67. pencilmark-1.0.0rc1/src/pencilmark/explain.py +5850 -0
  68. pencilmark-1.0.0rc1/src/pencilmark/export.py +575 -0
  69. pencilmark-1.0.0rc1/src/pencilmark/gen/__init__.py +26 -0
  70. pencilmark-1.0.0rc1/src/pencilmark/gen/api.py +482 -0
  71. pencilmark-1.0.0rc1/src/pencilmark/gen/board.py +103 -0
  72. pencilmark-1.0.0rc1/src/pencilmark/gen/checks.py +102 -0
  73. pencilmark-1.0.0rc1/src/pencilmark/gen/elements.py +255 -0
  74. pencilmark-1.0.0rc1/src/pencilmark/gen/search.py +283 -0
  75. pencilmark-1.0.0rc1/src/pencilmark/interop.py +461 -0
  76. pencilmark-1.0.0rc1/src/pencilmark/metrics.py +406 -0
  77. pencilmark-1.0.0rc1/src/pencilmark/model.py +474 -0
  78. pencilmark-1.0.0rc1/src/pencilmark/publish_flags.py +100 -0
  79. pencilmark-1.0.0rc1/src/pencilmark/puzzle_json.py +576 -0
  80. pencilmark-1.0.0rc1/src/pencilmark/scoring.py +534 -0
  81. pencilmark-1.0.0rc1/src/pencilmark/se.py +37 -0
  82. pencilmark-1.0.0rc1/src/pencilmark/solver.py +1224 -0
  83. pencilmark-1.0.0rc1/src/pencilmark/sources/__init__.py +141 -0
  84. pencilmark-1.0.0rc1/src/pencilmark/sources/_common.py +136 -0
  85. pencilmark-1.0.0rc1/src/pencilmark/sources/_fpuz_translate.py +366 -0
  86. pencilmark-1.0.0rc1/src/pencilmark/sources/_sp_translate.py +1174 -0
  87. pencilmark-1.0.0rc1/src/pencilmark/sources/fpuzzles.py +115 -0
  88. pencilmark-1.0.0rc1/src/pencilmark/sources/lmd.py +187 -0
  89. pencilmark-1.0.0rc1/src/pencilmark/sources/penpa.py +471 -0
  90. pencilmark-1.0.0rc1/src/pencilmark/sources/sudokupad.py +278 -0
  91. pencilmark-1.0.0rc1/src/pencilmark/spec_format.py +302 -0
  92. pencilmark-1.0.0rc1/src/pencilmark/usage.py +201 -0
  93. pencilmark-1.0.0rc1/src/pencilmark/variants.py +566 -0
  94. pencilmark-1.0.0rc1/src/pencilmark.egg-info/PKG-INFO +196 -0
  95. pencilmark-1.0.0rc1/src/pencilmark.egg-info/SOURCES.txt +105 -0
  96. pencilmark-1.0.0rc1/src/pencilmark.egg-info/dependency_links.txt +1 -0
  97. pencilmark-1.0.0rc1/src/pencilmark.egg-info/entry_points.txt +2 -0
  98. pencilmark-1.0.0rc1/src/pencilmark.egg-info/requires.txt +8 -0
  99. pencilmark-1.0.0rc1/src/pencilmark.egg-info/top_level.txt +1 -0
  100. pencilmark-1.0.0rc1/tests/conftest.py +13 -0
  101. pencilmark-1.0.0rc1/tests/fixtures/variants.json +7 -0
  102. pencilmark-1.0.0rc1/tests/smoke.py +20 -0
  103. pencilmark-1.0.0rc1/tests/test_api.py +148 -0
  104. pencilmark-1.0.0rc1/tests/test_cli.py +42 -0
  105. pencilmark-1.0.0rc1/tests/test_format.py +534 -0
  106. pencilmark-1.0.0rc1/tests/test_generate.py +69 -0
  107. pencilmark-1.0.0rc1/tests/test_sources.py +74 -0
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0rc1
4
+
5
+ First public release.
6
+
7
+ - `rate`, `rate_many`, `solve`, `to_export`, `load`: the "sudoku-grade" card (difficulty, time, band, contents, issues /
8
+ reject; on request time_p80, steps with two explanation levels, Sudoku Explainer-style ratings, describe, symmetry;
9
+ `fields=[...]` computes only the requested fields) for classic and variant sudoku.
10
+ - Publishing checks (`issues`) with adjustable limits: `quality={"trivial": 0.5, "trial": False}` / `--quality`, the same
11
+ object the generator spec's `quality` takes.
12
+ - Command line: `pencilmark rate | solve | json | export | fingerprint | describe` (also `python -m pencilmark`).
13
+ - Exchange format: `sudoku-puzzle` format_version 1, read and written, specified in `docs/FORMAT.md` with a JSON Schema (`docs/sudoku-puzzle.schema.json`):
14
+ every constraint is a rule (houses as `grid` / `box_grid` / `box`, explicit `row` / `column` for bent ones; nothing
15
+ implied), cells named `r1c1`, a rule
16
+ vocabulary of families and kinds with exact semantics and default drawings, outside clues anchored by `side`,
17
+ explicit `negative` rules, strict reading (unknown keys or rule types are refused).
18
+ - `sudoku-spec` generator requests (givens, locked rules and templates, symmetry, `where` card targets, quality,
19
+ limits); `spec_to_gen` runs what the generator supports and refuses the rest with the reason; showcase specs in
20
+ `docs/examples/specs/`.
21
+ - Format functions: `dumps` (canonical text), `fingerprint` (content hash), `validate`, `describe` / `rules_text` (variant
22
+ name and rules from a registry of placement variants and rule families), optional `to_ipuz`, `from_string` / `to_string`
23
+ (plain and sudokuwiki.org strings), `spec_from_gen` / `spec_to_gen` (the `sudoku-spec` generator request).
24
+ - Cards carry `"format_version": 1` and `"grader": {"name": "pencilmark", "version": ...}`.
25
+ - One engine form per mechanic: renban / between / zipper / region-sum constraints as lines, jigsaw pieces as boxes,
26
+ numbered pair clues (difference / sum / product / ratio, mathrax) as 2-cell tables, duplicated houses merged, global entropy and quadruple clues as named rules for
27
+ every importer (docs/ENGINE.md).
28
+ - Puzzle generator (`pencilmark.gen`) with band, difficulty, technique and rule-element targets.
29
+ - Puzzle importers (`pencilmark.sources`) for SudokuPad, f-puzzles, penpa-edit and Logic Masters links.
30
+ - C++ engine built with pybind11; OR-Tools is optional (`pencilmark[verify]`).
31
+ - No guessing by default: where no technique applies, the engine proves an elimination by full search (Trial and error,
32
+ band Extreme), so every uniquely solvable puzzle gets a band; `allow_guessing=True` / `--allow-guessing` guesses there
33
+ instead (band Unknown).
34
+ - Bands: forcing chains / nets, Nishio, pattern overlay and trial and error are Extreme; bounded case splits (either
35
+ 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,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: pencilmark
3
+ Version: 1.0.0rc1
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)](https://pypi.org/project/pencilmark/)
38
+ [![Python](https://img.shields.io/pypi/pyversions/pencilmark)](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,
47
+ f-puzzles, penpa-edit and Logic Masters links, and generates new puzzles to a target band or difficulty.
48
+
49
+ The engine is written in C++ (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"] # ('Medium', 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 Medium - difficulty 5.9 - time 10.7 min (80% of players within 16.4 min)
82
+ contains: Naked Pair (or PPR/HPR), X-Wing (or KTE/SCL)
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/FORMAT.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/FORMAT.md) section 11):
95
+
96
+ | Field | Meaning |
97
+ |---|---|
98
+ | `difficulty` | how hard each step feels, continuous from 0 (one empty cell) with no ceiling; a typical newspaper medium is about 5 |
99
+ | `time` | expected minutes for an average player (`time_p80` on request: 80% of players finish within it) |
100
+ | `band`, `band_index` | Very Easy (0), Easy, Medium, Hard, Very Hard, Extreme (5); `"Unknown"` (null) only with `allow_guessing=True`, when the engine had to guess |
101
+ | `contains` | techniques met going easy to hard: `code`, `chip`, `name`, `band`, `uses`, `alternatives`; with `contains_complete` and `contains_truncated` |
102
+ | `issues`, `reject` | publishing checks such as a difficulty spike or an unused rule (`code`, `severity` reject / warning, `text`, and `rules`: the offending rules' indices in the exported puzzle); `reject` is true when any issue is a reject |
103
+ | on request | `time_p80`, `steps` (technique, placement `r2c8=3` or eliminations `r4c5#7`; `explain="short"` / `"teach"` adds explanations), `se` / `se_diuf`, `describe`, `symmetry` |
104
+
105
+ A chip like `XWG/KTE/SCL` means any one of those techniques gets you through at that point.
106
+
107
+ Guessing is off by default: where no technique applies, the engine proves an elimination by full search (Trial and
108
+ error, band Extreme), so every uniquely solvable puzzle gets a band. `allow_guessing=True` (CLI `--allow-guessing`) lets it
109
+ guess there instead, which is faster on the hardest puzzles; the card then says Unknown, with `contains_complete: false`
110
+ and `stopped_at_step`. With `max_seconds` set, the contents search is capped and the card
111
+ may say `contains_truncated: true`. A puzzle without exactly one solution has no card: `rate` raises `NotUnique` /
112
+ `NoSolution`.
113
+
114
+ ## Exchange format
115
+
116
+ `to_export` writes, and every entry point reads, the `"sudoku-puzzle"` format, **format_version 1**, specified in
117
+ [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)):
118
+
119
+ ```json
120
+ {"format": "sudoku-puzzle", "format_version": 1,
121
+ "meta": {"title": "Heartstring", "author": "...", "source": "example.org", "type": "Killer Sudoku"},
122
+ "board": {"rows": 9, "cols": 9},
123
+ "digits": 9,
124
+ "givens": [0, 0, 5, 4, 0, ...],
125
+ "solution": [6, 1, 5, 4, 2, ...],
126
+ "rules": [
127
+ {"type": "box_grid", "origin": "r1c1", "box": [3, 3]},
128
+ {"type": "cage", "sum": 15, "cells": ["r1c4", "r1c5", "r2c4"]},
129
+ {"type": "edge", "kind": "kropki", "color": "white", "cells": ["r5c5", "r5c6"]}]}
130
+ ```
131
+
132
+ - Every constraint is a rule, houses included: a `grid` gives a sudoku grid's rows and columns, a `box_grid` adds its
133
+ regular boxes, so a classic is one `box_grid` and a jigsaw is a `grid` plus `box` rules. Nothing is implied. Cells are
134
+ named `r{row}c{col}`, 1-based.
135
+ `givens`: `0` = empty, `null` = no cell (multi-grid gaps).
136
+ - Each rule has one exact meaning and a default drawing, so a renderer needs nothing else; `style` and `decorations`
137
+ are cosmetic only. `meta` is free-form information that never changes solving.
138
+ - Strict reading: an unknown key or rule type makes the reader refuse the file (`FormatError`) instead of dropping a
139
+ rule.
140
+ - `pencilmark.dumps` writes the canonical text (one grid row per line, one rule per line); `pencilmark.fingerprint`
141
+ gives a content hash that ignores metadata and styling; `pencilmark.describe` names the variant and lists its rules.
142
+
143
+ ### Other formats
144
+
145
+ For convenience pencilmark also reads and writes sudokuwiki.org packed strings, exports ipuz, and loads SudokuPad,
146
+ f-puzzles, Penpa+ and Logic Masters links. None of these is needed to use the `sudoku-puzzle` format; see
147
+ [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
148
+ [docs/ENGINE.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/ENGINE.md).
149
+
150
+ ## Supported rules
151
+
152
+ Classic sudoku from 4x4 to 25x25, irregular (jigsaw) regions, diagonals and extra regions, multi-grid layouts (samurai,
153
+ twodoku, butterfly, windmill and more), killer cages, little killers, sandwich, X-sums, skyscrapers, frame sums,
154
+ arrows, thermometers (normal and slow), German and Dutch whispers, renban, region-sum, palindrome, zipper, between,
155
+ entropic, modular and parity lines, kropki, XV, greater-than, difference / ratio dots, anti-knight, anti-king,
156
+ non-consecutive, odd / even, low / high, Rossini, Vudoku, equal sums, counting rules, and given candidates (Sukaku).
157
+ Rules that cannot be translated exactly are refused, never dropped silently.
158
+
159
+ ## Generator
160
+
161
+ ```python
162
+ from pencilmark.gen import generate
163
+ r = generate({"size": 9, "target_band": "Hard", "seed": 1})
164
+ r["puzzle"], r["card"], r["info"]
165
+ ```
166
+
167
+ Targets: band, difficulty range, required techniques, symmetry, given counts, and rule elements (cages, thermos,
168
+ whispers, renban, between lines, arrows, kropki, XV, parity). Every result is checked for uniqueness. Multi-grid boards
169
+ are not generated.
170
+
171
+ ## Puzzle sources
172
+
173
+ ```python
174
+ from pencilmark import sources
175
+ pj = sources.load("https://sudokupad.app/...") # SudokuPad, f-puzzles, penpa-edit, Logic Masters, short links
176
+ card = pencilmark.rate(pj)
177
+ ```
178
+
179
+ Loaders that fetch pages identify themselves, obey robots.txt and pause between requests. Override the user agent with
180
+ the `PENCILMARK_UA` environment variable.
181
+
182
+ ## Limitations
183
+
184
+ - Difficulty and time are models fitted to human solving data. Differences under one point of difficulty are not
185
+ meaningful, and a single solve can vary a lot from the predicted time.
186
+ - Bands depend on the technique catalogue; a puzzle that needs a technique the engine does not have is solved with
187
+ Trial and error at that point and rated Extreme.
188
+ - Very hard puzzles with many-cell rules (killers, arrows, palindromes, thermos) can take minutes while the contents list
189
+ is minimised; use `max_seconds` to cap it (the card then says `contains_truncated: true`).
190
+ - The Sudoku Explainer-style ratings (`se=True`) are approximate and meaningful for classic sudoku only.
191
+
192
+ ## License
193
+
194
+ MIT, see [LICENSE](https://github.com/silly-puppyy/pencilmark/blob/main/LICENSE).
195
+
196
+ pencilmark was written with [Claude](https://claude.ai).
@@ -0,0 +1,162 @@
1
+ # pencilmark
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/pencilmark)](https://pypi.org/project/pencilmark/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/pencilmark)](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,
13
+ f-puzzles, penpa-edit and Logic Masters links, and generates new puzzles to a target band or difficulty.
14
+
15
+ The engine is written in C++ (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"] # ('Medium', 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 Medium - difficulty 5.9 - time 10.7 min (80% of players within 16.4 min)
48
+ contains: Naked Pair (or PPR/HPR), X-Wing (or KTE/SCL)
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/FORMAT.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/FORMAT.md) section 11):
61
+
62
+ | Field | Meaning |
63
+ |---|---|
64
+ | `difficulty` | how hard each step feels, continuous from 0 (one empty cell) with no ceiling; a typical newspaper medium is about 5 |
65
+ | `time` | expected minutes for an average player (`time_p80` on request: 80% of players finish within it) |
66
+ | `band`, `band_index` | Very Easy (0), Easy, Medium, Hard, Very Hard, Extreme (5); `"Unknown"` (null) only with `allow_guessing=True`, when the engine had to guess |
67
+ | `contains` | techniques met going easy to hard: `code`, `chip`, `name`, `band`, `uses`, `alternatives`; with `contains_complete` and `contains_truncated` |
68
+ | `issues`, `reject` | publishing checks such as a difficulty spike or an unused rule (`code`, `severity` reject / warning, `text`, and `rules`: the offending rules' indices in the exported puzzle); `reject` is true when any issue is a reject |
69
+ | on request | `time_p80`, `steps` (technique, placement `r2c8=3` or eliminations `r4c5#7`; `explain="short"` / `"teach"` adds explanations), `se` / `se_diuf`, `describe`, `symmetry` |
70
+
71
+ A chip like `XWG/KTE/SCL` means any one of those techniques gets you through at that point.
72
+
73
+ Guessing is off by default: where no technique applies, the engine proves an elimination by full search (Trial and
74
+ error, band Extreme), so every uniquely solvable puzzle gets a band. `allow_guessing=True` (CLI `--allow-guessing`) lets it
75
+ guess there instead, which is faster on the hardest puzzles; the card then says Unknown, with `contains_complete: false`
76
+ and `stopped_at_step`. With `max_seconds` set, the contents search is capped and the card
77
+ may say `contains_truncated: true`. A puzzle without exactly one solution has no card: `rate` raises `NotUnique` /
78
+ `NoSolution`.
79
+
80
+ ## Exchange format
81
+
82
+ `to_export` writes, and every entry point reads, the `"sudoku-puzzle"` format, **format_version 1**, specified in
83
+ [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)):
84
+
85
+ ```json
86
+ {"format": "sudoku-puzzle", "format_version": 1,
87
+ "meta": {"title": "Heartstring", "author": "...", "source": "example.org", "type": "Killer Sudoku"},
88
+ "board": {"rows": 9, "cols": 9},
89
+ "digits": 9,
90
+ "givens": [0, 0, 5, 4, 0, ...],
91
+ "solution": [6, 1, 5, 4, 2, ...],
92
+ "rules": [
93
+ {"type": "box_grid", "origin": "r1c1", "box": [3, 3]},
94
+ {"type": "cage", "sum": 15, "cells": ["r1c4", "r1c5", "r2c4"]},
95
+ {"type": "edge", "kind": "kropki", "color": "white", "cells": ["r5c5", "r5c6"]}]}
96
+ ```
97
+
98
+ - Every constraint is a rule, houses included: a `grid` gives a sudoku grid's rows and columns, a `box_grid` adds its
99
+ regular boxes, so a classic is one `box_grid` and a jigsaw is a `grid` plus `box` rules. Nothing is implied. Cells are
100
+ named `r{row}c{col}`, 1-based.
101
+ `givens`: `0` = empty, `null` = no cell (multi-grid gaps).
102
+ - Each rule has one exact meaning and a default drawing, so a renderer needs nothing else; `style` and `decorations`
103
+ are cosmetic only. `meta` is free-form information that never changes solving.
104
+ - Strict reading: an unknown key or rule type makes the reader refuse the file (`FormatError`) instead of dropping a
105
+ rule.
106
+ - `pencilmark.dumps` writes the canonical text (one grid row per line, one rule per line); `pencilmark.fingerprint`
107
+ gives a content hash that ignores metadata and styling; `pencilmark.describe` names the variant and lists its rules.
108
+
109
+ ### Other formats
110
+
111
+ For convenience pencilmark also reads and writes sudokuwiki.org packed strings, exports ipuz, and loads SudokuPad,
112
+ f-puzzles, Penpa+ and Logic Masters links. None of these is needed to use the `sudoku-puzzle` format; see
113
+ [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
114
+ [docs/ENGINE.md](https://github.com/silly-puppyy/pencilmark/blob/main/docs/ENGINE.md).
115
+
116
+ ## Supported rules
117
+
118
+ Classic sudoku from 4x4 to 25x25, irregular (jigsaw) regions, diagonals and extra regions, multi-grid layouts (samurai,
119
+ twodoku, butterfly, windmill and more), killer cages, little killers, sandwich, X-sums, skyscrapers, frame sums,
120
+ arrows, thermometers (normal and slow), German and Dutch whispers, renban, region-sum, palindrome, zipper, between,
121
+ entropic, modular and parity lines, kropki, XV, greater-than, difference / ratio dots, anti-knight, anti-king,
122
+ non-consecutive, odd / even, low / high, Rossini, Vudoku, equal sums, counting rules, and given candidates (Sukaku).
123
+ Rules that cannot be translated exactly are refused, never dropped silently.
124
+
125
+ ## Generator
126
+
127
+ ```python
128
+ from pencilmark.gen import generate
129
+ r = generate({"size": 9, "target_band": "Hard", "seed": 1})
130
+ r["puzzle"], r["card"], r["info"]
131
+ ```
132
+
133
+ Targets: band, difficulty range, required techniques, symmetry, given counts, and rule elements (cages, thermos,
134
+ whispers, renban, between lines, arrows, kropki, XV, parity). Every result is checked for uniqueness. Multi-grid boards
135
+ are not generated.
136
+
137
+ ## Puzzle sources
138
+
139
+ ```python
140
+ from pencilmark import sources
141
+ pj = sources.load("https://sudokupad.app/...") # SudokuPad, f-puzzles, penpa-edit, Logic Masters, short links
142
+ card = pencilmark.rate(pj)
143
+ ```
144
+
145
+ Loaders that fetch pages identify themselves, obey robots.txt and pause between requests. Override the user agent with
146
+ the `PENCILMARK_UA` environment variable.
147
+
148
+ ## Limitations
149
+
150
+ - Difficulty and time are models fitted to human solving data. Differences under one point of difficulty are not
151
+ meaningful, and a single solve can vary a lot from the predicted time.
152
+ - Bands depend on the technique catalogue; a puzzle that needs a technique the engine does not have is solved with
153
+ Trial and error at that point and rated Extreme.
154
+ - Very hard puzzles with many-cell rules (killers, arrows, palindromes, thermos) can take minutes while the contents list
155
+ is minimised; use `max_seconds` to cap it (the card then says `contains_truncated: true`).
156
+ - The Sudoku Explainer-style ratings (`se=True`) are approximate and meaningful for classic sudoku only.
157
+
158
+ ## License
159
+
160
+ MIT, see [LICENSE](https://github.com/silly-puppyy/pencilmark/blob/main/LICENSE).
161
+
162
+ pencilmark was written with [Claude](https://claude.ai).