diff --git a/src/server/game.ts b/src/server/game.ts index e85d359..027ae33 100644 --- a/src/server/game.ts +++ b/src/server/game.ts @@ -7,7 +7,8 @@ import { random } from "lodash-es" import type { DeepReadonly } from "vue" import { customAlphabet as createNanoIdWithCustomAlphabet } from "nanoid/non-secure" import { LOBBY_CODE_LENGTH, LOBBY_SIZE, SPECIAL_CARD_PROBABILITY } from "../shared/constants" -import { SpecialCardId, weightedSpecialCardIds } from "../shared/game/cards" +import type { SpecialCardId } from "../shared/game/cards" +import { specialCardsMeta } from "../shared/game/cards" const generateLobbyCode = createNanoIdWithCustomAlphabet("ABCDEFGHKMNPQRSTUVWXYZ", LOBBY_CODE_LENGTH) @@ -124,7 +125,7 @@ export class Game extends EventEmitter { this.addAction({ type: "start", targetSum: 21, - startingPlayerId: players[Math.floor(Math.random() * players.length)].id, + startingPlayerId: players[random(0, players.length - 1)].id, players }) @@ -152,6 +153,8 @@ export class Game extends EventEmitter { type: "hit", initiatingPlayerId: player.id }) + + this.endIfConditionsAreMet() } else this.dealSpecialTo(player.id) @@ -168,13 +171,16 @@ export class Game extends EventEmitter { initiatingPlayerId: playerId }) - if (this.state.players.every(p => p.stayed)) this.end() + this.endIfConditionsAreMet() } useSpecialCard(cardId: SpecialCardId) { const player = this.state.players.find(p => p.id === this.state.activePlayerId)! if (!player.specialCards.includes(cardId)) throw new Error("The player does not have this card.") + const round = this.state.playerChangeCount / this.state.players.length + if (round === 0 && !specialCardsMeta[cardId].isAllowedInFirstRound) throw new Error("This card is not allowed in the first round.") + this.addAction({ type: "use-special", initiatingPlayerId: player.id, @@ -182,6 +188,13 @@ export class Game extends EventEmitter { }) } + checkEndConditions() { + if (this.state.players.every(p => p.stayed)) return true + + const endGameCard = this.state.activeSpecialCards.find(c => c.id === "end-game") + return endGameCard !== undefined && endGameCard.ownerId === this.state.activePlayerId + } + end() { let closestSafeValue = 0 let closestSafePlayerIds: string[] = [] @@ -214,6 +227,10 @@ export class Game extends EventEmitter { }) } + endIfConditionsAreMet() { + if (this.checkEndConditions()) this.end() + } + sendAllOldActionsTo(receiverId: string) { this.actions.forEach(action => this.sendActionTo(action, receiverId)) } @@ -249,7 +266,7 @@ export class Game extends EventEmitter { this.addAction({ type: "deal-special", toPlayerId: playerId, - cardId: weightedSpecialCardIds[random(0, weightedSpecialCardIds.length - 1)] + cardId: this.state.weightedSpecialCardIdList[random(0, this.state.weightedSpecialCardIdList.length - 1)] }) } diff --git a/src/shared/game/cards.ts b/src/shared/game/cards.ts index 68b8f20..0482422 100644 --- a/src/shared/game/cards.ts +++ b/src/shared/game/cards.ts @@ -2,6 +2,8 @@ interface SpecialCardMeta { type: "single-use" | "permanent" description: string weight: number + minPlayers: number + isAllowedInFirstRound: boolean } export type SpecialCardId = @@ -12,55 +14,77 @@ export const specialCardsMeta: Record = { "return-last-opponent": { type: "single-use", description: "Return the last card your opponent drew to the stack.", - weight: 5 + weight: 5, + minPlayers: 0, + isAllowedInFirstRound: false }, "return-last-own": { type: "single-use", description: "Return the last card you drew to the stack.", - weight: 4 + weight: 4, + minPlayers: 0, + isAllowedInFirstRound: false }, "increase-target-by-2": { type: "permanent", description: "Increases the target card value by two.", - weight: 1 + weight: 1, + minPlayers: 0, + isAllowedInFirstRound: true }, "decrease-target-by-2": { type: "permanent", description: "Decreases the target card value by two.", - weight: 1 + weight: 1, + minPlayers: 0, + isAllowedInFirstRound: true }, "next-round-covert": { type: "permanent", description: "The next card you’ll draw will be covert.", - weight: 2 + weight: 2, + minPlayers: 0, + isAllowedInFirstRound: true }, "double-draw": { type: "permanent", description: "You will draw two number cards at once.", - weight: 5 + weight: 5, + minPlayers: 0, + isAllowedInFirstRound: true }, "force-hit": { type: "permanent", description: "Your opponent is not allowed to stay", - weight: 3 + weight: 3, + minPlayers: 0, + isAllowedInFirstRound: true }, "end-game": { type: "permanent", description: "The game ends after everyone had another turn.", - weight: 1000 + weight: 1000, + minPlayers: 3, + isAllowedInFirstRound: true } } export const specialCardIds = Object.keys(specialCardsMeta) as SpecialCardId[] -const weightedSpecialCardIds: SpecialCardId[] = [] +export function createWeightedSpecialCardIdList(ids: SpecialCardId[]) { + const list: SpecialCardId[] = [] -for (const id of specialCardIds) { - const meta = specialCardsMeta[id] + for (const id of ids) { + const meta = specialCardsMeta[id] - for (let i = 0; i < meta.weight; i++) { - weightedSpecialCardIds.push(id) + for (let i = 0; i < meta.weight; i++) { + list.push(id) + } } + + return list } -export { weightedSpecialCardIds } \ No newline at end of file +export function getActiveSpecialCardIds(playerCount: number) { + return specialCardIds.filter(id => playerCount >= specialCardsMeta[id].minPlayers) +} \ No newline at end of file diff --git a/src/shared/game/state.ts b/src/shared/game/state.ts index 3ba7ec9..7d1b86b 100644 --- a/src/shared/game/state.ts +++ b/src/shared/game/state.ts @@ -1,7 +1,7 @@ import type { SpecialCardId } from "./cards" import type { GameAction } from "./actions" import { produce } from "immer" -import { specialCardIds, specialCardsMeta } from "./cards" +import { createWeightedSpecialCardIdList, getActiveSpecialCardIds, specialCardIds, specialCardsMeta } from "./cards" import { cloneDeep, tail, without } from "lodash-es" export interface GameStateNumberCard { @@ -34,6 +34,8 @@ export interface GameState { 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 = { @@ -44,7 +46,9 @@ const UNINITIALIZED_GAME_STATE: GameState = { activeSpecialCards: [], numberCardsStack: [], actualNumberCardsStackSize: 0, - winnerIds: null + winnerIds: null, + weightedSpecialCardIdList: [], + playerChangeCount: 0 } export const getUninitializedGameState = () => cloneDeep(UNINITIALIZED_GAME_STATE) @@ -58,9 +62,10 @@ const getPreviousPlayer = (state: GameState) => { } export const produceNewState = (oldState: GameState, action: GameAction) => produce(oldState, state => { - const activateNextPlayer = () => { + 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) { @@ -83,6 +88,8 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod state.activeSpecialCards = [] state.numberCardsStack = getFullNumbersCardStack() state.actualNumberCardsStackSize = state.numberCardsStack.length + state.weightedSpecialCardIdList = createWeightedSpecialCardIdList(getActiveSpecialCardIds(state.players.length)) + state.playerChangeCount = 0 break @@ -112,7 +119,6 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod break case "hit": - activateNextPlayer() const p4 = state.players.find(p => p.id === action.initiatingPlayerId)! p4.stayed = false p4.nextRoundCovert = false @@ -129,7 +135,6 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod break case "stay": - activateNextPlayer() const p5 = state.players.find(p => p.id === action.initiatingPlayerId)! p5.stayed = true break @@ -203,6 +208,7 @@ function applySpecialCardUsage(state: GameState, id: SpecialCardId, player: Game break + case "end-game": case "force-hit": case "double-draw": state.activeSpecialCards.push({