1import * as _ from 'lodash-es' 16import { sql } from 'kysely' 19// eslint-disable-next-line @typescript-eslint/no-explicit-any 23 lastDescription?: string 24 lastFinishedTrails?: object[] 25 lastImageDataUrl?: string 27 isAiGenerated?: boolean 29type PaperStack = { responses: StackResponse[] } 30type PaperStacks = Record<string, PaperStack> 32 publicState?: { playerAlias?: string, myTaskName?: string, gameActiveAt?: number } 33 assignedStackName?: string 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) => { 45 publicState: {playerAlias} 48 if (paperStacks[playerAlias]) return 50 paperStacks[playerAlias] = { 54 _.set(playerState, ['publicState', 'myTaskName'], 'InitialPrompt') 55 Object.assign(playerState, { 56 assignedStackName: playerAlias, 58 delete playerState.taskPrompt 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 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}) 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) 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 105 return {...playerState, drawingsCount, descriptsCount, drawDescriptRat: drawingsCount / descriptsCount} 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 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] 123 let unfinPlayersH = getUnfinPlayersH() 125 const remainPlayersWCounts = _.filter(playerStatesWCounts, (playerState: AnyGame) => { 126 const {playerAlias} = playerState.publicState 127 return unfinPlayersH[playerAlias] 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 && 150 ps.assignedStackName === stackName 152 if (otherAssignee) return false 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 165 if (stackNeedsDescription(candStack)) { 166 return drawDescriptRat >= totDrawDescriptRat 168 if (stackNeedsDrawing(candStack)) { 169 return drawDescriptRat <= totDrawDescriptRat 173 const candStackLen = candStacks.length 174 const prefStacksLen = prefStacks.length 175 // betLog({playerAlias, drawDescriptRat, candStackLen, prefStacksLen}) 177 const nextStackName = prefStacks[0] as string | undefined 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}) 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) { 196 return {myTaskName: 'DescribeDrawing', taskPrompt: lastFinishedTrails} 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, 211 _.set(playerState, ['publicState', 'myTaskName'], 'WaitingTask') 212 delete playerState.taskPrompt 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 227 const isFin = unfinNames.length == 0 228 // betLog({existingGame, playerStatesA, unfinNames}) 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']) 240 if (getIsAllWaiting() && !isReplayGameCtx.getStore()) { 241 const {gameRoom: gameRoomName} = gameRoomCtx.getStore() || {} 243 gameId: existingGame.id, 245 unfinPlayersH: getUnfinPlayersH(), 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' : '?'), 252 assignTasks({allWaitingEscapeHatch: true}) 255 const ret = areAllPlayersFin() 259type UpdatePlayerStatesResult = { 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()) { 271 needsAiDrawings = stacksNeedingDrawing.length > 0 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) { 292 // Reset sandbox game state to "finished" while keeping actions hist 295 const row = await db.selectFrom('dbgames') 296 .where('id', '=', finishedGameId) 297 .select(['game_state']) 300 Object.assign(newState, { 301 id: finishedGameId, isEphemeralGame, gameActionHistA,