From 46002403c8d1c116608df1eb28d17bcbf321ba08 Mon Sep 17 00:00:00 2001 From: Daniel McCrystal Date: Sat, 6 Sep 2025 23:22:58 -0400 Subject: [PATCH] really solid --- pkg/client/src/components/Player.tsx | 18 +--- pkg/client/src/components/Table.tsx | 118 +++++++++++++-------- pkg/client/src/components/games/simple.tsx | 17 +-- pkg/client/src/fn.ts | 40 ++++--- pkg/client/src/profile.ts | 8 +- pkg/server/src/table.ts | 2 +- 6 files changed, 114 insertions(+), 89 deletions(-) diff --git a/pkg/client/src/components/Player.tsx b/pkg/client/src/components/Player.tsx index 3dff113..498aea6 100644 --- a/pkg/client/src/components/Player.tsx +++ b/pkg/client/src/components/Player.tsx @@ -1,29 +1,21 @@ import { createSignal, onMount, useContext } from "solid-js"; -import { createObservableWithInit } from "~/fn"; +import { createObservableWithInit, extractProperty } from "~/fn"; import { playerColor } from "~/profile"; import { TableContext } from "./Table"; import { Stylable } from "./toolbox"; export default (props: { playerKey: string } & Stylable) => { const table = useContext(TableContext); - const playerReady = - table?.wsEvents - .filter((evt) => evt.playersReady != null) - .map((evt) => evt.playersReady![props.playerKey]) - .thru((Evt) => createObservableWithInit(Evt, false)) ?? - createSignal(false)[0]; onMount(() => console.log("Player mounted")); return (
{ - if (table != null) table.playerRefs[props.playerKey] = e; - }} + ref={(e) => table?.setPlayers(props.playerKey, "ref", e)} style={{ ...props.style, "background-color": playerColor(props.playerKey), - ...(playerReady() && table?.view() == null + ...(table?.view() == null && table?.players[props.playerKey].ready ? { border: "10px solid green", } @@ -31,8 +23,8 @@ export default (props: { playerKey: string } & Stylable) => { }} class={`${props.class} w-20 h-20 rounded-full flex justify-center items-center`} > -

- {table?.playerNames[props.playerKey]} +

+ {table?.players[props.playerKey].name}

); diff --git a/pkg/client/src/components/Table.tsx b/pkg/client/src/components/Table.tsx index e2e23eb..7a45fdd 100644 --- a/pkg/client/src/components/Table.tsx +++ b/pkg/client/src/components/Table.tsx @@ -10,7 +10,7 @@ import { onCleanup, Show, } from "solid-js"; -import { Store } from "solid-js/store"; +import { createStore, SetStoreFunction, Store } from "solid-js/store"; import { Dynamic } from "solid-js/web"; import api, { fromWebsocket } from "~/api"; import { @@ -24,16 +24,25 @@ import { me, mePromise, name } from "~/profile"; import GAMES from "./games"; import Player from "./Player"; +type PlayerStore = Store<{ + [key: string]: { + name: string; + ready: boolean; + ref?: HTMLDivElement; + }; +}>; export const TableContext = createContext<{ - view: Accessor; - sendWs: (msg: TWsIn) => void; wsEvents: Stream; - playerNames: Store<{ [key: string]: string }>; - players: Accessor; - playerRefs: { [key: string]: HTMLDivElement }; + sendWs: (msg: TWsIn) => void; + + players: PlayerStore; + setPlayers: SetStoreFunction; + + view: Accessor; }>(); export default (props: { tableKey: string }) => { + // #region Websocket Setup const wsPromise = new Promise< ReturnType["subscribe"]> >((res) => { @@ -46,51 +55,82 @@ export default (props: { tableKey: string }) => { fromWebsocket(ws) ); onCleanup(() => wsPromise.then((ws) => ws.close())); + // #endregion - const gameConfig = extractProperty(wsEvents, "gameConfig").thru( - createObservable - ); - const view = extractProperty(wsEvents, "view").thru(createObservable); + // #region inbound table properties + const [players, setPlayers] = createStore({}); + const playerKeys = () => Object.keys(players); + wsEvents + .thru(extractProperty("playersPresent")) + .onValue((P) => + P.filter((p) => !(p in players)).forEach((p) => + setPlayers(p, { name: "", ready: false }) + ) + ); - const players = extractProperty(wsEvents, "playersPresent").thru( - createObservable - ); + wsEvents + .thru(extractProperty("playerNames")) + .onValue((P) => + Object.entries(P).map(([player, name]) => + setPlayers(player, "name", name) + ) + ); - const playerNames = createObservableStore( - extractProperty(wsEvents, "playerNames"), - {} - ); + wsEvents + .thru(extractProperty("playersReady")) + .onValue((P) => + Object.entries(P).map(([player, ready]) => + setPlayers(player, "ready", ready) + ) + ); + // #endregion + + // #region inbound game properties + const gameConfig = wsEvents + .thru(extractProperty("gameConfig")) + .thru(createObservable); + const view = wsEvents.thru(extractProperty("view")).thru(createObservable); + // #endregion + + // #region outbound signals const [ready, setReady] = createSignal(false); - mePromise.then( - (me) => - me && - wsEvents - .filter((evt) => evt.playersReady !== undefined) - .map((evt) => evt.playersReady?.[me] ?? false) - .onValue(setReady) - ); - createEffect(() => sendWs({ ready: ready() })); + createEffect(() => sendWs({ name: name() })); + // #endregion + + const GamePicker = () => { + return ( +
+ + +
+ ); + }; return ( {/* Player avatars around the table */}
- p != me())}> + p != me())}> {(player, i) => { const verticalOffset = () => { - const N = players().length - 1; + const N = playerKeys().length - 1; const x = Math.abs((2 * i() + 1) / (N * 2) - 0.5); const y = Math.sqrt(1 - x * x); return 1 - y; @@ -131,19 +171,7 @@ export default (props: { tableKey: string }) => { }} > -
- - -
+
diff --git a/pkg/client/src/components/games/simple.tsx b/pkg/client/src/components/games/simple.tsx index 28aa804..69b7d21 100644 --- a/pkg/client/src/components/games/simple.tsx +++ b/pkg/client/src/components/games/simple.tsx @@ -22,11 +22,12 @@ export default () => { const table = useContext(TableContext)!; const view = table.view as Accessor; - const results = ( - extractProperty(table.wsEvents, "results") as Property - ).thru(createObservable); const submitAction = (action: SimpleAction) => table.sendWs({ action }); + const results = table.wsEvents + .thru(extractProperty("results")) + .thru(createObservable); + return ( {/* Configuration */} @@ -53,13 +54,13 @@ export default () => { {/* Other players' hands */} - table.players().includes(key) + each={Object.entries(view().playerHandCounts).filter( + ([key, _]) => key in table.players )} > {([playerKey, handCount], i) => ( { console.log("Setting hand ref"); const midOffset = @@ -86,7 +87,7 @@ export default () => { {view().playerTurn == me() ? "your" - : table.playerNames[view().playerTurn] + "'s"} + : table.players[view().playerTurn].name + "'s"} {" "} turn @@ -105,7 +106,7 @@ export default () => { {/* Results */} - {table.playerNames[results()!]} won! + {table.players[results()!].name} won! diff --git a/pkg/client/src/fn.ts b/pkg/client/src/fn.ts index cba4cef..90d14c7 100644 --- a/pkg/client/src/fn.ts +++ b/pkg/client/src/fn.ts @@ -1,4 +1,4 @@ -import { Observable, Property } from "kefir"; +import { Observable, Property, Stream } from "kefir"; import { Accessor, createSignal } from "solid-js"; import { createStore } from "solid-js/store"; @@ -39,21 +39,27 @@ export const createObservableWithInit = ( export const cx = (...classes: string[]) => classes.join(" "); -export const createObservableStore = ( - obs: Observable, - init: T -) => { - const [store, setStore] = createStore(init); - obs.onValue((val) => setStore(val)); - return store; -}; +export const createObservableStore = + (init: T) => + (obs: Observable) => { + const [store, setStore] = createStore(init); + obs.onValue((val) => setStore(val)); + return store; + }; type UnionKeys = T extends any ? keyof T : never; -export const extractProperty = >( - obs: Observable, - property: P -): Property => - obs - .filter((o) => property in o) - .map((o) => o[property]!) - .toProperty(); +type ExtractPropertyType = T extends { + [K in P]: any; +} + ? T[P] + : never; + +export const extractProperty = + >(property: P) => + (obs: Observable): Property, any> => + obs + .filter((o) => property in o) + .map( + (o) => (o as { [K in P]: any })[property] as ExtractPropertyType + ) + .toProperty(); diff --git a/pkg/client/src/profile.ts b/pkg/client/src/profile.ts index b6b2cf1..8444a41 100644 --- a/pkg/client/src/profile.ts +++ b/pkg/client/src/profile.ts @@ -1,12 +1,10 @@ -import { createEffect, createResource, createSignal, Resource } from "solid-js"; -import { ApiType } from "./fn"; -import api from "./api"; -import hash from "object-hash"; import { makePersisted } from "@solid-primitives/storage"; +import hash from "object-hash"; +import { createResource, createSignal } from "solid-js"; +import api from "./api"; export const mePromise = api.whoami.post().then((r) => r.data); export const [me] = createResource(() => mePromise); -createEffect(() => console.log(me())); export const playerColor = (humanKey: string) => "#" + hash(humanKey).substring(0, 6); diff --git a/pkg/server/src/table.ts b/pkg/server/src/table.ts index 37248d3..fde1a04 100644 --- a/pkg/server/src/table.ts +++ b/pkg/server/src/table.ts @@ -16,7 +16,7 @@ import { log } from "./logging"; export const WsOut = t.Union([ t.Object({ playersPresent: t.Array(t.String()) }), t.Object({ playerNames: t.Record(t.String(), t.String()) }), - t.Object({ playersReady: t.Nullable(t.Record(t.String(), t.Boolean())) }), + t.Object({ playersReady: t.Record(t.String(), t.Boolean()) }), t.Object({ gameConfig: t.Object({ game: t.String(), players: t.Array(t.String()) }), }),