Add new special card

This commit is contained in:
Moritz Ruth 2023-05-27 14:44:19 +02:00
parent 5d685d0eb0
commit d292a9b6bc
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
3 changed files with 70 additions and 23 deletions

View file

@ -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<Events> {
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<Events> {
type: "hit",
initiatingPlayerId: player.id
})
this.endIfConditionsAreMet()
}
else this.dealSpecialTo(player.id)
@ -168,13 +171,16 @@ export class Game extends EventEmitter<Events> {
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<Events> {
})
}
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<Events> {
})
}
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<Events> {
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)]
})
}

View file

@ -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<SpecialCardId, SpecialCardMeta> = {
"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 youll 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) {
for (const id of ids) {
const meta = specialCardsMeta[id]
for (let i = 0; i < meta.weight; i++) {
weightedSpecialCardIds.push(id)
list.push(id)
}
}
return list
}
export { weightedSpecialCardIds }
export function getActiveSpecialCardIds(playerCount: number) {
return specialCardIds.filter(id => playerCount >= specialCardsMeta[id].minPlayers)
}

View file

@ -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({