This commit is contained in:
Moritz Ruth 2023-04-23 16:36:31 +02:00
parent 2975b2820b
commit 26772d9d30
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
17 changed files with 216 additions and 45 deletions

View file

@ -2,10 +2,11 @@ import { defineStore } from "pinia"
import { EventBusKey, useEventBus } from "@vueuse/core" import { EventBusKey, useEventBus } from "@vueuse/core"
import type { GameAction } from "./shared/game/actions" import type { GameAction } from "./shared/game/actions"
import { computed, reactive, readonly, ref } from "vue" import { computed, reactive, readonly, ref } from "vue"
import { GameState, getUninitializedGameState, produceNewState } from "./shared/game/state" import { GameState, getNumberCardsSum, getUninitializedGameState, produceNewState } from "./shared/game/state"
import { trpcClient } from "./trpc" import { trpcClient } from "./trpc"
import { useAuth } from "./auth" import { useAuth } from "./auth"
import { read } from "fs" import { read } from "fs"
import type { SpecialCardId } from "./shared/game/cards"
const gameActionsBusKey = Symbol() as EventBusKey<GameAction> const gameActionsBusKey = Symbol() as EventBusKey<GameAction>
const useGameActionsBus = () => useEventBus(gameActionsBusKey) const useGameActionsBus = () => useEventBus(gameActionsBusKey)
@ -63,6 +64,8 @@ export const useGame = defineStore("game", () => {
lobbyCode: readonly(lobbyCode), lobbyCode: readonly(lobbyCode),
isActive: computed(() => lobbyCode.value !== null), isActive: computed(() => lobbyCode.value !== null),
isOwnGame: computed(() => state.value.players.findIndex(p => p.id === (auth.authenticatedUser?.id ?? "")) === 0), 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),
state: readonly(state), state: readonly(state),
actions: readonly(actions), actions: readonly(actions),
join, join,
@ -75,6 +78,7 @@ export const useGame = defineStore("game", () => {
start: () => trpcClient.game.start.mutate({ lobbyCode: lobbyCode.value! }), start: () => trpcClient.game.start.mutate({ lobbyCode: lobbyCode.value! }),
hit: () => trpcClient.game.hit.mutate({ lobbyCode: lobbyCode.value! }), hit: () => trpcClient.game.hit.mutate({ lobbyCode: lobbyCode.value! }),
stay: () => trpcClient.game.stay.mutate({ lobbyCode: lobbyCode.value! }), stay: () => trpcClient.game.stay.mutate({ lobbyCode: lobbyCode.value! }),
useSpecialCard: (id: SpecialCardId) => trpcClient.game.useSpecialCard.mutate({ lobbyCode: lobbyCode.value!, specialCardId: id }),
newRound: () => trpcClient.game.newRound.mutate({ lobbyCode: lobbyCode.value! }), newRound: () => trpcClient.game.newRound.mutate({ lobbyCode: lobbyCode.value! }),
create: () => trpcClient.createGame.mutate() create: () => trpcClient.createGame.mutate()
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="flex-shrink-0 rounded-lg shadow-xl bg-gradient-to-br w-35 h-45" :class="$style.root"> <component :is="asButton ? 'button' : 'div'" class="block flex-shrink-0 rounded-lg shadow-xl bg-gradient-to-br" :class="$style.root">
<div class="relative flex flex-col items-center justify-center h-full"> <div class="relative flex flex-col items-center justify-center h-full">
<div class="absolute top-2 right-2 bg-dark-800 rounded-full flex gap-2 items-center px-2"> <div class="absolute top-2 right-2 bg-dark-800 rounded-full flex gap-2 items-center px-2">
<div v-for="tag in tags" :key="tag.label" :title="tag.label"> <div v-for="tag in tags" :key="tag.label" :title="tag.label">
@ -8,7 +8,7 @@
</div> </div>
<slot/> <slot/>
</div> </div>
</div> </component>
</template> </template>
<style module lang="scss"> <style module lang="scss">
@ -25,6 +25,10 @@
background: url("../assets/noise.png") repeat; background: url("../assets/noise.png") repeat;
opacity: 10%; opacity: 10%;
} }
button:disabled {
cursor: not-allowed;
}
} }
</style> </style>
@ -38,6 +42,7 @@
icon: FunctionalComponent icon: FunctionalComponent
}>>, }>>,
default: [] default: []
} },
asButton: Boolean
}) })
</script> </script>

View file

