fuck webstorm

This commit is contained in:
Moritz Ruth 2023-04-19 01:03:37 +02:00
parent e80536cf67
commit 14d7efe0bf
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
13 changed files with 231 additions and 34 deletions

View file

@ -2,13 +2,13 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>TapDB</title> <title>TwentyOne</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<script type="module" src="./src/index.ts"></script> <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/inter/variable.css"/>
<link rel="stylesheet" href="./node_modules/@fontsource/titan-one/400.css"/> <link rel="stylesheet" href="./node_modules/@fontsource/titan-one/400.css"/>
</head> </head>
<body class="mocha"> <body>
<div id="app"></div> <div id="app"></div>
</body> </body>
</html> </html>

View file

@ -34,6 +34,7 @@
"bufferutil": "^4.0.7", "bufferutil": "^4.0.7",
"eventemitter3": "^5.0.0", "eventemitter3": "^5.0.0",
"express": "^4.18.2", "express": "^4.18.2",
"immer": "^10.0.1",
"listhen": "^1.0.4", "listhen": "^1.0.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",

6
pnpm-lock.yaml generated
View file

@ -21,6 +21,7 @@ importers:
bufferutil: ^4.0.7 bufferutil: ^4.0.7
eventemitter3: ^5.0.0 eventemitter3: ^5.0.0
express: ^4.18.2 express: ^4.18.2
immer: ^10.0.1
listhen: ^1.0.4 listhen: ^1.0.4
lodash-es: ^4.17.21 lodash-es: ^4.17.21
nanoid: ^4.0.2 nanoid: ^4.0.2
@ -48,6 +49,7 @@ importers:
bufferutil: 4.0.7 bufferutil: 4.0.7
eventemitter3: 5.0.0 eventemitter3: 5.0.0
express: 4.18.2 express: 4.18.2
immer: 10.0.1
listhen: 1.0.4 listhen: 1.0.4
lodash-es: 4.17.21 lodash-es: 4.17.21
nanoid: 4.0.2 nanoid: 4.0.2
@ -1949,6 +1951,10 @@ packages:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
dev: false dev: false
/immer/10.0.1:
resolution: {integrity: sha512-zg++jJLsKKTwXGeSYIw0HgChSYQGtu0UDTnbKx5aGLYgte4CwTmH9eJDYyQ6FheyUtBe+lQW9FrGxya1G+Dtmg==}
dev: false
/immutable/4.2.4: /immutable/4.2.4:
resolution: {integrity: sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==} resolution: {integrity: sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==}
dev: true dev: true

View file

@ -15,8 +15,9 @@ model Game {
} }
model Player { model Player {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
token String @unique
gameActions GameAction[] gameActions GameAction[]
} }

View file

@ -1,14 +1,29 @@
import EventEmitter from "eventemitter3" 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 { interface Events {
broadcastAction: [GameAction] public_action: [GameAction]
private_action: [string, GameAction]
destroyed: []
} }
export class Game extends EventEmitter<Events> { export class Game extends EventEmitter<Events> {
constructor(public id: string) { constructor(public id: string) {
super() super()
} }
start() {
}
private destroy() {
gamesById.delete(this.id)
this.emit("destroyed")
}
} }

View file

@ -11,14 +11,18 @@ const expressApp = createExpressApp()
expressApp.use("/trpc", createTrpcMiddleware({ expressApp.use("/trpc", createTrpcMiddleware({
router: appRouter, router: appRouter,
createContext createContext: ({ req }) => createContext(req.headers.authorization)
})) }))
await seedDatabase() await seedDatabase()
const { server } = await listen(expressApp) const { server } = await listen(expressApp)
const wss = new WebSocketServer({ server, path: "/ws" }) 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", () => { process.on("SIGTERM", () => {
console.log("Received SIGTERM") console.log("Received SIGTERM")

View file

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

View file

@ -1,11 +1,22 @@
import { initTRPC } from "@trpc/server" import { initTRPC } from "@trpc/server"
import { prismaClient } from "../prisma"
export interface Context { export interface Context {
playerId: string
} }
export async function createContext(): Promise<Context> { export async function createContext(authorizationHeader: string | undefined): Promise<Context> {
return {} 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() export const t = initTRPC.context<Context>().create()

View file

@ -1,16 +1,38 @@
import { t } from "./base" import { t } from "./base"
import { prismaClient } from "../prisma" import { prismaClient } from "../prisma"
import type { Prisma } from "../prisma"
import z from "zod" 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({ export const appRouter = t.router({
create: t.procedure start: t.procedure
.input(z.object({ .mutation(async ({ ctx }) => {
skip: z.number().int().min(0),
groupId: z.string().cuid().optional()
}))
.query(async ({ input }) => {
}),
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)
}
})
}) })
}) })

View file

@ -1,8 +1,12 @@
export type SpecialCardType = const specialCardTypesObject = {
| "return-last-opponent" "return-last-opponent": true,
| "return-last-own" "return-last-own": true,
| "destroy-last-opponent-special" "destroy-last-opponent-special": true,
| "increase-target-by-2" "increase-target-by-2": true,
| "next-round-covert" "next-round-covert": true,
| "double-draw" "double-draw": true,
| "add-2-opponent" "add-2-opponent": true
}
export const specialCardTypes = Object.keys(specialCardTypesObject)
export type SpecialCardType = keyof typeof specialCardTypesObject

View file

@ -15,12 +15,11 @@ type PlayerAction = {
} }
) )
type ServerAction = { type ServerAction =
initiatingPlayerId: null
} & (
| { | {
type: "start" 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" type: "deal-number"
@ -32,7 +31,6 @@ type ServerAction = {
toPlayerId: string toPlayerId: string
cardType: SpecialCardType cardType: SpecialCardType
} }
)
export type GameAction = { export type GameAction = {
index: number index: number

View file

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

View file

@ -13,7 +13,11 @@ export default defineConfig({
], ],
server: { server: {
proxy: { 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
}
} }
} }
}) })