From ae6a79aaddf6778654ddca2641aa760023dd33ae Mon Sep 17 00:00:00 2001 From: Daniel McCrystal Date: Sun, 7 Sep 2025 22:56:56 -0400 Subject: [PATCH] [wip] configs are synced but gameplay is broken --- pkg/client/package.json | 1 + pkg/client/src/api.ts | 4 +- pkg/client/src/components/FannedHand.tsx | 47 +++-- pkg/client/src/components/Player.tsx | 3 +- pkg/client/src/components/Table.tsx | 65 +++--- pkg/client/src/components/games/simple.tsx | 222 ++++++++++++--------- pkg/client/src/fn.ts | 13 +- pkg/server/src/table.ts | 24 ++- pnpm-lock.yaml | 14 ++ 9 files changed, 239 insertions(+), 154 deletions(-) diff --git a/pkg/client/package.json b/pkg/client/package.json index 48c2fe9..ca96d39 100644 --- a/pkg/client/package.json +++ b/pkg/client/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@elysiajs/eden": "^1.3.2", + "@solid-primitives/memo": "^1.4.3", "@solid-primitives/scheduled": "^1.5.2", "@solid-primitives/storage": "^4.3.3", "@solidjs/router": "^0.15.3", diff --git a/pkg/client/src/api.ts b/pkg/client/src/api.ts index ecc177a..1c4cccd 100644 --- a/pkg/client/src/api.ts +++ b/pkg/client/src/api.ts @@ -11,6 +11,4 @@ const { api } = treaty( export default api; export const fromWebsocket = (ws: any) => - fromEvents(ws, "message").map( - (evt) => (evt as unknown as { data: T }).data - ); + fromEvents(ws, "message").map((evt) => (evt as unknown as { data: T }).data); diff --git a/pkg/client/src/components/FannedHand.tsx b/pkg/client/src/components/FannedHand.tsx index 48df972..838a361 100644 --- a/pkg/client/src/components/FannedHand.tsx +++ b/pkg/client/src/components/FannedHand.tsx @@ -1,29 +1,32 @@ import type { Hand } from "@games/shared/cards"; import { For } from "solid-js"; import Card from "./Card"; +import { Stylable } from "./toolbox"; -export default (props: { handCount: number }) => { +export default (props: { handCount: number } & Stylable) => { return ( - - {(_, i) => { - const midOffset = i() + 0.5 - props.handCount / 2; - return ( - - ); - }} - +
+ + {(_, i) => { + const midOffset = i() + 0.5 - props.handCount / 2; + return ( + + ); + }} + +
); }; diff --git a/pkg/client/src/components/Player.tsx b/pkg/client/src/components/Player.tsx index 498aea6..e4a21ec 100644 --- a/pkg/client/src/components/Player.tsx +++ b/pkg/client/src/components/Player.tsx @@ -1,5 +1,4 @@ -import { createSignal, onMount, useContext } from "solid-js"; -import { createObservableWithInit, extractProperty } from "~/fn"; +import { onMount, useContext } from "solid-js"; import { playerColor } from "~/profile"; import { TableContext } from "./Table"; import { Stylable } from "./toolbox"; diff --git a/pkg/client/src/components/Table.tsx b/pkg/client/src/components/Table.tsx index 7a45fdd..770ae44 100644 --- a/pkg/client/src/components/Table.tsx +++ b/pkg/client/src/components/Table.tsx @@ -1,6 +1,6 @@ import type { TWsIn, TWsOut } from "@games/server/src/table"; import games from "@games/shared/games/index"; -import { fromPromise, Stream } from "kefir"; +import { fromPromise, pool, Property, Stream } from "kefir"; import { Accessor, createContext, @@ -8,19 +8,15 @@ import { createSignal, For, onCleanup, + onMount, + Setter, Show, } from "solid-js"; import { createStore, SetStoreFunction, Store } from "solid-js/store"; import { Dynamic } from "solid-js/web"; import api, { fromWebsocket } from "~/api"; -import { - createObservable, - createObservableStore, - createObservableWithInit, - cx, - extractProperty, -} from "~/fn"; -import { me, mePromise, name } from "~/profile"; +import { createObservable, createSynced, cx, extractProperty } from "~/fn"; +import { me, name } from "~/profile"; import GAMES from "./games"; import Player from "./Player"; @@ -35,6 +31,10 @@ export const TableContext = createContext<{ wsEvents: Stream; sendWs: (msg: TWsIn) => void; + tableRef: HTMLDivElement; + + gameConfig: Accessor; + setGameConfig: Setter; players: PlayerStore; setPlayers: SetStoreFunction; @@ -42,19 +42,10 @@ export const TableContext = createContext<{ }>(); export default (props: { tableKey: string }) => { - // #region Websocket Setup - const wsPromise = new Promise< - ReturnType["subscribe"]> - >((res) => { - const ws = api.ws(props).subscribe(); - ws.on("open", () => res(ws)); - ws.on("error", () => res(ws)); - }); - const sendWs = (msg: TWsIn) => wsPromise.then((ws) => ws.send(msg)); - const wsEvents = fromPromise(wsPromise).flatMap((ws) => - fromWebsocket(ws) - ); - onCleanup(() => wsPromise.then((ws) => ws.close())); + // #region Websocket declaration + const wsEvents = pool(); + let sendWs: (msg: TWsIn) => void = () => {}; + // #endregion // #region inbound table properties @@ -87,18 +78,26 @@ export default (props: { tableKey: string }) => { // #endregion // #region inbound game properties - const gameConfig = wsEvents - .thru(extractProperty("gameConfig")) - .thru(createObservable); + const [gameConfig, setGameConfig] = createSynced({ + ws: wsEvents.thru(extractProperty("gameConfig")) as Property, + sendWs: (gameConfig) => sendWs({ gameConfig }), + }); const view = wsEvents.thru(extractProperty("view")).thru(createObservable); // #endregion - // #region outbound signals const [ready, setReady] = createSignal(false); - createEffect(() => sendWs({ ready: ready() })); - createEffect(() => sendWs({ name: name() })); - // #endregion + onMount(() => { + const ws = api.ws(props).subscribe(); + ws.on("open", () => { + wsEvents.plug(fromWebsocket(ws)); + sendWs = ws.send.bind(ws); + + createEffect(() => sendWs({ ready: ready() })); + createEffect(() => sendWs({ name: name() })); + }); + onCleanup(() => ws.close()); + }); const GamePicker = () => { return ( @@ -115,13 +114,19 @@ export default (props: { tableKey: string }) => { ); }; + let tableRef!: HTMLDivElement; return ( @@ -149,7 +154,7 @@ export default (props: { tableKey: string }) => { {/* The table body itself */}
; - submitAction: (action: SimpleAction) => any; -}>(); export default () => { const table = useContext(TableContext)!; const view = table.view as Accessor; + createEffect(() => console.log(table.gameConfig())); + + const Configuration = () => ( + + +
+ + + table.setGameConfig({ + ...table.gameConfig(), + "can discard": evt.target.checked, + }) + } + /> + + + + table.setGameConfig({ + ...table.gameConfig(), + "cards to win": evt.target.value, + }) + } + /> +
+
+
+ ); + const submitAction = (action: SimpleAction) => table.sendWs({ action }); + const ActiveGame = () => ( + + {/* Main pile in the middle of the table */} + submitAction({ type: "draw" })} + /> + + {/* Your own hand */} + submitAction({ type: "discard", card })} + /> + + {/* Other players' hands */} + key in table.players + )} + > + {([playerKey, handCount], i) => ( + { + console.log("Setting hand ref"); + const midOffset = + i() + 0.5 - Object.values(view().playerHandCounts).length / 2; + + ref.style = `position: absolute; display: flex; justify-content: center; top: 65%;`; + }} + > + + + )} + + + {/* Turn indicator */} +
+ It's{" "} + + {view().playerTurn == me() + ? "your" + : table.players[view().playerTurn].name + "'s"} + {" "} + turn +
+ + {/* Quit button */} + +
+ ); const results = table.wsEvents .thru(extractProperty("results")) .thru(createObservable); + const Results = () => ( + + + {table.players[results()!].name} won! + + + ); return ( - - {/* Configuration */} - -
Configuration!
-
- - {/* Active game */} - - {/* Main pile in the middle of the table */} - submitAction({ type: "draw" })} - /> - - {/* Your own hand */} - submitAction({ type: "discard", card })} - /> - - {/* Other players' hands */} - key in table.players - )} - > - {([playerKey, handCount], i) => ( - { - console.log("Setting hand ref"); - const midOffset = - i() + 0.5 - Object.values(view().playerHandCounts).length / 2; - - ref.style = `position: absolute; display: flex; justify-content: center; top: 65%;`; - }} - > - - - )} - - - {/* Turn indicator */} -
- It's{" "} - - {view().playerTurn == me() - ? "your" - : table.players[view().playerTurn].name + "'s"} - {" "} - turn -
- - {/* Quit button */} - -
- - {/* Results */} - - - {table.players[results()!].name} won! - - -
+ <> + + + + ); }; diff --git a/pkg/client/src/fn.ts b/pkg/client/src/fn.ts index 90d14c7..347c699 100644 --- a/pkg/client/src/fn.ts +++ b/pkg/client/src/fn.ts @@ -1,5 +1,6 @@ +import { createLatest } from "@solid-primitives/memo"; import { Observable, Property, Stream } from "kefir"; -import { Accessor, createSignal } from "solid-js"; +import { Accessor, createEffect, createSignal } from "solid-js"; import { createStore } from "solid-js/store"; declare global { @@ -63,3 +64,13 @@ export const extractProperty = (o) => (o as { [K in P]: any })[property] as ExtractPropertyType ) .toProperty(); + +export const createSynced = (p: { + ws: Stream; + sendWs: (o: T) => void; +}) => { + const [local, setLocal] = createSignal(); + const remote = createObservable(p.ws.toProperty()); + createEffect(() => local() !== undefined && p.sendWs(local()!)); + return [createLatest([local, remote]), setLocal] as const; +}; diff --git a/pkg/server/src/table.ts b/pkg/server/src/table.ts index fde1a04..c13e7a2 100644 --- a/pkg/server/src/table.ts +++ b/pkg/server/src/table.ts @@ -26,6 +26,9 @@ export const WsOut = t.Union([ export type TWsOut = typeof WsOut.static; export const WsIn = t.Union([ t.Object({ name: t.String() }), + t.Object({ + gameConfig: t.Object({ game: t.String(), players: t.Array(t.String()) }), + }), t.Object({ ready: t.Boolean() }), t.Object({ action: t.Any() }), t.Object({ quit: t.Literal(true) }), @@ -138,8 +141,14 @@ export const liveTable = < }); }); - const { name, ready, action, quit } = partition( - ["name", "ready", "action", "quit"], + const { + name, + ready, + action, + quit, + gameConfig: clientGameConfigs, + } = partition( + ["name", "ready", "action", "quit", "gameConfig"], messages ) as unknown as { // yuck @@ -147,6 +156,7 @@ export const liveTable = < ready: Observable; action: Observable; quit: Observable; + gameConfig: Observable; }; const gameEnds = quit.map((_) => null); @@ -265,13 +275,19 @@ export const liveTable = < players: [] as string[], }, [ - playersPresent.filterBy(gameIsActive.map((active) => !active)), + playersPresent.filterBy(gameIsActive.thru(invert)), (prev, players) => ({ ...prev, players, }), + ], + [ + clientGameConfigs + // .filterBy(gameIsActive.thru(invert)) + .map(({ gameConfig }) => gameConfig), + // @ts-ignore + (prev, config) => ({ ...config, players: prev.players }), ] - // TODO: Add player defined config changes ) as unknown as Observable ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d409c16..a865d55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@elysiajs/eden': specifier: ^1.3.2 version: 1.3.3(elysia@1.3.20(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(typescript@5.9.2)) + '@solid-primitives/memo': + specifier: ^1.4.3 + version: 1.4.3(solid-js@1.9.9) '@solid-primitives/scheduled': specifier: ^1.5.2 version: 1.5.2(solid-js@1.9.9) @@ -593,6 +596,11 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@solid-primitives/memo@1.4.3': + resolution: {integrity: sha512-CA+n9yaoqbYm+My5tY2RWb6EE16tVyehM4GzwQF4vCwvjYPAYk1JSRIVuMC0Xuj5ExD2XQJE5E2yAaKY2HTUsg==} + peerDependencies: + solid-js: ^1.6.12 + '@solid-primitives/scheduled@1.5.2': resolution: {integrity: sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA==} peerDependencies: @@ -2833,6 +2841,12 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@solid-primitives/memo@1.4.3(solid-js@1.9.9)': + dependencies: + '@solid-primitives/scheduled': 1.5.2(solid-js@1.9.9) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.9) + solid-js: 1.9.9 + '@solid-primitives/scheduled@1.5.2(solid-js@1.9.9)': dependencies: solid-js: 1.9.9