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 181 lines 5.4 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"; 5import { fetchGameRecord, fetchCloudGoProfile } from "$lib/atproto-client"; 6import { calculateNewRating, getDefaultRating } from "$lib/server/elo"; 7 8export const POST: RequestHandler = async (event) => { 9 const session = await getSession(event); 10 11 if (!session) { 12 throw error(401, "Not authenticated"); 13 } 14 15 const { status, wins, losses } = await event.request.json(); 16 17 if (status && !["playing", "watching", "offline"].includes(status)) { 18 throw error(400, "Invalid status. Must be: playing, watching, or offline"); 19 } 20 21 try { 22 const agent = await getAgent(event); 23 if (!agent) { 24 throw error(401, "Failed to get authenticated agent"); 25 } 26 27 const now = new Date().toISOString(); 28 29 // Check if profile exists 30 let existingProfile = null; 31 try { 32 const getResult = await (agent as any).get("com.atproto.repo.getRecord", { 33 params: { 34 repo: session.did, 35 collection: "boo.sky.go.profile", 36 rkey: "self", 37 }, 38 }); 39 if (getResult.ok) { 40 existingProfile = getResult.data.value; 41 } 42 } catch (err) { 43 console.error("Failed to get profile:", err); 44 // Profile doesn't exist yet 45 } 46 47 // Recalculate stats from completed games if status is being updated 48 let calculatedWins = existingProfile?.wins ?? 0; 49 let calculatedLosses = existingProfile?.losses ?? 0; 50 let calculatedRanking = existingProfile?.ranking ?? 1200; 51 52 if (status) { 53 try { 54 const stats = await recalculatePlayerStats(session.did, event.platform); 55 calculatedWins = stats.wins; 56 calculatedLosses = stats.losses; 57 calculatedRanking = stats.ranking; 58 } catch (err) { 59 console.error("Failed to recalculate stats:", err); 60 // Fall back to existing values 61 } 62 } 63 64 const record = { 65 $type: "boo.sky.go.profile", 66 wins: wins !== undefined ? wins : calculatedWins, 67 losses: losses !== undefined ? losses : calculatedLosses, 68 ranking: calculatedRanking, 69 status: status || existingProfile?.status || "offline", 70 createdAt: existingProfile?.createdAt || now, 71 updatedAt: now, 72 }; 73 74 if (existingProfile) { 75 // Update existing profile 76 const result = await (agent as any).post("com.atproto.repo.putRecord", { 77 input: { 78 repo: session.did, 79 collection: "boo.sky.go.profile", 80 rkey: "self", 81 record, 82 }, 83 }); 84 85 if (!result.ok) { 86 throw new Error(`Failed to update profile: ${result.data.message}`); 87 } 88 } else { 89 // Create new profile 90 const result = await (agent as any).post( 91 "com.atproto.repo.createRecord", 92 { 93 input: { 94 repo: session.did, 95 collection: "boo.sky.go.profile", 96 rkey: "self", 97 record, 98 }, 99 }, 100 ); 101 102 if (!result.ok) { 103 throw new Error(`Failed to create profile: ${result.data.message}`); 104 } 105 } 106 107 return json({ success: true, profile: record }); 108 } catch (err) { 109 console.error("Failed to update profile:", err); 110 throw error(500, "Failed to update profile"); 111 } 112}; 113 114/** 115 * Recalculate a player's wins, losses, and ranking from all completed games 116 */ 117async function recalculatePlayerStats( 118 did: string, 119 platform: App.Platform | undefined, 120): Promise<{ wins: number; losses: number; ranking: number }> { 121 const db = getDb(platform); 122 123 // Fetch all completed games where the player participated 124 const completedGames = await db 125 .selectFrom("games") 126 .selectAll() 127 .where("status", "=", "completed") 128 .where((eb) => 129 eb.or([eb("player_one", "=", did), eb("player_two", "=", did)]), 130 ) 131 .execute(); 132 133 let wins = 0; 134 let losses = 0; 135 let currentRanking = getDefaultRating(); 136 137 // Sort by updated_at to process games in chronological order 138 completedGames.sort( 139 (a, b) => 140 new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime(), 141 ); 142 143 // Process each game to calculate wins/losses and update ranking 144 for (const game of completedGames) { 145 if (!game.player_two) continue; // Skip games without opponent 146 147 try { 148 // Fetch the game record to get winner 149 const gameRecord = await fetchGameRecord(game.player_one, game.rkey); 150 if (!gameRecord?.winner) continue; 151 152 const won = gameRecord.winner === did; 153 const opponentDid = 154 game.player_one === did ? game.player_two : game.player_one; 155 156 // Update win/loss count 157 if (won) { 158 wins++; 159 } else { 160 losses++; 161 } 162 163 // Fetch opponent's ranking at time of calculation 164 const opponentProfile = await fetchCloudGoProfile(opponentDid); 165 const opponentRanking = opponentProfile?.ranking ?? getDefaultRating(); 166 167 // Calculate new ranking 168 const actualScore = won ? 1 : 0; 169 currentRanking = calculateNewRating( 170 currentRanking, 171 opponentRanking, 172 actualScore, 173 ); 174 } catch (err) { 175 console.error(`Failed to process game ${game.id}:`, err); 176 // Continue processing other games 177 } 178 } 179 180 return { wins, losses, ranking: currentRanking }; 181}