Add new special card
This commit is contained in:
parent
5d685d0eb0
commit
d292a9b6bc
3 changed files with 70 additions and 23 deletions
|
@ -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)]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 you’ll draw will be covert.",
|
description: "The next card you’ll 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { weightedSpecialCardIds }
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveSpecialCardIds(playerCount: number) {
|
||||||
|
return specialCardIds.filter(id => playerCount >= specialCardsMeta[id].minPlayers)
|
||||||
|
}
|
|
@ -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({
|
||||||
|
|
Loading…
Add table
Reference in a new issue