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 type { DeepReadonly } from "vue"
import { customAlphabet as createNanoIdWithCustomAlphabet } from "nanoid/non-secure" import { customAlphabet as createNanoIdWithCustomAlphabet } from "nanoid/non-secure"
import { LOBBY_CODE_LENGTH, LOBBY_SIZE, SPECIAL_CARD_PROBABILITY } from "../shared/constants" 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) const generateLobbyCode = createNanoIdWithCustomAlphabet("ABCDEFGHKMNPQRSTUVWXYZ", LOBBY_CODE_LENGTH)
@ -124,7 +125,7 @@ export class Game extends EventEmitter<Events> {
this.addAction({ this.addAction({
type: "start", type: "start",
targetSum: 21, targetSum: 21,
startingPlayerId: players[Math.floor(Math.random() * players.length)].id, startingPlayerId: players[random(0, players.length - 1)].id,
players players
}) })
@ -152,6 +153,8 @@ export class Game extends EventEmitter<Events> {
type: "hit", type: "hit",
initiatingPlayerId: player.id initiatingPlayerId: player.id
}) })
this.endIfConditionsAreMet()
} }
else this.dealSpecialTo(player.id) else this.dealSpecialTo(player.id)
@ -168,13 +171,16 @@ export class Game extends EventEmitter<Events> {
initiatingPlayerId: playerId initiatingPlayerId: playerId
}) })
if (this.state.players.every(p => p.stayed)) this.end() this.endIfConditionsAreMet()
} }
useSpecialCard(cardId: SpecialCardId) { useSpecialCard(cardId: SpecialCardId) {
const player = this.state.players.find(p => p.id === this.state.activePlayerId)! 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.") 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({ this.addAction({
type: "use-special", type: "use-special",
initiatingPlayerId: player.id, 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() { end() {
let closestSafeValue = 0 let closestSafeValue = 0
let closestSafePlayerIds: string[] = [] let closestSafePlayerIds: string[] = []
@ -214,6 +227,10 @@ export class Game extends EventEmitter<Events> {
}) })
} }
endIfConditionsAreMet() {
if (this.checkEndConditions()) this.end()
}
sendAllOldActionsTo(receiverId: string) { sendAllOldActionsTo(receiverId: string) {
this.actions.forEach(action => this.sendActionTo(action, receiverId)) this.actions.forEach(action => this.sendActionTo(action, receiverId))
} }
@ -249,7 +266,7 @@ export class Game extends EventEmitter<Events> {
this.addAction({ this.addAction({
type: "deal-special", type: "deal-special",
toPlayerId: playerId, 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" type: "single-use" | "permanent"
description: string description: string
weight: number weight: number
minPlayers: number
isAllowedInFirstRound: boolean
} }
export type SpecialCardId = export type SpecialCardId =
@ -12,55 +14,77 @@ export const specialCardsMeta: Record<SpecialCardId, SpecialCardMeta> = {
"return-last-opponent": { "return-last-opponent": {
type: "single-use", type: "single-use",
description: "Return the last card your opponent drew to the stack.", description: "Return the last card your opponent drew to the stack.",
weight: 5 weight: 5,
minPlayers: 0,
isAllowedInFirstRound: false
}, },
"return-last-own": { "return-last-own": {
type: "single-use", type: "single-use",
description: "Return the last card you drew to the stack.", description: "Return the last card you drew to the stack.",
weight: 4 weight: 4,
minPlayers: 0,
isAllowedInFirstRound: false
}, },
"increase-target-by-2": { "increase-target-by-2": {
type: "permanent", type: "permanent",
description: "Increases the target card value by two.", description: "Increases the target card value by two.",
weight: 1 weight: 1,
minPlayers: 0,
isAllowedInFirstRound: true
}, },
"decrease-target-by-2": { "decrease-target-by-2": {
type: "permanent", type: "permanent",
description: "Decreases the target card value by two.", description: "Decreases the target card value by two.",
weight: 1 weight: 1,
minPlayers: 0,
isAllowedInFirstRound: true
}, },
"next-round-covert": { "next-round-covert": {
type: "permanent", type: "permanent",
description: "The next card youll draw will be covert.", description: "The next card youll draw will be covert.",
weight: 2 weight: 2,
minPlayers: 0,
isAllowedInFirstRound: true
}, },
"double-draw": { "double-draw": {
type: "permanent", type: "permanent",
description: "You will draw two number cards at once.", description: "You will draw two number cards at once.",
weight: 5 weight: 5,
minPlayers: 0,
isAllowedInFirstRound: true
}, },
"force-hit": { "force-hit": {
type: "permanent", type: "permanent",
description: "Your opponent is not allowed to stay", description: "Your opponent is not allowed to stay",
weight: 3 weight: 3,
minPlayers: 0,
isAllowedInFirstRound: true
}, },
"end-game": { "end-game": {
type: "permanent", type: "permanent",
description: "The game ends after everyone had another turn.", 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[] 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] const meta = specialCardsMeta[id]
for (let i = 0; i < meta.weight; i++) { 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 { SpecialCardId } from "./cards"
import type { GameAction } from "./actions" import type { GameAction } from "./actions"
import { produce } from "immer" import { produce } from "immer"
import { specialCardIds, specialCardsMeta } from "./cards" import { createWeightedSpecialCardIdList, getActiveSpecialCardIds, specialCardIds, specialCardsMeta } from "./cards"
import { cloneDeep, tail, without } from "lodash-es" import { cloneDeep, tail, without } from "lodash-es"
export interface GameStateNumberCard { export interface GameStateNumberCard {
@ -34,6 +34,8 @@ export interface GameState {
numberCardsStack: number[] // if redacted: contains more cards than there actually are numberCardsStack: number[] // if redacted: contains more cards than there actually are
actualNumberCardsStackSize: number // the length of numberCardsStack, smaller if redacted actualNumberCardsStackSize: number // the length of numberCardsStack, smaller if redacted
winnerIds: string[] | null winnerIds: string[] | null
weightedSpecialCardIdList: SpecialCardId[]
playerChangeCount: number
} }
const UNINITIALIZED_GAME_STATE: GameState = { const UNINITIALIZED_GAME_STATE: GameState = {
@ -44,7 +46,9 @@ const UNINITIALIZED_GAME_STATE: GameState = {
activeSpecialCards: [], activeSpecialCards: [],
numberCardsStack: [], numberCardsStack: [],
actualNumberCardsStackSize: 0, actualNumberCardsStackSize: 0,
winnerIds: null winnerIds: null,
weightedSpecialCardIdList: [],
playerChangeCount: 0
} }
export const getUninitializedGameState = () => cloneDeep(UNINITIALIZED_GAME_STATE) 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 => { 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) const activePlayerIndex = state.players.findIndex(p => p.id === state.activePlayerId)
state.activePlayerId = state.players[(activePlayerIndex + 1) % state.players.length].id state.activePlayerId = state.players[(activePlayerIndex + 1) % state.players.length].id
state.playerChangeCount++
} }
switch (action.type) { switch (action.type) {
@ -83,6 +88,8 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
state.activeSpecialCards = [] state.activeSpecialCards = []
state.numberCardsStack = getFullNumbersCardStack() state.numberCardsStack = getFullNumbersCardStack()
state.actualNumberCardsStackSize = state.numberCardsStack.length state.actualNumberCardsStackSize = state.numberCardsStack.length
state.weightedSpecialCardIdList = createWeightedSpecialCardIdList(getActiveSpecialCardIds(state.players.length))
state.playerChangeCount = 0
break break
@ -112,7 +119,6 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
break break
case "hit": case "hit":
activateNextPlayer()
const p4 = state.players.find(p => p.id === action.initiatingPlayerId)! const p4 = state.players.find(p => p.id === action.initiatingPlayerId)!
p4.stayed = false p4.stayed = false
p4.nextRoundCovert = false p4.nextRoundCovert = false
@ -129,7 +135,6 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
break break
case "stay": case "stay":
activateNextPlayer()
const p5 = state.players.find(p => p.id === action.initiatingPlayerId)! const p5 = state.players.find(p => p.id === action.initiatingPlayerId)!
p5.stayed = true p5.stayed = true
break break
@ -203,6 +208,7 @@ function applySpecialCardUsage(state: GameState, id: SpecialCardId, player: Game
break break
case "end-game":
case "force-hit": case "force-hit":
case "double-draw": case "double-draw":
state.activeSpecialCards.push({ state.activeSpecialCards.push({