commit #8
This commit is contained in:
parent
26772d9d30
commit
a63315e44c
15 changed files with 236 additions and 174 deletions
|
@ -2,7 +2,7 @@
|
|||
<div class="bg-gray-900 h-100vh w-100vw overflow-y-auto text-white p-10">
|
||||
<div :class="$style.noise"/>
|
||||
<div :class="$style.vignette"/>
|
||||
<div class="relative max-w-1200px h-full mx-auto">
|
||||
<div class="relative h-full">
|
||||
<div v-if="isLoading" class="flex flex-col justify-center items-center text-4xl">
|
||||
<span>Loading…</span>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,6 @@ import { computed, reactive, readonly, ref } from "vue"
|
|||
import { GameState, getNumberCardsSum, getUninitializedGameState, produceNewState } from "./shared/game/state"
|
||||
import { trpcClient } from "./trpc"
|
||||
import { useAuth } from "./auth"
|
||||
import { read } from "fs"
|
||||
import type { SpecialCardId } from "./shared/game/cards"
|
||||
|
||||
const gameActionsBusKey = Symbol() as EventBusKey<GameAction>
|
||||
|
@ -60,12 +59,20 @@ export const useGame = defineStore("game", () => {
|
|||
})
|
||||
}
|
||||
|
||||
const ownNumberCardsSum = computed(() => getNumberCardsSum(state.value.players.find(p => p.id === auth.requiredUser.id)!.numberCards))
|
||||
const isForcedToHit = computed(
|
||||
() => state.value.activeSpecialCards.find(c => c.id === "force-hit" && c.ownerId !== auth.requiredUser.id) !== undefined
|
||||
)
|
||||
|
||||
return {
|
||||
lobbyCode: readonly(lobbyCode),
|
||||
isActive: computed(() => lobbyCode.value !== null),
|
||||
isOwnGame: computed(() => state.value.players.findIndex(p => p.id === (auth.authenticatedUser?.id ?? "")) === 0),
|
||||
isYourTurn: computed(() => state.value.activePlayerId === auth.requiredUser.id),
|
||||
isBust: computed(() => getNumberCardsSum(state.value.players.find(p => p.id === auth.requiredUser.id)!.numberCards) > state.value.targetSum),
|
||||
isBust: computed(() => ownNumberCardsSum.value > state.value.targetSum),
|
||||
mayHit: computed(() => ownNumberCardsSum.value < state.value.targetSum || isForcedToHit.value),
|
||||
isForcedToHit,
|
||||
ownNumberCardsSum,
|
||||
state: readonly(state),
|
||||
actions: readonly(actions),
|
||||
join,
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-14">
|
||||
<div class="flex gap-15 justify-center">
|
||||
<div>
|
||||
<div class="font-bold text-lg pb-2 pl-2">Active special cards</div>
|
||||
<div class="p-4 border border-dark-200 rounded-xl flex flex-col gap-5 flex-wrap w-full">
|
||||
<div
|
||||
v-if="game.state.activeSpecialCards.length === 0"
|
||||
class="font-fat opacity-40 text-xl w-50 flex p-2"
|
||||
>
|
||||
<span>There are none.</span>
|
||||
</div>
|
||||
<SpecialCard
|
||||
v-for="card in game.state.activeSpecialCards"
|
||||
:key="card.id"
|
||||
:id="card.id"
|
||||
:owner-id="card.ownerId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-14 max-w-1200px flex-grow">
|
||||
<PlayerCards v-for="playerId in reorderedPlayerIds" :key="playerId" :player-id="playerId"/>
|
||||
<div class="flex gap-5 justify-end items-center transform transition ease duration-500">
|
||||
<template v-if="game.state.phase === 'end'">
|
||||
|
@ -17,20 +35,21 @@
|
|||
</div>
|
||||
<BigButton
|
||||
class="bg-gradient-to-br from-red-800 to-red-900 uppercase"
|
||||
:disabled="!game.isYourTurn || game.isBust"
|
||||
:disabled="!game.isYourTurn || !game.mayHit"
|
||||
@click="game.hit()"
|
||||
>
|
||||
Hit
|
||||
</BigButton>
|
||||
<BigButton
|
||||
class="bg-gradient-to-br from-blue-800 to-blue-900 uppercase"
|
||||
:disabled="!game.isYourTurn"
|
||||
:disabled="!game.isYourTurn || game.isForcedToHit"
|
||||
@click="game.stay()"
|
||||
>
|
||||
Stay
|
||||
</BigButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<GameEndModal/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -45,8 +64,8 @@
|
|||
import BigButton from "./BigButton.vue"
|
||||
import { computed } from "vue"
|
||||
import { useAuth } from "../auth"
|
||||
import { getNumberCardsSum } from "../shared/game/state"
|
||||
import GameEndModal from "./GameEndModal.vue"
|
||||
import SpecialCard from "./SpecialCard.vue"
|
||||
|
||||
const game = useGame()
|
||||
const auth = useAuth()
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label :class="$style.label" :for="id">
|
||||
{{ label }}
|
||||
</label>
|
||||
<div>
|
||||
<slot :id="id"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.label {
|
||||
@apply font-bold text-lg;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nanoid } from "nanoid/non-secure"
|
||||
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
|
||||
const id = nanoid()
|
||||
</script>
|
|
@ -9,6 +9,12 @@
|
|||
>
|
||||
<CrownIcon/>
|
||||
</span>
|
||||
<span
|
||||
class="text-2xl absolute top-0.5 -left-10 transition"
|
||||
:class="game.state.activePlayerId === playerState.id ? '' : 'opacity-0'"
|
||||
>
|
||||
<DotsThreeOutlineIcon/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-5 w-full flex-wrap">
|
||||
<NumberCard
|
||||
|
@ -20,13 +26,18 @@
|
|||
/>
|
||||
</div>
|
||||
<div v-if="isYou" class="p-4 border border-dark-200 rounded-xl flex gap-5 flex-wrap w-full">
|
||||
<template v-for="(count, id) in playerState.specialCardCountByType" :key="id">
|
||||
<div
|
||||
v-if="playerState.specialCards.length === 0"
|
||||
class="font-fat opacity-40 text-2xl h-32 flex px-4 py-5"
|
||||
>
|
||||
<span>You don’t have any special cards.</span>
|
||||
</div>
|
||||
<SpecialCard
|
||||
v-if="count > 0"
|
||||
v-for="id in playerState.specialCards"
|
||||
:key="id"
|
||||
:id="id"
|
||||
:count="count"
|
||||
is-usable
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="font-fat opacity-30 text-3xl">
|
||||
{{ getNumberCardsSum(playerState.numberCards) }}
|
||||
|
@ -47,6 +58,7 @@
|
|||
import { useAuth } from "../auth"
|
||||
import { getNumberCardsSum } from "../shared/game/state"
|
||||
import CrownIcon from "virtual:icons/ph/crown-simple-bold"
|
||||
import DotsThreeOutlineIcon from "virtual:icons/ph/dots-three-outline"
|
||||
import SpecialCard from "./SpecialCard.vue"
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
<Card
|
||||
class="bg-gradient-to-br w-50 h-32"
|
||||
:class="meta_.type === 'permanent' ? 'from-green-800 to-green-900' : 'from-blue-700 to-blue-800'"
|
||||
as-button
|
||||
:tags="[{ label: `${count}x`, icon: numberCircleIcons[count] }]"
|
||||
:as-button="isUsable"
|
||||
:tags="ownerId === undefined ? [] : [{ label: `Owned by ${game.state.players.find(p => p.id === ownerId).name}`, icon: UserIcon }]"
|
||||
:disabled="!game.isYourTurn"
|
||||
@click="use()"
|
||||
>
|
||||
<div class="flex justify-center items-center text-5xl opacity-90 flex-grow">
|
||||
<div class="flex justify-center items-center text-5xl opacity-90 flex-grow relative top-1">
|
||||
<component :is="specialCardIcons[id]"/>
|
||||
</div>
|
||||
<div class="px-3 pb-2 text-sm flex items-end flex-grow">
|
||||
<div>
|
||||
<div class="text-center">
|
||||
{{ meta_.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,22 +25,24 @@
|
|||
<script setup lang="ts">
|
||||
import Card from "./Card.vue"
|
||||
import type { SpecialCardId } from "../shared/game/cards"
|
||||
import { numberCircleIcons, specialCardIcons } from "../icons"
|
||||
import { specialCardIcons } from "../icons"
|
||||
import { specialCardsMeta } from "../shared/game/cards"
|
||||
import { computed } from "vue"
|
||||
import { useGame } from "../clientGame"
|
||||
import { useThrottleFn } from "@vueuse/core"
|
||||
import UserIcon from "virtual:icons/ph/user-bold"
|
||||
|
||||
const props = defineProps<{
|
||||
id: SpecialCardId
|
||||
count: number
|
||||
isUsable?: boolean
|
||||
ownerId?: string
|
||||
}>()
|
||||
|
||||
const game = useGame()
|
||||
|
||||
const meta_ = computed(() => specialCardsMeta[props.id])
|
||||
|
||||
const use = useThrottleFn(() => {
|
||||
if (!props.isUsable) return
|
||||
game.useSpecialCard(props.id)
|
||||
}, 1000)
|
||||
</script>
|
|
@ -1,22 +0,0 @@
|
|||
<template>
|
||||
<TextualInput :class="$style.root" v-model="value"/>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
@apply bg-surface0 border-surface2 border rounded-lg;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import TextualInput from "./TextualInput.vue"
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const value = useVModel(props)
|
||||
</script>
|
|
@ -1,26 +0,0 @@
|
|||
<template>
|
||||
<input
|
||||
v-bind="$attrs"
|
||||
v-model="value"
|
||||
:class="$style.root"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
@apply bg-surface0 border-surface2 border rounded-lg focus:outline-none focus:border-blue-400 py-2 px-3;
|
||||
|
||||
width: 100%;
|
||||
transition: 200ms ease border-color;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const value = useVModel(props)
|
||||
</script>
|
11
src/icons.ts
11
src/icons.ts
|
@ -2,6 +2,10 @@ import type { SpecialCardId } from "./shared/game/cards"
|
|||
import type { Component } from "vue"
|
||||
import ArrowArcRightIcon from "virtual:icons/ph/arrow-arc-right-bold"
|
||||
import PlusCircleIcon from "virtual:icons/ph/plus-circle"
|
||||
import MinusCircleIcon from "virtual:icons/ph/minus-circle"
|
||||
import EyeSlashIcon from "virtual:icons/ph/eye-slash-bold"
|
||||
import FlagIcon from "virtual:icons/ph/flag-bold"
|
||||
import CaretCircleDoubleRightIcon from "virtual:icons/ph/caret-circle-double-right-bold"
|
||||
import NumberCircleOneIcon from "virtual:icons/ph/number-circle-one"
|
||||
import NumberCircleTwoIcon from "virtual:icons/ph/number-circle-two"
|
||||
import NumberCircleThreeIcon from "virtual:icons/ph/number-circle-three"
|
||||
|
@ -14,7 +18,12 @@ import NumberCircleNineIcon from "virtual:icons/ph/number-circle-nine"
|
|||
|
||||
export const specialCardIcons: Record<SpecialCardId, Component> = {
|
||||
"return-last-opponent": ArrowArcRightIcon,
|
||||
"increase-target-by-2": PlusCircleIcon
|
||||
"return-last-own": ArrowArcRightIcon,
|
||||
"increase-target-by-2": PlusCircleIcon,
|
||||
"decrease-target-by-2": MinusCircleIcon,
|
||||
"next-round-covert": EyeSlashIcon,
|
||||
"force-hit": FlagIcon,
|
||||
"double-draw": CaretCircleDoubleRightIcon
|
||||
}
|
||||
|
||||
export const numberCircleIcons = {
|
||||
|
|
|
@ -6,7 +6,7 @@ import type { RemoveKey } from "../shared/RemoveKey"
|
|||
import { random } from "lodash-es"
|
||||
import type { DeepReadonly } from "vue"
|
||||
import { customAlphabet as createNanoIdWithCustomAlphabet } from "nanoid/non-secure"
|
||||
import { LOBBY_CODE_LENGTH, 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"
|
||||
|
||||
const generateLobbyCode = createNanoIdWithCustomAlphabet("ABCDEFGHKMNPQRSTUVWXYZ", LOBBY_CODE_LENGTH)
|
||||
|
@ -96,7 +96,7 @@ export class Game extends EventEmitter<Events> {
|
|||
addPlayer(id: string, name: string) {
|
||||
if (this.lobbyPlayerIds.has(id)) return
|
||||
if (this.state.phase !== "pre-start") throw new Error("The game was already started.")
|
||||
if (this.lobbyPlayerIds.size >= 3) throw new Error("The game is full.")
|
||||
if (this.lobbyPlayerIds.size >= LOBBY_SIZE) throw new Error("The game is full.")
|
||||
this.lobbyPlayerIds.add(id)
|
||||
this.addAction({
|
||||
type: "join",
|
||||
|
@ -128,29 +128,40 @@ export class Game extends EventEmitter<Events> {
|
|||
})
|
||||
|
||||
for (const player of players) {
|
||||
this.dealNumberTo(player.id, true)
|
||||
this.dealNumberTo(player.id)
|
||||
this.dealSpecialTo(player.id)
|
||||
this.addAction({
|
||||
type: "hit",
|
||||
initiatingPlayerId: player.id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
hit() {
|
||||
const playerId = this.state.activePlayerId
|
||||
const player = this.state.players.find(p => p.id === this.state.activePlayerId)!
|
||||
const sum = getNumberCardsSum(player.numberCards)
|
||||
|
||||
if (sum >= this.state.targetSum && this.state.activeSpecialCards.find(c => c.id === "force-hit" && c.ownerId !== player.id) === undefined)
|
||||
throw new Error("The player is not allowed to hit if they have reached the target sum.")
|
||||
|
||||
if (Math.random() > SPECIAL_CARD_PROBABILITY) {
|
||||
this.dealNumberTo(playerId, false)
|
||||
this.dealNumberTo(player.id)
|
||||
|
||||
this.addAction({
|
||||
type: "hit",
|
||||
initiatingPlayerId: playerId
|
||||
initiatingPlayerId: player.id
|
||||
})
|
||||
}
|
||||
|
||||
else this.dealSpecialTo(playerId)
|
||||
else this.dealSpecialTo(player.id)
|
||||
}
|
||||
|
||||
stay() {
|
||||
const playerId = this.state.activePlayerId
|
||||
|
||||
if (this.state.activeSpecialCards.find(c => c.id === "force-hit" && c.ownerId !== playerId) !== undefined)
|
||||
throw new Error("An active special card forces the player to hit.")
|
||||
|
||||
this.addAction({
|
||||
type: "stay",
|
||||
initiatingPlayerId: playerId
|
||||
|
@ -161,7 +172,7 @@ export class Game extends EventEmitter<Events> {
|
|||
|
||||
useSpecialCard(cardId: SpecialCardId) {
|
||||
const player = this.state.players.find(p => p.id === this.state.activePlayerId)!
|
||||
if (player.specialCardCountByType[cardId] <= 0) 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.")
|
||||
|
||||
this.addAction({
|
||||
type: "use-special",
|
||||
|
@ -211,14 +222,25 @@ export class Game extends EventEmitter<Events> {
|
|||
this.destroy()
|
||||
}
|
||||
|
||||
private dealNumberTo(playerId: string, isCovert: boolean) {
|
||||
const number = this.state.numberCardsStack[random(0, this.state.numberCardsStack.length - 1)]
|
||||
private dealNumberTo(playerId: string) {
|
||||
const player = this.state.players.find(p => p.id === playerId)!
|
||||
|
||||
if (this.state.activeSpecialCards.find(c => c.id === "double-draw" && c.ownerId === player.id) !== undefined) {
|
||||
const number = this.state.numberCardsStack[random(0, this.state.numberCardsStack.length - 1)]
|
||||
this.addAction({
|
||||
type: "deal-number",
|
||||
number,
|
||||
toPlayerId: playerId,
|
||||
isCovert
|
||||
isCovert: player.nextRoundCovert
|
||||
})
|
||||
}
|
||||
|
||||
const number = this.state.numberCardsStack[random(0, this.state.numberCardsStack.length - 1)]
|
||||
this.addAction({
|
||||
type: "deal-number",
|
||||
number,
|
||||
toPlayerId: playerId,
|
||||
isCovert: player.nextRoundCovert
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { requireAuthentication, t } from "./base"
|
||||
import { getNumberCardsSum } from "../../shared/game/state"
|
||||
import { z } from "zod"
|
||||
import { createGame, getGameByLobbyCode } from "../game"
|
||||
import { SpecialCardId, specialCardIds } from "../../shared/game/cards"
|
||||
|
@ -30,9 +29,6 @@ export const gameRouter = t.router({
|
|||
hit: gameProcedure
|
||||
.mutation(async ({ ctx }) => {
|
||||
if (ctx.game.state.activePlayerId !== ctx.user.id) throw new Error("It is not the player’s turn")
|
||||
if (getNumberCardsSum(ctx.game.state.players.find(p => p.id === ctx.user.id)!.numberCards) > ctx.game.state.targetSum)
|
||||
throw new Error("The player cannot hit when bust")
|
||||
|
||||
await ctx.game.hit()
|
||||
}),
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export const LOBBY_CODE_LENGTH = 5
|
||||
export const SPECIAL_CARD_PROBABILITY = 0.2
|
||||
export const LOBBY_SIZE = 4
|
||||
export const SPECIAL_CARD_PROBABILITY = 0.5
|
|
@ -4,19 +4,8 @@ interface SpecialCardMeta {
|
|||
weight: number
|
||||
}
|
||||
|
||||
// const specialCardTypesObject = {
|
||||
// "return-last-opponent": true,
|
||||
// "return-last-own": true,
|
||||
// "destroy-last-opponent-special": true,
|
||||
// "increase-target-by-2": true,
|
||||
// "next-round-covert": true,
|
||||
// "double-draw": true,
|
||||
// "add-2-opponent": true,
|
||||
// "get-1": true,
|
||||
// "get-11": true
|
||||
// }
|
||||
|
||||
export type SpecialCardId = "return-last-opponent" | "increase-target-by-2"
|
||||
export type SpecialCardId =
|
||||
| "return-last-opponent" | "return-last-own" | "increase-target-by-2" | "decrease-target-by-2" | "next-round-covert" | "double-draw" | "force-hit"
|
||||
|
||||
export const specialCardsMeta: Record<SpecialCardId, SpecialCardMeta> = {
|
||||
"return-last-opponent": {
|
||||
|
@ -24,10 +13,35 @@ export const specialCardsMeta: Record<SpecialCardId, SpecialCardMeta> = {
|
|||
description: "Return the last card your opponent drew to the stack.",
|
||||
weight: 5
|
||||
},
|
||||
"return-last-own": {
|
||||
type: "single-use",
|
||||
description: "Return the last card you drew to the stack.",
|
||||
weight: 4
|
||||
},
|
||||
"increase-target-by-2": {
|
||||
type: "permanent",
|
||||
description: "Increases the target card value by two.",
|
||||
weight: 1
|
||||
},
|
||||
"decrease-target-by-2": {
|
||||
type: "permanent",
|
||||
description: "Decreases the target card value by two.",
|
||||
weight: 1
|
||||
},
|
||||
"next-round-covert": {
|
||||
type: "permanent",
|
||||
description: "The next card you’ll draw will be covert.",
|
||||
weight: 2
|
||||
},
|
||||
"double-draw": {
|
||||
type: "permanent",
|
||||
description: "You will draw two number cards at once.",
|
||||
weight: 5
|
||||
},
|
||||
"force-hit": {
|
||||
type: "permanent",
|
||||
description: "Your opponent is not allowed to stay",
|
||||
weight: 3
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,20 +4,23 @@ import { produce } from "immer"
|
|||
import { specialCardIds, specialCardsMeta } from "./cards"
|
||||
import { cloneDeep, tail, without } from "lodash-es"
|
||||
|
||||
type SpecialCardCountByType = Record<SpecialCardId, number>
|
||||
|
||||
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[]
|
||||
specialCardCountByType: SpecialCardCountByType
|
||||
specialCards: SpecialCardId[]
|
||||
stayed: boolean
|
||||
nextRoundCovert: boolean
|
||||
}
|
||||
|
@ -25,7 +28,7 @@ export interface GameStatePlayer {
|
|||
export interface GameState {
|
||||
phase: "pre-start" | "running" | "end"
|
||||
players: GameStatePlayer[]
|
||||
activeSpecialCardCountByType: SpecialCardCountByType
|
||||
activeSpecialCards: GameStateActiveSpecialCard[]
|
||||
activePlayerId: string
|
||||
targetSum: number
|
||||
numberCardsStack: number[] // if redacted: contains more cards than there actually are
|
||||
|
@ -33,18 +36,12 @@ export interface GameState {
|
|||
winnerIds: string[] | null
|
||||
}
|
||||
|
||||
const ALL_ZERO_SPECIAL_CARD_COUNTS: SpecialCardCountByType = Object.fromEntries(specialCardIds.map(t => [t, 0])) as SpecialCardCountByType
|
||||
|
||||
const ALL_ZERO_PERMANENT_SPECIAL_CARD_COUNTS: SpecialCardCountByType = Object.fromEntries(
|
||||
specialCardIds.filter(id => specialCardsMeta[id].type === "permanent").map(t => [t, 0])
|
||||
) as SpecialCardCountByType
|
||||
|
||||
const UNINITIALIZED_GAME_STATE: GameState = {
|
||||
phase: "pre-start",
|
||||
players: [],
|
||||
activePlayerId: "",
|
||||
targetSum: 0,
|
||||
activeSpecialCardCountByType: ALL_ZERO_PERMANENT_SPECIAL_CARD_COUNTS,
|
||||
activeSpecialCards: [],
|
||||
numberCardsStack: [],
|
||||
actualNumberCardsStackSize: 0,
|
||||
winnerIds: null
|
||||
|
@ -72,9 +69,9 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
|
|||
id: action.initiatingPlayerId,
|
||||
name: action.name,
|
||||
numberCards: [],
|
||||
specialCardCountByType: ALL_ZERO_SPECIAL_CARD_COUNTS,
|
||||
specialCards: [],
|
||||
stayed: false,
|
||||
nextRoundCovert: false
|
||||
nextRoundCovert: true
|
||||
})
|
||||
|
||||
break
|
||||
|
@ -83,7 +80,7 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
|
|||
state.phase = "running"
|
||||
state.activePlayerId = state.players[0].id
|
||||
state.targetSum = action.targetSum
|
||||
state.activeSpecialCardCountByType = ALL_ZERO_PERMANENT_SPECIAL_CARD_COUNTS
|
||||
state.activeSpecialCards = []
|
||||
state.numberCardsStack = getFullNumbersCardStack()
|
||||
state.actualNumberCardsStackSize = state.numberCardsStack.length
|
||||
|
||||
|
@ -96,7 +93,7 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
|
|||
isCovert: action.isCovert
|
||||
})
|
||||
|
||||
state.numberCardsStack = without(state.numberCardsStack, action.number)
|
||||
state.numberCardsStack.splice(state.numberCardsStack.indexOf(action.number), 1)
|
||||
state.actualNumberCardsStackSize--
|
||||
|
||||
if (state.actualNumberCardsStackSize === 0) {
|
||||
|
@ -109,7 +106,7 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
|
|||
case "deal-special":
|
||||
if (action.cardId !== undefined) {
|
||||
const p2 = state.players.find(p => p.id === action.toPlayerId)!
|
||||
p2.specialCardCountByType[action.cardId]++
|
||||
p2.specialCards.push(action.cardId)
|
||||
}
|
||||
|
||||
break
|
||||
|
@ -118,6 +115,17 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
|
|||
activateNextPlayer()
|
||||
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":
|
||||
|
@ -129,6 +137,11 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
|
|||
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":
|
||||
|
@ -140,15 +153,24 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
|
|||
}
|
||||
})
|
||||
|
||||
function applySpecialCardUsage(state: GameState, type: SpecialCardId, player: GameStatePlayer) {
|
||||
player.specialCardCountByType[type] = Math.max(player.specialCardCountByType[type] - 1, 0)
|
||||
function applySpecialCardUsage(state: GameState, id: SpecialCardId, player: GameStatePlayer) {
|
||||
player.specialCards.splice(player.specialCards.indexOf(id), 1)
|
||||
|
||||
switch (type) {
|
||||
switch (id) {
|
||||
case "return-last-opponent":
|
||||
const previousPlayer = getPreviousPlayer(state)
|
||||
const removedCard = previousPlayer.numberCards.pop()
|
||||
if (removedCard !== undefined) {
|
||||
state.numberCardsStack.push(removedCard.number)
|
||||
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++
|
||||
}
|
||||
|
||||
|
@ -156,6 +178,38 @@ function applySpecialCardUsage(state: GameState, type: SpecialCardId, player: Ga
|
|||
|
||||
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 "force-hit":
|
||||
case "double-draw":
|
||||
state.activeSpecialCards.push({
|
||||
id,
|
||||
ownerId: player.id
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@
|
|||
"stripInternal": true,
|
||||
"target": "esnext",
|
||||
"types": [
|
||||
"src/types.d.ts",
|
||||
"./src/types.d.ts",
|
||||
"vite/client",
|
||||
"unplugin-icons/types/vue",
|
||||
"vite-plugin-pages/client"
|
||||
|
|
Loading…
Add table
Reference in a new issue