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">
|
<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>
|
||||||
|
|
|
@ -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
6
pnpm-lock.yaml
generated
|
@ -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
|
||||||
|
|
|
@ -17,6 +17,7 @@ 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[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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")
|
||||||
|
|
|
@ -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 { 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()
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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: {
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Reference in a new issue