1import * as _ from 'lodash-es' 19const applyUpdatePlayerStates = async ({existingGame}) => { 21 Object.assign(existingGame, result) 24const updateActiveAtValAssStack = () => { 26 if ( !_.get(existingGame, ['playerStates', sessId]) ) { 32 _.set(existingGame, ['playerStates', sessId, 'publicState', 'gameActiveAt'], actionAtMs) 36export const setPlayerAlias = async (props) => { 37 const {existingGame, sessId, playerAlias} = props 38 const {ipAddress, userId} = gameActionCtx.getStore() || {} 40 const {settings} = existingGame 42 // Check if player's IP is blocked 43 if (ipAddress && settings?.blockedIps?.includes(ipAddress)) { 47 // Check if player is rejoining (same alias exists) 48 const trimmedAlias = _.trim(playerAlias).toLowerCase() 49 const existingPlayerEntry = _.find(_.toPairs(existingGame.playerStates), ([_sessId, playerState]) => { 50 const {publicState: {playerAlias: _playerAlias}} = playerState 51 return trimmedAlias === _playerAlias 54 // Check alias ownership - if alias exists, verify same user (by userId) or same IP 55 if (existingPlayerEntry && !getEnvConf().skipAliasOwnershipCheck) { 56 const [, existingPlayerState] = existingPlayerEntry 57 const existingUserId = existingPlayerState.userId 58 const existingIp = existingPlayerState.ipAddress 59 // Prefer userId match, fall back to IP match 60 const isOwner = existingUserId 61 ? (userId && existingUserId === userId) 62 : (ipAddress && existingIp === ipAddress) 63 if (!isOwner && (existingUserId || existingIp)) { 68 // If new player (not rejoining), check max players limit 69 const isNewPlayer = !existingPlayerEntry 70 if (isNewPlayer && settings?.maxHumPlayers) { 71 const activePlayers = _.filter(existingGame.playerStates, ps => !ps.hasBeenKicked) 72 if (activePlayers.length >= settings.maxHumPlayers) { 73 silPubErr(`Room is full (max ${settings.maxHumPlayers} players)`) 79 // Store player's IP/userId for ownership validation (after doSetPlayerAlias so sessId is correct) 80 if (ipAddress) _.set(existingGame, ['playerStates', sessId, 'ipAddress'], ipAddress) 81 if (userId) _.set(existingGame, ['playerStates', sessId, 'userId'], userId) 83 await applyUpdatePlayerStates({existingGame}) 86export const submitDrawing = async (props) => { 87 const {existingGame, sessId, finishedTrails, assignedStackName, pregenFncRet, isAiGenerated, aiModel, aiCostUsd} = props 89 // AI-generated drawings don't have a player - just add to stack directly 91 const {hiddenState} = existingGame 92 const {paperStacks} = hiddenState 93 const stack = paperStacks[assignedStackName] 96 const aiPlayerAlias = existingGame.settings?.aiDrawerUsername || getShortModelName(aiModel) || 'ai' 97 const {ipfsCid, imageDataUrl} = props 98 // old games have finishedTrails, newer ones imageDataUrl, newest ipfsCid — replay must handle all 99 /** @type {{lastFinishedTrails?: unknown, lastImageDataUrl?: string, lastIpfsCid?: string}} */ 100 let respH = {lastFinishedTrails: finishedTrails} 101 if (imageDataUrl) respH = {lastImageDataUrl: imageDataUrl} 102 if (ipfsCid) respH = {lastIpfsCid: ipfsCid} 103 stack.responses.push({...respH, playerAlias: aiPlayerAlias, isAiGenerated: true, aiCostUsd}) 107 if (!updateActiveAtValAssStack()) return 113 // Guard: last response should be a description, not a drawing (prevents race condition) 114 const lastResp = _.last(stack.responses) 115 if (lastResp?.lastFinishedTrails) { 116 return // race condition - another drawing was already submitted 119 stack.responses.push({lastFinishedTrails: finishedTrails, playerAlias}) 121 const {content: lastDescription, model, costUsd} = pregenFncRet 123 stack.responses.push({lastDescription, playerAlias: getShortModelName(model), aiCostUsd: costUsd}) 126 delete playerState.assignedStackName 128 await applyUpdatePlayerStates({existingGame}) 130submitDrawing.pregenFnc = aiDescribeDraw 132export const submitDescription = async (props) => { 133 const {existingGame, sessId, descriptionText, assignedStackName, pregenFncRet} = props 134 if (!updateActiveAtValAssStack()) return 140 // Guard: last response should be a drawing, not a description (prevents race condition) 141 const lastResp = _.last(stack.responses) 142 if (lastResp?.lastDescription) { 143 return // race condition - another description was already submitted 146 stack.responses.push({lastDescription: descriptionText, playerAlias}) 148 // Handle AI drawing from pregenFnc (aiDrawingAfterFirstHumanDescript) 149 if (pregenFncRet?.ipfsCid) { 150 const {ipfsCid, model, costUsd, genTimeMs} = pregenFncRet 152 const displayAlias = existingGame.settings?.aiDrawerUsername || modelDerivedAlias 153 stack.responses.push({ 154 lastIpfsCid: ipfsCid, 155 playerAlias: displayAlias, 156 aiModelAlias: modelDerivedAlias, 159 aiGenTimeMs: genTimeMs, 163 delete playerState.assignedStackName 165 await applyUpdatePlayerStates({existingGame}) 167submitDescription.pregenFnc = aiDraw 169export const leaveGame = async (props) => { 170 const {existingGame, sessId} = props 172 const playerState = existingGame.playerStates?.[sessId] 173 if (!playerState || playerState.hasBeenKicked) return 174 const {publicState: {playerAlias}} = playerState 176 await applyUpdatePlayerStates({existingGame}) 178export const debugUpdateGameState = async ({existingGame}) => { 179 await applyUpdatePlayerStates({existingGame}) 182export const kickPlayer = async (props) => { 183 const {existingGame, playerAlias, blockIp} = props 185 // If blockIp is requested, find and block the player's IP 187 const playerEntry = _.find(_.toPairs(existingGame.playerStates), ([_sessId, playerState]) => { 188 return _.get(playerState, ['publicState', 'playerAlias']) === playerAlias 191 const [, playerState] = playerEntry 192 const playerIp = playerState.ipAddress 194 existingGame.settings ||= {} 195 existingGame.settings.blockedIps ||= [] 196 if (!existingGame.settings.blockedIps.includes(playerIp)) { 197 existingGame.settings.blockedIps.push(playerIp) 204 await applyUpdatePlayerStates({existingGame}) 206kickPlayer.persistBlockedIps = true 208export const kickAwol = async (props) => { 209 const { existingGame, sessId } = props 210 const playerState = existingGame.playerStates?.[sessId] 211 if (!playerState) return 213 const { playerAlias } = playerState.publicState || {} 215 await applyUpdatePlayerStates({existingGame}) 218export const resetGame = async ({existingGame}) => { 219 await doResetGame({justFinishedGameId: existingGame.id, markOldGameFinished: true}) 221resetGame.pushToHistory = false 223export const gotoActionIdx = async ({existingGame, tgtActionIdx}) => { 224 const {id, gameActionHistA, gqActionsHistA} = existingGame 225 betLog('gotoActionIdx', {tgtActionIdx, id}) 228 betLog({isIncrOne, allActionsLen: gameActionHistA.length, appliedActionsLen: gqActionsHistA.length}) 234 _.each(existingGame, (val, key) => delete existingGame[key]) 236 Object.assign(existingGame, dbGame) 238gotoActionIdx.pushToHistory = false