""" Vykreslí Killer Sudoku puzzle do PDF z dat v MySQL tabulce puzzles. """ import json import os import re import sys from pathlib import Path sys.stdout.reconfigure(encoding="utf-8") sys.path.insert(0, str(Path(__file__).parent.parent.parent / "Knihovny")) from reportlab.lib import colors from reportlab.lib.pagesizes import A4 from reportlab.lib.units import cm from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen.canvas import Canvas from mysql_db import connect_mysql _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 / "killer_sudoku_31414.pdf" def parse_cages(puzzle_str: str) -> list[dict]: cages = [] for part in puzzle_str.split("|"): target, cells_str = part.split(",", 1) cells = [(int(m[1]), int(m[2])) for m in re.finditer(r"r(\d)c(\d)", cells_str)] cages.append({"sum": int(target), "cells": cells}) return cages def build_cage_map(cages: list[dict]) -> list[list[int]]: cage_map = [[-1] * 9 for _ in range(9)] for i, cage in enumerate(cages): for row, col in cage["cells"]: cage_map[row][col] = i return cage_map def cage_label_cell(cage: dict) -> tuple[int, int]: return min(cage["cells"], key=lambda c: (c[0], c[1])) def parse_solution(solution_str: str) -> list[list[int]]: return [[int(solution_str[r * 9 + c]) for c in range(9)] for r in range(9)] def draw_killer_sudoku(c: Canvas, x0: float, y0: float, cell: float, cages: list[dict], cage_map: list[list[int]], title: str = "", solution: list[list[int]] | None = None): label_font = max(cell * 0.22, 5) num_font = max(cell * 0.45, 7) thin = 0.3 cage_line = 1.0 thick = 2.2 if title: c.setFont("ArialBold", 12) c.drawString(x0, y0 + 5, title) # Bílé pozadí c.setFillColor(colors.white) c.rect(x0, y0 - 9 * cell, 9 * cell, 9 * cell, fill=1, stroke=0) # Řešení if solution: c.setFillColor(colors.Color(0.25, 0.25, 0.25)) c.setFont("Arial", num_font) for r in range(9): for co in range(9): cx = x0 + co * cell + cell / 2 cy = y0 - (r + 1) * cell + cell * 0.28 c.drawCentredString(cx, cy, str(solution[r][co])) # --- 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 3: tlusté 3×3 čáry + vnější okraj --- c.setStrokeColor(colors.black) c.setLineWidth(thick) for i in range(0, 10, 3): c.line(x0, y0 - i * cell, x0 + 9 * cell, y0 - i * cell) c.line(x0 + i * cell, y0, x0 + i * cell, y0 - 9 * cell) # Popisky klecí (součty) — nakonec, aby nebyly překryty čarami c.setFillColor(colors.white) c.setFont("ArialBold", label_font) for cage in cages: if not cage["cells"]: continue row, col = cage_label_cell(cage) lx = x0 + col * cell + cell * 0.05 ly = y0 - row * cell - label_font * 1.05 txt = str(cage["sum"]) tw = c.stringWidth(txt, "ArialBold", label_font) c.rect(lx - 0.5, ly - 0.5, tw + 1, label_font + 1, fill=1, stroke=0) c.setFillColor(colors.black) c.setFont("ArialBold", label_font) for cage in cages: if not cage["cells"]: continue row, col = cage_label_cell(cage) lx = x0 + col * cell + cell * 0.05 ly = y0 - row * cell - label_font * 1.05 c.drawString(lx, ly, str(cage["sum"])) def main(): conn = connect_mysql(database="puzzle") cur = conn.cursor() cur.execute( "SELECT difficulty, puzzle, solution, extra FROM puzzles " "WHERE game_type='killer_sudoku' AND extra LIKE '%%\"puzzle_number\": 31414%%' " "LIMIT 1" ) row = cur.fetchone() cur.close() conn.close() if not row: print("Žádná data.") return difficulty, puzzle_str, solution_str, extra_json = row extra = json.loads(extra_json) cages = parse_cages(puzzle_str) cage_map = build_cage_map(cages) solution = parse_solution(solution_str) page_w, page_h = A4 board_cm = 11 cell = board_cm * cm / 9 board_px = 9 * cell c = Canvas(str(OUTPUT), pagesize=A4) # Zadání x0 = (page_w - board_px) / 2 y0 = page_h - 2 * cm draw_killer_sudoku(c, x0, y0, cell, cages, cage_map, f"Killer Sudoku (difficulty {difficulty}) — {extra.get('puzzle_number', '')}") # Řešení y0_sol = y0 - board_px - 3 * cm draw_killer_sudoku(c, x0, y0_sol, cell, cages, cage_map, "Řešení", solution=solution) c.save() print(f"PDF uloženo: {OUTPUT}") if __name__ == "__main__": main()