@ -13,18 +13,18 @@
</template> </template>
<template v-else> <template v-else>
<div class="font-fat opacity-20 text-3xl pr-2"> <div class="font-fat opacity-20 text-3xl pr-2">
<template v-if="isBust">You are bust.</template> <template v-if="game.isBust">You are bust.</template>
</div> </div>
<BigButton <BigButton
class="bg-gradient-to-br from-red-800 to-red-900 uppercase" class="bg-gradient-to-br from-red-800 to-red-900 uppercase"
:disabled="!isYourTurn || isBust" :disabled="!game.isYourTurn || game.isBust"
@click="game.hit()" @click="game.hit()"
> >
Hit Hit
</BigButton> </BigButton>
<BigButton <BigButton
class="bg-gradient-to-br from-blue-800 to-blue-900 uppercase" class="bg-gradient-to-br from-blue-800 to-blue-900 uppercase"
:disabled="!isYourTurn" :disabled="!game.isYourTurn"
@click="game.stay()" @click="game.stay()"
> >
Stay Stay
@ -50,14 +50,12 @@
const game = useGame() const game = useGame()
const auth = useAuth() const auth = useAuth()
const isYourTurn = computed(() => game.state.activePlayerId === auth.requiredUser.id)
const isBust = computed(() => getNumberCardsSum(game.state.players.find(p => p.id === auth.requiredUser.id)!.numberCards) > game.state.targetSum)
const reorderedPlayerIds = computed(() => { const reorderedPlayerIds = computed(() => {
const ids = game.state.players.map(p => p.id) const ids = game.state.players.map(p => p.id)
const selfIndex = ids.findIndex(id => id === auth.requiredUser.id) const selfIndex = ids.findIndex(id => id === auth.requiredUser.id)
let before = ids.slice(0, selfIndex) let front = ids.slice(0, selfIndex + 1)
let rest = ids.slice(selfIndex) let back = ids.slice(selfIndex + 1)
return [...rest, ...before] return [...back, ...front]
}) })
</script> </script>

View file

@ -42,7 +42,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch, watchEffect } from "vue" import { computed, ref, watch, watchEffect } from "vue"
import { useGame } from "../clientGame" import { useGame } from "../clientGame"
import { LOBBY_CODE_LENGTH } from "../shared/lobbyCode" import { LOBBY_CODE_LENGTH } from "../shared/constants"
import { useBrowserLocation } from "@vueuse/core" import { useBrowserLocation } from "@vueuse/core"
const location = useBrowserLocation() const location = useBrowserLocation()

View file

@ -32,7 +32,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watchEffect } from "vue" import { ref, watchEffect } from "vue"
import { useGame } from "../clientGame" import { useGame } from "../clientGame"
import { LOBBY_CODE_LENGTH } from "../shared/lobbyCode" import { LOBBY_CODE_LENGTH } from "../shared/constants"
import { useBrowserLocation } from "@vueuse/core" import { useBrowserLocation } from "@vueuse/core"
import { useAuth } from "../auth" import { useAuth } from "../auth"

View file

@ -1,5 +1,6 @@
<template> <template>
<Card <Card
class="w-35 h-45"
:class="isUnknown ? 'bg-gradient-to-br from-gray-700 to-gray-800' : 'bg-gradient-to-br from-yellow-600 to-yellow-900'" :class="isUnknown ? 'bg-gradient-to-br from-gray-700 to-gray-800' : 'bg-gradient-to-br from-yellow-600 to-yellow-900'"
:tags="tags" :tags="tags"
> >

View file

