🌳
pt0/gamesapp/testsF/dbReplayAI.mts
5import { DateTime } from 'luxon'
12const getDtAgo = (updatedAt: Date | null) => {
13 if (!updatedAt) return ''
14 const dt = DateTime.fromJSDate(updatedAt)
15 const diff = luxNow().diff(dt, ['days', 'hours', 'minutes'])
16 if (diff.days > 0) return `${Math.round(diff.days)}d ago`
17 if (diff.hours > 0) return `${Math.round(diff.hours)}h ago`
18 return `${Math.round(diff.minutes)}m ago`
21type ReplayGameFromDbParams = {
22 gameState: GameStateJson | null
23 id: string
24 updatedAt: Date | null
25 verbose: boolean
28const replayGameFromDb = async ({ gameState, id: rawId, updatedAt, verbose }: ReplayGameFromDbParams) => {
29 const id = removeDashes(rawId)
30 const { gameActionHistA } = gameState || {}
31 const dtAgo = getDtAgo(updatedAt)
33 if (!gameActionHistA || gameActionHistA.length === 0) {
34 return { ok: false, message: 'No action history to replay', id, dtAgo }
35 }
37 const result = await replayGame({ gameActionHistA: gameActionHistA as import('./testTypesF.mts').GameAction[], seed: `db-${id}` })
39 if (!result.allPassed) {
40 const failedResult = result.results[result.failedAt] as any
41 return { ok: false, message: `Action ${result.failedAt} failed: ${failedResult?.error?.message || 'unknown'}`, id, actionCount: gameActionHistA.length, dtAgo }
42 }
44 try {
45 assertInvariants(result.harness.getState(), `db-game-${id}`)
46 } catch (err: any) {
47 if (verbose) {
48 // Build sessId -> alias mapping from setPlayerAlias actions
49 const sessIdToAlias: Record<string, string> = {}
50 gameActionHistA.forEach((a: GameActionH) => {
51 if (a.playerAction === 'setPlayerAlias' && a.sessId && a.playerAlias) {
52 sessIdToAlias[a.sessId] = a.playerAlias as string
53 }
54 })
55 console.log(`\n Actions for ${id}:`)
56 gameActionHistA.forEach((a: GameActionH, i: number) => {
57 const alias = sessIdToAlias[a.sessId!] || a.sessId?.slice(0, 8)
58 console.log(` ${i}: ${a.playerAction} by ${alias}${a.assignedStackName ? ` -> ${a.assignedStackName}` : ''}`)
59 })
60 // Show stacks with double responses
61 const paperStacks = result.harness.getState().hiddenState?.paperStacks || {}
62 for (const [stackName, stack] of Object.entries(paperStacks) as [string, any][]) {
63 const playerCounts: Record<string, number> = {}
64 ;(stack.responses || []).forEach((r: any) => {
65 if (r.playerAlias) playerCounts[r.playerAlias] = (playerCounts[r.playerAlias] || 0) + 1
66 })
67 const doubles = Object.entries(playerCounts).filter(([, c]) => c > 1)
68 if (doubles.length > 0) {
69 console.log(` Stack ${stackName} has double responses: ${doubles.map(([p, c]) => `${p}(${c}x)`).join(', ')}`)
70 console.log(` Responses: ${(stack.responses || []).map((r: any) => {
71 const type = r.lastDescription ? 'DESC' : respHasDrawing(r) ? 'DRAW' : '???'
72 return `${r.playerAlias}(${type})`
73 }).join(' -> ')}`)
74 }
75 }
76 }
77 return { ok: false, message: err.message, id, actionCount: gameActionHistA.length, dtAgo }
78 }
80 return {
81 ok: result.allPassed,
82 id,
83 actionCount: gameActionHistA.length,
84 failedAt: result.failedAt,
85 isGameFin: result.harness.getState().isGameFin,
86 dtAgo,
87 }
90type ReplayRecentGamesParams = {
91 limit?: number
92 verbose?: boolean
95export const replayRecentGames = async ({ limit = 10, verbose = false }: ReplayRecentGamesParams) => {
96 const db = getGamesDb()
97 const rows = await db.selectFrom('dbgames')
98 .select(['id', 'game_state', 'updated_at'])
99 .where('is_finished', '=', true)
100 .where(jsonbArrayLenGt('game_state', 'gameActionHistA', 0))
101 // Exclude test games
102 .where('game_id', 'not like', 'ui-test-%')
103 .where('game_id', 'not like', 'live-test-%')
104 .where('game_id', 'not like', 'scale-%')
105 .where(noPlayerAliasLike('game_state', `${testPlayerPrefix}%`))
106 .orderBy('updated_at', 'desc')
107 .limit(limit)
108 .execute()
110 const results: Awaited<ReturnType<typeof replayGameFromDb>>[] = []
112 for (const row of rows) {
113 const { id, game_state, updated_at } = row
114 const result = await replayGameFromDb({
115 gameState: game_state as GameStateJson,
116 id,
117 updatedAt: updated_at,
118 verbose
119 })
120 results.push(result)
121 }
123 const passed = results.filter(r => r.ok).length
124 const failed = results.filter(r => !r.ok).length
126 return {
127 total: results.length,
128 passed,
129 failed,
130 results,
131 allPassed: failed === 0,
132 }
135export const deleteTestGames = async () => {
136 const db = getGamesDb()
137 // Find games that are test games:
138 // 1. game_id starts with 'ui-test-' or 'live-test-'
139 // 2. OR has player with alias starting with testPlayerPrefix
140 const testGames = await db.selectFrom('dbgames')
141 .select(['id'])
142 .where(jsonbArrayLenGt('game_state', 'gameActionHistA', 0))
143 .where((eb) => eb.or([
144 eb('game_id', 'like', 'ui-test-%'),
145 eb('game_id', 'like', 'live-test-%'),
147 ]))
148 .execute()
150 const gameIds = testGames.map(r => r.id)
152 let deletedGames = 0
153 if (gameIds.length > 0) {
154 const deleteResult = await db.deleteFrom('dbgames')
155 .where('id', 'in', gameIds)
156 .executeTakeFirst()
157 deletedGames = Number(deleteResult.numDeletedRows)
158 }
160 // Also delete test rooms from gamerooms table
161 const roomsResult = await db.deleteFrom('gamerooms')
162 .where((eb) => eb.or([
163 eb('room_label', 'like', 'ui-test-%'),
164 eb('room_label', 'like', 'live-test-%'),
165 eb('room_label', 'like', 'scale-%'),
166 ]))
167 .executeTakeFirst()
168 const deletedRooms = Number(roomsResult.numDeletedRows)
170 return { deleted: deletedGames, deletedRooms }