extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342import tenuki from 'tenuki';import type { MoveRecord } from '$lib/types';
const { Game } = tenuki;
export interface ScoreResult { black: number; white: number; winner: 'black' | 'white' | 'tie';}
/** * Calculate the score for a completed Go game using tenuki's scoring engine. * This replays all moves and uses territory scoring (default). * * Note: This provides an estimate - proper scoring requires marking dead stones, * which may need manual adjustment by players. */export function calculateScore( moves: MoveRecord[], boardSize: number = 19, komi: number = 6.5, deadStones: string[] = []): ScoreResult { console.log('[calculateScore] Starting with', moves.length, 'moves, boardSize:', boardSize, 'komi:', komi, 'deadStones:', deadStones.length);
// Validate boardSize if (!boardSize || typeof boardSize !== 'number' || boardSize < 1 || boardSize > 25) { console.error('[calculateScore] Invalid boardSize:', boardSize); return { black: 0, white: 0, winner: 'tie', }; }
// Create a new tenuki game instance (no DOM element needed for scoring) console.log('[calculateScore] Creating tenuki Game instance...'); const game = new Game({ boardSize, komi, scoring: 'territory', }); console.log('[calculateScore] Game instance created');
// Replay all moves console.log('[calculateScore] Replaying moves...'); for (let i = 0; i < moves.length; i++) { const move = moves[i]; // tenuki uses (y, x) coordinates, not (x, y) const success = game.playAt(move.y, move.x); if (!success) { console.warn(`[calculateScore] Failed to replay move ${i + 1} at (${move.x}, ${move.y})`); } } console.log('[calculateScore] All moves replayed');
// Extract current board state to calculate territory manually console.log('[calculateScore] Extracting board state for manual scoring...'); const boardState: Array<Array<'empty' | 'black' | 'white'>> = []; let blackStones = 0; let whiteStones = 0;
for (let y = 0; y < boardSize; y++) { const row: Array<'empty' | 'black' | 'white'> = []; for (let x = 0; x < boardSize; x++) { const intersection = game.intersectionAt(y, x); const value = intersection.value as 'empty' | 'black' | 'white'; row.push(value); if (value === 'black') blackStones++; if (value === 'white') whiteStones++; } boardState.push(row); }
console.log('[calculateScore] Board state extracted. Black stones:', blackStones, 'White stones:', whiteStones);
// Process dead stones - remove them from the board and count as captures for opponent let blackCaptures = 0; let whiteCaptures = 0;
if (deadStones.length > 0) { console.log('[calculateScore] Processing', deadStones.length, 'dead stones...'); for (const notation of deadStones) { // Parse notation like "bA01" or "wT19" const color = notation[0] === 'b' ? 'black' : 'white'; const col = notation.charCodeAt(1) - 65; // A=0, B=1, etc. const row = parseInt(notation.slice(2)) - 1; // 1-indexed to 0-indexed
if (row >= 0 && row < boardSize && col >= 0 && col < boardSize) { if (boardState[row][col] === color) { // Remove the stone from the board boardState[row][col] = 'empty';
// Count as capture for opponent if (color === 'black') { blackStones--; whiteCaptures++; } else { whiteStones--; blackCaptures++; }
console.log(`[calculateScore] Removed dead ${color} stone at (${col}, ${row})`); } else { console.warn(`[calculateScore] Dead stone notation ${notation} doesn't match board state at (${col}, ${row})`); } } else { console.warn(`[calculateScore] Invalid dead stone notation: ${notation}`); } } console.log('[calculateScore] After removing dead stones - Black stones:', blackStones, 'White stones:', whiteStones); console.log('[calculateScore] Captures - Black captured:', blackCaptures, 'White captured:', whiteCaptures); }
// Calculate territory using flood fill (same algorithm as calculateTerritory in board-svg.ts) console.log('[calculateScore] Calculating territory...'); const visited: Array<Array<boolean>> = Array.from( { length: boardSize }, () => Array.from({ length: boardSize }, () => false) );
let blackTerritory = 0; let whiteTerritory = 0;
function floodFill(startY: number, startX: number): { points: Array<[number, number]>; owner: 'black' | 'white' | 'neutral' } { const points: Array<[number, number]> = []; const stack: Array<[number, number]> = [[startY, startX]]; let touchesBlack = false; let touchesWhite = false;
while (stack.length > 0) { const [y, x] = stack.pop()!;
if (y < 0 || y >= boardSize || x < 0 || x >= boardSize) continue; if (visited[y][x]) continue;
const cell = boardState[y][x];
if (cell === 'black') { touchesBlack = true; continue; } if (cell === 'white') { touchesWhite = true; continue; }
// Empty intersection visited[y][x] = true; points.push([y, x]);
// Add neighbors stack.push([y - 1, x], [y + 1, x], [y, x - 1], [y, x + 1]); }
let owner: 'black' | 'white' | 'neutral'; if (touchesBlack && !touchesWhite) { owner = 'black'; } else if (touchesWhite && !touchesBlack) { owner = 'white'; } else { owner = 'neutral'; }
return { points, owner }; }
// Find all empty regions for (let y = 0; y < boardSize; y++) { for (let x = 0; x < boardSize; x++) { if (!visited[y][x] && boardState[y][x] === 'empty') { const { points, owner } = floodFill(y, x); if (owner === 'black') { blackTerritory += points.length; } else if (owner === 'white') { whiteTerritory += points.length; } } } }
console.log('[calculateScore] Territory calculated. Black:', blackTerritory, 'White:', whiteTerritory);
// Calculate final scores (territory + stones + captures) const blackScore = blackStones + blackTerritory + blackCaptures; const whiteScore = whiteStones + whiteTerritory + whiteCaptures + komi;
console.log('[calculateScore] Final scores. Black:', blackScore, 'White:', whiteScore);
// Determine winner let winner: 'black' | 'white' | 'tie'; if (blackScore > whiteScore) { winner = 'black'; } else if (whiteScore > blackScore) { winner = 'white'; } else { winner = 'tie'; }
console.log('[calculateScore] Winner determined:', winner);
return { black: blackScore, white: whiteScore, winner, };}
/** * Get a board state representation from moves for display or analysis. */export function getBoardState( moves: MoveRecord[], boardSize: number = 19): Array<Array<'empty' | 'black' | 'white'>> { // Validate boardSize if (!boardSize || typeof boardSize !== 'number' || boardSize < 1 || boardSize > 25) { console.error('Invalid boardSize:', boardSize); return []; }
const game = new Game({ boardSize, });
// Replay all moves for (const move of moves) { game.playAt(move.y, move.x); }
// Build board state array const state: Array<Array<'empty' | 'black' | 'white'>> = []; for (let y = 0; y < boardSize; y++) { const row: Array<'empty' | 'black' | 'white'> = []; for (let x = 0; x < boardSize; x++) { const intersection = game.intersectionAt(y, x); row.push(intersection.value as 'empty' | 'black' | 'white'); } state.push(row); }
return state;}
/** * Calculate liberties for all stones on the board. * Returns a 2D array where each position contains the liberty count for that stone's group. */export function calculateLiberties( moves: MoveRecord[], boardSize: number = 19): Array<Array<number>> { // Validate boardSize if (!boardSize || typeof boardSize !== 'number' || boardSize < 1 || boardSize > 25) { console.error('Invalid boardSize:', boardSize); return []; }
const game = new Game({ boardSize, });
// Replay all moves for (const move of moves) { game.playAt(move.y, move.x); }
// Get current board state const state: Array<Array<'empty' | 'black' | 'white'>> = []; for (let y = 0; y < boardSize; y++) { const row: Array<'empty' | 'black' | 'white'> = []; for (let x = 0; x < boardSize; x++) { const intersection = game.intersectionAt(y, x); row.push(intersection.value as 'empty' | 'black' | 'white'); } state.push(row); }
// Initialize liberty counts const liberties: Array<Array<number>> = Array(boardSize).fill(0).map(() => Array(boardSize).fill(0)); const visited: Array<Array<boolean>> = Array(boardSize).fill(false).map(() => Array(boardSize).fill(false));
// Helper to get adjacent coordinates const getAdjacent = (y: number, x: number): Array<[number, number]> => { const adj: Array<[number, number]> = []; if (y > 0) adj.push([y - 1, x]); if (y < boardSize - 1) adj.push([y + 1, x]); if (x > 0) adj.push([y, x - 1]); if (x < boardSize - 1) adj.push([y, x + 1]); return adj; };
// Find all stones in a group and their liberties using flood fill const findGroup = (startY: number, startX: number): { stones: Array<[number, number]>, libertyCount: number } => { const color = state[startY][startX]; if (color === 'empty') return { stones: [], libertyCount: 0 };
const stones: Array<[number, number]> = []; const libertySet = new Set<string>(); const queue: Array<[number, number]> = [[startY, startX]]; const groupVisited = new Set<string>();
while (queue.length > 0) { const [y, x] = queue.shift()!; const key = `${y},${x}`;
if (groupVisited.has(key)) continue; groupVisited.add(key); stones.push([y, x]);
for (const [ny, nx] of getAdjacent(y, x)) { const adjKey = `${ny},${nx}`; if (state[ny][nx] === 'empty') { libertySet.add(adjKey); } else if (state[ny][nx] === color && !groupVisited.has(adjKey)) { queue.push([ny, nx]); } } }
return { stones, libertyCount: libertySet.size }; };
// Calculate liberties for each group for (let y = 0; y < boardSize; y++) { for (let x = 0; x < boardSize; x++) { if (state[y][x] !== 'empty' && !visited[y][x]) { const { stones, libertyCount } = findGroup(y, x);
// Set the liberty count for all stones in this group for (const [sy, sx] of stones) { liberties[sy][sx] = libertyCount; visited[sy][sx] = true; } } } }
return liberties;}