fuck webstorm
This commit is contained in:
parent
e80536cf67
commit
14d7efe0bf
13 changed files with 231 additions and 34 deletions
|
@ -2,13 +2,13 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>TapDB</title>
|
||||
<title>TwentyOne</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<script type="module" src="./src/index.ts"></script>
|
||||
<link rel="stylesheet" href="./node_modules/@fontsource/inter/variable.css"/>
|
||||
<link rel="stylesheet" href="./node_modules/@fontsource/titan-one/400.css"/>
|
||||
</head>
|
||||
<body class="mocha">
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
"bufferutil": "^4.0.7",
|
||||
"eventemitter3": "^5.0.0",
|
||||
"express": "^4.18.2",
|
||||
"immer": "^10.0.1",
|
||||
"listhen": "^1.0.4",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^4.0.2",
|
||||
|
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
|
@ -21,6 +21,7 @@ importers:
|
|||
bufferutil: ^4.0.7
|
||||
eventemitter3: ^5.0.0
|
||||
express: ^4.18.2
|
||||
immer: ^10.0.1
|
||||
listhen: ^1.0.4
|
||||
lodash-es: ^4.17.21
|
||||
nanoid: ^4.0.2
|
||||
|
@ -48,6 +49,7 @@ importers:
|
|||
bufferutil: 4.0.7
|
||||
eventemitter3: 5.0.0
|
||||
express: 4.18.2
|
||||
immer: 10.0.1
|
||||
listhen: 1.0.4
|
||||
lodash-es: 4.17.21
|
||||
nanoid: 4.0.2
|
||||
|
@ -1949,6 +1951,10 @@ packages:
|
|||
safer-buffer: 2.1.2
|
||||
dev: false
|
||||
|
||||
/immer/10.0.1:
|
||||
resolution: {integrity: sha512-zg++jJLsKKTwXGeSYIw0HgChSYQGtu0UDTnbKx5aGLYgte4CwTmH9eJDYyQ6FheyUtBe+lQW9FrGxya1G+Dtmg==}
|
||||
dev: false
|
||||
|
||||
/immutable/4.2.4:
|
||||
resolution: {integrity: sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==}
|
||||
dev: true
|
||||
|
|
|
@ -15,8 +15,9 @@ model Game {
|
|||
}
|
||||
|
||||
model Player {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
token String @unique
|
||||
|
||||
gameActions GameAction[]
|
||||
}
|
||||
|
|
|
@ -1,14 +1,29 @@
|
|||
import EventEmitter from "eventemitter3"
|
||||
import type { GameAction } from "../shared/gameActions"
|
||||
import type { GameAction } from "../shared/game/gameActions"
|
||||
|
||||
export const gameActionsBus = new EventEmitter<Record<string, [GameAction]>>()
|
||||
const gamesById = new Map<string, Game>()
|
||||
|
||||
export function getGameById(id: string): Game {
|
||||
return gamesById.get(id)!
|
||||
}
|
||||
|
||||
interface Events {
|
||||
broadcastAction: [GameAction]
|
||||
public_action: [GameAction]
|
||||
private_action: [string, GameAction]
|
||||
destroyed: []
|
||||
}
|
||||
|
||||
export class Game extends EventEmitter<Events> {
|
||||
constructor(public id: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
start() {
|
||||
|
||||
}
|
||||
|
||||
private destroy() {
|
||||
gamesById.delete(this.id)
|
||||
this.emit("destroyed")
|
||||
}
|
||||
}
|
|
@ -11,14 +11,18 @@ const expressApp = createExpressApp()
|
|||
|
||||
expressApp.use("/trpc", createTrpcMiddleware({
|
||||
router: appRouter,
|
||||
createContext
|
||||
createContext: ({ req }) => createContext(req.headers.authorization)
|
||||
}))
|
||||
|
||||
await seedDatabase()
|
||||
const { server } = await listen(expressApp)
|
||||
|
||||
const wss = new WebSocketServer({ server, path: "/ws" })
|
||||
const wssTrpcHandler = applyWSSHandler({ wss, createContext, router: appRouter })
|
||||
const wssTrpcHandler = applyWSSHandler({
|
||||
wss,
|
||||
router: appRouter,
|
||||
createContext: ({ req }) => createContext(req.headers.authorization)
|
||||
})
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("Received SIGTERM")
|
||||
|
|
|
@ -1,3 +1,28 @@
|
|||
export async function seedDatabase() {
|
||||
import { prismaClient } from "./prisma"
|
||||
|
||||
export async function seedDatabase() {
|
||||
if (await prismaClient.player.count() === 0) {
|
||||
await prismaClient.player.create({
|
||||
data: {
|
||||
name: "max",
|
||||
token: "max"
|
||||
}
|
||||
})
|
||||
|
||||
await prismaClient.player.create({
|
||||
data: {
|
||||
name: "moritz",
|
||||
token: "moritz"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (await prismaClient.game.count() === 0) {
|
||||
await prismaClient.game.create({
|
||||
data: {
|
||||
id: "game",
|
||||
lobbyCodeIfActive: "game"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,11 +1,22 @@
|
|||
import { initTRPC } from "@trpc/server"
|
||||
import { prismaClient } from "../prisma"
|
||||
|
||||
export interface Context {
|
||||
|
||||
playerId: string
|
||||
}
|
||||
|
||||
export async function createContext(): Promise<Context> {
|
||||
return {}
|
||||
export async function createContext(authorizationHeader: string | undefined): Promise<Context> {
|
||||
const token = authorizationHeader?.slice(7) ?? null // "Bearer " → 7 characters
|
||||
|
||||
if (token !== null && token.length > 0) {
|
||||
const player = await prismaClient.player.findUnique({ where: { token }, select: { id: true } })
|
||||
|
||||
if (player !== null) return {
|
||||
playerId: player.id
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Invalid token")
|
||||
}
|
||||
|
||||
export const t = initTRPC.context<Context>().create()
|
||||
|
|
|
@ -1,16 +1,38 @@
|
|||
import { t } from "./base"
|
||||
import { prismaClient } from "../prisma"
|
||||
import type { Prisma } from "../prisma"
|
||||
import z from "zod"
|
||||
import { observable } from "@trpc/server/observable"
|
||||
import type { GameAction } from "../../shared/game/gameActions"
|
||||
import { getGameById } from "../game"
|
||||
|
||||
export const appRouter = t.router({
|
||||
create: t.procedure
|
||||
.input(z.object({
|
||||
skip: z.number().int().min(0),
|
||||
groupId: z.string().cuid().optional()
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
start: t.procedure
|
||||
.mutation(async ({ ctx }) => {
|
||||
|
||||
}),
|
||||
|
||||
join: t.procedure
|
||||
.input(z.object({
|
||||
gameId: z.string().nonempty()
|
||||
}))
|
||||
.subscription(({ input, ctx }) => {
|
||||
const game = getGameById(input.gameId)
|
||||
|
||||
return observable(emit => {
|
||||
const handlePublicAction = (action: GameAction) => emit.next(action)
|
||||
|
||||
const handlePrivateAction = (playerId: string, action: GameAction) => {
|
||||
if (playerId === ctx.playerId) emit.next(action)
|
||||
}
|
||||
|
||||
game.on("public_action", handlePublicAction)
|
||||
game.on("private_action", handlePrivateAction)
|
||||
|
||||
return () => {
|
||||
game.off("public_action", handlePublicAction)
|
||||
game.off("private_action", handlePrivateAction)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
export type SpecialCardType =
|
||||
| "return-last-opponent"
|
||||
| "return-last-own"
|
||||
| "destroy-last-opponent-special"
|
||||
| "increase-target-by-2"
|
||||
| "next-round-covert"
|
||||
| "double-draw"
|
||||
| "add-2-opponent"
|
||||
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
|
||||
}
|
||||
|
||||
export const specialCardTypes = Object.keys(specialCardTypesObject)
|
||||
export type SpecialCardType = keyof typeof specialCardTypesObject
|
|
@ -15,12 +15,11 @@ type PlayerAction = {
|
|||
}
|
||||
)
|
||||
|
||||
type ServerAction = {
|
||||
initiatingPlayerId: null
|
||||
} & (
|
||||
type ServerAction =
|
||||
| {
|
||||
type: "start"
|
||||
cardsByPlayerId: Record<string, number> // if redacted: only contains the player themself
|
||||
numberCardsByPlayerId: Record<string, number[]> // if redacted: only contains the player themself
|
||||
targetSum: number
|
||||
}
|
||||
| {
|
||||
type: "deal-number"
|
||||
|
@ -32,7 +31,6 @@ type ServerAction = {
|
|||
toPlayerId: string
|
||||
cardType: SpecialCardType
|
||||
}
|
||||
)
|
||||
|
||||
export type GameAction = {
|
||||
index: number
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import type { SpecialCardType } from "./cards"
|
||||
import type { GameAction } from "./gameActions"
|
||||
import { produce } from "immer"
|
||||
import { specialCardTypes } from "./cards"
|
||||
|
||||
type SpecialCardCountByType = Record<SpecialCardType, number>
|
||||
|
||||
export interface GameStatePlayer {
|
||||
id: string
|
||||
numberCards: number[]
|
||||
specialCardCountByType: SpecialCardCountByType
|
||||
stayed: boolean
|
||||
nextRoundCovert: boolean
|
||||
}
|
||||
|
||||
export interface GameState {
|
||||
players: GameStatePlayer[]
|
||||
activeSpecialCardCountByType: SpecialCardCountByType
|
||||
activePlayerId: string
|
||||
targetSum: number
|
||||
}
|
||||
|
||||
const ALL_ZERO_SPECIAL_CARD_COUNTS: SpecialCardCountByType = Object.fromEntries(specialCardTypes.map(t => [t, 0])) as SpecialCardCountByType
|
||||
|
||||
const UNINITIALIZED_GAME_STATE: GameState = {
|
||||
players: [],
|
||||
activePlayerId: "",
|
||||
targetSum: 0,
|
||||
activeSpecialCardCountByType: ALL_ZERO_SPECIAL_CARD_COUNTS
|
||||
}
|
||||
|
||||
export const produceNewState = (oldState: GameState, action: GameAction) => produce(oldState, state => {
|
||||
switch (action.type) {
|
||||
case "start":
|
||||
state.players = Object.entries(action.numberCardsByPlayerId).map(([playerId, numberCards]): GameStatePlayer => ({
|
||||
id: playerId,
|
||||
numberCards,
|
||||
specialCardCountByType: ALL_ZERO_SPECIAL_CARD_COUNTS,
|
||||
stayed: false,
|
||||
nextRoundCovert: false
|
||||
}))
|
||||
|
||||
state.activePlayerId = state.players[0].id
|
||||
state.targetSum = action.targetSum
|
||||
state.activeSpecialCardCountByType = ALL_ZERO_SPECIAL_CARD_COUNTS
|
||||
|
||||
break
|
||||
|
||||
case "deal-number":
|
||||
const p1 = state.players.find(p => p.id === action.toPlayerId)!
|
||||
p1.numberCards.push(action.number)
|
||||
break
|
||||
|
||||
case "deal-special":
|
||||
const p2 = state.players.find(p => p.id === action.toPlayerId)!
|
||||
p2.specialCardCountByType[action.cardType]++
|
||||
break
|
||||
|
||||
case "draw":
|
||||
case "stay":
|
||||
const activePlayerIndex = state.players.findIndex(p => p.id === state.activePlayerId)
|
||||
|
||||
// activate the next player
|
||||
state.activePlayerId = state.players[(activePlayerIndex + 1) % state.players.length].id
|
||||
break
|
||||
|
||||
case "use-special":
|
||||
const p3 = state.players.find(p => p.id === action.initiatingPlayerId)!
|
||||
applySpecialCardUsage(state, action.cardType, p3)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
function applySpecialCardUsage(state: GameState, type: SpecialCardType, 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
|
||||
}
|
||||
}
|
|
@ -13,7 +13,11 @@ export default defineConfig({
|
|||
],
|
||||
server: {
|
||||
proxy: {
|
||||
"/trpc": "http://127.0.0.1:3000"
|
||||
"/trpc": "http://127.0.0.1:3000",
|
||||
"/ws": {
|
||||
target: "http://127.0.0.1:3000",
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue