#!/usr/bin/env tsx /** * Puzzle Trainer — Daily Bot * Runs at 09:00 every day, generates all 5 puzzles for today's date, * scrapes their grids, solves them, and logs results to /var/log/puzzle-bot.log */ import { generateQueens, QUEEN_COLORS } from "@/lib/generators/queens"; import { generateTango } from "@/lib/generators/tango"; import { generateZip } from "@/lib/generators/zip"; import { generateSudoku } from "@/lib/generators/sudoku"; import { generatePatches, PATCH_COLORS } from "@/lib/generators/patches"; import * as fs from "fs"; import * as path from "path"; const DATE = new Date().toISOString().slice(0, 10); const LOG_FILE = "/var/log/puzzle-bot.log"; const OUTPUT_DIR = "/srv/stacks/puzzle-trainer/bot-output"; function log(msg: string) { const line = `[${new Date().toISOString()}] ${msg}`; console.log(line); fs.appendFileSync(LOG_FILE, line + "\n"); } function ensureDir(dir: string) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } // ── Queens ──────────────────────────────────────────────────────────────────── function scrapeQueens(date: string) { const puzzle = generateQueens(date); const { size, regions, solution } = puzzle; // ASCII grid with region letters const regionLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const grid: string[] = []; for (let r = 0; r < size; r++) { let row = ""; for (let c = 0; c < size; c++) { const reg = regions[r][c]; const isQueen = solution.some(([sr, sc]) => sr === r && sc === c); row += isQueen ? `[${regionLetters[reg]}]` : ` ${regionLetters[reg]} `; } grid.push(row); } return { game: "queens", date, size, grid, solution, regionCount: size, }; } // ── Tango ───────────────────────────────────────────────────────────────────── function scrapeTango(date: string) { const puzzle = generateTango(date); const { size, given, hEdges, vEdges, solution } = puzzle; const symbols = { sun: "☀", moon: "◐", null: "·" } as const; const grid: string[] = []; for (let r = 0; r < size; r++) { const solRow = solution[r].map(v => symbols[v as keyof typeof symbols] ?? "?").join(" "); const givenRow = given[r].map((v, c) => { const s = symbols[v as keyof typeof symbols] ?? "·"; return v !== null ? `[${s}]` : ` ${symbols[solution[r][c] as keyof typeof symbols]} `; }).join(""); grid.push(givenRow); } return { game: "tango", date, size, grid, solution, given, hEdges, vEdges, }; } // ── Zip ─────────────────────────────────────────────────────────────────────── function scrapeZip(date: string) { const puzzle = generateZip(date); const { size, path, numberedCells, walls } = puzzle; // Build a grid showing numbers and path order const grid: string[] = []; const pathIdx = new Map(); path.forEach(([r, c], i) => pathIdx.set(`${r},${c}`, i + 1)); for (let r = 0; r < size; r++) { let row = ""; for (let c = 0; c < size; c++) { const key = `${r},${c}`; const num = (numberedCells as Record)[key]; if (num !== undefined) row += `[${String(num).padStart(2)}]`; else row += ` ${String(pathIdx.get(key) ?? "?").padStart(2)} `; } grid.push(row); } return { game: "zip", date, size, grid, path, numberedCells, walls, }; } // ── Sudoku ──────────────────────────────────────────────────────────────────── function scrapeSudoku(date: string) { const puzzle = generateSudoku(date); const { size, given, solution } = puzzle; const grid: string[] = []; for (let r = 0; r < size; r++) { let row = ""; for (let c = 0; c < size; c++) { const g = given[r][c]; // 0 = empty, 1-6 = given const s = solution[r][c]; row += g !== 0 ? `[${g}]` : ` ${s} `; } grid.push(row); } return { game: "sudoku", date, size, grid, given, solution }; } // ── Patches ─────────────────────────────────────────────────────────────────── function scrapePatches(date: string) { const puzzle = generatePatches(date); const { size, regions, grid } = puzzle; const letters = "ABCDEFGH"; const asciiGrid: string[] = []; for (let r = 0; r < size; r++) { let row = ""; for (let c = 0; c < size; c++) { row += ` ${letters[grid[r][c]]} `; } asciiGrid.push(row); } // Solution: each region maps to itself const solution = Object.fromEntries(regions.map(r => [r.id, r.id])); return { game: "patches", date, size, grid: asciiGrid, regions: regions.map(r => ({ id: r.id, size: r.size, color: r.color, cells: r.cells, hintCell: r.hintCell, })), solution, }; } // ── Main ────────────────────────────────────────────────────────────────────── async function main() { ensureDir(OUTPUT_DIR); log(`=== Daily puzzle bot starting — ${DATE} ===`); const results: Record = {}; const errors: string[] = []; const scrapers = [ { name: "queens", fn: scrapeQueens }, { name: "tango", fn: scrapeTango }, { name: "zip", fn: scrapeZip }, { name: "sudoku", fn: scrapeSudoku }, { name: "patches", fn: scrapePatches }, ]; for (const { name, fn } of scrapers) { try { const data = fn(DATE); results[name] = data; log(`✓ ${name} (${data.size}x${data.size}) — solved OK`); // Print grid data.grid.forEach((row: string) => log(` ${row}`)); } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); errors.push(`${name}: ${msg}`); log(`✗ ${name} ERROR: ${msg}`); } } // Save JSON output for today const outputFile = path.join(OUTPUT_DIR, `${DATE}.json`); fs.writeFileSync(outputFile, JSON.stringify({ date: DATE, puzzles: results, errors }, null, 2)); log(`Saved → ${outputFile}`); if (errors.length === 0) { log(`=== All 5 puzzles OK — ${DATE} ===`); } else { log(`=== ${errors.length} error(s) — ${DATE} ===`); process.exit(1); } } main().catch(e => { log(`FATAL: ${e}`); process.exit(1); });