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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114import type { Kysely } from 'kysely';import { fetchGameMoves, fetchGamePasses } from '$lib/atproto-client';import type { Database, GameRecord } from './db';
export interface Turn { /** Actions played so far (moves and passes), as the records have it. */ actionCount: number; /** Colour of the action about to be played. */ color: 'black' | 'white'; /** Who is expected to play it, or null while the game has no opponent yet. */ expectedPlayer: string | null; /** Type of the last recorded action, for consecutive-pass detection. */ lastActionType: string | null;}
/** Black plays the first action, unless handicap stones reverse the order. */function nextColor(actionCount: number, handicap: number): 'black' | 'white' { const black = handicap > 0 ? actionCount % 2 === 1 : actionCount % 2 === 0; return black ? 'black' : 'white';}
function turnFrom(actionCount: number, game: GameRecord, lastActionType: string | null): Turn { const color = nextColor(actionCount, game.handicap || 0); return { actionCount, color, expectedPlayer: color === 'black' ? game.player_one : game.player_two, lastActionType, };}
/** * Count the actions a game really has, straight from its records. * * Returns null when the records cannot be read, so callers can fall back to * the index instead of failing the request. */async function countRecordedActions( gameAtUri: string): Promise<{ actionCount: number; lastActionType: string | null; updatedAt: string } | null> { try { const [moves, passes] = await Promise.all([ fetchGameMoves(gameAtUri), fetchGamePasses(gameAtUri), ]);
const actions = [ ...moves.map((move) => ({ type: 'move', createdAt: move.createdAt })), ...passes.map((pass) => ({ type: 'pass', createdAt: pass.createdAt })), ].sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
const latest = actions[0]; return { actionCount: actions.length, lastActionType: latest ? latest.type : null, updatedAt: latest ? latest.createdAt : new Date().toISOString(), }; } catch (err) { console.error('[turn] could not read the game records:', err); return null; }}
/** * Whose turn is it, and how many actions have been played? * * The discovery index answers this cheaply, but it is only as fresh as its * writers: this app updates it on its own writes, and the firehose consumer * applies records written through other clients whenever it gets to them. A * count that is one move behind therefore says the turn belongs to the player * who just moved, and the player who is genuinely next is refused — which * looks exactly like the app failing to record moves. * * So when the caller does not match what the index expects, the count is * re-derived from the records themselves and the index is repaired, rather * than trusting a number that has been shown to disagree with the game. */export async function resolveTurn( game: GameRecord, db: Kysely<Database>, sessionDid: string): Promise<Turn> { const indexed = turnFrom(game.action_count, game, game.last_action_type);
if (sessionDid === indexed.expectedPlayer) { return indexed; }
const recorded = await countRecordedActions(game.id);
if (!recorded || recorded.actionCount === game.action_count) { return indexed; }
// The index is behind the game: bring it forward so later requests, and the // games list, see the position the records describe. await db .updateTable('games') .set({ action_count: recorded.actionCount, last_action_type: recorded.lastActionType, updated_at: recorded.updatedAt, }) .where('rkey', '=', game.rkey) .execute();
console.log( `[turn] index was ${game.action_count}, records say ${recorded.actionCount}; ` + `repaired game ${game.rkey}` );
return turnFrom(recorded.actionCount, game, recorded.lastActionType);}