diff --git a/SběrDatRůzné/SudokuKiller/NOTES.md b/SběrDatRůzné/SudokuKiller/NOTES.md index 5199384..bd4658c 100644 --- a/SběrDatRůzné/SudokuKiller/NOTES.md +++ b/SběrDatRůzné/SudokuKiller/NOTES.md @@ -1,22 +1,13 @@ # SudokuKiller — technické poznámky -## Přehled skriptů - -### Stahování PDF (původní pipeline) +## Hlavní skripty | Skript | Popis | |--------|-------| -| `stahni_killer_sudoku.py` | Stáhne puzzle + solution PDF z dailykillersudoku.com | -| `stahni_greater_than.py` | Stáhne Greater-Than variantu, přejmenuje existující | -| `import_do_mysql.py` | Importuje PDF soubory do MySQL tabulky `sudoku_killer` (binární bloby) | -| `30_BatchCrop.py` | Ořeže PDF (odstraní hlavičky/patičky), uloží zpět do DB | +| `stahni_killer_structured.py` | Stáhne strukturovaná data (cage definice + řešení) z dailykillersudoku.com do MySQL tabulky `puzzles`. Průběžně ukládá zálohu do `killer_structured_data.json` | +| `vykresli_killer_sudoku.py` | Vygeneruje PDF z dat v MySQL — Killer Sudoku zadání + řešení, vektorové, vzhledem identické s originálem z webu | -### Strukturovaná data (nový pipeline) - -| Skript | Popis | -|--------|-------| -| `stahni_killer_structured.py` | Stáhne strukturovaná data (cage definice + řešení) z webu do sdílené tabulky `puzzles` | -| `preskumaj_killer_data*.py` | Průzkumné skripty pro reverzní inženýrství datového formátu | +Ostatní (stará pipeline s PDF bloby, průzkumné skripty, testovací PDF) je v podadresáři `Testy/`. ## Zdroj dat @@ -56,33 +47,61 @@ DKS.puzzle = new DKS.Puzzle({ Škála 1–10 (z webu), uložena v `difficulty`. -## MySQL — původní tabulka `sudoku_killer` - -Obsahuje binární PDF v `file_puzzle` / `file_solution` / `file_puzzle_cropped`. -- 19 106 KillerSudoku (puzzle 1–31414, 2009–2026) -- 11 405 GreaterThan (puzzle 1730–31416, 2010–2026) - ## MySQL — sdílená tabulka `puzzles` -Strukturovaná data (cage definice + řešení): +Strukturovaná data: - `game_type` = `'killer_sudoku'` / `'killer_sudoku_gt'` - `difficulty` = `'1'` až `'10'` -- `puzzle` = klece ve formátu `sum,r0c1r0c2|sum,r3c4r3c5|...` -- `solution` = flat string 81 číslic +- `puzzle` = klece ve formátu `sum,r0c1r0c2|sum,r3c4r3c5|...` (`VARCHAR(1000)`) +- `solution` = flat string 81 číslic (`VARCHAR(1000)`) - `extra` = `{"grid_size": 9, "puzzle_number": 376, "original_difficulty": 4}` - `source` = `'dailykillersudoku.com'` -## Layout a tisk +**Pozor:** `puzzle` a `solution` byly původně `VARCHAR(200)` — nedostačovalo, cage stringy mají až ~500 znaků. Sloupce rozšířeny na `VARCHAR(1000)`. -V podadresáři `Testy/` jsou experimentální skripty pro: -- Ořezávání PDF (ray-cast detekce mřížky) -- Škálování a umístění 2 puzzle na A4 -- Layout konfigurace (`layouts.json`) +## Stav stažených dat + +- ~28 700 puzzlů (1–31 416) +- Killer Sudoku: ~17 200, Greater-Than: ~11 500 +- Zdrojová data v `killer_structured_data.json` (záloha pro případ MySQL chyby) + +## PDF rendering — pořadí vrstev + +Klíčové pro vzhled identický s originálem z webu (`vykresli_killer_sudoku.py`): + +1. **Bílé pozadí** +2. **Čísla řešení** (jen pro řešovou variantu, šedě) +3. **Tečkované ohraničení klecí** — odsazené dovnitř buněk o `cell * 0.10`, slévání segmentů v rámci stejné klece (jeden `c.line()` přes víc buněk → pattern teček neresetuje) +4. **Tenká plná mřížka** — všechny řádky/sloupce, šedě (překryje přesahy tečkovaných v křížení) +5. **Tlusté čáry 3×3** + obvod, černě +6. **Popisky součtů** — bíle podsvícené, ArialBold + +### Vnější vs vnitřní rohy klecí + +Při slévání tečkovaných segmentů endpoints buďto **zkrátit** o inset (vnější roh) nebo **prodloužit** o inset (vnitřní roh — kde klec zahýbá L-tvarem). + +Detekce: pro horizontální segment top borderu od sloupce `s` do `co` (exclusive): +- Levý konec vnitřní roh = `cage_map[r][s-1] == cid` → prodloužit +- Pravý konec vnitřní roh = `cage_map[r][co] == cid` → prodloužit + +Bez tohoto fixu se na vnitřních rozích L-tvarů objevují viditelné mezery. ## Závislosti -- `requests`, `beautifulsoup4` — HTTP + HTML parsing -- `fitz` (PyMuPDF) — PDF manipulace, ray-cast cropping -- `pypdf` — PDF čtení/zápis -- `playwright` — průzkumné skripty (není potřeba pro produkční stahování) +- `requests` — HTTP fetch (bez Playwright, data jsou inline v HTML) +- `reportlab` — PDF generation (vektorová grafika) - `tqdm` — progress bar +- `mysql_db` (lokální Knihovny) — DB připojení + +## Použití + +```bash +# Stažení dat (s pokračováním z JSON pokud existuje) +python stahni_killer_structured.py --run + +# Pouze import už stažených JSON dat do MySQL +python stahni_killer_structured.py --import + +# Vygenerování PDF pro puzzle 31414 +python vykresli_killer_sudoku.py +``` diff --git a/SběrDatRůzné/SudokuKiller/30_BatchCrop.py b/SběrDatRůzné/SudokuKiller/Testy/30_BatchCrop.py similarity index 100% rename from SběrDatRůzné/SudokuKiller/30_BatchCrop.py rename to SběrDatRůzné/SudokuKiller/Testy/30_BatchCrop.py diff --git a/SběrDatRůzné/SudokuKiller/export_original_pdf.py b/SběrDatRůzné/SudokuKiller/Testy/export_original_pdf.py similarity index 100% rename from SběrDatRůzné/SudokuKiller/export_original_pdf.py rename to SběrDatRůzné/SudokuKiller/Testy/export_original_pdf.py diff --git a/SběrDatRůzné/SudokuKiller/import_do_mysql.py b/SběrDatRůzné/SudokuKiller/Testy/import_do_mysql.py similarity index 100% rename from SběrDatRůzné/SudokuKiller/import_do_mysql.py rename to SběrDatRůzné/SudokuKiller/Testy/import_do_mysql.py diff --git a/SběrDatRůzné/SudokuKiller/layouts.json b/SběrDatRůzné/SudokuKiller/Testy/layouts.json similarity index 100% rename from SběrDatRůzné/SudokuKiller/layouts.json rename to SběrDatRůzné/SudokuKiller/Testy/layouts.json diff --git a/SběrDatRůzné/SudokuKiller/preskumaj_killer_data.py b/SběrDatRůzné/SudokuKiller/Testy/preskumaj_killer_data.py similarity index 100% rename from SběrDatRůzné/SudokuKiller/preskumaj_killer_data.py rename to SběrDatRůzné/SudokuKiller/Testy/preskumaj_killer_data.py diff --git a/SběrDatRůzné/SudokuKiller/preskumaj_killer_data2.py b/SběrDatRůzné/SudokuKiller/Testy/preskumaj_killer_data2.py similarity index 100% rename from SběrDatRůzné/SudokuKiller/preskumaj_killer_data2.py rename to SběrDatRůzné/SudokuKiller/Testy/preskumaj_killer_data2.py diff --git a/SběrDatRůzné/SudokuKiller/preskumaj_killer_data3.py b/SběrDatRůzné/SudokuKiller/Testy/preskumaj_killer_data3.py similarity index 100% rename from SběrDatRůzné/SudokuKiller/preskumaj_killer_data3.py rename to SběrDatRůzné/SudokuKiller/Testy/preskumaj_killer_data3.py diff --git a/SběrDatRůzné/SudokuKiller/preskumaj_killer_data4.py b/SběrDatRůzné/SudokuKiller/Testy/preskumaj_killer_data4.py similarity index 100% rename from SběrDatRůzné/SudokuKiller/preskumaj_killer_data4.py rename to SběrDatRůzné/SudokuKiller/Testy/preskumaj_killer_data4.py diff --git a/SběrDatRůzné/SudokuKiller/preskumaj_killer_data5.py b/SběrDatRůzné/SudokuKiller/Testy/preskumaj_killer_data5.py similarity index 100% rename from SběrDatRůzné/SudokuKiller/preskumaj_killer_data5.py rename to SběrDatRůzné/SudokuKiller/Testy/preskumaj_killer_data5.py diff --git a/SběrDatRůzné/SudokuKiller/preskumaj_rozsah.py b/SběrDatRůzné/SudokuKiller/Testy/preskumaj_rozsah.py similarity index 100% rename from SběrDatRůzné/SudokuKiller/preskumaj_rozsah.py rename to SběrDatRůzné/SudokuKiller/Testy/preskumaj_rozsah.py diff --git a/SběrDatRůzné/SudokuKiller/stahni_greater_than.py b/SběrDatRůzné/SudokuKiller/Testy/stahni_greater_than.py similarity index 100% rename from SběrDatRůzné/SudokuKiller/stahni_greater_than.py rename to SběrDatRůzné/SudokuKiller/Testy/stahni_greater_than.py diff --git a/SběrDatRůzné/SudokuKiller/stahni_killer_sudoku.py b/SběrDatRůzné/SudokuKiller/Testy/stahni_killer_sudoku.py similarity index 100% rename from SběrDatRůzné/SudokuKiller/stahni_killer_sudoku.py rename to SběrDatRůzné/SudokuKiller/Testy/stahni_killer_sudoku.py diff --git a/SběrDatRůzné/SudokuKiller/vykresli_killer_sudoku.py b/SběrDatRůzné/SudokuKiller/vykresli_killer_sudoku.py index 5d9ac9d..83d947f 100644 --- a/SběrDatRůzné/SudokuKiller/vykresli_killer_sudoku.py +++ b/SběrDatRůzné/SudokuKiller/vykresli_killer_sudoku.py @@ -24,7 +24,7 @@ _fonts_dir = os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts") pdfmetrics.registerFont(TTFont("Arial", os.path.join(_fonts_dir, "arial.ttf"))) pdfmetrics.registerFont(TTFont("ArialBold", os.path.join(_fonts_dir, "arialbd.ttf"))) -OUTPUT = Path(__file__).parent / "test_killer_sudoku_v6.pdf" +OUTPUT = Path(__file__).parent / "killer_sudoku_31414.pdf" def parse_cages(puzzle_str: str) -> list[dict]: @@ -79,78 +79,101 @@ def draw_killer_sudoku(c: Canvas, x0: float, y0: float, cell: float, cy = y0 - (r + 1) * cell + cell * 0.28 c.drawCentredString(cx, cy, str(solution[r][co])) - # --- Vrstva 1: kompletní sudoku mřížka (tenké plné čáry) --- + # --- Vrstva 1: tečkované ohraničení klecí (KRESLÍ SE JAKO PRVNÍ) --- + # Tečkované jdou ZESPODU, mřížka přes ně. Tím se zachová čistý vzhled — + # tenká mřížka překryje místa, kde tečkovaná protíná čáru mřížky. + inset = cell * 0.10 + c.setStrokeColor(colors.Color(0.2, 0.2, 0.2)) + c.setLineWidth(cage_line * 0.5) + c.setDash(3, 2) + + has_top = [[r == 0 or cage_map[r - 1][co] != cage_map[r][co] + for co in range(9)] for r in range(9)] + has_bot = [[r == 8 or cage_map[r + 1][co] != cage_map[r][co] + for co in range(9)] for r in range(9)] + has_lft = [[co == 0 or cage_map[r][co - 1] != cage_map[r][co] + for co in range(9)] for r in range(9)] + has_rgt = [[co == 8 or cage_map[r][co + 1] != cage_map[r][co] + for co in range(9)] for r in range(9)] + + def in_cg(rr, cc, cid): + return 0 <= rr <= 8 and 0 <= cc <= 8 and cage_map[rr][cc] == cid + + # Top borders — slévání v řádku + # Vnější roh: zkrácení o inset. Vnitřní roh: prodloužení o inset. + for r in range(9): + co = 0 + while co < 9: + if not has_top[r][co]: + co += 1 + continue + cid = cage_map[r][co] + s = co + while co < 9 and cage_map[r][co] == cid and has_top[r][co]: + co += 1 + # Levý konec: vnitřní roh když (r, s-1) je v kleci (cage tam pokračuje směrem nahoru) + x_s = x0 + s * cell + (-inset if in_cg(r, s - 1, cid) else inset) + # Pravý konec: vnitřní roh když (r, co) je v kleci + x_e = x0 + co * cell + (inset if in_cg(r, co, cid) else -inset) + c.line(x_s, y0 - r * cell - inset, x_e, y0 - r * cell - inset) + + # Bottom borders + for r in range(9): + co = 0 + while co < 9: + if not has_bot[r][co]: + co += 1 + continue + cid = cage_map[r][co] + s = co + while co < 9 and cage_map[r][co] == cid and has_bot[r][co]: + co += 1 + x_s = x0 + s * cell + (-inset if in_cg(r, s - 1, cid) else inset) + x_e = x0 + co * cell + (inset if in_cg(r, co, cid) else -inset) + c.line(x_s, y0 - (r + 1) * cell + inset, x_e, y0 - (r + 1) * cell + inset) + + # Left borders — slévání ve sloupci + for co in range(9): + r = 0 + while r < 9: + if not has_lft[r][co]: + r += 1 + continue + cid = cage_map[r][co] + s = r + while r < 9 and cage_map[r][co] == cid and has_lft[r][co]: + r += 1 + # Horní konec: vnitřní roh když (s-1, co) je v kleci + y_s = y0 - s * cell + (inset if in_cg(s - 1, co, cid) else -inset) + # Dolní konec: vnitřní roh když (r, co) je v kleci + y_e = y0 - r * cell + (-inset if in_cg(r, co, cid) else inset) + c.line(x0 + co * cell + inset, y_s, x0 + co * cell + inset, y_e) + + # Right borders + for co in range(9): + r = 0 + while r < 9: + if not has_rgt[r][co]: + r += 1 + continue + cid = cage_map[r][co] + s = r + while r < 9 and cage_map[r][co] == cid and has_rgt[r][co]: + r += 1 + y_s = y0 - s * cell + (inset if in_cg(s - 1, co, cid) else -inset) + y_e = y0 - r * cell + (-inset if in_cg(r, co, cid) else inset) + c.line(x0 + (co + 1) * cell - inset, y_s, x0 + (co + 1) * cell - inset, y_e) + + c.setDash() + + # --- Vrstva 2: kompletní sudoku mřížka (tenké plné čáry přes tečkované) --- c.setStrokeColor(colors.Color(0.55, 0.55, 0.55)) c.setLineWidth(thin) for i in range(1, 9): c.line(x0, y0 - i * cell, x0 + 9 * cell, y0 - i * cell) c.line(x0 + i * cell, y0, x0 + i * cell, y0 - 9 * cell) - # --- Vrstva 2: tečkované ohraničení klecí (odsazené dovnitř buněk) --- - inset = cell * 0.10 - c.setStrokeColor(colors.Color(0.2, 0.2, 0.2)) - c.setLineWidth(cage_line * 0.5) - c.setDash(3, 2) - - # Horizontální hrany klecí — top borders - for r in range(9): - co = 0 - while co < 9: - cid = cage_map[r][co] - if not (r == 0 or cage_map[r - 1][co] != cid): - co += 1 - continue - seg_start = co - while co < 9 and cage_map[r][co] == cid and (r == 0 or cage_map[r - 1][co] != cid): - co += 1 - c.line(x0 + seg_start * cell + inset, y0 - r * cell - inset, - x0 + co * cell - inset, y0 - r * cell - inset) - - # Horizontální hrany klecí — bottom borders - for r in range(9): - co = 0 - while co < 9: - cid = cage_map[r][co] - if not (r == 8 or cage_map[r + 1][co] != cid): - co += 1 - continue - seg_start = co - while co < 9 and cage_map[r][co] == cid and (r == 8 or cage_map[r + 1][co] != cid): - co += 1 - c.line(x0 + seg_start * cell + inset, y0 - (r + 1) * cell + inset, - x0 + co * cell - inset, y0 - (r + 1) * cell + inset) - - # Vertikální hrany klecí — left borders - for co in range(9): - r = 0 - while r < 9: - cid = cage_map[r][co] - if not (co == 0 or cage_map[r][co - 1] != cid): - r += 1 - continue - seg_start = r - while r < 9 and cage_map[r][co] == cid and (co == 0 or cage_map[r][co - 1] != cid): - r += 1 - c.line(x0 + co * cell + inset, y0 - seg_start * cell - inset, - x0 + co * cell + inset, y0 - r * cell + inset) - - # Vertikální hrany klecí — right borders - for co in range(9): - r = 0 - while r < 9: - cid = cage_map[r][co] - if not (co == 8 or cage_map[r][co + 1] != cid): - r += 1 - continue - seg_start = r - while r < 9 and cage_map[r][co] == cid and (co == 8 or cage_map[r][co + 1] != cid): - r += 1 - c.line(x0 + (co + 1) * cell - inset, y0 - seg_start * cell - inset, - x0 + (co + 1) * cell - inset, y0 - r * cell + inset) - - c.setDash() - - # Tlusté 3×3 čáry + vnější okraj + # --- Vrstva 3: tlusté 3×3 čáry + vnější okraj --- c.setStrokeColor(colors.black) c.setLineWidth(thick) for i in range(0, 10, 3):