221 lines
No EOL
6.1 KiB
TypeScript
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
|
|
}
|
|
} |