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.
at master 193 lines 5.9 kB view raw
1import { json, error } from '@sveltejs/kit'; 2import type { RequestHandler } from './$types'; 3import { getSession, getAgent } from '$lib/server/auth'; 4import { getDb } from '$lib/server/db'; 5 6function generateTid(): string { 7 const timestamp = Date.now() * 1000; 8 const clockid = Math.floor(Math.random() * 1024); 9 const tid = timestamp.toString(32).padStart(11, '0') + clockid.toString(32).padStart(2, '0'); 10 return tid; 11} 12 13async function resolveHandleToDid(handle: string): Promise<string | null> { 14 try { 15 // Remove @ prefix if present 16 const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle; 17 18 const response = await fetch( 19 `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(cleanHandle)}` 20 ); 21 22 if (!response.ok) return null; 23 24 const data = await response.json(); 25 return data.did || null; 26 } catch (err) { 27 console.error('Failed to resolve handle:', err); 28 return null; 29 } 30} 31 32// Convert Go notation (e.g., "D4") to board coordinates 33function notationToCoords(notation: string, boardSize: number): { x: number; y: number } { 34 const col = notation.charAt(0); 35 const row = parseInt(notation.substring(1)); 36 37 // Convert column letter to x coordinate (A=0, B=1, C=2, D=3, etc., skipping I) 38 let x = col.charCodeAt(0) - 65; // A=0 39 if (col >= 'J') { 40 x -= 1; // Skip I 41 } 42 43 // Convert row number to y coordinate (bottom to top) 44 const y = boardSize - row; 45 46 return { x, y }; 47} 48 49// Get handicap stone positions for a given board size and handicap count 50function getHandicapPositions(boardSize: number, handicap: number): Array<{ x: number; y: number }> { 51 const positions19x19 = ['D4', 'Q16', 'D16', 'Q4', 'D10', 'Q10', 'K16', 'K4', 'K10']; 52 const positions13x13 = ['D4', 'K10', 'D10', 'K4', 'G7']; 53 54 const notations = boardSize === 19 ? positions19x19 : boardSize === 13 ? positions13x13 : []; 55 56 return notations 57 .slice(0, handicap) 58 .map(notation => notationToCoords(notation, boardSize)); 59} 60 61export const POST: RequestHandler = async (event) => { 62 const session = await getSession(event); 63 64 if (!session) { 65 throw error(401, 'Not authenticated'); 66 } 67 68 const { 69 boardSize = 19, 70 opponentHandle, 71 colorChoice = 'black', 72 handicap = 0 73 } = await event.request.json(); 74 75 if (![5, 7, 9, 13, 19].includes(boardSize)) { 76 throw error(400, 'Invalid board size. Supported: 5x5, 7x7, 9x9, 13x13, 19x19'); 77 } 78 79 // Validate handicap 80 const maxHandicap = boardSize === 19 ? 9 : boardSize === 13 ? 5 : 0; 81 if (handicap < 0 || handicap > maxHandicap) { 82 throw error(400, `Invalid handicap. Max for ${boardSize}x${boardSize} is ${maxHandicap}`); 83 } 84 85 try { 86 const agent = await getAgent(event); 87 if (!agent) { 88 throw error(401, 'Failed to get authenticated agent'); 89 } 90 91 // Resolve opponent handle if provided 92 let opponentDid: string | null = null; 93 if (opponentHandle) { 94 opponentDid = await resolveHandleToDid(opponentHandle); 95 if (!opponentDid) { 96 return json({ error: 'Could not find user with that handle. Please check the handle and try again.' }, { status: 400 }); 97 } 98 if (opponentDid === session.did) { 99 return json({ error: 'You cannot play against yourself!' }, { status: 400 }); 100 } 101 } 102 103 // Determine player assignments based on color choice and handicap 104 let playerOne: string; 105 let playerTwo: string | null; 106 107 if (handicap > 0) { 108 // With handicap, creator is always white (playerTwo) 109 playerOne = opponentDid || ''; // Will be filled when opponent joins 110 playerTwo = session.did; 111 } else if (colorChoice === 'random') { 112 // Random assignment 113 const creatorIsBlack = Math.random() < 0.5; 114 if (creatorIsBlack) { 115 playerOne = session.did; 116 playerTwo = opponentDid; 117 } else { 118 playerOne = opponentDid || ''; 119 playerTwo = session.did; 120 } 121 } else if (colorChoice === 'white') { 122 // Creator wants white 123 playerOne = opponentDid || ''; 124 playerTwo = session.did; 125 } else { 126 // Creator wants black (default) 127 playerOne = session.did; 128 playerTwo = opponentDid; 129 } 130 131 const rkey = generateTid(); 132 const now = new Date().toISOString(); 133 134 // Create game record in AT Protocol 135 const record: any = { 136 $type: 'boo.sky.go.game', 137 playerOne: playerOne || undefined, 138 playerTwo: playerTwo || undefined, 139 boardSize, 140 status: opponentDid ? 'active' : 'waiting', 141 createdAt: now, 142 }; 143 144 // Add handicap stones if needed 145 if (handicap > 0) { 146 const handicapPositions = getHandicapPositions(boardSize, handicap); 147 record.handicap = handicap; 148 record.handicapStones = handicapPositions; 149 } 150 151 // Publish to AT Protocol 152 const result = await (agent as any).post('com.atproto.repo.createRecord', { 153 input: { 154 repo: session.did, 155 collection: 'boo.sky.go.game', 156 rkey, 157 record, 158 }, 159 }); 160 161 if (!result.ok) { 162 throw new Error(`Failed to create record: ${result.data.message}`); 163 } 164 165 const uri = result.data.uri; 166 167 // Store in local discovery index 168 const db = getDb(event.platform); 169 await db 170 .insertInto('games') 171 .values({ 172 id: uri, 173 rkey, 174 creator_did: session.did, // The person who created the game owns the ATProto record 175 player_one: playerOne || null, 176 player_two: playerTwo || null, 177 board_size: boardSize, 178 status: opponentDid ? 'active' : 'waiting', 179 action_count: 0, 180 last_action_type: null, 181 winner: null, 182 handicap: handicap, 183 created_at: now, 184 updated_at: now, 185 }) 186 .execute(); 187 188 return json({ gameId: rkey, uri }); 189 } catch (err) { 190 console.error('Failed to create game:', err); 191 throw error(500, 'Failed to create game'); 192 } 193};