puzzle-trainer/daily-bot.ts
2026-05-23 01:05:21 +00:00

214 lines
6.8 KiB
TypeScript

#!/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<string, number>();
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<string, number>)[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<string, unknown> = {};
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); });