@ -19,6 +19,15 @@
:is-own="isYou" :is-own="isYou"
/> />
</div> </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">
<SpecialCard
v-if="count > 0"
:id="id"
:count="count"
/>
</template>
</div>
<div class="font-fat opacity-30 text-3xl"> <div class="font-fat opacity-30 text-3xl">
{{ getNumberCardsSum(playerState.numberCards) }} {{ getNumberCardsSum(playerState.numberCards) }}
<template v-if="!isYou && game.state.phase !== 'end' && playerState.numberCards.some(c => c.isCovert)">+ ?</template> <template v-if="!isYou && game.state.phase !== 'end' && playerState.numberCards.some(c => c.isCovert)">+ ?</template>
@ -38,6 +47,7 @@
import { useAuth } from "../auth" import { useAuth } from "../auth"
import { getNumberCardsSum } from "../shared/game/state" import { getNumberCardsSum } from "../shared/game/state"
import CrownIcon from "virtual:icons/ph/crown-simple-bold" import CrownIcon from "virtual:icons/ph/crown-simple-bold"
import SpecialCard from "./SpecialCard.vue"
const props = defineProps<{ const props = defineProps<{
playerId: string playerId: string

View file

@ -0,0 +1,46 @@
<template>
<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] }]"
:disabled="!game.isYourTurn"
@click="use()"
>
<div class="flex justify-center items-center text-5xl opacity-90 flex-grow">
<component :is="specialCardIcons[id]"/>
</div>
<div class="px-3 pb-2 text-sm flex items-end flex-grow">
<div>
{{ meta_.description }}
</div>
</div>
</Card>
</template>
<style module lang="scss">
</style>
<script setup lang="ts">
import Card from "./Card.vue"
import type { SpecialCardId } from "../shared/game/cards"
import { numberCircleIcons, specialCardIcons } from "../icons"
import { specialCardsMeta } from "../shared/game/cards"
import { computed } from "vue"
import { useGame } from "../clientGame"
import { useThrottleFn } from "@vueuse/core"
const props = defineProps<{
id: SpecialCardId
count: number
}>()
const game = useGame()
const meta_ = computed(() => specialCardsMeta[props.id])
const use = useThrottleFn(() => {
game.useSpecialCard(props.id)
}, 1000)
</script>

30
src/icons.ts Normal file
View file

@ -0,0 +1,30 @@
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 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"
import NumberCircleFourIcon from "virtual:icons/ph/number-circle-four"
import NumberCircleFiveIcon from "virtual:icons/ph/number-circle-five"
import NumberCircleSixIcon from "virtual:icons/ph/number-circle-six"
import NumberCircleSevenIcon from "virtual:icons/ph/number-circle-seven"
import NumberCircleEightIcon from "virtual:icons/ph/number-circle-eight"
import NumberCircleNineIcon from "virtual:icons/ph/number-circle-nine"
export const specialCardIcons: Record<SpecialCardId, Component> = {
"return-last-opponent": ArrowArcRightIcon,
"increase-target-by-2": PlusCircleIcon
}
export const numberCircleIcons = {
1: NumberCircleOneIcon,
2: NumberCircleTwoIcon,
3: NumberCircleThreeIcon,
4: NumberCircleFourIcon,
5: NumberCircleFiveIcon,
6: NumberCircleSixIcon,
7: NumberCircleSevenIcon,
8: NumberCircleEightIcon,
9: NumberCircleNineIcon
}

View file

