/** * Simple SGF parser for extracting moves from SGF files and puzzle positions */ export interface SgfMove { color: 'black' | 'white'; x: number; y: number; } export interface SgfData { boardSize: number; moves: SgfMove[]; } export interface SgfPuzzle { number: number; comment: string; blackStones: Array<{ x: number; y: number }>; whiteStones: Array<{ x: number; y: number }>; boardSize: number; solution?: SgfMove[]; } /** * Parse SGF content and extract board size and moves */ export function parseSgf(sgfContent: string): SgfData { const moves: SgfMove[] = []; let boardSize = 19; // default // Extract board size const sizeMatch = sgfContent.match(/SZ\[(\d+)\]/); if (sizeMatch) { boardSize = parseInt(sizeMatch[1], 10); } // Extract moves - pattern like ;B[dd] or ;W[ed] const movePattern = /;([BW])\[([a-z]{0,2})\]/g; let match; while ((match = movePattern.exec(sgfContent)) !== null) { const color = match[1] === 'B' ? 'black' : 'white'; const coords = match[2]; // Empty brackets mean pass if (!coords) { continue; } // Convert SGF coordinates (aa = top-left) to 0-based x,y const x = coords.charCodeAt(0) - 97; // 'a' = 0 const y = coords.charCodeAt(1) - 97; moves.push({ color, x, y }); } return { boardSize, moves }; } /** * Load and parse an SGF file from a URL */ export async function loadSgf(url: string): Promise { const response = await fetch(url); const content = await response.text(); return parseSgf(content); } /** * Parse coordinates from SGF format (e.g., "be" -> {x: 1, y: 4}) */ function parseCoords(coords: string): { x: number; y: number } { const x = coords.charCodeAt(0) - 97; // 'a' = 0 const y = coords.charCodeAt(1) - 97; return { x, y }; } /** * Extract all coordinates from a property (e.g., AB[be][bf][cb]) */ function extractStones(propertyMatch: string): Array<{ x: number; y: number }> { const stones: Array<{ x: number; y: number }> = []; const coordPattern = /\[([a-z]{2})\]/g; let match; while ((match = coordPattern.exec(propertyMatch)) !== null) { stones.push(parseCoords(match[1])); } return stones; } /** * Parse a multi-problem SGF file into individual puzzles * Format: (;AB[...][...]AW[...][...]C[problem X]) */ export function parseMultiProblemSgf(sgfContent: string): SgfPuzzle[] { const puzzles: SgfPuzzle[] = []; let boardSize = 19; // default // Extract board size if specified const sizeMatch = sgfContent.match(/SZ\[(\d+)\]/); if (sizeMatch) { boardSize = parseInt(sizeMatch[1], 10); } // Split into individual problems - each starts with (; // Match pattern: (;...AB[...]...AW[...]...C[problem X]...) const problemPattern = /\(;[^()]+\)/g; let match; while ((match = problemPattern.exec(sgfContent)) !== null) { const problemStr = match[0]; // Extract black stones (AB property) const blackMatch = problemStr.match(/AB(\[[a-z]{2}\])+/); const blackStones = blackMatch ? extractStones(blackMatch[0]) : []; // Extract white stones (AW property) const whiteMatch = problemStr.match(/AW(\[[a-z]{2}\])+/); const whiteStones = whiteMatch ? extractStones(whiteMatch[0]) : []; // Extract comment (C property) const commentMatch = problemStr.match(/C\[([^\]]+)\]/); const comment = commentMatch ? commentMatch[1] : ''; // Extract problem number from comment const numberMatch = comment.match(/problem (\d+)/i); const number = numberMatch ? parseInt(numberMatch[1], 10) : puzzles.length + 1; puzzles.push({ number, comment, blackStones, whiteStones, boardSize, solution: [] // Solution moves can be added later if needed }); } return puzzles; } /** * Load and parse a multi-problem SGF file from a URL */ export async function loadPuzzleCollection(url: string): Promise { const response = await fetch(url); const content = await response.text(); return parseMultiProblemSgf(content); }