Files
ordinaceprojekt/SběrDatRůzné/DailyStr8ts/stahni_str8ts.py
T
Vladimir Buzalka c9903646f1 notebookvb
2026-05-08 13:30:47 +02:00

265 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Stáhne daily Str8ts puzzle data ze solitaire.org, uloží do MySQL,
vygeneruje vlastní PDF (reportlab) a odešle emailem.
"""
import asyncio
import json
import sys
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
from datetime import date
from pathlib import Path
from playwright.async_api import async_playwright
import os
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
_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")))
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "Knihovny"))
from EmailMessagingGraph import send_mail
from mysql_db import connect_mysql
from najdi_dropbox import get_dropbox_root
OUTPUT_DIR = Path(get_dropbox_root()) / "!!!Days" / "Downloads Z230"
URL = "https://www.solitaire.org/daily-str8ts/"
RECIPIENT = ["vladimir.buzalka@buzalka.cz"] # TODO: vrátit alica.buzalkova@buzalka.cz
EMAIL_BODY = """Str8ts — pravidla
Hrací pole 9×9, každá buňka je buď bílá nebo černá.
1. Číslice 19 — do bílých buněk piš čísla 19.
2. Žádné opakování v řádku/sloupci — stejné číslo se nesmí opakovat v celém řádku ani sloupci (jako Sudoku).
3. Straights (sekvence) — bílé buňky oddělené černými tvoří skupiny. Čísla v každé skupině musí tvořit sadu po sobě jdoucích čísel (v libovolném pořadí). Např. skupinka tří buněk může obsahovat {3,4,5} nebo {7,8,9}, ale ne {1,3,5}.
4. Délka sekvence — skupina o délce n musí obsahovat právě n různých čísel jdoucích za sebou.
5. Černé buňky s číslem — někdy mají předvyplněné číslo jako nápovědu; toto číslo se nepočítá do sekvencí, ale blokuje opakování v řádku/sloupci.
Rozdíl od Sudoku: nemusíš vyplnit 19 do každé skupiny — jen zajistit, že čísla v každé skupině tvoří „straight" (jako v pokeru).
"""
GRID = 9
BOARD_CM = 11
BOARD = BOARD_CM * cm
CELL = BOARD / GRID
DIFFICULTIES = ["easy", "medium", "hard"]
# --- PDF generování ---
def draw_str8ts(c: Canvas, x0: float, y0: float, puzzle: str, bw: str, title: str = ""):
font_size = CELL * 0.55
if title:
c.setFont("ArialBold", 12)
c.drawString(x0, y0 + 5, title)
for idx in range(81):
row, col = divmod(idx, 9)
cell_x = x0 + col * CELL
cell_y = y0 - (row + 1) * CELL
cx = cell_x + CELL / 2
cy = cell_y + CELL * 0.3
is_black = bw[idx] == "1"
ch = puzzle[idx]
if is_black:
c.setFillColor(colors.black)
c.rect(cell_x, cell_y, CELL, CELL, fill=1, stroke=0)
if ch in "123456789":
c.setFillColor(colors.yellow if is_black else colors.black)
c.setFont("ArialBold", font_size)
c.drawCentredString(cx, cy, ch)
c.setFillColor(colors.black)
for i in range(GRID + 1):
c.setLineWidth(0.8)
c.line(x0, y0 - i * CELL, x0 + BOARD, y0 - i * CELL)
c.line(x0 + i * CELL, y0, x0 + i * CELL, y0 - BOARD)
def draw_str8ts_small(c: Canvas, x0: float, y0: float, board_cm: float,
solution: str, bw: str, title: str = ""):
board = board_cm * cm
cell = board / GRID
font_size = cell * 0.55
if title:
c.setFont("ArialBold", 8)
c.drawString(x0, y0 + 4, title)
for idx in range(81):
row, col = divmod(idx, 9)
cell_x = x0 + col * cell
cell_y = y0 - (row + 1) * cell
cx = cell_x + cell / 2
cy = cell_y + cell * 0.3
is_black = bw[idx] == "1"
ch = solution[idx]
if is_black:
c.setFillColor(colors.black)
c.rect(cell_x, cell_y, cell, cell, fill=1, stroke=0)
if ch in "123456789":
c.setFillColor(colors.yellow if is_black else colors.black)
c.setFont("ArialBold", max(font_size, 4))
c.drawCentredString(cx, cy, ch)
c.setFillColor(colors.black)
for i in range(GRID + 1):
c.setLineWidth(0.5)
c.line(x0, y0 - i * cell, x0 + board, y0 - i * cell)
c.line(x0 + i * cell, y0, x0 + i * cell, y0 - board)
def generate_pdf(output_path: Path, puzzles: list[dict], title_date: str):
"""puzzles = [{"difficulty": ..., "puzzle": ..., "bw": ..., "solution": ...}, ...]"""
page_w, page_h = A4
x0 = (page_w - BOARD) / 2
c = Canvas(str(output_path), pagesize=A4)
# Stránky se zadáním — 2 puzzle nad sebou
for i in range(0, len(puzzles), 2):
page_puzzles = puzzles[i:i + 2]
for j, p in enumerate(page_puzzles):
y0 = page_h - 2 * cm - j * (BOARD + 3 * cm)
draw_str8ts(c, x0, y0, p["puzzle"], p["bw"], p["difficulty"].capitalize())
c.showPage()
# Poslední stránka — řešení 6×6 cm
sol_cm = 6
sol_board = sol_cm * cm
gap = 1.5 * cm
sol_x0 = (page_w - sol_board) / 2
c.setFont("ArialBold", 14)
c.drawCentredString(page_w / 2, page_h - 2 * cm, "Řešení")
y_cursor = page_h - 3.5 * cm
for p in puzzles:
draw_str8ts_small(c, sol_x0, y_cursor, sol_cm, p["solution"], p["bw"],
p["difficulty"].capitalize())
y_cursor -= sol_board + gap
c.showPage()
c.save()
# --- MySQL ---
def save_to_mysql(puzzle_date: str, puzzles: list[dict]):
conn = connect_mysql(database="puzzle")
cur = conn.cursor()
for p in puzzles:
cur.execute(
"INSERT IGNORE INTO puzzles (game_type, difficulty, puzzle_date, puzzle, solution, extra, source) "
"VALUES (%s, %s, %s, %s, %s, %s, %s)",
("str8ts", p["difficulty"], puzzle_date, p["puzzle"], p["solution"],
json.dumps({"bw": p["bw"]}), "solitaire.org"),
)
inserted = cur.rowcount
cur.close()
conn.close()
return inserted
# --- Extrakce z webu ---
async def fetch_puzzles(mmdd: str) -> list[dict]:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(viewport={"width": 1280, "height": 900})
page = await context.new_page()
print(f"Načítám {URL} ...")
await page.goto(URL, wait_until="networkidle", timeout=60_000)
game_url = None
for frame in page.frames:
if frame.url != page.url and frame.url.strip() not in ("", "about:blank"):
game_url = frame.url
break
if not game_url:
iframe_src = await page.get_attribute("iframe", "src")
if iframe_src:
game_url = iframe_src if iframe_src.startswith("http") else f"https://www.solitaire.org{iframe_src}"
await page.close()
game_page = await context.new_page()
target_url = game_url if game_url else URL
print(f"Načítám hru: {target_url} ...")
await game_page.goto(target_url, wait_until="networkidle", timeout=60_000)
data = await game_page.evaluate("""(key) => {
const result = {};
for (const diff of ['easy', 'medium', 'hard']) {
if (gameLevels[diff] && gameLevels[diff][key]) {
result[diff] = gameLevels[diff][key];
}
}
return result;
}""", mmdd)
await browser.close()
puzzles = []
for diff in DIFFICULTIES:
if diff in data:
puzzles.append({
"difficulty": diff,
"puzzle": data[diff]["puzzle"],
"bw": data[diff]["bw"],
"solution": data[diff]["solution"],
})
return puzzles
# --- Main ---
async def main():
today = date.today()
today_str = today.strftime("%Y-%m-%d")
mmdd = today.strftime("%m-%d")
output_path = OUTPUT_DIR / f"{today_str} Daily Str8ts puzzle.pdf"
if output_path.exists():
print(f"Soubor již existuje: {output_path}")
return
puzzles = await fetch_puzzles(mmdd)
if not puzzles:
raise RuntimeError(f"Žádná data pro {mmdd}")
print(f"Staženo {len(puzzles)} puzzle pro {today_str}")
inserted = save_to_mysql(today_str, puzzles)
print(f"MySQL: vloženo {inserted} nových řádků")
generate_pdf(output_path, puzzles, today_str)
print(f"PDF uloženo: {output_path}")
send_mail(
to=RECIPIENT,
subject="Posílám dnešní Str8ts puzzle v příloze",
body=EMAIL_BODY,
attachments=output_path,
)
print(f"Email odeslán na {RECIPIENT}")
if __name__ == "__main__":
asyncio.run(main())