Skip to content

Navigation Menu

Sign in
Sign up

Add puzzle story source (Sudoku, Binoxxo, Futoshiki, Kakuro, Shikaku) with solutions in the appendix and optional explanations - #124

Open
Smengerl wants to merge 19 commits into
j6k4m8:master from
Smengerl:feature/puzzle-explanations
Open

Add puzzle story source (Sudoku, Binoxxo, Futoshiki, Kakuro, Shikaku) with solutions in the appendix and optional explanations #124
Smengerl wants to merge 19 commits into
j6k4m8:master from
Smengerl:feature/puzzle-explanations

Conversation

@Smengerl

@Smengerl Smengerl commented Jul 29, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

This PR now covers the whole puzzle feature (combined with #123)

Per request: this PR is now the single place for all puzzle logic — the "puzzle" source itself (Sudoku, Binoxxo, Futoshiki, Kakuro, Shikaku, from #123) plus the appendix/explanations work this PR originally added on top of it.

Nothing needed to change code-wise to get here: this branch (feature/puzzle-explanations) was branched off #123's branch and has been kept in sync with it via merge ever since, so it already fully contains #123git diff between the two branches on goosepaper/puzzlegen/ is empty. I've closed #123 as superseded by this PR.

Both other former dependencies are also resolved: #122 (PlacementPreference.APPENDIX) and #130 (deduplicate wired through to_html()/to_pdf()) are merged, so the "Correction" caveat further down about deduplication not reaching the normal CLI/Docker path no longer applies — it does now.

Why

Two things this PR fixes, both problems with the puzzle provider as it stood on its own before this PR:

1. Solutions don't actually collect at the end with multiple puzzle
sources.
Batching via PlacementPreference.FULLPAGE alone only reorders within one
provider's own output
. With two separate "puzzle" sources (the
realistic case - a real newspaper mixing Sudoku, Binoxxo, etc.), each
provider's solution would land right after that provider's own puzzle,
not collected together at the true end of the issue.

Reproduced directly: two-source config (one Sudoku source, one Binoxxo source) - before,
each solution appeared immediately next to its own puzzle; after, both
puzzles render together first, then both solutions together in the
appendix. Screenshots attached.

2. No way to show a puzzle's rules. A Sudoku, sure, everyone knows
it - but Futoshiki or Shikaku aren't universally familiar, and a print
puzzle book almost always includes a one-line reminder of the rules
somewhere. There was no way to add one.

What

  • The "puzzle" source type itself: Sudoku, Binoxxo, Futoshiki, Kakuro, Shikaku generators + PuzzleStoryProvider (from Add a "puzzle" story source (Sudoku, Binoxxo, Futoshiki, Kakuro, Shikaku) #123 ).
  • Solutions use PlacementPreference.APPENDIX
    instead of FULLPAGE. "Batched like a puzzle book's answer
    section" intent, working regardless of
    how many separate puzzle sources a config has.
  • New explanation option on the "puzzle" source:
    "none" (default) / "inline" (a short rules
    blurb repeated under every puzzle instance) / "footer" (a real CSS
    footnote - float: footnote - at the bottom of whichever page the
    puzzle lands on, not a paragraph glued into the reading flow; every
    instance of that puzzle_type in the paper shares one footnote via a
    small reference mark) / "appendix" (one rules blurb per puzzle_type,
    grouped with the solutions).
  • Both "footer" and "appendix" explanations use a stable per-
    puzzle_type headline, so Goosepaper.get_stories(deduplicate=True)
    collapses a rules blurb requested by several sources of the same type
    down to one copy - same dedup mechanism Add PlacementPreference.APPENDIX for reliable end-of-document content #122 already proved works for
    appendix content generally, no new mechanism needed here. Now that fix: actually expose the existing deduplicate option to callers #130 is
    merged, to_html()/to_pdf()/the CLI pass deduplicate=True through, so this
    reaches a normal render, not just a direct get_stories() call.

Reproduction

Two sources:

{
 "sources": [
 {"type": "puzzle", "puzzle_type": "sudoku", "difficulty": "medium", "name": "Sudoku"},
 {"type": "puzzle", "puzzle_type": "binoxxo", "difficulty": "hard", "name": "Binoxxo (schwer)"}
 ]
}

Before:
puzzle solution renders immediately next to its own puzzle (same page, second column).

After (this branch):
puzzles are shown together and appendix holds the solutions. Explanations given in footnotes.

Example attached:
Rätselheft 2026年07月29日.pdf

Testing

  • Full test suite passes (101+ tests, now including Add a "puzzle" story source (Sudoku, Binoxxo, Futoshiki, Kakuro, Shikaku) #123 's puzzle-generation tests directly since it's part of this PR).
  • Verified end-to-end against all three real newspaper configs I run
    daily - all of them use explanation: "footer" with multiple puzzle
    sources per issue, exactly the scenario this PR fixes - including a
    real delivery run to a reMarkable.

Merge-order note (siblings, not dependencies)

This PR has no remaining hard dependencies - #122, #123, and #130 are all resolved (merged or absorbed into this PR). What's left is mechanical (non-semantic) overlap with other open PRs that touch the same shared files.

I ran a local pairwise merge check across all my currently open PRs to find real conflicts (not just shared files). Full breakdown and reasoning in my comment below on this PR - short version: this PR conflicts with #133 and #132 (same config.py source-type schema block) and with #121 (same block, plus rss.py/test_rss.py for #121 separately). Suggested landing order for the whole queue is in that comment too.

Smengerl added a commit to Smengerl/goosepaper-logicpuzzles that referenced this pull request Jul 29, 2026
All 7 branches now have real PRs open against j6k4m8/goosepaper (j6k4m8#118-j6k4m8#124,
two already merged) - added a PR column to the branch table so it's clear
at a glance what's been submitted where.
j6k4m8 added a commit that referenced this pull request Aug 8, 2026
## Summary
- `Goosepaper.get_stories(deduplicate=...)` already existed, but nothing
else ever passed a value through: `_render_html_document()` (used by
both `to_html()` and `to_pdf()`) calls `self.get_stories()` with no
arguments, so the option was unreachable except by calling
`get_stories()` directly.
- Add a `deduplicate` constructor argument that `get_stories()` now
falls back to when its own parameter is omitted, so
`to_html()`/`to_pdf()` honor it too.
- Default unchanged (`False`), so existing behavior is untouched unless
a caller opts in.
## Why this matters beyond this PR
This closes a gap that #124 (puzzle-explanations) currently has: #124's
`"footer"`/`"appendix"` explanation modes rely on
`Goosepaper.get_stories(deduplicate=True)` to collapse a rules blurb
requested by several sources of the same puzzle type down to one copy.
#124's own tests call `get_stories(deduplicate=True)` directly to verify
this, but neither `goosepaper/__main__.py` nor `to_html()`/`to_pdf()`
pass `deduplicate=True` through today - so a real user generating a
paper via the CLI/Docker wouldn't actually get that dedup behavior, only
a caller that bypasses `to_html()`/`to_pdf()` and calls `get_stories()`
directly would. This PR is what makes that dedup reachable from a real
render, e.g. `Goosepaper(providers, deduplicate=True).to_pdf(...)`.
## Test plan
- [x] `pytest goosepaper/test_goosepaper.py` - 15 passed (13 existing +
2 new: `to_html()` deduplicates when the constructor flag is set;
doesn't by default)
- [x] Full suite (`pytest`) - 80 passed

j6k4m8 commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Can you combine this with #123 so all puzzle logic is in the same place? It does make it a bigger PR, but I'd rather that it's not across two PRs. Shouldn't be too hard since #122 is now merged!

@Smengerl Smengerl changed the title (削除) Puzzle solutions use the appendix; add optional rules explanations (削除ここまで) (追記) Add puzzle story source (Sudoku, Binoxxo, Futoshiki, Kakuro, Shikaku) with solutions in the appendix and optional explanations (追記ここまで) Aug 9, 2026

Smengerl commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Combined with #123, and a full merge-order check across all open PRs

Consolidation: this PR (feature/puzzle-explanations) was branched off #123's branch (feature/puzzle-provider) and has been kept in sync with it via merge ever since — git diff origin/feature/puzzle-provider origin/feature/puzzle-explanations -- goosepaper/puzzlegen is empty, so this PR already fully contains #123's code. No rebase/squash needed to combine them; I've closed #123 as superseded by this one. This PR is also conflict-free against current master.

Why post an order here at all: combining two PRs into one changes the shape of the queue, so before doing that I checked all 13 currently-open PRs against each other — not just "do they touch the same file" but an actual local git merge --no-commit per pair, to find real conflicts. Posting the full result here since this PR is now the biggest node in that graph; I've left a short pointer comment on each other affected PR linking back to this one.

Conflict clusters found (real merge conflicts, not just shared files)

A. styles.py#120#126 (both insert a new CSS block at the same anchor point)

B. storyprovider/rss.py + test_rss.py#135, #134, #129, #128, #127, #121
Conflicting pairs: (135,134), (135,127), (129,121), (128,127), (128,121), (127,121)

C. config.py/test_config.py/test_utils.py/util.py/README.md (each adds a new source-type registration to the same schema dict) — #133, #132, this PR, #121
Every pair conflicts: (133,132), (133,this), (133,121), (132,this), (132,121), (this,121)

#136 also touches config.py (DeliverySettings) but a different section entirely — zero conflicts with anything, despite the shared file.

#121 is the hub: it's in both cluster B and C, conflicting with 6 of the other 12 PRs. Merging it last in both clusters means it absorbs one final rebase instead of forcing repeated conflicts onto its 6 siblings one at a time. This is inherent to the overlapping code (13 conflicting pairs total, independent of ordering) — the order below is about predictability, not eliminating conflicts that can't be eliminated.

Suggested merge order

  1. feat: add retention_keep_last_n/retention_prefix delivery settings #136 , feat: clean two-level PDF outline (sections/headlines) by default, configurable #131 — no dependencies on anything else, merge whenever
  2. fix: wrap long code blocks instead of letting them overflow the column #120 fix: hide interactive <button> elements in print/PDF output #126 — styles.py pair
  3. feat: optionally prefer the feed's own title over readability's extracted title #129 Work around a headline duplicated inside readability's extracted article body #134 fix(rss): resolve relative URLs in readability-extracted article bodies #135 fix: don't drop an entire feed's stories when one entry fails to fetch #128 fix: fall back to sniffed encoding when a page omits an explicit charset #127 — rss.py-only PRs
  4. feat: add "comic" source type for daily comic strips (XKCD, gocomics.com, arcamax.com) #133 feat: add SectionProvider wrapper for tagging any provider's stories with a section #132 → this PR (Add puzzle story source (Sudoku, Binoxxo, Futoshiki, Kakuro, Shikaku) with solutions in the appendix and optional explanations #124 ) — config.py source-schema cluster
  5. Add native content_filters/skip_title_patterns to "rss" sources #121 last — touches both clusters B and C

Happy to rebase this PR (or any of the others) at whatever point in that order works for you — just say the word.

Smengerl commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my comment above: the "conflict-free against current master" line was wrong — I'd tested against my fork's own stale master mirror, not j6k4m8/goosepaper:master (the actual PR base). Against the real base, GitHub reports this PR as dirty (real conflict), caused by #138 (register_story_provider, merged 2026年08月08日 — not one of my PRs) inserting a test in test_config.py at the same spot this PR's own new tests land. Trivial to resolve with a rebase, and unrelated to the sibling-PR conflict clusters/order described above — those were tested branch-to-branch directly, not against master, so they're unaffected by this. I'll rebase this PR once we're ready to act on the merge order.

Copy link
Copy Markdown
Contributor Author

Fixed a real rendering bug in Kakuro clue cells: the right-sum ("h") was rendering bottom-right of its clue cell instead of top-right, and the down-sum ("v") top-left instead of bottom-left - backwards from the standard Kakuro convention (right-sum upper-right, down-sum lower-left). Root cause: WeasyPrint inverts the vertical axis for an absolutely-positioned span nested inside a table cell this way (position:relative div inside a position:relative td) - top: 0 renders at the visual bottom, bottom: 0 at the visual top. Confirmed by rendering an isolated, distinctly-labelled clue cell before/after. Fix: swap top/bottom between .kakuro-h/.kakuro-v (left/right unaffected).

Comment thread README.md Outdated
Comment thread goosepaper/util.py
Comment thread goosepaper/storyprovider/puzzle.py Outdated
Comment thread goosepaper/storyprovider/puzzle.py Outdated
Comment thread goosepaper/storyprovider/puzzle.py Outdated
Smengerl added a commit to Smengerl/goosepaper-logicpuzzles that referenced this pull request Aug 14, 2026
...English
No need for the fork-only German localization - switch the rule blurbs,
" - Solution" suffix, and "How does X work?" explanation headline to match
what PR j6k4m8#124 already uses upstream (English, unlocalized).
Also add docs/reference/storyprovider/puzzle.py.md (copied from j6k4m8#124) and
point the README's Logic Puzzles link at it, matching every other source
type's docs convention instead of the inline "see below" section.
...Shikaku)
New "puzzle" source type: generates a puzzle grid (and its solution, on a
following page) and renders both as plain HTML/CSS - no images, no
external dependencies, renders natively through the existing WeasyPrint
pipeline.
- goosepaper/puzzlegen/{sudoku,binoxxo,futoshiki,kakuro,shikaku}/: vendored
 generation cores (grid/rules/solver/puzzle - no rendering, no CLI, no
 reportlab) adapted from a private, unpublished puzzle-generator project.
 Each is a self-contained backtracking generator + uniqueness solver with
 a random.Random seed parameter for reproducible sets.
- goosepaper/storyprovider/puzzle.py: PuzzleStoryProvider dispatches to the
 right generator/renderer pair and returns a Story per puzzle plus one
 per solution. Solutions are batched at the end of the returned list
 (rather than interleaved after each puzzle) and marked
 PlacementPreference.FULLPAGE with a `break-before: page` rule scoped to
 this provider's own inline CSS - worked around like this because
 `break-before`/`page-break-before` do not force a page break for content
 inside a `column-count` container in this WeasyPrint version (verified
 against isolated minimal-HTML test cases); the workaround gives a clean
 page per puzzle/solution under `layout: "1col"`, and degrades gracefully
 (no crash, just no forced break) under multi-column layouts.
- goosepaper/config.py, goosepaper/util.py: schema validation and provider
 wiring for the new "puzzle" source type
 (puzzle_type/box_size/size/difficulty/count/seed).
Verified end-to-end for all five puzzle types (grid + solution rendering,
uniqueness of generated puzzles, config validation) via a downstream
project's local + real reMarkable-upload runs.
Two related changes, both only possible now that PlacementPreference.
APPENDIX exists:
1. Puzzle solutions now use PlacementPreference.APPENDIX instead of
 FULLPAGE. They're still collected separately from their puzzles, but
 now render in the paper's shared end-of-document appendix block
 (grouped with every other appendix-placed story, puzzle or not) rather
 than inline after this provider's own stories. This fixes the
 remaining gap in the old approach: FULLPAGE's break-before: page only
 reliably starts a new page under layout: "1col" - APPENDIX stories
 render outside any column-count container, so the page break is
 reliable under multi-column layouts too.
2. New `explanation` option on the "puzzle" source type: "none" (default,
 unchanged behavior), "inline" (a short rules blurb appended to every
 puzzle instance's own body), "footer" (one blurb in normal reading
 order, right after this source's puzzles), or "appendix" (one blurb
 grouped into the end-of-document appendix alongside the solutions).
 "footer"/"appendix" each add exactly one extra Story per get_stories()
 call, not one per puzzle instance - and its headline is stable per
 puzzle_type (not per instance/date), so if several "puzzle" sources
 across a config all request an explanation for the same type,
 Goosepaper's own get_stories(deduplicate=True) collapses them to a
 single explanation, the same generic mechanism proven for
 PlacementPreference.APPENDIX in general (see
 test_goosepaper.py::test_appendix_stories_with_identical_headline_are_deduplicated).
- goosepaper/storyprovider/puzzle.py: _EXPLANATIONS (short German rules
 text per puzzle type), explanation handling in get_stories(), solutions
 switched to APPENDIX. Removed the now-unused
 `article.placement-fullpage` CSS rule this provider carried as its own
 workaround - no longer needed now that solutions use APPENDIX.
- goosepaper/config.py, goosepaper/util.py: schema validation and
 provider wiring for the new "explanation" field.
- goosepaper/storyprovider/test_puzzle.py: new test module - solution
 placement, all four explanation modes, invalid-mode rejection, and an
 end-to-end cross-source dedup proof via a real Goosepaper instance.
Verified visually via a real 2-column PDF render: two Sudoku sources
(easy + hard) both requesting a "footer" explanation for the same type
produce exactly one "Wie funktioniert Sudoku?" note (after the first
Sudoku, not repeated after the second), and a Kakuro source requesting
"appendix" placement lands its explanation alongside the solutions at
the very end of the document.
...ulty
Two puzzles of the same type+difficulty within one get_stories() call
(count > 1) previously got the identical headline (e.g. two "Medium
Sudoku" puzzles from one count=2 source). Story.date defaults to None and
this fork's headline-based cross-provider deduplicate=True mechanism
matches on (headline, date), so two undated same-headline stories look
like duplicates - meaning the second puzzle (and its solution) would
silently vanish under deduplicate=True, exactly the mode this fork's
downstream consumers default to.
Fix: append an instance index - " (1)", " (2)", ... - whenever count > 1.
A single puzzle (the common case, count == 1) keeps its plain headline,
unchanged.
goosepaper/storyprovider/test_puzzle.py: new test module covering both
the unchanged count == 1 case and the count > 1 disambiguation.
Previously "footer" mode placed the explanation as a normal Story in the
reading flow (PlacementPreference.NONE) - visually indistinguishable from
any other paragraph, landing wherever it happened to fall, not at the
bottom of the page like a reader would expect from something called a
footer/footnote.
Now it uses WeasyPrint's actual CSS footnote support (float: footnote):
the explanation lands at the bottom of whichever page it ends up on, with
a numbered marker, cleanly separated from the flow.
Deduplication (one explanation Story per puzzle_type, unchanged mechanism)
still applies, which creates a real constraint: with only one puzzle_type
across three difficulties as three separate sources (the common shape -
e.g. "Sudoku Leicht"/"Sudoku Mittel"/"Sudoku Schwer" as three sections),
only one of the three ever carries the actual footnote content after
dedup; the other two need a reference mark showing the *same* number as
that one real footnote, not their own separate footnote. Two approaches
that would normally do this were tried and don't work in this WeasyPrint
version: target-counter() (for pointing a plain reference at "whatever
number this specific footnote ended up with") returned 0 instead of the
real value, and reusing WeasyPrint's own auto-incrementing `footnote`
counter directly doesn't work either, since it counts every float:
footnote in the whole document in appearance order - a later occurrence's
counter value has nothing to do with an earlier, unrelated footnote's
number.
So the number is entirely ours: _EXPLANATION_NUMBER assigns each
puzzle_type a fixed number (1-5, from _EXPLANATIONS' dict order). Every
puzzle instance gets a plain <sup class="puzzle-footnote-xref"> showing
that fixed number, regardless of whether it's the one difficulty that
happens to carry the real footnote after dedup or not; the real footnote
overrides WeasyPrint's own footnote-marker content to match. Both
::footnote-call (the auto marker at the footnote's own origin point,
now redundant since every instance already carries its own xref) and the
now-otherwise-empty explanation Story's own headline/divider are
suppressed via :has() (confirmed working in WeasyPrint's actual layout
engine, not just the soupsieve selector matching used elsewhere in this
fork) - without that, dedup leaves an empty heading with a stray divider
line behind in the normal flow, since only the floated span itself moves
to the page bottom.
goosepaper/storyprovider/test_puzzle.py: new tests covering the CSS
footnote class name, the xref marker on every instance, and (end-to-end
through Goosepaper) that the xref number on a second difficulty's puzzle
matches the one real footnote surviving deduplication.
...enerated label
PuzzleStoryProvider previously always rendered its own generated
"Medium Sudoku"-style text as a visible heading over every puzzle
instance. That's the only thing distinguishing puzzle types/difficulties
when several of them share one section (goosepaper has no other concept
of "this source's own label") - but it's pure duplication once a config
gives each type+difficulty its own dedicated section (e.g. a "Sudoku
Mittel" section containing only sudoku/medium sources), where the
section's own heading already says the same thing.
New optional `name` constructor arg controls what's actually shown:
 - given: rendered as that instance's own heading (and "{name} -
 Lösung" on its solution) - instead of the auto-generated text, not
 alongside it.
 - not given: nothing extra renders: only the enclosing section's own
 heading identifies the puzzle. Correct for a dedicated section;
 leaves same-section puzzles of different types indistinguishable
 otherwise, so this is a per-source config choice, not a global one.
The auto-generated "Medium Sudoku (1)"-style text keeps existing as the
Story's internal `headline` no matter what - Goosepaper's own
deduplicate=True (matches on headline+date) and per-story anchor-
uniqueness both need a stable, distinct identity that doesn't depend on
what a user did or didn't type into `name`, so it's computed exactly as
before and just never rendered (article:has(.puzzle-body) >
.story-headline { display: none; }, the same :has()-based suppression
technique already used for "footer" explanations, confirmed working in
WeasyPrint's actual layout engine).
goosepaper/storyprovider/test_puzzle.py: new tests for both states (no
name -> no visible label, internal headline unaffected; name given ->
becomes the visible label, internal headline still the auto-generated
one) plus HTML-escaping of a custom name.
Two independent tightenings to the "puzzle" source's config surface:
- `puzzle_type` no longer defaults to "sudoku". A config that forgets it should
 fail loudly (TypeError) instead of silently always generating Sudoku - there's
 no sensible implicit choice among five otherwise-equal puzzle types.
- For every type but sudoku, `size` no longer needs to be spelled out at every
 difficulty. Each puzzlegen module's `Difficulty` dataclass now also carries a
 `size` (binoxxo: 8/10/14, futoshiki: 5/6/7, kakuro: 5/6/7, shikaku: 5/10/20 for
 easy/medium/hard), and PuzzleStoryProvider falls back to that table instead of
 one flat DEFAULT_SIZE when `size` is left unset. This isn't just a config
 convenience: shikaku's own DIFFICULTIES comment already documented "hard" as
 tuned for a 20x20 grid specifically (min_blocks=20 doesn't even fit a small
 grid's cell count) - so leaving size and difficulty fully decoupled meant the
 config *could* ask for combinations the generator was never measured against.
 Passing `size` explicitly still overrides the table, same as before.
 Sudoku's `box_size` stays a flat default (3, independent of difficulty) since
 that's genuinely how it's used today - no difficulty currently asks for a
 different box_size, so there's no table to add.
Both changes are pure config-surface tightening: every real config already sets
puzzle_type, and box_size/size can now be omitted entirely once a difficulty is
picked - removing was the whole point (see the sibling goosepaper-addon repo's
config cleanup).
Adds a full options table (required/optional, default, behavior) for the
"puzzle" source type to the README, and a working example to
example-config.json, verified by generating a real PDF from it end-to-end
(the rendered Sudoku grid appears on its own page, "Medium Sudoku" in the
table of contents).
Merges forward feature/puzzle-provider's new options table/example, then
extends both with this branch's own addition: the `explanation` field
("none"/"inline"/"footer"/"appendix").
Found during review, reproduced end-to-end before fixing:
1. goosepaper/config.py's own "puzzle" source schema still listed
 puzzle_type as optional, even though PuzzleStoryProvider.__init__ takes
 it as a plain required positional argument with no default. A config
 omitting puzzle_type passed schema validation and then crashed with a
 raw, unhandled `TypeError: PuzzleStoryProvider.__init__() missing 1
 required positional argument: 'puzzle_type'` instead of this codebase's
 normal clean ConfigError. Moved puzzle_type from "optional" to
 "required" in _source_schema() - the existing required-field-checking
 loop in _parse_source() already does the rest, no new mechanism needed.
2. `name` - this branch's own headline feature - was never added to
 config.py's "puzzle" optional-keys set or to util.py's puzzle
 options-forwarding lambda. A config setting `"name"` on a puzzle source
 was rejected outright with "Unknown field(s) in source #N: name",
 making the feature this branch introduces completely unreachable
 through goosepaper's own standalone --config path (it only worked
 through the separate addon-side pydantic schema, which was wired up
 correctly). Added `name` to both the schema and the util.py filter, with
 a string validator matching how other free-text fields (headline, text,
 subreddit) are already validated.
Added regression tests for both: goosepaper/test_config.py now covers a
puzzle source with `name` loading successfully and a puzzle source missing
puzzle_type raising a clean ConfigError; goosepaper/test_utils.py gained
test_construct_story_providers_passes_puzzle_options, verifying every
option (including `name`) actually reaches the constructed
PuzzleStoryProvider - the exact thing that was silently missing before.
Smengerl added 10 commits August 18, 2026 22:25
Found during review: PuzzleStoryProvider's count-loop disambiguation (the
"(i+1)" suffix) only protects against collisions within one provider
instance. Two separate "puzzle" sources sharing the same puzzle_type and
difficulty, both left at count=1, produce Stories with the identical
internal headline - it's derived from puzzle_type + difficulty only, never
from seed - so Goosepaper's deduplicate=True silently drops one puzzle and
its solution. Reproduced directly: two providers, same type+difficulty,
different seeds (genuinely different puzzles) -> only one survives
get_stories(deduplicate=True).
Doesn't affect any of the three production goosepaper-addon configs (none
repeat an exact type+difficulty pair across sources), and a real fix would
mean redesigning how cross-instance identity works without a shared
registry between independently-constructed providers - out of scope for
this review pass. Documented the limitation on get_stories() with the
correct mitigation (use one source with `count: N`, not N separate
count-1 sources - seed does not help, it was almost documented as a fix
here before double-checking against base_label's actual definition, which
doesn't reference seed at all), and added a test that pins down the
current behavior so it can only change on purpose.
Two cleanups:
- Removed the `size` parameter entirely. It let a config override the
 per-difficulty grid size table, but size and difficulty were never
 meant to be independent - the whole point of deriving size from
 difficulty (previous commit) was that combinations like shikaku "hard"
 at a small size were never measured/tested. Letting `size` override
 that reintroduces exactly the risk the derivation was added to remove,
 for no real benefit (nothing in this fork or the addon-side project it
 was built for ever used the override). Removed from
 PuzzleStoryProvider.__init__, config.py's schema/validators, and
 util.py's provider-construction filter; a config that still sets `size`
 now gets a clean "Unknown field(s)" error instead of it being silently
 ignored or accepted.
- Documented `box_size`'s actual supported values (2 -> 4x4, 3 -> the
 classic 9x9) instead of just naming the default.
- Removed a stray reference to "a sibling puzzle-generation project" in
 puzzlegen/__init__.py's docstring - irrelevant to this fork on its own.
Updated tests accordingly: renamed the difficulty-derives-size test (it's
no longer "when unset", there's nothing else to set), replaced the
override test with one confirming `size` is rejected outright. 85 passed.
Same fix as feature/puzzle-provider (which this branch already merged
before that fix landed there) - "this fork's addition"/"added by this
fork" only made sense in the staging fork's own combined README.
The right-sum ("h") was rendering bottom-right of its clue cell
instead of top-right, and the down-sum ("v") top-left instead of
bottom-left - exactly backwards. Confirmed by rendering an isolated,
distinctly-labelled clue cell: WeasyPrint inverts the vertical axis
for an absolutely-positioned span nested this way inside a table cell
(position:relative div inside a position:relative td) - "top: 0"
lands at the visual bottom, "bottom: 0" at the visual top.
Swapping top/bottom between .kakuro-h and .kakuro-v (right/left stay
as they were) corrects both labels to the standard Kakuro convention:
right-sum in the upper-right triangle, down-sum in the lower-left.
Verified by re-rendering a sample puzzle before/after.
🤖 Generated with Claude Code
... rule text
The maintainer flagged " - Lösung" in the README; the same
inconsistency also existed in the actual provider code (the solution
heading suffix and the "explanation" story's headline), not just the
docs, and in the German puzzle rule blurbs shown to readers. Rest of
the codebase is English-only, so match that for now rather than
partially localizing.
Same pattern the maintainer suggested for comic.py's comic_type in
j6k4m8#133. Mirrors the "kept in sync by hand" comment convention already
used there.
puzzle_type and explanation validation followed the identical
not-in/raise pattern. Error message wording is unchanged.
...ide convention
Follows the same format j6k4m8#140 established for the other built-in
providers (and that j6k4m8#133 already used for comic.py.md). README link
deliberately left out for now - the README section is one of the
files already conflicting with master, better resolved together.
Kakuro's row/column sum labels weren't flush-centered in their clue
triangles: absolute positioning had an inverted vertical axis under
WeasyPrint, and a later CSS Grid attempt turned out to duplicate each
grid item into two overlapping boxes internally. A nested 2x2 <table>
per clue cell (matching kakuros.com's own markup) sidesteps both bugs
and centers correctly via plain table-cell layout.
Also moves puzzle.py's embedded CSS out of a Python string into a real
puzzle.css file (loaded via importlib.resources, package-data updated
to ship it), matching the styles.py convention - and restructures/
trims that stylesheet by puzzle type along the way.
Smengerl force-pushed the feature/puzzle-explanations branch from 9981369 to 3fbf94c Compare August 18, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@j6k4m8 j6k4m8 Awaiting requested review from j6k4m8

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

2 participants

AltStyle によって変換されたページ (->オリジナル) /