@ -6,7 +6,8 @@ import type { RemoveKey } from "../shared/RemoveKey"
import { random } from "lodash-es" 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 } from "../shared/lobbyCode" import { LOBBY_CODE_LENGTH, SPECIAL_CARD_PROBABILITY } from "../shared/constants"
import { SpecialCardId, weightedSpecialCardIds } from "../shared/game/cards"
const generateLobbyCode = createNanoIdWithCustomAlphabet("ABCDEFGHKMNPQRSTUVWXYZ", LOBBY_CODE_LENGTH) const generateLobbyCode = createNanoIdWithCustomAlphabet("ABCDEFGHKMNPQRSTUVWXYZ", LOBBY_CODE_LENGTH)
@ -35,6 +36,16 @@ function redactGameAction<T extends GameAction>(action: T, receiverId: string):
number: 0 number: 0
} }
} }
break
case "deal-special":
if (action.toPlayerId === receiverId) return action
return {
...action,
cardId: undefined
}
} }
return NO_REDACTION return NO_REDACTION
@ -118,18 +129,23 @@ export class Game extends EventEmitter<Events> {
for (const player of players) { for (const player of players) {
this.dealNumberTo(player.id, true) this.dealNumberTo(player.id, true)
this.dealSpecialTo(player.id)
} }
} }
hit() { hit() {
const playerId = this.state.activePlayerId const playerId = this.state.activePlayerId
this.addAction({ if (Math.random() > SPECIAL_CARD_PROBABILITY) {
type: "hit", this.dealNumberTo(playerId, false)
initiatingPlayerId: playerId
})
this.dealNumberTo(playerId, false) this.addAction({
type: "hit",
initiatingPlayerId: playerId
})
}
else this.dealSpecialTo(playerId)
} }
stay() { stay() {
@ -140,9 +156,18 @@ export class Game extends EventEmitter<Events> {
initiatingPlayerId: playerId initiatingPlayerId: playerId
}) })
if (this.state.players.every(p => p.stayed)) { if (this.state.players.every(p => p.stayed)) this.end()
this.end() }
}
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")
this.addAction({
type: "use-special",
initiatingPlayerId: player.id,
cardId
})
} }
end() { end() {
@ -197,6 +222,14 @@ export class Game extends EventEmitter<Events> {
}) })
} }
private dealSpecialTo(playerId: string) {
this.addAction({
type: "deal-special",
toPlayerId: playerId,
cardId: weightedSpecialCardIds[random(0, weightedSpecialCardIds.length - 1)]
})
}
private addAction<T extends RemoveKey<GameAction, "index">>(action: T): T & { index: number } { private addAction<T extends RemoveKey<GameAction, "index">>(action: T): T & { index: number } {
const fullAction = { const fullAction = {
...action, ...action,

View file

@ -2,6 +2,7 @@ import { requireAuthentication, t } from "./base"
import { getNumberCardsSum } from "../../shared/game/state" import { getNumberCardsSum } from "../../shared/game/state"
import { z } from "zod" import { z } from "zod"
import { createGame, getGameByLobbyCode } from "../game" import { createGame, getGameByLobbyCode } from "../game"
import { SpecialCardId, specialCardIds } from "../../shared/game/cards"
const gameProcedure = t.procedure const gameProcedure = t.procedure
.use(requireAuthentication) .use(requireAuthentication)
@ -41,6 +42,15 @@ export const gameRouter = t.router({
await ctx.game.stay() await ctx.game.stay()
}), }),
useSpecialCard: gameProcedure
.input(z.object({
specialCardId: z.string().refine(a => specialCardIds.includes(a as any)).transform(a => a as SpecialCardId)
}))
.mutation(async ({ ctx, input }) => {
if (ctx.game.state.activePlayerId !== ctx.user.id) throw new Error("It is not the players turn")
await ctx.game.useSpecialCard(input.specialCardId)
}),
newRound: gameProcedure newRound: gameProcedure
.mutation(async ({ ctx }) => { .mutation(async ({ ctx }) => {
if (ctx.game.state.phase !== "end") throw new Error(`Cannot start a new round in this phase: ${ctx.game.state.phase}`) if (ctx.game.state.phase !== "end") throw new Error(`Cannot start a new round in this phase: ${ctx.game.state.phase}`)

2
src/shared/constants.ts Normal file
View file

@ -0,0 +1,2 @@
export const LOBBY_CODE_LENGTH = 5
export const SPECIAL_CARD_PROBABILITY = 0.2

View file

@ -15,7 +15,7 @@ type PlayerAction = {
} }
| { | {
type: "use-special" type: "use-special"
cardType: SpecialCardId cardId: SpecialCardId
} }
| { | {
type: "leave" type: "leave"
@ -38,7 +38,7 @@ type ServerAction = {
| { | {
type: "deal-special" type: "deal-special"
toPlayerId: string toPlayerId: string
cardType?: SpecialCardId // undefined if redacted cardId?: SpecialCardId // undefined if redacted
} }
| { | {
type: "end" type: "end"

View file

@ -1,6 +1,7 @@
interface SpecialCardMeta { interface SpecialCardMeta {
type: "single-use" | "permanent" type: "single-use" | "permanent"
description: string description: string
weight: number
} }
// const specialCardTypesObject = { // const specialCardTypesObject = {
@ -15,13 +16,31 @@ interface SpecialCardMeta {
// "get-11": true // "get-11": true
// } // }
export type SpecialCardId = "return-last-opponent" export type SpecialCardId = "return-last-opponent" | "increase-target-by-2"
export const specialCardsMeta: Record<SpecialCardId, SpecialCardMeta> = { 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
},
"increase-target-by-2": {
type: "permanent",
description: "Increases the target card value by two.",
weight: 1
} }
} }
export const specialCardTypes = Object.keys(specialCardsMeta) export const specialCardIds = Object.keys(specialCardsMeta) as SpecialCardId[]
const weightedSpecialCardIds: SpecialCardId[] = []
for (const id of specialCardIds) {
const meta = specialCardsMeta[id]
for (let i = 0; i < meta.weight; i++) {
weightedSpecialCardIds.push(id)
}
}
export { weightedSpecialCardIds }

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 { specialCardTypes } from "./cards" import { specialCardIds, specialCardsMeta } from "./cards"
import { cloneDeep, tail, without } from "lodash-es" import { cloneDeep, tail, without } from "lodash-es"
type SpecialCardCountByType = Record<SpecialCardId, number> type SpecialCardCountByType = Record<SpecialCardId, number>
@ -33,14 +33,18 @@ export interface GameState {
winnerIds: string[] | null winnerIds: string[] | null
} }
const ALL_ZERO_SPECIAL_CARD_COUNTS: SpecialCardCountByType = Object.fromEntries(specialCardTypes.map(t => [t, 0])) as SpecialCardCountByType 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 = { const UNINITIALIZED_GAME_STATE: GameState = {
phase: "pre-start", phase: "pre-start",
players: [], players: [],
activePlayerId: "", activePlayerId: "",
targetSum: 0, targetSum: 0,
activeSpecialCardCountByType: ALL_ZERO_SPECIAL_CARD_COUNTS, activeSpecialCardCountByType: ALL_ZERO_PERMANENT_SPECIAL_CARD_COUNTS,
numberCardsStack: [], numberCardsStack: [],
actualNumberCardsStackSize: 0, actualNumberCardsStackSize: 0,
winnerIds: null winnerIds: null
@ -49,6 +53,13 @@ const UNINITIALIZED_GAME_STATE: GameState = {
export const getUninitializedGameState = () => cloneDeep(UNINITIALIZED_GAME_STATE) export const getUninitializedGameState = () => cloneDeep(UNINITIALIZED_GAME_STATE)
export const getFullNumbersCardStack = () => tail([...Array(12).keys()]) export const getFullNumbersCardStack = () => tail([...Array(12).keys()])
const getPreviousPlayer = (state: GameState) => {
const activePlayerIndex = state.players.findIndex(p => p.id === state.activePlayerId)
let index = (activePlayerIndex - 1) % state.players.length
if (index < 0) index = state.players.length + index
return state.players[index]
}
export const produceNewState = (oldState: GameState, action: GameAction) => produce(oldState, state => { export const produceNewState = (oldState: GameState, action: GameAction) => produce(oldState, state => {
const activateNextPlayer = () => { const activateNextPlayer = () => {
const activePlayerIndex = state.players.findIndex(p => p.id === state.activePlayerId) const activePlayerIndex = state.players.findIndex(p => p.id === state.activePlayerId)
@ -72,7 +83,7 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
state.phase = "running" state.phase = "running"
state.activePlayerId = state.players[0].id state.activePlayerId = state.players[0].id
state.targetSum = action.targetSum state.targetSum = action.targetSum
state.activeSpecialCardCountByType = ALL_ZERO_SPECIAL_CARD_COUNTS state.activeSpecialCardCountByType = ALL_ZERO_PERMANENT_SPECIAL_CARD_COUNTS
state.numberCardsStack = getFullNumbersCardStack() state.numberCardsStack = getFullNumbersCardStack()
state.actualNumberCardsStackSize = state.numberCardsStack.length state.actualNumberCardsStackSize = state.numberCardsStack.length
@ -96,9 +107,9 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
break break
case "deal-special": case "deal-special":
if (action.cardType !== undefined) { if (action.cardId !== undefined) {
const p2 = state.players.find(p => p.id === action.toPlayerId)! const p2 = state.players.find(p => p.id === action.toPlayerId)!
p2.specialCardCountByType[action.cardType]++ p2.specialCardCountByType[action.cardId]++
} }
break break
@ -117,7 +128,7 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
case "use-special": case "use-special":
const p3 = state.players.find(p => p.id === action.initiatingPlayerId)! const p3 = state.players.find(p => p.id === action.initiatingPlayerId)!
applySpecialCardUsage(state, action.cardType, p3) applySpecialCardUsage(state, action.cardId, p3)
break break
case "end": case "end":
@ -130,11 +141,21 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
}) })
function applySpecialCardUsage(state: GameState, type: SpecialCardId, player: GameStatePlayer) { function applySpecialCardUsage(state: GameState, type: SpecialCardId, player: GameStatePlayer) {
player.specialCardCountByType[type]-- player.specialCardCountByType[type] = Math.max(player.specialCardCountByType[type] - 1, 0)
switch (type) { switch (type) {
case "return-last-opponent": case "return-last-opponent":
// TODO const previousPlayer = getPreviousPlayer(state)
const removedCard = previousPlayer.numberCards.pop()
if (removedCard !== undefined) {
state.numberCardsStack.push(removedCard.number)
state.actualNumberCardsStackSize++
}
break
case "increase-target-by-2":
state.targetSum += 2
break break
} }
} }

View file

@ -1 +0,0 @@
export const LOBBY_CODE_LENGTH = 5

View file

@ -1,7 +0,0 @@
import type { SpecialCardId } from "./shared/game/cards"
import type { Component } from "vue"
import ArrowArcRightIcon from "virtual:icons/*"
export const specialCardIcons: Record<SpecialCardId, Component> = {
"return-last-opponent": ArrowArcRightIcon
}