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

View file

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

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

View file

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

View file

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

View file

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

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 { 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()

View file

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

View file

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

View file

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

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: {
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
}
}
}
})