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()) }),
}),