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.
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}