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 = { 24 updatedAt: Date | null 28const replayGameFromDb = async ({ gameState, id: rawId, updatedAt, verbose }: ReplayGameFromDbParams) => { 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 } 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 } 48 // Build sessId -> alias mapping from setPlayerAlias actions 49 const sessIdToAlias: Record<string, string> = {} 51 if (a.playerAction === 'setPlayerAlias' && a.sessId && a.playerAlias) { 52 sessIdToAlias[a.sessId] = a.playerAlias as string 55 console.log(`\n Actions for ${id}:`) 57 const alias = sessIdToAlias[a.sessId!] || a.sessId?.slice(0, 8) 58 console.log(` ${i}: ${a.playerAction} by ${alias}${a.assignedStackName ? ` -> ${a.assignedStackName}` : ''}`) 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 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) => { 72 return `${r.playerAlias}(${type})` 77 return { ok: false, message: err.message, id, actionCount: gameActionHistA.length, dtAgo } 83 actionCount: gameActionHistA.length, 84 failedAt: result.failedAt, 85 isGameFin: result.harness.getState().isGameFin, 90type ReplayRecentGamesParams = { 95export const replayRecentGames = async ({ limit = 10, verbose = false }: ReplayRecentGamesParams) => { 97 const rows = await db.selectFrom('dbgames') 98 .select(['id', 'game_state', 'updated_at']) 99 .where('is_finished', '=', true) 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-%') 106 .orderBy('updated_at', 'desc') 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({ 117 updatedAt: updated_at, 123 const passed = results.filter(r => r.ok).length 124 const failed = results.filter(r => !r.ok).length 127 total: results.length, 131 allPassed: failed === 0, 135export const deleteTestGames = async () => { 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') 143 .where((eb) => eb.or([ 144 eb('game_id', 'like', 'ui-test-%'), 145 eb('game_id', 'like', 'live-test-%'), 150 const gameIds = testGames.map(r => r.id) 153 if (gameIds.length > 0) { 154 const deleteResult = await db.deleteFrom('dbgames') 155 .where('id', 'in', gameIds) 157 deletedGames = Number(deleteResult.numDeletedRows) 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-%'), 168 const deletedRooms = Number(roomsResult.numDeletedRows) 170 return { deleted: deletedGames, deletedRooms }