Skip to content

Commit

Permalink
Log errors and hide client AI errors (#138)
Browse files Browse the repository at this point in the history
* log errors and don't return to client if during AI move check

* this didn't solve the problem it set out to solve

* extract canPlayerChooseActionResponse

* extract more functions
  • Loading branch information
lounsbrough authored Nov 4, 2024
1 parent 51fb5c3 commit b0100be
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 62 deletions.
16 changes: 0 additions & 16 deletions client/src/components/pages/Game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,9 @@ import { Button, Grid2, Typography } from "@mui/material"
import GameBoard from "../game/GameBoard"
import WaitingRoom from "../game/WaitingRoom"
import { useGameStateContext } from "../../contexts/GameStateContext"
import { useEffect } from "react"

const warnWhenLeavingGame = (event: BeforeUnloadEvent): void => {
event.preventDefault()
}

function Game() {
const { gameState } = useGameStateContext()
const isPlayerAlive = gameState?.selfPlayer?.influences.length

useEffect(() => {
if (isPlayerAlive) {
window.addEventListener("beforeunload", warnWhenLeavingGame)
}

return () => {
window.removeEventListener("beforeunload", warnWhenLeavingGame)
}
}, [isPlayerAlive])

return (
<>
Expand Down
33 changes: 20 additions & 13 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type InterServerEvents = object

type SocketData = { playerId: string }

const genericErrorMessage = 'Unexpected error processing request'

const port = process.env.EXPRESS_PORT || 8008

const app = express()
Expand Down Expand Up @@ -356,7 +358,6 @@ io.on('connection', (socket) => {
getObjectEntries(eventHandlers).forEach(([event, { handler, joiSchema }]) => {
socket.on(event, async (params, callback) => {
const result = joiSchema.validate(params, { abortEarly: false })
const genericErrorMessage = 'Unexpected error processing request'

if (result.error) {
const error = result.error.details.map(({ message }) => message).join(', ')
Expand Down Expand Up @@ -384,14 +385,14 @@ io.on('connection', (socket) => {
pushToSocket.emit(ServerEvents.gameStateChanged, publicGameState)
if (isCallerSocket) callback?.({ gameState: publicGameState })
} catch (error) {
console.error(error, { event, params })
if (event === PlayerActions.checkAiMove) {
return
}
if (error instanceof GameMutationInputError) {
if (error.httpCode >= 500 && error.httpCode <= 599) {
console.error(error)
}
pushToSocket.emit(ServerEvents.error, error.message)
if (isCallerSocket) callback?.({ error: error.message })
} else {
console.error(error)
pushToSocket.emit(ServerEvents.error, genericErrorMessage)
if (isCallerSocket) callback?.({ error: genericErrorMessage })
}
Expand All @@ -412,14 +413,14 @@ io.on('connection', (socket) => {
await emitGameStateChanged(socket)
}
} catch (error) {
console.error(error, { event, params })
if (event === PlayerActions.checkAiMove) {
return
}
if (error instanceof GameMutationInputError) {
if (error.httpCode >= 500 && error.httpCode <= 599) {
console.error(error)
}
socket.emit(ServerEvents.error, error.message)
callback?.({ error: error.message })
} else {
console.error(error)
socket.emit(ServerEvents.error, genericErrorMessage)
callback?.({ error: genericErrorMessage })
}
Expand All @@ -429,23 +430,29 @@ io.on('connection', (socket) => {
})
})

const responseHandler = <T>(handler: (props: T) =>
Promise<{ roomId: string, playerId: string }>) => async (res: Response<PublicGameStateOrError>, props: T) => {
const responseHandler = <T>(
event: PlayerActions,
handler: (props: T) => Promise<{ roomId: string, playerId: string }>
) => async (res: Response<PublicGameStateOrError>, props: T) => {
try {
const publicGameState = await getPublicGameState(await handler(props))
res.status(200).json({ gameState: publicGameState })
} catch (error) {
console.error(error, { event, props })
if (event === PlayerActions.checkAiMove) {
return
}
if (error instanceof GameMutationInputError) {
res.status(error.httpCode).send({ error: error.message })
} else {
res.status(500).send({ error: 'Unexpected error processing request' })
res.status(500).send({ error: genericErrorMessage })
}
}
}

getObjectEntries(eventHandlers).forEach(([event, { express, handler, joiSchema }]) => {
app[express.method](`/${event}`, express.validator(joiSchema), (req, res) => {
return responseHandler(handler)(res, express.parseParams(req))
return responseHandler(event, handler)(res, express.parseParams(req))
})
})

Expand Down
53 changes: 22 additions & 31 deletions server/src/game/actionHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ActionAttributes, Actions, GameState, InfluenceAttributes, Influences,
import { getActionMessage } from '../../../shared/utilities/message'
import { getGameState, getPublicGameState, logEvent, mutateGameState } from "../utilities/gameState"
import { generateRoomId } from "../utilities/identifiers"
import { addPlayerToGame, canPlayerTakeAction, createNewGame, humanOpponentsRemain, killPlayerInfluence, moveTurnToNextPlayer, processPendingAction, promptPlayerToLoseInfluence, removePlayerFromGame, resetGame, revealAndReplaceInfluence, startGame } from "./logic"
import { addPlayerToGame, canPlayerChooseAction, canPlayerChooseActionChallengeResponse, canPlayerChooseActionResponse, canPlayerChooseBlockChallengeResponse, canPlayerChooseBlockResponse, createNewGame, humanOpponentsRemain, killPlayerInfluence, moveTurnToNextPlayer, processPendingAction, promptPlayerToLoseInfluence, removePlayerFromGame, resetGame, revealAndReplaceInfluence, startGame } from "./logic"
import { decideAction, decideActionChallengeResponse, decideActionResponse, decideBlockChallengeResponse, decideBlockResponse, decideInfluencesToLose } from './ai'

const getPlayerInRoom = (gameState: GameState, playerId: string) => {
Expand Down Expand Up @@ -269,7 +269,7 @@ export const checkAiMoveHandler = async ({ roomId, playerId }: {

const turnPlayer = gameState.players.find(({ name }) => name === gameState.turnPlayer)

if (turnPlayer?.ai && canPlayerTakeAction(gameState, turnPlayer)) {
if (turnPlayer?.ai && canPlayerChooseAction(gameState, turnPlayer.name)) {
const { action, targetPlayer } = decideAction(
await getPublicGameState({ gameState, playerId: turnPlayer.id })
)
Expand All @@ -285,9 +285,7 @@ export const checkAiMoveHandler = async ({ roomId, playerId }: {
}

let nextPendingAiPlayer = gameState.players.find(({ ai, name }) =>
ai
&& !gameState.pendingActionChallenge
&& gameState.pendingAction?.pendingPlayers.includes(name))
ai && canPlayerChooseActionResponse(gameState, name))
if (nextPendingAiPlayer) {
const { response, claimedInfluence } = decideActionResponse(
await getPublicGameState({ gameState, playerId: nextPendingAiPlayer.id })
Expand All @@ -303,7 +301,7 @@ export const checkAiMoveHandler = async ({ roomId, playerId }: {
return changedResponse
}

if (turnPlayer?.ai && gameState.pendingActionChallenge) {
if (turnPlayer?.ai && canPlayerChooseActionChallengeResponse(gameState, turnPlayer.name)) {
const { influence } = decideActionChallengeResponse(
await getPublicGameState({ gameState, playerId: turnPlayer.id })
)
Expand All @@ -318,9 +316,7 @@ export const checkAiMoveHandler = async ({ roomId, playerId }: {
}

nextPendingAiPlayer = gameState.players.find(({ ai, name }) =>
ai
&& !gameState.pendingBlockChallenge
&& gameState.pendingBlock?.pendingPlayers.includes(name))
ai && canPlayerChooseBlockResponse(gameState, name))
if (nextPendingAiPlayer) {
const { response } = decideBlockResponse(
await getPublicGameState({ gameState, playerId: nextPendingAiPlayer.id })
Expand All @@ -335,8 +331,8 @@ export const checkAiMoveHandler = async ({ roomId, playerId }: {
return changedResponse
}

nextPendingAiPlayer = gameState.pendingBlockChallenge && gameState.players.find(({ ai, name }) =>
ai && gameState.pendingBlock?.sourcePlayer === name)
nextPendingAiPlayer = gameState.players.find(({ ai, name }) =>
ai && canPlayerChooseBlockChallengeResponse(gameState, name))
if (nextPendingAiPlayer) {
const { influence } = decideBlockChallengeResponse(
await getPublicGameState({ gameState, playerId: nextPendingAiPlayer.id })
Expand Down Expand Up @@ -409,7 +405,7 @@ export const actionHandler = async ({ roomId, playerId, action, targetPlayer }:
throw new GameMutationInputError('Unexpected player state, refusing mutation')
}

if (!canPlayerTakeAction(state, coupingPlayer)) {
if (!canPlayerChooseAction(state, coupingPlayer.name)) {
throw new GameMutationInputError('You can\'t choose an action right now')
}

Expand All @@ -434,7 +430,7 @@ export const actionHandler = async ({ roomId, playerId, action, targetPlayer }:
throw new GameMutationInputError('Unexpected player state, refusing mutation')
}

if (!canPlayerTakeAction(state, incomePlayer)) {
if (!canPlayerChooseAction(state, incomePlayer.name)) {
throw new GameMutationInputError('You can\'t choose an action right now')
}

Expand All @@ -449,7 +445,7 @@ export const actionHandler = async ({ roomId, playerId, action, targetPlayer }:
}
} else {
await mutateGameState(gameState, (state) => {
if (!canPlayerTakeAction(state, player)) {
if (!canPlayerChooseAction(state, player.name)) {
throw new GameMutationInputError('You can\'t choose an action right now')
}

Expand Down Expand Up @@ -490,9 +486,7 @@ export const actionResponseHandler = async ({ roomId, playerId, response, claime
throw new GameMutationInputError('You had your chance')
}

if (!gameState.pendingAction
|| gameState.pendingActionChallenge
|| !gameState.pendingAction.pendingPlayers.includes(player.name)) {
if (!canPlayerChooseActionResponse(gameState, player.name)) {
throw new GameMutationInputError('You can\'t choose an action response right now')
}

Expand All @@ -512,12 +506,12 @@ export const actionResponseHandler = async ({ roomId, playerId, response, claime
}
})
} else if (response === Responses.Challenge) {
if (gameState.pendingAction.claimConfirmed) {
if (gameState.pendingAction!.claimConfirmed) {
throw new GameMutationInputError(`${gameState.turnPlayer} has already confirmed their claim`)
}

if (!ActionAttributes[gameState.pendingAction.action].challengeable) {
throw new GameMutationInputError(`${gameState.pendingAction.action} is not challengeable`)
if (!ActionAttributes[gameState.pendingAction!.action].challengeable) {
throw new GameMutationInputError(`${gameState.pendingAction!.action} is not challengeable`)
}

await mutateGameState(gameState, (state) => {
Expand All @@ -531,12 +525,12 @@ export const actionResponseHandler = async ({ roomId, playerId, response, claime
throw new GameMutationInputError('claimedInfluence is required when blocking')
}

if (InfluenceAttributes[claimedInfluence as Influences].legalBlock !== gameState.pendingAction.action) {
if (InfluenceAttributes[claimedInfluence as Influences].legalBlock !== gameState.pendingAction!.action) {
throw new GameMutationInputError('claimedInfluence can\'t block this action')
}

if (gameState.pendingAction.targetPlayer &&
player.name !== gameState.pendingAction.targetPlayer
if (gameState.pendingAction!.targetPlayer &&
player.name !== gameState.pendingAction!.targetPlayer
) {
throw new GameMutationInputError(`You are not the target of the pending action`)
}
Expand Down Expand Up @@ -577,15 +571,15 @@ export const actionChallengeResponseHandler = async ({ roomId, playerId, influen
throw new GameMutationInputError('You had your chance')
}

if (!gameState.pendingAction || !gameState.pendingActionChallenge || gameState.turnPlayer !== player.name) {
if (!canPlayerChooseActionChallengeResponse(gameState, player.name)) {
throw new GameMutationInputError('You can\'t choose a challenge response right now')
}

if (!player.influences.includes(influence)) {
throw new GameMutationInputError('You don\'t have that influence')
}

if (InfluenceAttributes[influence as Influences].legalAction === gameState.pendingAction.action) {
if (InfluenceAttributes[influence as Influences].legalAction === gameState.pendingAction!.action) {
await mutateGameState(gameState, (state) => {
if (!state.pendingAction || !state.pendingActionChallenge) {
throw new GameMutationInputError('Unable to find pending action or pending action challenge')
Expand Down Expand Up @@ -658,10 +652,7 @@ export const blockResponseHandler = async ({ roomId, playerId, response }: {
throw new GameMutationInputError('You had your chance')
}

if (!gameState.pendingBlock
|| gameState.pendingBlockChallenge
|| !gameState.pendingBlock.pendingPlayers.includes(player.name)
) {
if (!canPlayerChooseBlockResponse(gameState, player.name)) {
throw new GameMutationInputError('You can\'t choose a block response right now')
}

Expand Down Expand Up @@ -732,15 +723,15 @@ export const blockChallengeResponseHandler = async ({ roomId, playerId, influenc
throw new GameMutationInputError('You had your chance')
}

if (!gameState.pendingBlockChallenge || gameState.pendingBlock?.sourcePlayer !== player.name) {
if (!canPlayerChooseBlockChallengeResponse(gameState, player.name)) {
throw new GameMutationInputError('You can\'t choose a challenge response right now')
}

if (!player.influences.includes(influence)) {
throw new GameMutationInputError('You don\'t have that influence')
}

if (influence === gameState.pendingBlock.claimedInfluence) {
if (influence === gameState.pendingBlock!.claimedInfluence) {
await mutateGameState(gameState, (state) => {
if (!state.pendingAction || !state.pendingBlock) {
throw new GameMutationInputError('Unable to find pending action or pending block')
Expand Down
25 changes: 23 additions & 2 deletions server/src/game/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,28 @@ export const moveTurnToNextPlayer = (state: GameState) => {
state.turnPlayer = state.players[nextIndex % state.players.length].name
}

export const canPlayerTakeAction = (state: GameState, player: Player) =>
state.turnPlayer === player.name
export const canPlayerChooseAction = (state: GameState, playerName: string) =>
state.turnPlayer === playerName
&& !state.pendingAction
&& !Object.keys(state.pendingInfluenceLoss).length

export const canPlayerChooseActionResponse = (state: GameState, playerName: string) =>
state.pendingAction
&& !state.pendingActionChallenge
&& !state.pendingBlock
&& state.pendingAction.pendingPlayers.includes(playerName)

export const canPlayerChooseActionChallengeResponse = (state: GameState, playerName: string) =>
state.turnPlayer === playerName
&& state.pendingAction
&& state.pendingActionChallenge

export const canPlayerChooseBlockResponse = (state: GameState, playerName: string) =>
state.pendingBlock
&& !state.pendingBlockChallenge
&& state.pendingBlock.pendingPlayers.includes(playerName)

export const canPlayerChooseBlockChallengeResponse = (state: GameState, playerName: string) =>
state.pendingBlock
&& state.pendingBlockChallenge
&& state.pendingBlock.sourcePlayer === playerName

0 comments on commit b0100be

Please sign in to comment.