1import { sql } from "kysely" 13const resumeOldGameIds: Record<string, string> = {} 15;(global as any).gamesStore = (global as any).gamesStore || {} 16const gamesById: Record<string, any> = (global as any).gamesStore 18const gameCreationLocks: Record<string, Promise<void> | undefined> = {} 20export const mkBaseGameState = () => ({ 21 settings: null as Record<string, unknown> | null, 22 playerStates: {} as Record<string, any>, 25export const getDefaultGameState = ({gameRoom}: {gameRoom: string}) => { 33export const setGameState = async ({gameRoom}: {gameRoom: string}, newGameState: any) => { 35 gamesById[gameRoom] = newGameState 38const loadActiveGameFromDb = async ({gameRoom}: {gameRoom: string}) => { 40 const dbRow = await db.selectFrom('dbgames') 41 .where('game_id', '=', gameRoom) 42 .where('is_finished', '=', false) 43 .where('updated_at', '>', sql`now() - interval '24 hours'` as any) 44 .select(['id', 'game_state']) 45 .orderBy('updated_at', 'desc') 49 if (!dbRow?.game_state?.gameActionHistA) return null 51 const playerCount = Object.keys(dbRow.game_state.gameActionHistA.filter((a: any) => a.playerAction === 'setPlayerAlias')).length 52 betLog('loadActiveGameFromDb', {gameRoom, id: dbRow.id, playerCount}) 54 isReplayGameCtx.enterWith({...isReplayGameCtx.getStore(), isReplay: true}) // ctx:clear 56 gameActionHistA: dbRow.game_state.gameActionHistA, 59 const gqGameState = _gqGameState as any 60 gqGameState.gameActionHistA = gameActionHistA 61 isReplayGameCtx.enterWith(undefined) // ctx:clear 63 if (gqGameState.isGameFin && !gqGameState.needsAiDrawings) { 64 await db.updateTable('dbgames') 65 .set({is_finished: true, created_at: sql`now()`}) 66 .where('id', '=', dbRow.id) 67 .where('is_finished', '=', false) 78const staleCheckIntervalMs = 10_000 79const isGameStaleInDb = async (existingGame: any) => { 80 const {id: gameId} = existingGame 81 if (!gameId) return false 82 const now = Date.now() 83 if (existingGame._lastStaleCheckMs && (now - existingGame._lastStaleCheckMs) < staleCheckIntervalMs) return false 84 existingGame._lastStaleCheckMs = now 86 if (existingGame.justFinishedGameId) { 87 const finRow = await db.selectFrom('dbgames') 88 .where('id', '=', existingGame.justFinishedGameId) 89 .select(['created_at']) 91 const finishedAtMs = finRow?.created_at ? new Date(finRow.created_at).getTime() : 0 92 return (now - finishedAtMs) > 5 * 60_000 94 const row = await db.selectFrom('dbgames') 95 .where('id', '=', gameId) 96 .select(['is_finished']) 98 if (!row) return !existingGame.gameActionHistA?.length 99 return row.is_finished 102export const fetchGameState = async ({gameRoom}: {gameRoom: string}) => { 103 let existingGame = gamesById[gameRoom] 105 const isFinNoAi = existingGame.isGameFin && !existingGame.needsAiDrawings 106 const isStale = !isFinNoAi && await isGameStaleInDb(existingGame) 107 if (isFinNoAi || isStale) { 108 const reason = isFinNoAi ? 'gameFinNoAi' : existingGame.justFinishedGameId ? 'justFinished5min' : 'dbCheck' 109 betLog('fetchGameState:evictingStale', {gameRoom, id: existingGame.id, isGameFin: existingGame.isGameFin, reason}) 110 delete gamesById[gameRoom] 111 existingGame = undefined 117 if (gameCreationLocks[gameRoom]) { 118 await gameCreationLocks[gameRoom] 119 return gamesById[gameRoom] 122 let resolveLock: () => void 123 gameCreationLocks[gameRoom] = new Promise(r => { resolveLock = r }) 126 existingGame = gamesById[gameRoom] 127 if (existingGame) return existingGame 129 const resumeId = resumeOldGameIds[gameRoom] 131 isReplayGameCtx.enterWith({...isReplayGameCtx.getStore(), isReplay: true}) // ctx:clear 133 isReplayGameCtx.enterWith(undefined) // ctx:clear 134 if (oldGame && !oldGame.isGameFin) { 135 oldGame.gameActionHistA ||= [] 138 gamesById[gameRoom] = oldGame 143 existingGame = await loadActiveGameFromDb({gameRoom}) 145 gamesById[gameRoom] = existingGame 146 const activePs = Object.values(existingGame.playerStates || {}).filter((ps: any) => !ps.hasBeenKicked) 147 const hasTestPlayers = activePs.some((ps: any) => ps?.publicState?.playerAlias?.startsWith(testPlayerPrefix)) 148 betLog('fetchGameState:loadedFromDb', {gameRoom, id: existingGame.id, activePlayers: activePs.length, hasTestPlayers}) 152 existingGame = getDefaultGameState({gameRoom}) 153 existingGame._lastStaleCheckMs = Date.now() 156 gamesById[gameRoom] = existingGame 157 betLog('fetchGameState:freshState', {gameRoom, id: existingGame.id}) 161 delete gameCreationLocks[gameRoom]