diff --git a/package.json b/package.json index 90131cc..8ac3ace 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "games", "type": "module", - "version": "0.0.5", + "version": "0.0.6", "scripts": { "dev": "pnpm --parallel dev", "build": "pnpm run -F client build", diff --git a/pkg/client/src/components/Game.tsx b/pkg/client/src/components/Game.tsx index dcdd87c..2f4c3a3 100644 --- a/pkg/client/src/components/Game.tsx +++ b/pkg/client/src/components/Game.tsx @@ -2,7 +2,7 @@ import { Accessor, createContext, For, useContext } from "solid-js"; import type { SimpleAction, SimplePlayerView, -} from "@games/server/src/games/simple"; +} from "@games/shared/games/simple"; import { me, profile } from "~/profile"; import Hand from "./Hand"; import Pile from "./Pile"; @@ -44,7 +44,9 @@ export default () => { @@ -54,10 +56,7 @@ export default () => { mount={document.getElementById(`player-${playerKey}`)!} ref={(ref) => { const midOffset = - i() + - 0.5 - - Object.values(view().playerHandCounts).length / - 2; + i() + 0.5 - Object.values(view().playerHandCounts).length / 2; ref.style = `position: absolute; display: flex; justify-content: center; top: 65%; transform: translate(${Math.abs( midOffset * 0 diff --git a/pkg/client/src/components/Table.tsx b/pkg/client/src/components/Table.tsx index 7eed922..7453c72 100644 --- a/pkg/client/src/components/Table.tsx +++ b/pkg/client/src/components/Table.tsx @@ -37,11 +37,11 @@ export default (props: { tableKey: string }) => { ); onCleanup(() => wsPromise.then((ws) => ws.close())); - const presenceEvents = wsEvents.filter((evt) => evt.players != null); + const presenceEvents = wsEvents.filter((evt) => evt.playersPresent != null); const gameEvents = wsEvents.filter((evt) => evt.view !== undefined); const players = createObservableWithInit( - presenceEvents.map((evt) => evt.players!), + presenceEvents.map((evt) => evt.playersPresent!), [] ); diff --git a/pkg/server/src/api.ts b/pkg/server/src/api.ts index 943aaee..2ec4da6 100644 --- a/pkg/server/src/api.ts +++ b/pkg/server/src/api.ts @@ -80,14 +80,13 @@ const api = new Elysia({ prefix: "/api" }) body: WsIn, response: WsOut, - open: ({ + open({ data: { params: { tableKey }, humanKey, }, send, - }) => { - console.log("websocket opened"); + }) { const table = liveTable(tableKey); table.inputs.connectionChanges.emit({ @@ -103,7 +102,7 @@ const api = new Elysia({ prefix: "/api" }) ); }, - message: ( + message( { data: { humanKey, @@ -111,20 +110,25 @@ const api = new Elysia({ prefix: "/api" }) }, }, body - ) => liveTable(tableKey).inputs.messages.emit({ ...body, humanKey }), + ) { + liveTable(tableKey).inputs.messages.emit({ ...body, humanKey }); + }, - close: ({ + close({ data: { params: { tableKey }, humanKey, }, - }) => + }) { liveTable(tableKey).inputs.connectionChanges.emit({ humanKey, presence: "left", - }), + }); + }, - error: (error) => err(error), + error(error) { + err(error); + }, }); export default api; diff --git a/pkg/server/src/index.ts b/pkg/server/src/index.ts index adcb906..fed3ac3 100644 --- a/pkg/server/src/index.ts +++ b/pkg/server/src/index.ts @@ -16,7 +16,6 @@ new Elysia() }) ) - .onRequest(({ request }) => console.log(request.url)) .onError(({ error }) => console.error(error)) .get("/ping", () => "pong") @@ -25,4 +24,4 @@ new Elysia() .listen(port); -log.log(`server started on ${port}`); +console.log(`server started on ${port}`); diff --git a/pkg/server/src/table.ts b/pkg/server/src/table.ts index 421c3e4..1b61e70 100644 --- a/pkg/server/src/table.ts +++ b/pkg/server/src/table.ts @@ -13,8 +13,9 @@ import Bus, { type Bus as TBus } from "kefir-bus"; import { log } from "./logging"; export const WsOut = t.Object({ - players: t.Optional(t.Array(t.String())), + playersPresent: t.Optional(t.Array(t.String())), playersReady: t.Optional(t.Nullable(t.Record(t.String(), t.Boolean()))), + gameConfig: t.Optional(t.Any()), view: t.Optional(t.Any()), }); export type TWsOut = typeof WsOut.static; @@ -53,7 +54,7 @@ type TablePayload< }; player: { [key: string]: { - view: Property; + view: Property; }; }; }; @@ -69,7 +70,8 @@ export const liveTable = < players: string[]; }, GameState, - GameAction extends Attributed + GameAction extends Attributed, + GameView >( key: string ) => { @@ -98,11 +100,9 @@ export const liveTable = < .map((counts) => Object.keys(counts)) .toProperty(); - const playerStreams: TablePayload< - GameConfig, - GameState, - GameAction - >["outputs"]["player"] = {}; + const playerStreams: { + [key: string]: { view: Property }; + } = {}; playersPresent .map(set) .slidingWindow(2, 2) @@ -110,7 +110,12 @@ export const liveTable = < .onValue(({ added, removed }) => { added.forEach((p) => { playerStreams[p] = { - view: Bus(), + view: combine([gameState], [gameImpl], (a, b) => [a, b] as const) + .map( + ([state, game]) => + state && (game.getView({ state, humanKey: p }) as GameView) + ) + .toProperty(), }; }); removed.forEach((p) => { @@ -135,12 +140,12 @@ export const liveTable = < [key: string]: boolean; } | null, [ - playersPresent, + playersPresent, // TODO: filter to only outside active games (prev, players: ValueWithin) => Object.fromEntries(players.map((p) => [p, prev?.[p] ?? false])), ], [ - ready, + ready, // TODO: filter to only outside active games (prev, evt: ValueWithin) => prev?.[evt.humanKey] != null ? { @@ -155,9 +160,7 @@ export const liveTable = < (_, players: ValueWithin) => Object.fromEntries(players.map((p) => [p, false])), ] - ) - .toProperty() - .log("playersReady"); + ).toProperty(); const gameStarts = playersReady .filter( @@ -165,16 +168,9 @@ export const liveTable = < Object.values(pr ?? {}).length > 0 && Object.values(pr!).every((ready) => ready) ) - .map((_) => null) - .log("gameStarts"); + .map((_) => null); - const gameConfigPool = pool< - { - game: GameKey; - players: string[]; - }, - any - >(); + const gameConfigPool = pool(); const gameConfig = gameConfigPool.toProperty(); const gameImpl = gameConfig @@ -191,24 +187,19 @@ export const liveTable = < prev || (game.init() as GameState), ], [ - combine([action], [gameImpl], (action, impl) => ({ - action, - ...impl, - })), + combine([action], [gameImpl], (act, game) => [act, game] as const), ( prev, - { - game, - action, - }: { - game: Game; - action: Attributed & GameAction; - } + [{ action, humanKey }, game]: [ + Attributed & { action: GameAction }, + Game + ] ) => prev && (game.resolveAction({ state: prev, action, + humanKey, }) as GameState), ], [quit, () => null] @@ -233,7 +224,7 @@ export const liveTable = < }), ] // TODO: Add player defined config changes - ) + ) as unknown as Observable ); tables[key] = { @@ -241,10 +232,10 @@ export const liveTable = < outputs: { global: { playersPresent, - playersReady: playersReady.toProperty(), - gameConfig: gameConfig as Property, + playersReady, + gameConfig, }, - player: {}, + player: playerStreams, }, }; diff --git a/pkg/shared/games/index.ts b/pkg/shared/games/index.ts index 5da0d32..bfa5093 100644 --- a/pkg/shared/games/index.ts +++ b/pkg/shared/games/index.ts @@ -2,24 +2,24 @@ import simple from "./simple"; export type Game< S = unknown, - A extends { humanKey: string } = { humanKey: string }, + A = unknown, E extends { error: any } = { error: any }, V = unknown > = { title: string; rules: string; init: () => S; - resolveAction: (p: { state: S; action: A }) => S | E; + resolveAction: (p: { state: S; action: A; humanKey: string }) => S | E; getView: (p: { state: S; humanKey: string }) => V; resolveQuit: (p: { state: S; humanKey: string }) => S; }; -export const GAMES = { +export const GAMES: { + [key: string]: (config: { game: string; players: string[] }) => Game; +} = { // renaissance, simple, -} satisfies { - [key: string]: (config: { game: string; players: string[] }) => Game; }; export default GAMES; -export type GameKey = keyof typeof GAMES; +export type GameKey = string; diff --git a/pkg/shared/games/simple.ts b/pkg/shared/games/simple.ts index 7d80c93..712dd0f 100644 --- a/pkg/shared/games/simple.ts +++ b/pkg/shared/games/simple.ts @@ -22,10 +22,7 @@ export type SimplePlayerView = { myHand: Hand; }; -export type SimpleAction = { humanKey: string } & ( - | { type: "draw" } - | { type: "discard"; card: Card } -); +export type SimpleAction = { type: "draw" } | { type: "discard"; card: Card }; export const newSimpleGameState = ( config: SimpleConfiguration @@ -34,9 +31,7 @@ export const newSimpleGameState = ( return { deck: shuffle(newDeck()), turnIdx: 0, - playerHands: Object.fromEntries( - players.map((humanKey) => [humanKey, []]) - ), + playerHands: Object.fromEntries(players.map((humanKey) => [humanKey, []])), }; }; @@ -59,12 +54,13 @@ export const resolveSimpleAction = ({ config, state, action, + humanKey, }: { config: SimpleConfiguration; state: SimpleGameState; action: SimpleAction; + humanKey: string; }): SimpleGameState => { - const { humanKey } = action; const playerHand = state.playerHands[humanKey]; if (playerHand == null) { throw new Error(