This commit is contained in:
Moritz Ruth 2023-04-23 15:14:44 +02:00
parent 14c4a434f7
commit 2975b2820b
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
10 changed files with 102 additions and 76 deletions

View file

@ -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()
} }
}) })

View file

@ -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>

View file

@ -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")
} }
} }

View file

@ -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 players turn") if (ctx.game.state.activePlayerId !== ctx.user.id) throw new Error("It is not the players 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()
}) })
}) })

View file

@ -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)
} }
}) })

View file

@ -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"

View file

@ -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)

View file

@ -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"
} }

View file

@ -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
View 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
}