mirror of
https://github.com/ekrizdis367/obsidian-caissa.git
synced 2026-07-22 06:50:08 +00:00
211 lines
6.2 KiB
JavaScript
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);
|
|
});
|