import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { getSession, getAgent } from '$lib/server/auth'; import { getDb } from '$lib/server/db'; function generateTid(): string { const timestamp = Date.now() * 1000; const clockid = Math.floor(Math.random() * 1024); const tid = timestamp.toString(32).padStart(11, '0') + clockid.toString(32).padStart(2, '0'); return tid; } async function resolveHandleToDid(handle: string): Promise { try { // Remove @ prefix if present const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle; const response = await fetch( `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(cleanHandle)}` ); if (!response.ok) return null; const data = await response.json(); return data.did || null; } catch (err) { console.error('Failed to resolve handle:', err); return null; } } // Convert Go notation (e.g., "D4") to board coordinates function notationToCoords(notation: string, boardSize: number): { x: number; y: number } { const col = notation.charAt(0); const row = parseInt(notation.substring(1)); // Convert column letter to x coordinate (A=0, B=1, C=2, D=3, etc., skipping I) let x = col.charCodeAt(0) - 65; // A=0 if (col >= 'J') { x -= 1; // Skip I } // Convert row number to y coordinate (bottom to top) const y = boardSize - row; return { x, y }; } // Get handicap stone positions for a given board size and handicap count function getHandicapPositions(boardSize: number, handicap: number): Array<{ x: number; y: number }> { const positions19x19 = ['D4', 'Q16', 'D16', 'Q4', 'D10', 'Q10', 'K16', 'K4', 'K10']; const positions13x13 = ['D4', 'K10', 'D10', 'K4', 'G7']; const notations = boardSize === 19 ? positions19x19 : boardSize === 13 ? positions13x13 : []; return notations .slice(0, handicap) .map(notation => notationToCoords(notation, boardSize)); } export const POST: RequestHandler = async (event) => { const session = await getSession(event); if (!session) { throw error(401, 'Not authenticated'); } const { boardSize = 19, opponentHandle, colorChoice = 'black', handicap = 0 } = await event.request.json(); if (![5, 7, 9, 13, 19].includes(boardSize)) { throw error(400, 'Invalid board size. Supported: 5x5, 7x7, 9x9, 13x13, 19x19'); } // Validate handicap const maxHandicap = boardSize === 19 ? 9 : boardSize === 13 ? 5 : 0; if (handicap < 0 || handicap > maxHandicap) { throw error(400, `Invalid handicap. Max for ${boardSize}x${boardSize} is ${maxHandicap}`); } try { const agent = await getAgent(event); if (!agent) { throw error(401, 'Failed to get authenticated agent'); } // Resolve opponent handle if provided let opponentDid: string | null = null; if (opponentHandle) { opponentDid = await resolveHandleToDid(opponentHandle); if (!opponentDid) { return json({ error: 'Could not find user with that handle. Please check the handle and try again.' }, { status: 400 }); } if (opponentDid === session.did) { return json({ error: 'You cannot play against yourself!' }, { status: 400 }); } } // Determine player assignments based on color choice and handicap let playerOne: string; let playerTwo: string | null; if (handicap > 0) { // With handicap, creator is always white (playerTwo) playerOne = opponentDid || ''; // Will be filled when opponent joins playerTwo = session.did; } else if (colorChoice === 'random') { // Random assignment const creatorIsBlack = Math.random() < 0.5; if (creatorIsBlack) { playerOne = session.did; playerTwo = opponentDid; } else { playerOne = opponentDid || ''; playerTwo = session.did; } } else if (colorChoice === 'white') { // Creator wants white playerOne = opponentDid || ''; playerTwo = session.did; } else { // Creator wants black (default) playerOne = session.did; playerTwo = opponentDid; } const rkey = generateTid(); const now = new Date().toISOString(); // Create game record in AT Protocol const record: any = { $type: 'boo.sky.go.game', playerOne: playerOne || undefined, playerTwo: playerTwo || undefined, boardSize, status: opponentDid ? 'active' : 'waiting', createdAt: now, }; // Add handicap stones if needed if (handicap > 0) { const handicapPositions = getHandicapPositions(boardSize, handicap); record.handicap = handicap; record.handicapStones = handicapPositions; } // Publish to AT Protocol const result = await (agent as any).post('com.atproto.repo.createRecord', { input: { repo: session.did, collection: 'boo.sky.go.game', rkey, record, }, }); if (!result.ok) { throw new Error(`Failed to create record: ${result.data.message}`); } const uri = result.data.uri; // Store in local discovery index const db = getDb(event.platform); await db .insertInto('games') .values({ id: uri, rkey, creator_did: session.did, // The person who created the game owns the ATProto record player_one: playerOne || null, player_two: playerTwo || null, board_size: boardSize, status: opponentDid ? 'active' : 'waiting', action_count: 0, last_action_type: null, winner: null, handicap: handicap, created_at: now, updated_at: now, }) .execute(); return json({ gameId: rkey, uri }); } catch (err) { console.error('Failed to create game:', err); throw error(500, 'Failed to create game'); } };