commit #7
This commit is contained in:
parent
2975b2820b
commit
26772d9d30
17 changed files with 216 additions and 45 deletions
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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()
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
@ -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
|
||||||
|
|
46
src/components/SpecialCard.vue
Normal file
46
src/components/SpecialCard.vue
Normal 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
30
src/icons.ts
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
if (Math.random() > SPECIAL_CARD_PROBABILITY) {
|
||||||
|
this.dealNumberTo(playerId, false)
|
||||||
|
|
||||||
this.addAction({
|
this.addAction({
|
||||||
type: "hit",
|
type: "hit",
|
||||||
initiatingPlayerId: playerId
|
initiatingPlayerId: playerId
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.dealNumberTo(playerId, false)
|
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,
|
||||||
|
|
|
@ -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 player’s 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
2
src/shared/constants.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const LOBBY_CODE_LENGTH = 5
|
||||||
|
export const SPECIAL_CARD_PROBABILITY = 0.2
|
|
@ -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"
|
||||||
|
|
|
@ -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 }
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1 +0,0 @@
|
||||||
export const LOBBY_CODE_LENGTH = 5
|
|
|
@ -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
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue