commit #6
This commit is contained in:
parent
14c4a434f7
commit
2975b2820b
10 changed files with 102 additions and 76 deletions
|
@ -32,13 +32,7 @@ export const useGame = defineStore("game", () => {
|
|||
console.log(`⏩ ${action.type}`, action)
|
||||
})
|
||||
|
||||
return {
|
||||
lobbyCode: readonly(lobbyCode),
|
||||
isActive: computed(() => lobbyCode.value !== null),
|
||||
isOwnGame: computed(() => state.value.players.findIndex(p => p.id === (auth.authenticatedUser?.id ?? "")) === 0),
|
||||
state: readonly(state),
|
||||
actions: readonly(actions),
|
||||
join(code: string) {
|
||||
function join(code: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
trpcClient.join.subscribe({ lobbyCode: code }, {
|
||||
onStarted: () => {
|
||||
|
@ -50,6 +44,11 @@ export const useGame = defineStore("game", () => {
|
|||
case "action":
|
||||
actionsBus.emit(event.action)
|
||||
break
|
||||
|
||||
case "new_round":
|
||||
state.value = getUninitializedGameState()
|
||||
actions.splice(0, actions.length)
|
||||
join(code).catch(() => {})
|
||||
}
|
||||
},
|
||||
onError: error => {
|
||||
|
@ -58,7 +57,15 @@ export const useGame = defineStore("game", () => {
|
|||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
lobbyCode: readonly(lobbyCode),
|
||||
isActive: computed(() => lobbyCode.value !== null),
|
||||
isOwnGame: computed(() => state.value.players.findIndex(p => p.id === (auth.authenticatedUser?.id ?? "")) === 0),
|
||||
state: readonly(state),
|
||||
actions: readonly(actions),
|
||||
join,
|
||||
async reset() {
|
||||
window.location.hash = ""
|
||||
lobbyCode.value = null
|
||||
|
@ -68,6 +75,7 @@ export const useGame = defineStore("game", () => {
|
|||
start: () => trpcClient.game.start.mutate({ lobbyCode: lobbyCode.value! }),
|
||||
hit: () => trpcClient.game.hit.mutate({ lobbyCode: lobbyCode.value! }),
|
||||
stay: () => trpcClient.game.stay.mutate({ lobbyCode: lobbyCode.value! }),
|
||||
newRound: () => trpcClient.game.newRound.mutate({ lobbyCode: lobbyCode.value! }),
|
||||
create: () => trpcClient.createGame.mutate()
|
||||
}
|
||||
})
|
|
@ -4,10 +4,11 @@
|
|||
<div class="flex gap-5 justify-end items-center transform transition ease duration-500">
|
||||
<template v-if="game.state.phase === 'end'">
|
||||
<BigButton
|
||||
class="bg-gradient-to-br from-gray-600 to-gray-800"
|
||||
@click="game.reset()"
|
||||
class="bg-gradient-to-br from-green-700 to-green-800"
|
||||
v-if="game.isOwnGame"
|
||||
@click="game.newRound()"
|
||||
>
|
||||
Leave
|
||||
New round
|
||||
</BigButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
|
|
@ -16,8 +16,8 @@ export function getGameByLobbyCode(code: string): Game | null {
|
|||
return gamesByLobbyCode.get(code.toUpperCase()) ?? null
|
||||
}
|
||||
|
||||
export function createGame() {
|
||||
const game = new Game()
|
||||
export function createGame(lobbyCode: string = generateLobbyCode()) {
|
||||
const game = new Game(lobbyCode)
|
||||
gamesByLobbyCode.set(game.lobbyCode, game)
|
||||
return game
|
||||
}
|
||||
|
@ -43,12 +43,11 @@ function redactGameAction<T extends GameAction>(action: T, receiverId: string):
|
|||
interface Events {
|
||||
broadcast_action: [GameAction]
|
||||
private_action: [string, GameAction]
|
||||
new_round: []
|
||||
destroyed: []
|
||||
}
|
||||
|
||||
export class Game extends EventEmitter<Events> {
|
||||
lobbyCode = generateLobbyCode()
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
|
@ -65,7 +64,7 @@ export class Game extends EventEmitter<Events> {
|
|||
return this.actions[this.actions.length - 1].index + 1
|
||||
}
|
||||
|
||||
constructor() {
|
||||
constructor(public lobbyCode: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
|
@ -107,6 +106,7 @@ export class Game extends EventEmitter<Events> {
|
|||
}
|
||||
|
||||
async start() {
|
||||
if (this.state.phase !== "pre-start") throw new Error(`Cannot start the game in this phase: ${this.state.phase}`)
|
||||
const players = await this.getPlayers()
|
||||
if (players.length < 2) throw new Error("At least two players are required for starting the game")
|
||||
|
||||
|
@ -181,6 +181,11 @@ export class Game extends EventEmitter<Events> {
|
|||
this.actions.forEach(action => this.sendActionTo(action, receiverId))
|
||||
}
|
||||
|
||||
announceNewRoundAndDestroy() {
|
||||
this.emit("new_round")
|
||||
this.destroy()
|
||||
}
|
||||
|
||||
private dealNumberTo(playerId: string, isCovert: boolean) {
|
||||
const number = this.state.numberCardsStack[random(0, this.state.numberCardsStack.length - 1)]
|
||||
|
||||
|
@ -230,7 +235,7 @@ export class Game extends EventEmitter<Events> {
|
|||
}
|
||||
|
||||
private destroy() {
|
||||
gamesByLobbyCode.delete(this.lobbyCode)
|
||||
if (gamesByLobbyCode.get(this.lobbyCode) === this) gamesByLobbyCode.delete(this.lobbyCode)
|
||||
this.emit("destroyed")
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { requireAuthentication, t } from "./base"
|
||||
import { getNumberCardsSum } from "../../shared/game/state"
|
||||
import { z } from "zod"
|
||||
import { getGameByLobbyCode } from "../game"
|
||||
import { createGame, getGameByLobbyCode } from "../game"
|
||||
|
||||
const gameProcedure = t.procedure
|
||||
.use(requireAuthentication)
|
||||
|
@ -22,7 +22,6 @@ const gameProcedure = t.procedure
|
|||
export const gameRouter = t.router({
|
||||
start: gameProcedure
|
||||
.mutation(async ({ ctx }) => {
|
||||
if (ctx.game.state.phase !== "pre-start") throw new Error(`Cannot start the game in this phase: ${ctx.game.state.phase}`)
|
||||
if (ctx.game.state.players.findIndex(p => p.id === ctx.user.id) !== 0) throw new Error("Only the creator can start the game")
|
||||
ctx.game.start()
|
||||
}),
|
||||
|
@ -40,5 +39,14 @@ export const gameRouter = t.router({
|
|||
.mutation(async ({ ctx }) => {
|
||||
if (ctx.game.state.activePlayerId !== ctx.user.id) throw new Error("It is not the player’s turn")
|
||||
await ctx.game.stay()
|
||||
}),
|
||||
|
||||
newRound: gameProcedure
|
||||
.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.players.findIndex(p => p.id === ctx.user.id) !== 0) throw new Error("Only the creator can start a new round")
|
||||
const newGame = createGame(ctx.game.lobbyCode)
|
||||
newGame.addPlayer(ctx.user.id, ctx.user.name)
|
||||
ctx.game.announceNewRoundAndDestroy()
|
||||
})
|
||||
})
|
|
@ -87,6 +87,7 @@ export const appRouter = t.router({
|
|||
|
||||
return observable<GameEvent>(emit => {
|
||||
const handleBroadcastAction = (action: GameAction) => emit.next({ type: "action", action })
|
||||
const handleNewRound = () => emit.next({ type: "new_round" })
|
||||
const handleDestroyed = () => setTimeout(() => emit.complete(), 500)
|
||||
|
||||
const handlePrivateAction = (playerId: string, action: GameAction) => {
|
||||
|
@ -95,6 +96,7 @@ export const appRouter = t.router({
|
|||
|
||||
game.on("broadcast_action", handleBroadcastAction)
|
||||
game.on("private_action", handlePrivateAction)
|
||||
game.on("new_round", handleNewRound)
|
||||
game.on("destroyed", handleDestroyed)
|
||||
|
||||
game.sendAllOldActionsTo(ctx.user.id)
|
||||
|
@ -102,6 +104,7 @@ export const appRouter = t.router({
|
|||
return () => {
|
||||
game.off("broadcast_action", handleBroadcastAction)
|
||||
game.off("private_action", handlePrivateAction)
|
||||
game.off("new_round", handleNewRound)
|
||||
game.off("destroyed", handleDestroyed)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { SpecialCardType } from "./cards"
|
||||
import type { SpecialCardId } from "./cards"
|
||||
|
||||
type PlayerAction = {
|
||||
initiatingPlayerId: string
|
||||
|
@ -15,7 +15,7 @@ type PlayerAction = {
|
|||
}
|
||||
| {
|
||||
type: "use-special"
|
||||
cardType: SpecialCardType
|
||||
cardType: SpecialCardId
|
||||
}
|
||||
| {
|
||||
type: "leave"
|
||||
|
@ -38,7 +38,7 @@ type ServerAction = {
|
|||
| {
|
||||
type: "deal-special"
|
||||
toPlayerId: string
|
||||
cardType: SpecialCardType
|
||||
cardType?: SpecialCardId // undefined if redacted
|
||||
}
|
||||
| {
|
||||
type: "end"
|
||||
|
|
|
@ -1,14 +1,27 @@
|
|||
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
|
||||
interface SpecialCardMeta {
|
||||
type: "single-use" | "permanent"
|
||||
description: string
|
||||
}
|
||||
|
||||
export const specialCardTypes = Object.keys(specialCardTypesObject)
|
||||
export type SpecialCardType = keyof typeof specialCardTypesObject
|
||||
// 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"
|
||||
|
||||
export const specialCardsMeta: Record<SpecialCardId, SpecialCardMeta> = {
|
||||
"return-last-opponent": {
|
||||
type: "single-use",
|
||||
description: "Return the last card your opponent drew to the stack"
|
||||
}
|
||||
}
|
||||
|
||||
export const specialCardTypes = Object.keys(specialCardsMeta)
|
|
@ -3,4 +3,6 @@ import type { GameAction } from "./actions"
|
|||
export type GameEvent = {
|
||||
type: "action"
|
||||
action: GameAction
|
||||
} | {
|
||||
type: "new_round"
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import type { SpecialCardType } from "./cards"
|
||||
import type { SpecialCardId } from "./cards"
|
||||
import type { GameAction } from "./actions"
|
||||
import { produce } from "immer"
|
||||
import { specialCardTypes } from "./cards"
|
||||
import { cloneDeep, tail, without } from "lodash-es"
|
||||
|
||||
type SpecialCardCountByType = Record<SpecialCardType, number>
|
||||
type SpecialCardCountByType = Record<SpecialCardId, number>
|
||||
|
||||
export interface GameStateNumberCard {
|
||||
number: number
|
||||
|
@ -96,8 +96,11 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
|
|||
break
|
||||
|
||||
case "deal-special":
|
||||
if (action.cardType !== undefined) {
|
||||
const p2 = state.players.find(p => p.id === action.toPlayerId)!
|
||||
p2.specialCardCountByType[action.cardType]++
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case "hit":
|
||||
|
@ -126,36 +129,12 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
|
|||
}
|
||||
})
|
||||
|
||||
function applySpecialCardUsage(state: GameState, type: SpecialCardType, player: GameStatePlayer) {
|
||||
function applySpecialCardUsage(state: GameState, type: SpecialCardId, player: GameStatePlayer) {
|
||||
player.specialCardCountByType[type]--
|
||||
|
||||
switch (type) {
|
||||
case "add-2-opponent":
|
||||
// nothing
|
||||
break
|
||||
|
||||
case "destroy-last-opponent-special":
|
||||
// TODO
|
||||
break
|
||||
|
||||
case "double-draw":
|
||||
// nothing
|
||||
break
|
||||
|
||||
case "increase-target-by-2":
|
||||
state.targetSum += 2
|
||||
break
|
||||
|
||||
case "next-round-covert":
|
||||
player.nextRoundCovert = true
|
||||
break
|
||||
|
||||
case "return-last-opponent":
|
||||
// TODO
|
||||
break
|
||||
|
||||
case "return-last-own":
|
||||
// TODO
|
||||
break
|
||||
}
|
||||
}
|
7
src/specialCards.ts
Normal file
7
src/specialCards.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
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