🌳
pt0/gamesapp/serverF/telgame/updatePlayerStatesF.mts
1import * as _ from 'lodash-es'
16import { sql } from 'kysely'
17import type { GameStateJson } from '../gamesDbF.d.ts'
19// eslint-disable-next-line @typescript-eslint/no-explicit-any
20type AnyGame = any
21type StackResponse = {
22 playerAlias?: string
23 lastDescription?: string
24 lastFinishedTrails?: object[]
25 lastImageDataUrl?: string
26 lastIpfsCid?: string
27 isAiGenerated?: boolean
29type PaperStack = { responses: StackResponse[] }
30type PaperStacks = Record<string, PaperStack>
31type PlayerState = {
32 publicState?: { playerAlias?: string, myTaskName?: string, gameActiveAt?: number }
33 assignedStackName?: string
34 taskPrompt?: unknown
35 hasBeenKicked?: boolean
36 drawingsCount?: number
37 descriptsCount?: number
38 drawDescriptRat?: number
41const initPaperStacks = ({playerStatesA, paperStacks}: {playerStatesA: AnyGame[], paperStacks: AnyGame}) => {
42 // initialize paper stacks & initial player task etc
43 _.each(playerStatesA, (playerState: AnyGame) => {
44 const {
45 publicState: {playerAlias}
46 } = playerState
48 if (paperStacks[playerAlias]) return
50 paperStacks[playerAlias] = {
51 responses: [],
52 }
54 _.set(playerState, ['publicState', 'myTaskName'], 'InitialPrompt')
55 Object.assign(playerState, {
56 assignedStackName: playerAlias,
57 })
58 delete playerState.taskPrompt
59 })
64export const isBlankStackNoPrompt = ({responses}: {responses: AnyGame[]}) => responses.length == 0
66export const stackNeedsDescription = ({responses}: {responses: AnyGame[]}) => !_.last(responses)?.lastDescription
67export const stackNeedsDrawing = ({responses}: {responses: AnyGame[]}) => !respHasDrawing(_.last(responses) || {})
69export const getGamePlayerStatesPaperStacks = ({existingGame}: {existingGame: AnyGame}) => {
70 const playerStatesA = _.filter(_.values(existingGame.playerStates) as AnyGame[], (playerState: AnyGame) => {
71 const {hasBeenKicked} = playerState
72 return !hasBeenKicked
73 })
75 const paperStacks = getOrSet(existingGame, ['hiddenState', 'paperStacks'].join('.'), {}) as AnyGame
76 initPaperStacks({playerStatesA, paperStacks})
77 return {playerStatesA, paperStacks}
80export const update1PlayerStates = ({existingGame}: {existingGame: AnyGame}) => {
81 const {playerStatesA, paperStacks} = getGamePlayerStatesPaperStacks({existingGame})
83 const {actionAtMs} = getReqGameActionCtx()
85 // Set gameActiveAt for InitialPrompt players who don't have it yet
86 _.each(playerStatesA, (playerState) => {
87 const { myTaskName, gameActiveAt } = playerState.publicState || {}
88 if (myTaskName === 'InitialPrompt' && !gameActiveAt) {
89 _.set(playerState, ['publicState', 'gameActiveAt'], actionAtMs)
90 }
91 })
93 const playerStatesWCounts = _.map(playerStatesA, playerState => {
94 const {publicState: {playerAlias}} = playerState
96 let drawingsCount = 0, descriptsCount = 0
97 _.each(paperStacks, (stack) => {
98 _.each(stack.responses, ({playerAlias: _playerAlias, lastDescription, lastFinishedTrails}) => {
99 if (_playerAlias != playerAlias) return
100 if (lastDescription) descriptsCount += 1
101 if (lastFinishedTrails) drawingsCount += 1
102 })
103 })
105 return {...playerState, drawingsCount, descriptsCount, drawDescriptRat: drawingsCount / descriptsCount}
106 })
108 const getMyFinishedStackNames = ({playerAlias}: {playerAlias: string}) => {
109 return _.compact(_.map(paperStacks, (stack: AnyGame, stackName: string) => {
110 const didRespond = _.find(stack.responses, ({playerAlias: _playerAlias}: AnyGame) => _playerAlias == playerAlias)
111 return didRespond ? stackName : null
112 }))
113 }
114 const getUnfinPlayersH = () => {
115 const paperStackKeys = _.compact(_.map(paperStacks, (stack: AnyGame, key: string) => isBlankStackNoPrompt(stack) ? null : key))
116 return _.fromPairs(_.compact(_.map(playerStatesA, ({publicState: {playerAlias}}: AnyGame) => {
117 const myFinStackNames = getMyFinishedStackNames({playerAlias})
118 const unfinStackNames = _.difference(paperStackKeys, myFinStackNames)
119 return unfinStackNames.length == 0 ? null : [playerAlias, unfinStackNames]
120 })))
121 }
123 let unfinPlayersH = getUnfinPlayersH()
125 const remainPlayersWCounts = _.filter(playerStatesWCounts, (playerState: AnyGame) => {
126 const {playerAlias} = playerState.publicState
127 return unfinPlayersH[playerAlias]
128 })
130 const totNoHumanDrawings = _.sumBy(remainPlayersWCounts, 'drawingsCount')
131 const totNoHumanDescript = _.sumBy(remainPlayersWCounts, 'descriptsCount')
132 const totDrawDescriptRat = totNoHumanDrawings / totNoHumanDescript
133 // betLog({totDrawDescriptRat, totNoHumanDescript, totNoHumanDrawings})
135 const assignTasks = ({allWaitingEscapeHatch}: {allWaitingEscapeHatch?: boolean}={}) => {
136 const paperStackNames = _.keys(paperStacks)
138 _.reject(playerStatesWCounts, 'assignedStackName').forEach(({publicState: {playerAlias}, drawDescriptRat}) => {
139 const playerState = _.find(playerStatesA, (state) => state.publicState.playerAlias == playerAlias)
140 const myFinishedStackNames = getMyFinishedStackNames({playerAlias})
142 const candStacks = _.filter(paperStackNames, (stackName) => {
143 if (_.includes(myFinishedStackNames, stackName)) return false
144 const candStack = paperStacks[stackName]
145 if (isBlankStackNoPrompt(candStack)) return false
146 // Another active player assigned to this stack — avoid collision
147 const otherAssignee = _.find(playerStatesA, ps =>
148 ps.publicState?.playerAlias !== playerAlias &&
149 !ps.hasBeenKicked &&
150 ps.assignedStackName === stackName
151 )
152 if (otherAssignee) return false
153 return true
154 })
156 const prefStacks = _.filter(candStacks, (stackName) => {
157 if (allWaitingEscapeHatch) return true
159 const candStack = paperStacks[stackName]
161 const promptResp = candStack.responses[candStack.responses.length - 1]
162 if (_.isNaN(drawDescriptRat)) {
163 return true // 0/0 from just joined
164 }
165 if (stackNeedsDescription(candStack)) {
166 return drawDescriptRat >= totDrawDescriptRat
167 }
168 if (stackNeedsDrawing(candStack)) {
169 return drawDescriptRat <= totDrawDescriptRat
170 }
171 throPtErr('209fj3209fj23f', {promptResp})
172 })
173 const candStackLen = candStacks.length
174 const prefStacksLen = prefStacks.length
175 // betLog({playerAlias, drawDescriptRat, candStackLen, prefStacksLen})
177 const nextStackName = prefStacks[0] as string | undefined
179 if (nextStackName) {
180 const nextStack = paperStacks[nextStackName]
181 const {responses} = nextStack
182 const lastResponse = _.last(responses) as AnyGame
183 const taskInfo = ((): {myTaskName: string, taskPrompt: AnyGame} | undefined => {
184 const {lastDescription, lastFinishedTrails, lastImageDataUrl, lastIpfsCid} = lastResponse as AnyGame
185 if (lastDescription) {
186 if (!_.isString(lastDescription)) {
187 throPtErr(`!_.isString(lastDescription)`, {lastResponse, nextStack})
188 }
189 return {myTaskName: 'DrawDescription', taskPrompt: lastDescription}
190 } else if (lastIpfsCid) {
191 return {myTaskName: 'DescribeDrawing', taskPrompt: lastIpfsCid}
192 } else if (lastImageDataUrl) {
193 return {myTaskName: 'DescribeDrawing', taskPrompt: lastImageDataUrl}
194 } else if (lastFinishedTrails) {
195 assertArray(lastFinishedTrails, {lastResponse, nextStack})
196 return {myTaskName: 'DescribeDrawing', taskPrompt: lastFinishedTrails}
197 } else {
198 throPtErr('??????', {lastResponse, nextStack})
199 }
200 })()
202 _.set(playerState, ['publicState', 'myTaskName'], taskInfo?.myTaskName)
203 _.set(playerState, ['publicState', 'gameActiveAt'], actionAtMs)
204 // fixes^ inaccurate "drawing for " is actually "current drawing + last wait for"
206 Object.assign(playerState, {
207 taskPrompt: taskInfo?.taskPrompt,
208 assignedStackName: nextStackName,
209 })
210 } else {
211 _.set(playerState, ['publicState', 'myTaskName'], 'WaitingTask')
212 delete playerState.taskPrompt
213 }
214 })
215 }
216 assignTasks()
218 // Require minimum stacks with content, not minimum players (allows game to complete after kicks)
219 const nonBlankStacks = _.reject(paperStacks, isBlankStackNoPrompt)
220 if (nonBlankStacks.length < minNoPlayers) return false
222 const areAllPlayersFin = () => {
223 const unfinNames = _.keys(getUnfinPlayersH())
224 if (_.reject(paperStacks, isBlankStackNoPrompt).length == 0) {
225 return // fix for bug where 4 players join none input prompt, game ends. todo fix inside getUnfinPlayersH instead
226 }
227 const isFin = unfinNames.length == 0
228 // betLog({existingGame, playerStatesA, unfinNames})
229 return isFin
230 }
232 // return areAllPlayersFin() // uncomment to disable escape hatch
233 if (areAllPlayersFin()) return true
235 const getIsAllWaiting = () => {
236 const taskNamesA = _.uniq(_.map(playerStatesA, (state) => _.get(state, ['publicState', 'myTaskName'])))
237 return _.isEqual(taskNamesA, ['WaitingTask'])
238 }
240 if (getIsAllWaiting() && !isReplayGameCtx.getStore()) {
241 const {gameRoom: gameRoomName} = gameRoomCtx.getStore() || {}
242 noThrowNotifErr('allWaitingEscapeHatch', {
243 gameId: existingGame.id,
244 gameRoomName,
245 unfinPlayersH: getUnfinPlayersH(),
246 totDrawDescriptRat,
247 playerRatiosH: _.mapValues(_.keyBy(remainPlayersWCounts, 'publicState.playerAlias'),
248 p => ({draw: p.drawingsCount, desc: p.descriptsCount, rat: p.drawDescriptRat})),
249 stackNeedsH: _.mapValues(_.reject(paperStacks, isBlankStackNoPrompt),
250 s => stackNeedsDrawing(s) ? 'draw' : stackNeedsDescription(s) ? 'desc' : '?'),
251 })
252 assignTasks({allWaitingEscapeHatch: true})
253 }
255 const ret = areAllPlayersFin()
256 return ret
259type UpdatePlayerStatesResult = {
260 isGameFin: boolean
261 pendingAiActionHistA: any[]
262 needsAiDrawings: boolean
265export const updatePlayerStates = async ({existingGame}: {existingGame: AnyGame}): Promise<UpdatePlayerStatesResult> => {
266 const isGameFin = update1PlayerStates({existingGame})
267 let needsAiDrawings = false
269 if (isGameFin && !isReplayGameCtx.getStore()) {
270 const stacksNeedingDrawing = findStacksNeedingAiDrawing({existingGame})
271 needsAiDrawings = stacksNeedingDrawing.length > 0
272 }
274 return {isGameFin: isGameFin ?? false, pendingAiActionHistA: [], needsAiDrawings}
277/** Handle post-game-finish DB operations. Called from doGameAction, not during replay/test. */
278export const handleGameFinished = async ({existingGame, gameRoom}: {existingGame: AnyGame, gameRoom: string}) => {
279 const {isEphemeralGame, id: finishedGameId} = existingGame
280 if (!isEphemeralGame) {
281 await saveResults({existingGame})
282 await doResetGame({justFinishedGameId: finishedGameId})
284 return
285 }
287 if (!isDevPc) {
288 noThrowNotifErr('!isDevPc & isEphemeralGame', {finishedGameId}, {existingGame})
289 return
290 }
292 // Reset sandbox game state to "finished" while keeping actions hist
293 const db = getGamesDb()
294 const newState = getDefaultGameState({gameRoom}) as AnyGame
295 const row = await db.selectFrom('dbgames')
296 .where('id', '=', finishedGameId)
297 .select(['game_state'])
298 .executeTakeFirst()
299 const {gameActionHistA} = (row?.game_state || {}) as GameStateJson
300 Object.assign(newState, {
301 id: finishedGameId, isEphemeralGame, gameActionHistA,
302 })
303 await setGameState({gameRoom}, newState)