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)
|
console.log(`⏩ ${action.type}`, action)
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
function join(code: string) {
|
||||||
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) {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
trpcClient.join.subscribe({ lobbyCode: code }, {
|
trpcClient.join.subscribe({ lobbyCode: code }, {
|
||||||
onStarted: () => {
|
onStarted: () => {
|
||||||
|
@ -50,6 +44,11 @@ export const useGame = defineStore("game", () => {
|
||||||
case "action":
|
case "action":
|
||||||
actionsBus.emit(event.action)
|
actionsBus.emit(event.action)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case "new_round":
|
||||||
|
state.value = getUninitializedGameState()
|
||||||
|
actions.splice(0, actions.length)
|
||||||
|
join(code).catch(() => {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: error => {
|
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() {
|
async reset() {
|
||||||
window.location.hash = ""
|
window.location.hash = ""
|
||||||
lobbyCode.value = null
|
lobbyCode.value = null
|
||||||
|
@ -68,6 +75,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! }),
|
||||||
|
newRound: () => trpcClient.game.newRound.mutate({ lobbyCode: lobbyCode.value! }),
|
||||||
create: () => trpcClient.createGame.mutate()
|
create: () => trpcClient.createGame.mutate()
|
||||||
}
|
}
|
||||||
})
|
})
|
|
@ -4,10 +4,11 @@
|
||||||
<div class="flex gap-5 justify-end items-center transform transition ease duration-500">
|
<div class="flex gap-5 justify-end items-center transform transition ease duration-500">
|
||||||
<template v-if="game.state.phase === 'end'">
|
<template v-if="game.state.phase === 'end'">
|
||||||
<BigButton
|
<BigButton
|
||||||
class="bg-gradient-to-br from-gray-600 to-gray-800"
|
class="bg-gradient-to-br from-green-700 to-green-800"
|
||||||
@click="game.reset()"
|
v-if="game.isOwnGame"
|
||||||
|
@click="game.newRound()"
|
||||||
>
|
>
|
||||||
Leave
|
New round
|
||||||
</BigButton>
|
</BigButton>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|
|
@ -16,8 +16,8 @@ export function getGameByLobbyCode(code: string): Game | null {
|
||||||
return gamesByLobbyCode.get(code.toUpperCase()) ?? null
|
return gamesByLobbyCode.get(code.toUpperCase()) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGame() {
|
export function createGame(lobbyCode: string = generateLobbyCode()) {
|
||||||
const game = new Game()
|
const game = new Game(lobbyCode)
|
||||||
gamesByLobbyCode.set(game.lobbyCode, game)
|
gamesByLobbyCode.set(game.lobbyCode, game)
|
||||||
return game
|
return game
|
||||||
}
|
}
|
||||||
|
@ -43,12 +43,11 @@ function redactGameAction<T extends GameAction>(action: T, receiverId: string):
|
||||||
interface Events {
|
interface Events {
|
||||||
broadcast_action: [GameAction]
|
broadcast_action: [GameAction]
|
||||||
private_action: [string, GameAction]
|
private_action: [string, GameAction]
|
||||||
|
new_round: []
|
||||||
destroyed: []
|
destroyed: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Game extends EventEmitter<Events> {
|
export class Game extends EventEmitter<Events> {
|
||||||
lobbyCode = generateLobbyCode()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
|
@ -65,7 +64,7 @@ export class Game extends EventEmitter<Events> {
|
||||||
return this.actions[this.actions.length - 1].index + 1
|
return this.actions[this.actions.length - 1].index + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor(public lobbyCode: string) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +106,7 @@ export class Game extends EventEmitter<Events> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
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()
|
const players = await this.getPlayers()
|
||||||
if (players.length < 2) throw new Error("At least two players are required for starting the game")
|
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))
|
this.actions.forEach(action => this.sendActionTo(action, receiverId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
announceNewRoundAndDestroy() {
|
||||||
|
this.emit("new_round")
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
private dealNumberTo(playerId: string, isCovert: boolean) {
|
private dealNumberTo(playerId: string, isCovert: boolean) {
|
||||||
const number = this.state.numberCardsStack[random(0, this.state.numberCardsStack.length - 1)]
|
const number = this.state.numberCardsStack[random(0, this.state.numberCardsStack.length - 1)]
|
||||||
|
|
||||||
|
@ -230,7 +235,7 @@ export class Game extends EventEmitter<Events> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private destroy() {
|
private destroy() {
|
||||||
gamesByLobbyCode.delete(this.lobbyCode)
|
if (gamesByLobbyCode.get(this.lobbyCode) === this) gamesByLobbyCode.delete(this.lobbyCode)
|
||||||
this.emit("destroyed")
|
this.emit("destroyed")
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { requireAuthentication, t } from "./base"
|
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 { getGameByLobbyCode } from "../game"
|
import { createGame, getGameByLobbyCode } from "../game"
|
||||||
|
|
||||||
const gameProcedure = t.procedure
|
const gameProcedure = t.procedure
|
||||||
.use(requireAuthentication)
|
.use(requireAuthentication)
|
||||||
|
@ -22,7 +22,6 @@ const gameProcedure = t.procedure
|
||||||
export const gameRouter = t.router({
|
export const gameRouter = t.router({
|
||||||
start: gameProcedure
|
start: gameProcedure
|
||||||
.mutation(async ({ ctx }) => {
|
.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")
|
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()
|
ctx.game.start()
|
||||||
}),
|
}),
|
||||||
|
@ -40,5 +39,14 @@ export const gameRouter = t.router({
|
||||||
.mutation(async ({ ctx }) => {
|
.mutation(async ({ ctx }) => {
|
||||||
if (ctx.game.state.activePlayerId !== ctx.user.id) throw new Error("It is not the player’s turn")
|
if (ctx.game.state.activePlayerId !== ctx.user.id) throw new Error("It is not the player’s turn")
|
||||||
await ctx.game.stay()
|
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 => {
|
return observable<GameEvent>(emit => {
|
||||||
const handleBroadcastAction = (action: GameAction) => emit.next({ type: "action", action })
|
const handleBroadcastAction = (action: GameAction) => emit.next({ type: "action", action })
|
||||||
|
const handleNewRound = () => emit.next({ type: "new_round" })
|
||||||
const handleDestroyed = () => setTimeout(() => emit.complete(), 500)
|
const handleDestroyed = () => setTimeout(() => emit.complete(), 500)
|
||||||
|
|
||||||
const handlePrivateAction = (playerId: string, action: GameAction) => {
|
const handlePrivateAction = (playerId: string, action: GameAction) => {
|
||||||
|
@ -95,6 +96,7 @@ export const appRouter = t.router({
|
||||||
|
|
||||||
game.on("broadcast_action", handleBroadcastAction)
|
game.on("broadcast_action", handleBroadcastAction)
|
||||||
game.on("private_action", handlePrivateAction)
|
game.on("private_action", handlePrivateAction)
|
||||||
|
game.on("new_round", handleNewRound)
|
||||||
game.on("destroyed", handleDestroyed)
|
game.on("destroyed", handleDestroyed)
|
||||||
|
|
||||||
game.sendAllOldActionsTo(ctx.user.id)
|
game.sendAllOldActionsTo(ctx.user.id)
|
||||||
|
@ -102,6 +104,7 @@ export const appRouter = t.router({
|
||||||
return () => {
|
return () => {
|
||||||
game.off("broadcast_action", handleBroadcastAction)
|
game.off("broadcast_action", handleBroadcastAction)
|
||||||
game.off("private_action", handlePrivateAction)
|
game.off("private_action", handlePrivateAction)
|
||||||
|
game.off("new_round", handleNewRound)
|
||||||
game.off("destroyed", handleDestroyed)
|
game.off("destroyed", handleDestroyed)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { SpecialCardType } from "./cards"
|
import type { SpecialCardId } from "./cards"
|
||||||
|
|
||||||
type PlayerAction = {
|
type PlayerAction = {
|
||||||
initiatingPlayerId: string
|
initiatingPlayerId: string
|
||||||
|
@ -15,7 +15,7 @@ type PlayerAction = {
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "use-special"
|
type: "use-special"
|
||||||
cardType: SpecialCardType
|
cardType: SpecialCardId
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "leave"
|
type: "leave"
|
||||||
|
@ -38,7 +38,7 @@ type ServerAction = {
|
||||||
| {
|
| {
|
||||||
type: "deal-special"
|
type: "deal-special"
|
||||||
toPlayerId: string
|
toPlayerId: string
|
||||||
cardType: SpecialCardType
|
cardType?: SpecialCardId // undefined if redacted
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "end"
|
type: "end"
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
const specialCardTypesObject = {
|
interface SpecialCardMeta {
|
||||||
"return-last-opponent": true,
|
type: "single-use" | "permanent"
|
||||||
"return-last-own": true,
|
description: string
|
||||||
"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 const specialCardTypes = Object.keys(specialCardTypesObject)
|
// const specialCardTypesObject = {
|
||||||
export type SpecialCardType = keyof typeof 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 = {
|
export type GameEvent = {
|
||||||
type: "action"
|
type: "action"
|
||||||
action: GameAction
|
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 type { GameAction } from "./actions"
|
||||||
import { produce } from "immer"
|
import { produce } from "immer"
|
||||||
import { specialCardTypes } from "./cards"
|
import { specialCardTypes } from "./cards"
|
||||||
import { cloneDeep, tail, without } from "lodash-es"
|
import { cloneDeep, tail, without } from "lodash-es"
|
||||||
|
|
||||||
type SpecialCardCountByType = Record<SpecialCardType, number>
|
type SpecialCardCountByType = Record<SpecialCardId, number>
|
||||||
|
|
||||||
export interface GameStateNumberCard {
|
export interface GameStateNumberCard {
|
||||||
number: number
|
number: number
|
||||||
|
@ -96,8 +96,11 @@ export const produceNewState = (oldState: GameState, action: GameAction) => prod
|
||||||
break
|
break
|
||||||
|
|
||||||
case "deal-special":
|
case "deal-special":
|
||||||
|
if (action.cardType !== 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.cardType]++
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case "hit":
|
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]--
|
player.specialCardCountByType[type]--
|
||||||
|
|
||||||
switch (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":
|
case "return-last-opponent":
|
||||||
// TODO
|
// TODO
|
||||||
break
|
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