ekrizdis367_obsidian-caissa/scripts/fetch-wcc-games.mjs
Matthew Newton 5777b12257 chore: use Node globals for scripts and drop blanket eslint-disable
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-02 15:12:41 -07:00

211 lines
6.2 KiB
JavaScript

/**
* One-shot build script. Reads scripts/wcc-curated.json, downloads each
* match's PGN from PGN Mentor, extracts the curated game rounds, and emits
* src/chess/wcc-data.generated.ts with everything inlined.
*
* Run from the plugin root: `node scripts/fetch-wcc-games.mjs`
*
* The generated file is committed to the repo so end users never make any
* network calls. Only re-run this script when curating new matches/games.
*/
import { Chess } from "chess.js";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..");
const CURATED_PATH = path.join(ROOT, "scripts", "wcc-curated.json");
const OUT_PATH = path.join(ROOT, "src", "chess", "wcc-data.generated.ts");
const USER_AGENT = "obsidian-caissa-build-script";
async function fetchText(url) {
const res = await fetch(url, {
headers: { "User-Agent": USER_AGENT, Accept: "application/x-chess-pgn,*/*" },
});
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText} for ${url}`);
}
return await res.text();
}
/**
* Split a multi-game PGN file on blank lines between games. Returns an array
* of single-game PGN strings (each still containing its [Tag "value"] block
* and the move text).
*/
function splitPgnIntoGames(text) {
const lines = text.split(/\r?\n/);
const games = [];
let current = [];
let inMoves = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith("[")) {
if (inMoves && current.length > 0) {
games.push(current.join("\n").trim());
current = [];
}
inMoves = false;
current.push(line);
} else if (trimmed === "") {
if (current.length > 0) current.push(line);
} else {
inMoves = true;
current.push(line);
}
}
if (current.length > 0) games.push(current.join("\n").trim());
return games.filter((g) => g.length > 0);
}
/** Pull a single tag value out of a single-game PGN string. */
function getHeader(pgn, tag) {
const re = new RegExp(`\\[\\s*${tag}\\s+"((?:[^"\\\\]|\\\\.)*)"\\s*\\]`, "i");
const m = pgn.match(re);
if (!m) return undefined;
return m[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\");
}
/** Verify the PGN is parseable and at least one move was played. */
function validatePgn(pgn) {
const game = new Chess();
game.loadPgn(pgn, { strict: false });
const history = game.history();
return history.length;
}
function jsString(s) {
return JSON.stringify(s);
}
async function main() {
const curated = JSON.parse(fs.readFileSync(CURATED_PATH, "utf8"));
const matches = curated.matches;
const collected = [];
let totalSizeBytes = 0;
for (const match of matches) {
console.log(`\n=== ${match.matchLabel} ===`);
let pgnText;
try {
pgnText = await fetchText(match.pgnUrl);
} catch (e) {
console.warn(` FETCH FAILED: ${e.message}`);
continue;
}
const games = splitPgnIntoGames(pgnText);
console.log(` Downloaded ${games.length} games (${pgnText.length} bytes)`);
for (const targetRound of match.games) {
const found = games.find((g) => {
const round = getHeader(g, "Round");
if (!round) return false;
const num = parseInt(round, 10);
return !isNaN(num) && num === targetRound;
});
if (!found) {
console.warn(` MISS round ${targetRound}`);
continue;
}
let plyCount;
try {
plyCount = validatePgn(found);
} catch (e) {
console.warn(` INVALID round ${targetRound}: ${e.message}`);
continue;
}
if (plyCount === 0) {
console.warn(` EMPTY round ${targetRound} (no moves recorded — forfeit?)`);
continue;
}
const slug = `${match.matchSlug}-${String(targetRound).padStart(2, "0")}`;
const white = getHeader(found, "White") ?? "?";
const black = getHeader(found, "Black") ?? "?";
const result = getHeader(found, "Result") ?? "*";
console.log(` OK round ${targetRound}: ${white} vs ${black} (${result}, ${plyCount} ply)`);
totalSizeBytes += found.length;
collected.push({
id: slug,
matchSlug: match.matchSlug,
matchLabel: match.matchLabel,
matchYear: match.matchYear,
gameNumber: targetRound,
white,
black,
result,
pgn: found,
});
}
}
console.log(`\nCollected ${collected.length} games (${totalSizeBytes} bytes of raw PGN).`);
const matchesOrdered = matches
.map((m) => ({
matchSlug: m.matchSlug,
matchLabel: m.matchLabel,
matchYear: m.matchYear,
}))
.filter((m) => collected.some((g) => g.matchSlug === m.matchSlug));
const out = [];
out.push("// AUTO-GENERATED by scripts/fetch-wcc-games.mjs — do not edit by hand.");
out.push("// Re-run that script to refresh.");
out.push("");
out.push("export interface WccGameData {");
out.push("\tid: string;");
out.push("\tmatchSlug: string;");
out.push("\tmatchLabel: string;");
out.push("\tmatchYear: number;");
out.push("\tgameNumber: number;");
out.push("\twhite: string;");
out.push("\tblack: string;");
out.push("\tresult: string;");
out.push("\tpgn: string;");
out.push("}");
out.push("");
out.push("export interface WccMatchData {");
out.push("\tmatchSlug: string;");
out.push("\tmatchLabel: string;");
out.push("\tmatchYear: number;");
out.push("}");
out.push("");
out.push("export const WCC_MATCHES: WccMatchData[] = [");
for (const m of matchesOrdered) {
out.push("\t{");
out.push(`\t\tmatchSlug: ${jsString(m.matchSlug)},`);
out.push(`\t\tmatchLabel: ${jsString(m.matchLabel)},`);
out.push(`\t\tmatchYear: ${m.matchYear},`);
out.push("\t},");
}
out.push("];");
out.push("");
out.push("export const WCC_GAMES: WccGameData[] = [");
for (const g of collected) {
out.push("\t{");
out.push(`\t\tid: ${jsString(g.id)},`);
out.push(`\t\tmatchSlug: ${jsString(g.matchSlug)},`);
out.push(`\t\tmatchLabel: ${jsString(g.matchLabel)},`);
out.push(`\t\tmatchYear: ${g.matchYear},`);
out.push(`\t\tgameNumber: ${g.gameNumber},`);
out.push(`\t\twhite: ${jsString(g.white)},`);
out.push(`\t\tblack: ${jsString(g.black)},`);
out.push(`\t\tresult: ${jsString(g.result)},`);
out.push(`\t\tpgn: ${jsString(g.pgn)},`);
out.push("\t},");
}
out.push("];");
out.push("");
fs.writeFileSync(OUT_PATH, out.join("\n"), "utf8");
console.log(`\nWrote ${OUT_PATH}`);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});