From 2ff5d781fd7a26d8933217da9841db64dac2e133 Mon Sep 17 00:00:00 2001 From: Daniel McCrystal Date: Sat, 9 Aug 2025 01:40:42 -0400 Subject: [PATCH] wip but I need to sleep --- package.json | 8 ++ packages/client/src/components/Game.tsx | 39 +++--- packages/client/src/components/Hand.tsx | 15 +-- packages/server/db/schema.prisma | 10 +- packages/server/package.json | 4 +- packages/server/src/api.ts | 27 +---- packages/server/src/games/index.ts | 10 ++ packages/server/src/games/renaissance.ts | 1 + packages/server/src/games/simple.ts | 145 +++++++++++++++++++++++ packages/shared/{types => }/cards.ts | 19 +-- packages/shared/utils.ts | 3 + pnpm-lock.yaml | 14 ++- 12 files changed, 213 insertions(+), 82 deletions(-) create mode 100644 packages/server/src/games/index.ts create mode 100644 packages/server/src/games/renaissance.ts create mode 100644 packages/server/src/games/simple.ts rename packages/shared/{types => }/cards.ts (77%) create mode 100644 packages/shared/utils.ts diff --git a/package.json b/package.json index 8b3a916..e419b82 100644 --- a/package.json +++ b/package.json @@ -6,5 +6,13 @@ "dev": "pnpm --parallel dev", "build": "pnpm run -F client build", "start": "pnpm run -F server start" + }, + "pnpm": { + "overrides": { + "object-hash": "^3.0.0" + } + }, + "devDependencies": { + "@types/object-hash": "^3.0.6" } } diff --git a/packages/client/src/components/Game.tsx b/packages/client/src/components/Game.tsx index 5dc9d85..a236cb1 100644 --- a/packages/client/src/components/Game.tsx +++ b/packages/client/src/components/Game.tsx @@ -1,34 +1,29 @@ import { Accessor, createContext, createResource, Show } from "solid-js"; -import { GameState } from "../../../shared/types/cards"; +import { GameState, Action } from "../../../server/src/games/simple"; import api from "../api"; import Hand from "./Hand"; import Pile from "./Pile"; export const GameContext = createContext<{ gameState: Accessor; - setGameState: (state: GameState) => Promise; + submitAction: (action: Action) => Promise; }>(); -export default (props: { instanceId: number }) => { - const [gameState, { refetch, mutate }] = createResource(() => +export default (props: { instanceId: string }) => { + const [gameState, { mutate }] = createResource(() => api - .gameState({ gameId: props.instanceId.toString() }) + .simple(props) .get() .then((res) => res.data as GameState) ); - - const setGameState = (state: GameState) => { - mutate(state); - return api - .gameState({ gameId: props.instanceId.toString() }) - .put({ - gameState: state, - }) - .then(refetch); - }; + const submitAction = (action: Action) => + api + .simple(props) + .post({ action }) + .then((res) => mutate(res.data as GameState)); return ( - +
{ { - const [drawn, ...rest] = gameState()!.deck; - setGameState({ - deck: rest, - hand: [drawn, ...gameState()!.hand], - }); - }} + onClick={() => + api.simple({ instanceId: props.instanceId }) + } />
- +
diff --git a/packages/client/src/components/Hand.tsx b/packages/client/src/components/Hand.tsx index 15332bf..90c9b96 100644 --- a/packages/client/src/components/Hand.tsx +++ b/packages/client/src/components/Hand.tsx @@ -1,11 +1,11 @@ import { Component, For, useContext } from "solid-js"; import Card from "./Card"; -import { Hand } from "../../../shared/types/cards"; +import { Hand } from "../../../shared/cards"; import { GameContext } from "./Game"; import { produce } from "solid-js/store"; export default ((props) => { - const { setGameState, gameState } = useContext(GameContext)!; + const { submitAction, gameState } = useContext(GameContext)!; return (
{ style={{ cursor: "pointer", }} - onClick={() => { - const index = gameState()!.hand.indexOf(card); - setGameState({ - deck: [card, ...gameState()!.deck], - hand: [ - ...gameState()!.hand.slice(0, index), - ...gameState()!.hand.slice(index + 1), - ], - }); - }} + onClick={() => submitAction({ type: "discard", card })} /> )} diff --git a/packages/server/db/schema.prisma b/packages/server/db/schema.prisma index a82568e..e425d86 100644 --- a/packages/server/db/schema.prisma +++ b/packages/server/db/schema.prisma @@ -8,17 +8,17 @@ datasource db { } model Game { - id Int @id @default(autoincrement()) + key String @id createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - name String @unique + name String rules String? instances Instance[] } model Instance { - id Int @id @default(autoincrement()) - gameId Int - game Game @relation(fields: [gameId], references: [id]) + id Int @id @default(autoincrement()) + gameKey String + game Game @relation(fields: [gameKey], references: [key]) gameState Json } diff --git a/packages/server/package.json b/packages/server/package.json index b2ac74a..b40976c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -5,7 +5,9 @@ "devserver": "NODE_ENV=development PORT=5001 bun run --hot src/index.ts", "dbstudio": "pnpm dlx prisma studio --browser none", "dbdeploy": "pnpm dlx prisma migrate deploy", - "dbsync": "concurrently 'pnpm dlx prisma generate' 'pnpm dlx prisma migrate dev'", + "dbtypes": "pnpm dlx prisma generate", + "dbsync": "pnpm dlx prisma migrate dev", + "dbwipe": "pnpm dlx prisma migrate reset", "start": "NODE_ENV=production PORT=5001 bun run src/index.ts" }, "dependencies": { diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index 4354a28..a3b7e7f 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -1,6 +1,7 @@ import { prisma } from "./db/db"; import { Elysia, t } from "elysia"; import { Prisma } from "@prisma/client"; +import { simpleApi } from "./games/simple"; const api = new Elysia({ prefix: "/api" }) // [wip] @@ -30,31 +31,7 @@ const api = new Elysia({ prefix: "/api" }) }, }) ) + .use(simpleApi); - .get("/gameState/:gameId", ({ params: { gameId } }) => - prisma.instance - .findUnique({ - where: { - id: Number(gameId), - }, - }) - .then((game) => game?.gameState) - ) - - .put( - "/gameState/:gameId", - ({ params: { gameId }, body: { gameState } }) => - prisma.instance.update({ - data: { gameState }, - where: { - id: Number(gameId), - }, - }), - { - body: t.Object({ - gameState: t.Any(), - }), - } - ); export default api; export type Api = typeof api; diff --git a/packages/server/src/games/index.ts b/packages/server/src/games/index.ts new file mode 100644 index 0000000..92b28bd --- /dev/null +++ b/packages/server/src/games/index.ts @@ -0,0 +1,10 @@ +import * as renaissance from "./renaissance"; +import * as simple from "./simple"; + +const games = { + renaissance, + simple, +}; +export default games; + +export type Game = keyof typeof games; diff --git a/packages/server/src/games/renaissance.ts b/packages/server/src/games/renaissance.ts new file mode 100644 index 0000000..ff8b4c5 --- /dev/null +++ b/packages/server/src/games/renaissance.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/server/src/games/simple.ts b/packages/server/src/games/simple.ts new file mode 100644 index 0000000..852631d --- /dev/null +++ b/packages/server/src/games/simple.ts @@ -0,0 +1,145 @@ +import { + Card, + Hand, + newDeck, + Pile, + shuffle, + vCard, +} from "../../../shared/cards"; +import hash from "object-hash"; +import { heq } from "../../../shared/utils"; +import { Elysia, t } from "elysia"; +import { prisma } from "../db/db"; + +// omniscient game state +export type GameState = { + prev?: { + hash: string; + action: Action; + }; + + deck: Pile; + players: { [humanId: string]: Hand }; +}; + +// a particular player's knowledge of the global game state +export type vGameState = { + humanId: string; + + deck: Pile; + players: { [humanId: string]: Hand }; +}; + +// a particular player's point of view in the game +export type PlayerView = { + humanId: string; + + deckCount: number; + playerHandCounts: { [humanId: string]: number }; + myHand: Hand; +}; + +export type Action = { type: "draw" } | { type: "discard"; card: Card }; + +export const newGame = (players: string[]) => + ({ + deck: shuffle(newDeck()), + players: Object.fromEntries(players.map((humanId) => [humanId, []])), + } as GameState); + +export const getKnowledge = (state: GameState, humanId: string) => ({ + hash: hash(state), + humanId, + deck: state.deck.map((_) => null), + players: Object.fromEntries( + Object.entries(state.players).map(([id, hand]) => [ + id, + hand.map(id === humanId ? (card) => card : (_) => null), + ]) + ), +}); + +export const resolveAction = (state: GameState, action: Action): GameState => { + if (action.prevHash != hash(state)) { + throw new Error( + `action thinks it's applying to ${ + action.prevHash + }, but we're checking it against ${hash(state)}` + ); + } + + const playerHand = state.players[action.humanId]; + if (action.type == "draw") { + const [drawn, ...rest] = state.deck; + return { + deck: rest, + players: { + ...state.players, + [action.humanId]: [drawn, ...playerHand], + }, + }; + } + + // action.type == discard + const index = playerHand.findIndex(heq(action.card)); + return { + deck: [action.card, ...state.deck], + players: { + ...state.players, + [action.humanId]: playerHand + .slice(0, index) + .concat(playerHand.slice(index + 1)), + }, + }; +}; + +export const simpleApi = new Elysia({ prefix: "/simple" }) + .post( + "/newGame", + ({ body: { players } }: { body: { players: string[] } }) => + newGame(players) + ) + .group("/:instanceId", (app) => + app + .get("/", ({ params: { instanceId } }) => + prisma.instance + .findUnique({ + where: { + id: Number(instanceId), + }, + }) + .then((game) => game?.gameState) + ) + .post( + "/", + ({ params: { instanceId }, body: { action } }) => + prisma.instance + .findUniqueOrThrow({ + where: { + id: Number(instanceId), + }, + }) + .then((game) => { + const newState = resolveAction( + game.gameState as GameState, + action + ); + const knownState = getKnowledge( + newState, + action.humanId + ); + void prisma.instance.update({ + data: { gameState: newState }, + where: { + id: Number(instanceId), + }, + }); + return knownState; + }), + { + body: t.Object({ + action: t.Any(), + }), + } + ) + ); diff --git a/packages/shared/types/cards.ts b/packages/shared/cards.ts similarity index 77% rename from packages/shared/types/cards.ts rename to packages/shared/cards.ts index 4f8c37f..9f8249a 100644 --- a/packages/shared/types/cards.ts +++ b/packages/shared/cards.ts @@ -26,10 +26,11 @@ export type Card = } | { kind: "joker"; color: "red" | "black" }; -export type Pile = Card[]; -export type Stack = Card[]; -export type Hand = Card[]; -export type Board = Card[]; +export type vCard = Card | null | number; +export type Pile = C[]; +export type Stack = C[]; +export type Hand = C[]; +export type Board = C[]; export const newDeck = (withJokers = false): Pile => suits @@ -55,13 +56,3 @@ export const shuffle = (cards: Card[]) => { } return cards; }; - -export type GameState = { - deck: Pile; - hand: Hand; -}; -export const newGame = () => - ({ - deck: shuffle(newDeck()), - hand: [], - } as GameState); diff --git a/packages/shared/utils.ts b/packages/shared/utils.ts new file mode 100644 index 0000000..dee0865 --- /dev/null +++ b/packages/shared/utils.ts @@ -0,0 +1,3 @@ +import hash, { NotUndefined } from "object-hash"; + +export const heq = (a: NotUndefined) => (b: NotUndefined) => hash(a) == hash(b); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d038132..7d9cb66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,9 +4,16 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + object-hash: ^3.0.0 + importers: - .: {} + .: + devDependencies: + '@types/object-hash': + specifier: ^3.0.6 + version: 3.0.6 packages/client: dependencies: @@ -513,6 +520,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/object-hash@3.0.6': + resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==} + '@types/react@19.1.9': resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==} @@ -1447,6 +1457,8 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/object-hash@3.0.6': {} + '@types/react@19.1.9': dependencies: csstype: 3.1.3