You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
temptress-bot/src/game/handleEvent.ts

367 lines
12 KiB

import { ErrorType, EventResult, IncomingEvent, IncomingEventType, OutgoingEventType } from './gameEvent'
import { GamePhase, GameState, oppositePlayer, PlayerSide, PlayerState, RoundStarter } from './gameState'
import {
DieFace,
DieResult,
DieState,
DieType,
isSelectedState,
setDeselected,
setSelected,
toggleSelected,
} from './dieState'
import { isValidIndex } from '../util/precondition'
import {
calculateBonuses,
calculateDamage,
calculateFails,
calculateRecovery,
calculateStops,
determineNextRoundStarter,
} from './calculateDamage'
export function handleEvent(state: GameState, event: IncomingEvent): EventResult<GameState> {
try {
switch (event.type) {
case IncomingEventType.SelectAction:
return handleSelectActionEvent<GameState>(state, event)
case IncomingEventType.ToggleSelectDie:
return handleToggleSelectIndexEvent<DieResult, GameState>(state, event)
case IncomingEventType.SelectAllDice:
return handleSelectAllEvent<DieResult, GameState>(state, true)
case IncomingEventType.DeselectAllDice:
return handleSelectAllEvent<DieResult, GameState>(state, false)
case IncomingEventType.HoldSelectedDice:
return handleHoldDiceEvent<DieResult, GameState>(state, true)
case IncomingEventType.ReleaseSelectedDice:
return handleHoldDiceEvent<DieResult, GameState>(state, false)
case IncomingEventType.EndTurn:
return handleEndTurnEvent<PlayerState, GameState>(state)
case IncomingEventType.Abort:
return handleAbortEvent<GameState>(state, event)
default:
return {
newState: state,
error: {
type: ErrorType.UnexpectedError,
message: 'Event type not yet implemented',
},
}
}
} catch (e) {
return {
newState: state,
error: {
type: ErrorType.UnexpectedError,
message: `Failed handling event: ${e}`,
},
}
}
}
export interface SelectActionState<A> {
readonly gamePhase: GamePhase
readonly theme: {
readonly actions: readonly A[]
}
readonly action: A
}
export function handleSelectActionEvent<T extends SelectActionState<unknown>>(
state: T,
{ index }: { readonly index: number },
): EventResult<T> {
if (state.gamePhase !== GamePhase.ROUND_START) {
return {
error: {
type: ErrorType.NotValidRightNow,
message: `You can only change actions when the turn has not started yet.`,
},
newState: state,
}
}
if (index < 0 || index >= state.theme.actions.length || index !== Math.floor(index)) {
return {
error: {
type: ErrorType.InvalidIndex,
message: `Select an integer action index between 0 (inclusive) and ${state.theme.actions.length} (exclusive)`,
},
newState: state,
}
}
return {
newState: {
...state,
action: state.theme.actions[index],
},
events: [],
}
}
export interface DiceStatesState<D extends { readonly state: DieState }> {
readonly gamePhase: GamePhase
readonly lastRoll: readonly D[]
}
export function handleToggleSelectIndexEvent<D extends { readonly state: DieState }, T extends DiceStatesState<D>>(
state: T,
{ index }: { readonly index: number },
): EventResult<T> {
if (state.gamePhase !== GamePhase.TURN_ROLLED) {
return {
error: {
type: ErrorType.NotValidRightNow,
message: `You can only toggle the selected state of a die when dice have been rolled.`,
},
newState: state,
}
}
if (!isValidIndex(index, state.lastRoll.length)) {
return {
newState: state,
error: {
type: ErrorType.InvalidIndex,
message: `Select an integer action index between 0 (inclusive) and ${state.lastRoll.length} (exclusive)`,
},
}
}
return {
newState: {
...state,
lastRoll: [
...state.lastRoll.slice(0, index),
{
...state.lastRoll[index],
state: toggleSelected(state.lastRoll[index].state),
},
...state.lastRoll.slice(index + 1),
],
},
events: [],
}
}
function handleSelectAllEvent<D extends { readonly state: DieState }, T extends DiceStatesState<D>>(
state: T,
select: boolean,
): EventResult<T> {
if (state.gamePhase !== GamePhase.TURN_ROLLED) {
return {
error: {
type: ErrorType.NotValidRightNow,
message: `You can only change the selected state of the dice when dice have been rolled.`,
},
newState: state,
}
}
return {
newState: {
...state,
lastRoll: state.lastRoll.map<D>((x) => ({
...x,
state: select ? setSelected(x.state) : setDeselected(x.state),
})),
},
events: [],
}
}
function handleHoldDiceEvent<D extends { readonly state: DieState }, T extends DiceStatesState<D>>(
state: T,
hold: boolean,
): EventResult<T> {
if (state.gamePhase !== GamePhase.TURN_ROLLED) {
return {
error: {
type: ErrorType.NotValidRightNow,
message: `You can only toggle the hold state of dice when dice have been rolled.`,
},
newState: state,
}
}
return {
newState: {
...state,
lastRoll: state.lastRoll.map<D>((v) => {
if (isSelectedState(v.state)) {
return {
...v,
state: hold ? DieState.HELD_SELECTED : DieState.SELECTED,
}
} else {
return v
}
}),
},
events: [],
}
}
export interface EndTurnPlayer {
readonly damage: number
readonly damageMax: number
readonly damageTotal: number
readonly stopCount: number
readonly failCount: number
readonly damageBonuses: number
readonly nextRecoverAt: number
readonly timesRecovered: number
}
export interface EndTurnState<P extends EndTurnPlayer> {
readonly difficulty: {
readonly roundStarter?: PlayerSide | RoundStarter
readonly stats: {
[key in PlayerSide]: {
readonly maxDamageBonuses?: number
readonly minFailCount?: number
readonly maxStopCount?: number
readonly damageBonusBase?: number
readonly damageBonusIncrement?: number
readonly recoverBase?: number
readonly recoverIncrement?: number
readonly recoverPercentBase?: number
readonly recoverPercentIncrement?: number
}
}
}
readonly gamePhase: GamePhase
readonly phaseOwner: PlayerSide
readonly roundOwner: PlayerSide
readonly action: {
readonly dice: readonly DieType[]
readonly givesDamageBonus?: boolean
readonly givesStopCount?: boolean
readonly givesFailCount?: boolean
readonly canFinish?: boolean
}
readonly players: {
readonly [key in PlayerSide]: P
}
readonly lastRoll: readonly { readonly face: DieFace }[]
readonly lastTurnTotal: number
readonly currentTurnTotal: number
readonly selectedDiceValue: number
readonly currentDiceEndTurn: boolean
readonly countedFails: number
}
function handleEndTurnEvent<P extends EndTurnPlayer, T extends EndTurnState<P>>(state: T): EventResult<T> {
if (state.gamePhase !== GamePhase.TURN_ROLLED && state.gamePhase !== GamePhase.TURN_FAILED) {
return {
error: {
type: ErrorType.NotValidRightNow,
message: `You can only end your turn when it's in progress.`,
},
newState: state,
}
}
const failed = state.gamePhase === GamePhase.TURN_FAILED || !state.currentDiceEndTurn
const delta = state.lastTurnTotal - (failed ? 0 : state.currentTurnTotal)
if (delta < 0) {
// The winner of this round has not yet been decided; this is better than the last turn this round.
// Moving on to the next turn...
return {
newState: {
...state,
lastTurnTotal: state.currentTurnTotal,
currentTurnTotal: 0,
selectedDiceValue: 0,
lastRoll: [],
phaseOwner: oppositePlayer(state.phaseOwner),
},
events: [
{
type: OutgoingEventType.PassTurn,
target: state.phaseOwner,
},
{
type: OutgoingEventType.StartTurn,
target: oppositePlayer(state.phaseOwner),
},
],
}
} else {
// That's the round.
// Time to calculate your punishment.
const player = state.players[state.phaseOwner]
const opponent = state.players[oppositePlayer(state.phaseOwner)]
const stats = state.difficulty.stats[state.phaseOwner]
const damageDelta = calculateDamage(delta, player, stats)
const newDamage = player.damage + damageDelta
const newTotal = player.damageTotal + damageDelta
const stops = state.action.givesStopCount ? calculateStops(player, stats) : player.stopCount
const fails = state.action.givesFailCount ? calculateFails(player, stats) : player.failCount
const bonuses = state.action.givesDamageBonus ? calculateBonuses(player, stats) : player.damageBonuses
const opponentDamage = calculateRecovery(
player,
opponent,
state.difficulty.stats[oppositePlayer(state.phaseOwner)],
)
const playerResult: P = {
...player,
damage: newDamage,
damageTotal: newTotal,
stopCount: stops,
failCount: fails,
damageBonuses: bonuses,
}
const opponentResult: P = {
...player,
damage: opponentDamage,
}
const players =
state.phaseOwner === PlayerSide.TOP
? { top: playerResult, bottom: opponentResult }
: { top: opponentResult, bottom: playerResult }
// Oh, no... That's game over for you, my friend...
const newPhase =
state.action.canFinish && playerResult.damage >= playerResult.damageMax
? GamePhase.VICTORY
: GamePhase.ROUND_START
const newRoundOwner =
newPhase === GamePhase.VICTORY
? oppositePlayer(state.phaseOwner)
: determineNextRoundStarter(state, state.difficulty)
return {
newState: {
...state,
players,
currentDiceEndTurn: false,
gamePhase: newPhase,
phaseOwner: newRoundOwner,
roundOwner: newRoundOwner,
lastRoll: [],
currentTurnTotal: 0,
lastTurnTotal: 0,
countedFails: 0,
selectedDiceValue: 0,
},
events: [],
}
}
}
function handleAbortEvent<T extends { readonly gamePhase: GamePhase; readonly phaseOwner: PlayerSide }>(
state: T,
{ player }: { readonly player: PlayerSide },
): EventResult<T> {
if (state.gamePhase === GamePhase.ABORTED || state.gamePhase === GamePhase.VICTORY) {
return {
error: {
type: ErrorType.NotValidRightNow,
message: `The game is already over.`,
},
newState: state,
}
}
return {
newState: {
...state,
gamePhase: GamePhase.ABORTED,
phaseOwner: player ?? state.phaseOwner,
},
events: [],
}
}