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.
- pencilmark-1.0.0rc1/CHANGELOG.md +35 -0
- pencilmark-1.0.0rc1/LICENSE +21 -0
- pencilmark-1.0.0rc1/MANIFEST.in +6 -0
- pencilmark-1.0.0rc1/PKG-INFO +196 -0
- pencilmark-1.0.0rc1/README.md +162 -0
- pencilmark-1.0.0rc1/csrc/bindings.cpp +993 -0
- pencilmark-1.0.0rc1/csrc/canon.cpp +182 -0
- pencilmark-1.0.0rc1/csrc/chains.cpp +763 -0
- pencilmark-1.0.0rc1/csrc/colour.cpp +1636 -0
- pencilmark-1.0.0rc1/csrc/desc.cpp +355 -0
- pencilmark-1.0.0rc1/csrc/explain.cpp +297 -0
- pencilmark-1.0.0rc1/csrc/extra.cpp +2297 -0
- pencilmark-1.0.0rc1/csrc/forcing.cpp +433 -0
- pencilmark-1.0.0rc1/csrc/human.cpp +1281 -0
- pencilmark-1.0.0rc1/csrc/mental.cpp +933 -0
- pencilmark-1.0.0rc1/csrc/model.cpp +1270 -0
- pencilmark-1.0.0rc1/csrc/model.hpp +171 -0
- pencilmark-1.0.0rc1/csrc/nary.cpp +1095 -0
- pencilmark-1.0.0rc1/csrc/nary.hpp +123 -0
- pencilmark-1.0.0rc1/csrc/portable.hpp +46 -0
- pencilmark-1.0.0rc1/csrc/pycompat.hpp +225 -0
- pencilmark-1.0.0rc1/csrc/refute.cpp +439 -0
- pencilmark-1.0.0rc1/csrc/rich.cpp +523 -0
- pencilmark-1.0.0rc1/csrc/rollplace.cpp +199 -0
- pencilmark-1.0.0rc1/csrc/solver.cpp +3651 -0
- pencilmark-1.0.0rc1/csrc/solver.hpp +754 -0
- pencilmark-1.0.0rc1/csrc/sudokuwiki.cpp +631 -0
- pencilmark-1.0.0rc1/docs/ENGINE.md +67 -0
- pencilmark-1.0.0rc1/docs/FORMAT.md +1062 -0
- pencilmark-1.0.0rc1/docs/INTEROP.md +72 -0
- pencilmark-1.0.0rc1/docs/USAGE.md +269 -0
- pencilmark-1.0.0rc1/docs/api.md +134 -0
- pencilmark-1.0.0rc1/docs/examples/specs/README.md +18 -0
- pencilmark-1.0.0rc1/docs/examples/specs/displaced_jigsaw.json +36 -0
- pencilmark-1.0.0rc1/docs/examples/specs/giant_16x16_32cages.json +34 -0
- pencilmark-1.0.0rc1/docs/examples/specs/hardest_0clue_6x6_kropki.json +39 -0
- pencilmark-1.0.0rc1/docs/examples/specs/killer_whispers_2givens.json +56 -0
- pencilmark-1.0.0rc1/docs/examples/specs/nine_variants_9x9.json +103 -0
- pencilmark-1.0.0rc1/docs/examples/specs/one_xwing_classic.json +49 -0
- pencilmark-1.0.0rc1/docs/examples/specs/picture_mask_heart.json +116 -0
- pencilmark-1.0.0rc1/docs/examples/specs/super_samurai_variants.json +391 -0
- pencilmark-1.0.0rc1/docs/examples/specs/zero_clue_6x6_kropki_today.json +41 -0
- pencilmark-1.0.0rc1/docs/sudoku-grade.schema.json +356 -0
- pencilmark-1.0.0rc1/docs/sudoku-puzzle.schema.json +1568 -0
- pencilmark-1.0.0rc1/pyproject.toml +66 -0
- pencilmark-1.0.0rc1/setup.cfg +4 -0
- pencilmark-1.0.0rc1/setup.py +62 -0
- pencilmark-1.0.0rc1/src/pencilmark/__init__.py +11 -0
- pencilmark-1.0.0rc1/src/pencilmark/__main__.py +234 -0
- pencilmark-1.0.0rc1/src/pencilmark/_data.py +38 -0
- pencilmark-1.0.0rc1/src/pencilmark/api.py +237 -0
- pencilmark-1.0.0rc1/src/pencilmark/band_path.py +611 -0
- pencilmark-1.0.0rc1/src/pencilmark/card.py +240 -0
- pencilmark-1.0.0rc1/src/pencilmark/card_score.py +227 -0
- pencilmark-1.0.0rc1/src/pencilmark/cbackend.py +994 -0
- pencilmark-1.0.0rc1/src/pencilmark/cpsat_verify.py +339 -0
- pencilmark-1.0.0rc1/src/pencilmark/data/card_calibration.json +1 -0
- pencilmark-1.0.0rc1/src/pencilmark/data/card_score.json +27 -0
- pencilmark-1.0.0rc1/src/pencilmark/data/explain_links.json +372 -0
- pencilmark-1.0.0rc1/src/pencilmark/data/layouts_sudoku_one.json +510 -0
- pencilmark-1.0.0rc1/src/pencilmark/data/params.json +227 -0
- pencilmark-1.0.0rc1/src/pencilmark/data/price_params.json +227 -0
- pencilmark-1.0.0rc1/src/pencilmark/data/technique_bands.csv +88 -0
- pencilmark-1.0.0rc1/src/pencilmark/data/tiers.json +115 -0
- pencilmark-1.0.0rc1/src/pencilmark/data/time_model.json +21 -0
- pencilmark-1.0.0rc1/src/pencilmark/exchange.py +2006 -0
- pencilmark-1.0.0rc1/src/pencilmark/explain.py +5850 -0
- pencilmark-1.0.0rc1/src/pencilmark/export.py +575 -0
- pencilmark-1.0.0rc1/src/pencilmark/gen/__init__.py +26 -0
- pencilmark-1.0.0rc1/src/pencilmark/gen/api.py +482 -0
- pencilmark-1.0.0rc1/src/pencilmark/gen/board.py +103 -0
- pencilmark-1.0.0rc1/src/pencilmark/gen/checks.py +102 -0
- pencilmark-1.0.0rc1/src/pencilmark/gen/elements.py +255 -0
- pencilmark-1.0.0rc1/src/pencilmark/gen/search.py +283 -0
- pencilmark-1.0.0rc1/src/pencilmark/interop.py +461 -0
- pencilmark-1.0.0rc1/src/pencilmark/metrics.py +406 -0
- pencilmark-1.0.0rc1/src/pencilmark/model.py +474 -0
- pencilmark-1.0.0rc1/src/pencilmark/publish_flags.py +100 -0
- pencilmark-1.0.0rc1/src/pencilmark/puzzle_json.py +576 -0
- pencilmark-1.0.0rc1/src/pencilmark/scoring.py +534 -0
- pencilmark-1.0.0rc1/src/pencilmark/se.py +37 -0
- pencilmark-1.0.0rc1/src/pencilmark/solver.py +1224 -0
- pencilmark-1.0.0rc1/src/pencilmark/sources/__init__.py +141 -0
- pencilmark-1.0.0rc1/src/pencilmark/sources/_common.py +136 -0
- pencilmark-1.0.0rc1/src/pencilmark/sources/_fpuz_translate.py +366 -0
- pencilmark-1.0.0rc1/src/pencilmark/sources/_sp_translate.py +1174 -0
- pencilmark-1.0.0rc1/src/pencilmark/sources/fpuzzles.py +115 -0
- pencilmark-1.0.0rc1/src/pencilmark/sources/lmd.py +187 -0
- pencilmark-1.0.0rc1/src/pencilmark/sources/penpa.py +471 -0
- pencilmark-1.0.0rc1/src/pencilmark/sources/sudokupad.py +278 -0
- pencilmark-1.0.0rc1/src/pencilmark/spec_format.py +302 -0
- pencilmark-1.0.0rc1/src/pencilmark/usage.py +201 -0
- pencilmark-1.0.0rc1/src/pencilmark/variants.py +566 -0
- pencilmark-1.0.0rc1/src/pencilmark.egg-info/PKG-INFO +196 -0
- pencilmark-1.0.0rc1/src/pencilmark.egg-info/SOURCES.txt +105 -0
- pencilmark-1.0.0rc1/src/pencilmark.egg-info/dependency_links.txt +1 -0
- pencilmark-1.0.0rc1/src/pencilmark.egg-info/entry_points.txt +2 -0
- pencilmark-1.0.0rc1/src/pencilmark.egg-info/requires.txt +8 -0
- pencilmark-1.0.0rc1/src/pencilmark.egg-info/top_level.txt +1 -0
- pencilmark-1.0.0rc1/tests/conftest.py +13 -0
- pencilmark-1.0.0rc1/tests/fixtures/variants.json +7 -0
- pencilmark-1.0.0rc1/tests/smoke.py +20 -0
- pencilmark-1.0.0rc1/tests/test_api.py +148 -0
- pencilmark-1.0.0rc1/tests/test_cli.py +42 -0
- pencilmark-1.0.0rc1/tests/test_format.py +534 -0
- pencilmark-1.0.0rc1/tests/test_generate.py +69 -0
- 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,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
|
+
[](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,
|
|
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
|
+
[](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,
|
|
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).
|