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) => 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 } }