twenty-one/shared/game/state.ts

221 lines
No EOL
6.1 KiB
TypeScript

import type { SpecialCardId } from "./cards"
import type { GameAction } from "./actions"
import { produce } from "immer"
import { createWeightedSpecialCardIdList, getActiveSpecialCardIds, specialCardIds, specialCardsMeta } from "./cards"
import { cloneDeep, tail, without } from "lodash-es"
export interface GameStateNumberCard {
number: number
isCovert: boolean
}
export interface GameStateActiveSpecialCard {
id: SpecialCardId
ownerId: string
}
export const getNumberCardsSum = (cards: Readonly<GameStateNumberCard[]>) => cards.reduce((acc, card) => acc + card.number, 0)
export interface GameStatePlayer {
id: string
name: string
numberCards: GameStateNumberCard[]
specialCards: SpecialCardId[]
stayed: boolean
nextRoundCovert: boolean
}
export interface GameState {
phase: "pre-start" | "running" | "end"
players: GameStatePlayer[]
activeSpecialCards: GameStateActiveSpecialCard[]
activePlayerId: string
targetSum: number
numberCardsStack: number[] // if redacted: contains more cards than there actually are
actualNumberCardsStackSize: number // the length of numberCardsStack, smaller if redacted
winnerIds: string[] | null
weightedSpecialCardIdList: SpecialCardId[]
playerChangeCount: number
}
const UNINITIALIZED_GAME_STATE: GameState = {
phase: "pre-start",
players: [],
activePlayerId: "",
targetSum: 0,
activeSpecialCards: [],
numberCardsStack: [],
actualNumberCardsStackSize: 0,
winnerIds: null,
weightedSpecialCardIdList: [],
playerChangeCount: 0
}
export const getUninitializedGameState = () => cloneDeep(UNINITIALIZED_GAME_STATE)
export const getFullNumbersCardStack = () => tail([...Array(12).keys()])
const getPreviousPlayer = (state: GameState) => {
const activePlayerIndex = state.players.findIndex(p => p.id === state.activePlayerId)
let index = (activePlayerIndex - 1) % state.players.length
if (index < 0) index = state.players.length + index
return state.players[index]
}
export const produceNewState = (oldState: GameState, action: GameAction) => produce(oldState, state => {
if (action.type === "hit" || action.type === "stay") {
const activePlayerIndex = state.players.findIndex(p => p.id === state.activePlayerId)
state.activePlayerId = state.players[(activePlayerIndex + 1) % state.players.length].id
state.playerChangeCount++
}
switch (action.type) {
case "join":
state.players.push({
id: action.initiatingPlayerId,
name: action.name,
numberCards: [],
specialCards: [],
stayed: false,
nextRoundCovert: true
})
break
case "start":
state.phase = "running"
state.activePlayerId = action.startingPlayerId
state.targetSum = action.targetSum
state.activeSpecialCards = []
state.numberCardsStack = getFullNumbersCardStack()
state.actualNumberCardsStackSize = state.numberCardsStack.length
state.weightedSpecialCardIdList = createWeightedSpecialCardIdList(getActiveSpecialCardIds(state.players.length))
state.playerChangeCount = 0
break
case "deal-number":
const p1 = state.players.find(p => p.id === action.toPlayerId)!
p1.numberCards.push({
number: action.number,
isCovert: action.isCovert
})
state.numberCardsStack.splice(state.numberCardsStack.indexOf(action.number), 1)
state.actualNumberCardsStackSize--
if (state.actualNumberCardsStackSize === 0) {
state.numberCardsStack = getFullNumbersCardStack()
state.actualNumberCardsStackSize = state.numberCardsStack.length
}
break
case "deal-special":
if (action.cardId !== undefined) {
const p2 = state.players.find(p => p.id === action.toPlayerId)!
p2.specialCards.push(action.cardId)
}
break
case "hit":
const p4 = state.players.find(p => p.id === action.initiatingPlayerId)!
p4.stayed = false
p4.nextRoundCovert = false
const specialCardIndex1 = state.activeSpecialCards.findIndex(c => c.id === "double-draw" && c.ownerId === p4.id)
if (specialCardIndex1 !== -1) state.activeSpecialCards.splice(specialCardIndex1, 1)
const specialCardIndex2 = state.activeSpecialCards.findIndex(c => c.id === "next-round-covert" && c.ownerId === p4.id)
if (specialCardIndex2 !== -1) state.activeSpecialCards.splice(specialCardIndex2, 1)
const specialCardIndex3 = state.activeSpecialCards.findIndex(c => c.id === "force-hit" && c.ownerId !== p4.id)
if (specialCardIndex3 !== -1) state.activeSpecialCards.splice(specialCardIndex3, 1)
break
case "stay":
const p5 = state.players.find(p => p.id === action.initiatingPlayerId)!
p5.stayed = true
break
case "use-special":
const p3 = state.players.find(p => p.id === action.initiatingPlayerId)!
applySpecialCardUsage(state, action.cardId, p3)
for (const player of state.players) {
player.stayed = false
}
break
case "end":
state.phase = "end"
state.winnerIds = action.winnerIds
for (let player of state.players) {
player.numberCards = action.cardsByPlayerId[player.id].map((number, index) => ({ number, isCovert: player.numberCards[index].isCovert }))
}
}
})
function applySpecialCardUsage(state: GameState, id: SpecialCardId, player: GameStatePlayer) {
player.specialCards.splice(player.specialCards.indexOf(id), 1)
switch (id) {
case "return-last-opponent":
const previousPlayer = getPreviousPlayer(state)
const removedCard1 = previousPlayer.numberCards.pop()
if (removedCard1 !== undefined) {
state.numberCardsStack.push(removedCard1.number)
state.actualNumberCardsStackSize++
}
break
case "return-last-own":
const removedCard2 = player.numberCards.pop()
if (removedCard2 !== undefined) {
state.numberCardsStack.push(removedCard2.number)
state.actualNumberCardsStackSize++
}
break
case "increase-target-by-2":
state.targetSum += 2
state.activeSpecialCards.push({
id,
ownerId: player.id
})
break
case "decrease-target-by-2":
state.targetSum -= 2
state.activeSpecialCards.push({
id,
ownerId: player.id
})
break
case "next-round-covert":
player.nextRoundCovert = true
state.activeSpecialCards.push({
id,
ownerId: player.id
})
break
case "end-game":
case "force-hit":
case "double-draw":
state.activeSpecialCards.push({
id,
ownerId: player.id
})
break
}
}