🌳
pt0/gamesapp/serverF/getSetGameStateF.mts
1import { sql } from "kysely"
6import { getGamesDb } from "./getGamesDbF.mts"
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>,
23})
25export const getDefaultGameState = ({gameRoom}: {gameRoom: string}) => {
26 const id = genUuid([gameRoom, luxUtcNow().toISO()].join('-'))
27 return {
28 ...mkBaseGameState(),
29 id,
30 }
33export const setGameState = async ({gameRoom}: {gameRoom: string}, newGameState: any) => {
34 if (!gameRoom) throPtErr('!gameRoom')
35 gamesById[gameRoom] = newGameState
38const loadActiveGameFromDb = async ({gameRoom}: {gameRoom: string}) => {
39 const db = getGamesDb()
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')
46 .limit(1)
47 .executeTakeFirst()
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
55 const {gqGameState: _gqGameState, gameActionHistA} = await gameStateFromActionA({
56 gameActionHistA: dbRow.game_state.gameActionHistA,
57 cpGameStateH: {id: dbRow.id, settings: getGameSettings(getPubEnv())},
58 })
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)
68 .execute()
69 return null
70 }
72 const blockedIps = await fetchRoomBlockedIps({gameRoom})
73 gqGameState.settings = {...getGameSettings(getPubEnv()), blockedIps}
75 return gqGameState
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
85 const db = getGamesDb()
86 if (existingGame.justFinishedGameId) {
87 const finRow = await db.selectFrom('dbgames')
88 .where('id', '=', existingGame.justFinishedGameId)
89 .select(['created_at'])
90 .executeTakeFirst()
91 const finishedAtMs = finRow?.created_at ? new Date(finRow.created_at).getTime() : 0
92 return (now - finishedAtMs) > 5 * 60_000
93 }
94 const row = await db.selectFrom('dbgames')
95 .where('id', '=', gameId)
96 .select(['is_finished'])
97 .executeTakeFirst()
98 if (!row) return !existingGame.gameActionHistA?.length
99 return row.is_finished
102export const fetchGameState = async ({gameRoom}: {gameRoom: string}) => {
103 let existingGame = gamesById[gameRoom]
104 if (existingGame) {
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
112 } else {
113 return existingGame
114 }
115 }
117 if (gameCreationLocks[gameRoom]) {
118 await gameCreationLocks[gameRoom]
119 return gamesById[gameRoom]
120 }
122 let resolveLock: () => void
123 gameCreationLocks[gameRoom] = new Promise(r => { resolveLock = r })
125 try {
126 existingGame = gamesById[gameRoom]
127 if (existingGame) return existingGame
129 const resumeId = resumeOldGameIds[gameRoom]
130 if (resumeId) {
131 isReplayGameCtx.enterWith({...isReplayGameCtx.getStore(), isReplay: true}) // ctx:clear
132 const oldGame = await getGameStateAtIdx({id: resumeId}) as any
133 isReplayGameCtx.enterWith(undefined) // ctx:clear
134 if (oldGame && !oldGame.isGameFin) {
135 oldGame.gameActionHistA ||= []
136 const blockedIps = await fetchRoomBlockedIps({gameRoom})
137 oldGame.settings = {...getGameSettings(getPubEnv()), blockedIps}
138 gamesById[gameRoom] = oldGame
139 return oldGame
140 }
141 }
143 existingGame = await loadActiveGameFromDb({gameRoom})
144 if (existingGame) {
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})
149 return existingGame
150 }
152 existingGame = getDefaultGameState({gameRoom})
153 existingGame._lastStaleCheckMs = Date.now()
154 const blockedIps = await fetchRoomBlockedIps({gameRoom})
155 existingGame.settings = {...getGameSettings(getPubEnv()), blockedIps}
156 gamesById[gameRoom] = existingGame
157 betLog('fetchGameState:freshState', {gameRoom, id: existingGame.id})
159 return existingGame
160 } finally {
161 delete gameCreationLocks[gameRoom]
162 resolveLock!()
163 }