wip but I need to sleep

This commit is contained in:
2025-08-09 01:40:42 -04:00
parent a7e339a8ce
commit 2ff5d781fd
12 changed files with 213 additions and 82 deletions

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

@@ -0,0 +1 @@
export default {};

View File

@@ -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<vCard>;
players: { [humanId: string]: Hand<vCard> };
};
// a particular player's point of view in the game
export type PlayerView = {
humanId: string;
deckCount: number;
playerHandCounts: { [humanId: string]: number };
myHand: Hand<Card>;
};
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(),
}),
}
)
);