wip but I need to sleep
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GameState | undefined>;
|
||||
setGameState: (state: GameState) => Promise<any>;
|
||||
submitAction: (action: Action) => Promise<any>;
|
||||
}>();
|
||||
|
||||
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 (
|
||||
<GameContext.Provider value={{ gameState, setGameState }}>
|
||||
<GameContext.Provider value={{ gameState, submitAction }}>
|
||||
<Show when={gameState.latest != undefined}>
|
||||
<div
|
||||
class="full column center"
|
||||
@@ -38,16 +33,12 @@ export default (props: { instanceId: number }) => {
|
||||
<Pile
|
||||
pile={gameState.latest!.deck}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
const [drawn, ...rest] = gameState()!.deck;
|
||||
setGameState({
|
||||
deck: rest,
|
||||
hand: [drawn, ...gameState()!.hand],
|
||||
});
|
||||
}}
|
||||
onClick={() =>
|
||||
api.simple({ instanceId: props.instanceId })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Hand hand={gameState.latest!.hand} />
|
||||
<Hand hand={gameState.latest!.players[0]} />
|
||||
</div>
|
||||
</Show>
|
||||
</GameContext.Provider>
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
@@ -33,16 +33,7 @@ export default ((props) => {
|
||||
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 })}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
10
packages/server/src/games/index.ts
Normal file
10
packages/server/src/games/index.ts
Normal 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;
|
||||
1
packages/server/src/games/renaissance.ts
Normal file
1
packages/server/src/games/renaissance.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
145
packages/server/src/games/simple.ts
Normal file
145
packages/server/src/games/simple.ts
Normal 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(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -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 extends vCard = Card> = C[];
|
||||
export type Stack<C extends vCard = Card> = C[];
|
||||
export type Hand<C extends vCard = Card> = C[];
|
||||
export type Board<C extends vCard = Card> = 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);
|
||||
3
packages/shared/utils.ts
Normal file
3
packages/shared/utils.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import hash, { NotUndefined } from "object-hash";
|
||||
|
||||
export const heq = (a: NotUndefined) => (b: NotUndefined) => hash(a) == hash(b);
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user