wip but I need to sleep
This commit is contained in:
@@ -6,5 +6,13 @@
|
|||||||
"dev": "pnpm --parallel dev",
|
"dev": "pnpm --parallel dev",
|
||||||
"build": "pnpm run -F client build",
|
"build": "pnpm run -F client build",
|
||||||
"start": "pnpm run -F server start"
|
"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 { 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 api from "../api";
|
||||||
import Hand from "./Hand";
|
import Hand from "./Hand";
|
||||||
import Pile from "./Pile";
|
import Pile from "./Pile";
|
||||||
|
|
||||||
export const GameContext = createContext<{
|
export const GameContext = createContext<{
|
||||||
gameState: Accessor<GameState | undefined>;
|
gameState: Accessor<GameState | undefined>;
|
||||||
setGameState: (state: GameState) => Promise<any>;
|
submitAction: (action: Action) => Promise<any>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
export default (props: { instanceId: number }) => {
|
export default (props: { instanceId: string }) => {
|
||||||
const [gameState, { refetch, mutate }] = createResource(() =>
|
const [gameState, { mutate }] = createResource(() =>
|
||||||
api
|
api
|
||||||
.gameState({ gameId: props.instanceId.toString() })
|
.simple(props)
|
||||||
.get()
|
.get()
|
||||||
.then((res) => res.data as GameState)
|
.then((res) => res.data as GameState)
|
||||||
);
|
);
|
||||||
|
const submitAction = (action: Action) =>
|
||||||
const setGameState = (state: GameState) => {
|
api
|
||||||
mutate(state);
|
.simple(props)
|
||||||
return api
|
.post({ action })
|
||||||
.gameState({ gameId: props.instanceId.toString() })
|
.then((res) => mutate(res.data as GameState));
|
||||||
.put({
|
|
||||||
gameState: state,
|
|
||||||
})
|
|
||||||
.then(refetch);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GameContext.Provider value={{ gameState, setGameState }}>
|
<GameContext.Provider value={{ gameState, submitAction }}>
|
||||||
<Show when={gameState.latest != undefined}>
|
<Show when={gameState.latest != undefined}>
|
||||||
<div
|
<div
|
||||||
class="full column center"
|
class="full column center"
|
||||||
@@ -38,16 +33,12 @@ export default (props: { instanceId: number }) => {
|
|||||||
<Pile
|
<Pile
|
||||||
pile={gameState.latest!.deck}
|
pile={gameState.latest!.deck}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
const [drawn, ...rest] = gameState()!.deck;
|
api.simple({ instanceId: props.instanceId })
|
||||||
setGameState({
|
}
|
||||||
deck: rest,
|
|
||||||
hand: [drawn, ...gameState()!.hand],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Hand hand={gameState.latest!.hand} />
|
<Hand hand={gameState.latest!.players[0]} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</GameContext.Provider>
|
</GameContext.Provider>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Component, For, useContext } from "solid-js";
|
import { Component, For, useContext } from "solid-js";
|
||||||
import Card from "./Card";
|
import Card from "./Card";
|
||||||
import { Hand } from "../../../shared/types/cards";
|
import { Hand } from "../../../shared/cards";
|
||||||
import { GameContext } from "./Game";
|
import { GameContext } from "./Game";
|
||||||
import { produce } from "solid-js/store";
|
import { produce } from "solid-js/store";
|
||||||
|
|
||||||
export default ((props) => {
|
export default ((props) => {
|
||||||
const { setGameState, gameState } = useContext(GameContext)!;
|
const { submitAction, gameState } = useContext(GameContext)!;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -33,16 +33,7 @@ export default ((props) => {
|
|||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => submitAction({ type: "discard", card })}
|
||||||
const index = gameState()!.hand.indexOf(card);
|
|
||||||
setGameState({
|
|
||||||
deck: [card, ...gameState()!.deck],
|
|
||||||
hand: [
|
|
||||||
...gameState()!.hand.slice(0, index),
|
|
||||||
...gameState()!.hand.slice(index + 1),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Game {
|
model Game {
|
||||||
id Int @id @default(autoincrement())
|
key String @id
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
name String @unique
|
name String
|
||||||
rules String?
|
rules String?
|
||||||
instances Instance[]
|
instances Instance[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Instance {
|
model Instance {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
gameId Int
|
gameKey String
|
||||||
game Game @relation(fields: [gameId], references: [id])
|
game Game @relation(fields: [gameKey], references: [key])
|
||||||
gameState Json
|
gameState Json
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
"devserver": "NODE_ENV=development PORT=5001 bun run --hot src/index.ts",
|
"devserver": "NODE_ENV=development PORT=5001 bun run --hot src/index.ts",
|
||||||
"dbstudio": "pnpm dlx prisma studio --browser none",
|
"dbstudio": "pnpm dlx prisma studio --browser none",
|
||||||
"dbdeploy": "pnpm dlx prisma migrate deploy",
|
"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"
|
"start": "NODE_ENV=production PORT=5001 bun run src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { prisma } from "./db/db";
|
import { prisma } from "./db/db";
|
||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { simpleApi } from "./games/simple";
|
||||||
|
|
||||||
const api = new Elysia({ prefix: "/api" })
|
const api = new Elysia({ prefix: "/api" })
|
||||||
// [wip]
|
// [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 default api;
|
||||||
export type Api = typeof 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" };
|
| { kind: "joker"; color: "red" | "black" };
|
||||||
|
|
||||||
export type Pile = Card[];
|
export type vCard = Card | null | number;
|
||||||
export type Stack = Card[];
|
export type Pile<C extends vCard = Card> = C[];
|
||||||
export type Hand = Card[];
|
export type Stack<C extends vCard = Card> = C[];
|
||||||
export type Board = Card[];
|
export type Hand<C extends vCard = Card> = C[];
|
||||||
|
export type Board<C extends vCard = Card> = C[];
|
||||||
|
|
||||||
export const newDeck = (withJokers = false): Pile =>
|
export const newDeck = (withJokers = false): Pile =>
|
||||||
suits
|
suits
|
||||||
@@ -55,13 +56,3 @@ export const shuffle = (cards: Card[]) => {
|
|||||||
}
|
}
|
||||||
return cards;
|
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
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
object-hash: ^3.0.0
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.: {}
|
.:
|
||||||
|
devDependencies:
|
||||||
|
'@types/object-hash':
|
||||||
|
specifier: ^3.0.6
|
||||||
|
version: 3.0.6
|
||||||
|
|
||||||
packages/client:
|
packages/client:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -513,6 +520,9 @@ packages:
|
|||||||
'@types/normalize-package-data@2.4.4':
|
'@types/normalize-package-data@2.4.4':
|
||||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||||
|
|
||||||
|
'@types/object-hash@3.0.6':
|
||||||
|
resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==}
|
||||||
|
|
||||||
'@types/react@19.1.9':
|
'@types/react@19.1.9':
|
||||||
resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==}
|
resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==}
|
||||||
|
|
||||||
@@ -1447,6 +1457,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/normalize-package-data@2.4.4': {}
|
'@types/normalize-package-data@2.4.4': {}
|
||||||
|
|
||||||
|
'@types/object-hash@3.0.6': {}
|
||||||
|
|
||||||
'@types/react@19.1.9':
|
'@types/react@19.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|||||||
Reference in New Issue
Block a user