[wip] configs are synced but gameplay is broken

This commit is contained in:
2025-09-07 22:56:56 -04:00
parent 46002403c8
commit ae6a79aadd
9 changed files with 239 additions and 154 deletions

View File

@@ -8,6 +8,7 @@
}, },
"dependencies": { "dependencies": {
"@elysiajs/eden": "^1.3.2", "@elysiajs/eden": "^1.3.2",
"@solid-primitives/memo": "^1.4.3",
"@solid-primitives/scheduled": "^1.5.2", "@solid-primitives/scheduled": "^1.5.2",
"@solid-primitives/storage": "^4.3.3", "@solid-primitives/storage": "^4.3.3",
"@solidjs/router": "^0.15.3", "@solidjs/router": "^0.15.3",

View File

@@ -11,6 +11,4 @@ const { api } = treaty<Api>(
export default api; export default api;
export const fromWebsocket = <T>(ws: any) => export const fromWebsocket = <T>(ws: any) =>
fromEvents(ws, "message").map( fromEvents(ws, "message").map((evt) => (evt as unknown as { data: T }).data);
(evt) => (evt as unknown as { data: T }).data
);

View File

@@ -1,9 +1,11 @@
import type { Hand } from "@games/shared/cards"; import type { Hand } from "@games/shared/cards";
import { For } from "solid-js"; import { For } from "solid-js";
import Card from "./Card"; import Card from "./Card";
import { Stylable } from "./toolbox";
export default (props: { handCount: number }) => { export default (props: { handCount: number } & Stylable) => {
return ( return (
<div class={props.class} style={props.style}>
<For each={Array(props.handCount)}> <For each={Array(props.handCount)}>
{(_, i) => { {(_, i) => {
const midOffset = i() + 0.5 - props.handCount / 2; const midOffset = i() + 0.5 - props.handCount / 2;
@@ -25,5 +27,6 @@ export default (props: { handCount: number }) => {
); );
}} }}
</For> </For>
</div>
); );
}; };

View File

@@ -1,5 +1,4 @@
import { createSignal, onMount, useContext } from "solid-js"; import { onMount, useContext } from "solid-js";
import { createObservableWithInit, extractProperty } from "~/fn";
import { playerColor } from "~/profile"; import { playerColor } from "~/profile";
import { TableContext } from "./Table"; import { TableContext } from "./Table";
import { Stylable } from "./toolbox"; import { Stylable } from "./toolbox";

View File

@@ -1,6 +1,6 @@
import type { TWsIn, TWsOut } from "@games/server/src/table"; import type { TWsIn, TWsOut } from "@games/server/src/table";
import games from "@games/shared/games/index"; import games from "@games/shared/games/index";
import { fromPromise, Stream } from "kefir"; import { fromPromise, pool, Property, Stream } from "kefir";
import { import {
Accessor, Accessor,
createContext, createContext,
@@ -8,19 +8,15 @@ import {
createSignal, createSignal,
For, For,
onCleanup, onCleanup,
onMount,
Setter,
Show, Show,
} from "solid-js"; } from "solid-js";
import { createStore, SetStoreFunction, Store } from "solid-js/store"; import { createStore, SetStoreFunction, Store } from "solid-js/store";
import { Dynamic } from "solid-js/web"; import { Dynamic } from "solid-js/web";
import api, { fromWebsocket } from "~/api"; import api, { fromWebsocket } from "~/api";
import { import { createObservable, createSynced, cx, extractProperty } from "~/fn";
createObservable, import { me, name } from "~/profile";
createObservableStore,
createObservableWithInit,
cx,
extractProperty,
} from "~/fn";
import { me, mePromise, name } from "~/profile";
import GAMES from "./games"; import GAMES from "./games";
import Player from "./Player"; import Player from "./Player";
@@ -35,6 +31,10 @@ export const TableContext = createContext<{
wsEvents: Stream<TWsOut, any>; wsEvents: Stream<TWsOut, any>;
sendWs: (msg: TWsIn) => void; sendWs: (msg: TWsIn) => void;
tableRef: HTMLDivElement;
gameConfig: Accessor<any>;
setGameConfig: Setter<any>;
players: PlayerStore; players: PlayerStore;
setPlayers: SetStoreFunction<PlayerStore>; setPlayers: SetStoreFunction<PlayerStore>;
@@ -42,19 +42,10 @@ export const TableContext = createContext<{
}>(); }>();
export default (props: { tableKey: string }) => { export default (props: { tableKey: string }) => {
// #region Websocket Setup // #region Websocket declaration
const wsPromise = new Promise< const wsEvents = pool<TWsOut, any>();
ReturnType<ReturnType<typeof api.ws>["subscribe"]> let sendWs: (msg: TWsIn) => void = () => {};
>((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<TWsOut>(ws)
);
onCleanup(() => wsPromise.then((ws) => ws.close()));
// #endregion // #endregion
// #region inbound table properties // #region inbound table properties
@@ -87,18 +78,26 @@ export default (props: { tableKey: string }) => {
// #endregion // #endregion
// #region inbound game properties // #region inbound game properties
const gameConfig = wsEvents const [gameConfig, setGameConfig] = createSynced({
.thru(extractProperty("gameConfig")) ws: wsEvents.thru(extractProperty("gameConfig")) as Property<any, any>,
.thru(createObservable); sendWs: (gameConfig) => sendWs({ gameConfig }),
});
const view = wsEvents.thru(extractProperty("view")).thru(createObservable); const view = wsEvents.thru(extractProperty("view")).thru(createObservable);
// #endregion // #endregion
// #region outbound signals
const [ready, setReady] = createSignal(false); const [ready, setReady] = createSignal(false);
createEffect(() => sendWs({ ready: ready() }));
onMount(() => {
const ws = api.ws(props).subscribe();
ws.on("open", () => {
wsEvents.plug(fromWebsocket<TWsOut>(ws));
sendWs = ws.send.bind(ws);
createEffect(() => sendWs({ ready: ready() }));
createEffect(() => sendWs({ name: name() })); createEffect(() => sendWs({ name: name() }));
// #endregion });
onCleanup(() => ws.close());
});
const GamePicker = () => { const GamePicker = () => {
return ( return (
@@ -115,13 +114,19 @@ export default (props: { tableKey: string }) => {
); );
}; };
let tableRef!: HTMLDivElement;
return ( return (
<TableContext.Provider <TableContext.Provider
value={{ value={{
wsEvents, wsEvents,
sendWs, sendWs,
tableRef,
players, players,
setPlayers, setPlayers,
gameConfig,
setGameConfig,
view, view,
}} }}
> >
@@ -149,7 +154,7 @@ export default (props: { tableKey: string }) => {
{/* The table body itself */} {/* The table body itself */}
<div <div
id="table" ref={tableRef}
class={cx( class={cx(
"fixed", "fixed",

View File

@@ -1,41 +1,68 @@
import { Accessor, createContext, For, Show, useContext } from "solid-js";
import type { import type {
SimpleAction, SimpleAction,
SimplePlayerView, SimplePlayerView,
SimpleResult,
} from "@games/shared/games/simple"; } from "@games/shared/games/simple";
import { Accessor, createEffect, For, Show, useContext } from "solid-js";
import { Portal } from "solid-js/web";
import { me } from "~/profile"; import { me } from "~/profile";
import { createObservable, extractProperty } from "../../fn";
import FannedHand from "../FannedHand";
import Hand from "../Hand"; import Hand from "../Hand";
import Pile from "../Pile"; import Pile from "../Pile";
import { TableContext } from "../Table"; import { TableContext } from "../Table";
import { Portal } from "solid-js/web";
import FannedHand from "../FannedHand";
import { createObservable, extractProperty } from "../../fn";
import { Property } from "kefir";
export const GameContext = createContext<{
view: Accessor<SimplePlayerView>;
submitAction: (action: SimpleAction) => any;
}>();
export default () => { export default () => {
const table = useContext(TableContext)!; const table = useContext(TableContext)!;
const view = table.view as Accessor<SimplePlayerView>; const view = table.view as Accessor<SimplePlayerView>;
const submitAction = (action: SimpleAction) => table.sendWs({ action }); createEffect(() => console.log(table.gameConfig()));
const results = table.wsEvents const Configuration = () => (
.thru(extractProperty("results"))
.thru(createObservable);
return (
<GameContext.Provider value={{ view, submitAction }}>
{/* Configuration */}
<Show when={view() == null}> <Show when={view() == null}>
<div>Configuration!</div> <Portal mount={table.tableRef}>
</Show> <div class="absolute center grid grid-cols-2 gap-col-2 text-xl">
<label for="allow discards" style={{ "text-align": "right" }}>
Allow discards
</label>
<input
type="checkbox"
id="allow discards"
style={{ width: "50px" }}
checked={table.gameConfig()?.["can discard"] ?? false}
onChange={(evt) =>
table.setGameConfig({
...table.gameConfig(),
"can discard": evt.target.checked,
})
}
/>
{/* Active game */} <label for="to win" style={{ "text-align": "right" }}>
Cards to win
</label>
<input
type="number"
id="to win"
style={{
"text-align": "center",
width: "50px",
color: "var(--yellow)",
}}
value={table.gameConfig()["cards to win"]}
onChange={(evt) =>
table.setGameConfig({
...table.gameConfig(),
"cards to win": evt.target.value,
})
}
/>
</div>
</Portal>
</Show>
);
const submitAction = (action: SimpleAction) => table.sendWs({ action });
const ActiveGame = () => (
<Show when={view() != null}> <Show when={view() != null}>
{/* Main pile in the middle of the table */} {/* Main pile in the middle of the table */}
<Pile <Pile
@@ -102,13 +129,24 @@ export default () => {
Quit Quit
</button> </button>
</Show> </Show>
);
{/* Results */} const results = table.wsEvents
.thru(extractProperty("results"))
.thru(createObservable);
const Results = () => (
<Show when={results() != null}> <Show when={results() != null}>
<span class="bg-[var(--light)] text-[var(--dark)] rounded-[24px] border-2 border-[var(--dark)] absolute center p-4 shadow-lg text-[4em] text-center"> <span class="bg-[var(--light)] text-[var(--dark)] rounded-[24px] border-2 border-[var(--dark)] absolute center p-4 shadow-lg text-[4em] text-center">
{table.players[results()!].name} won! {table.players[results()!].name} won!
</span> </span>
</Show> </Show>
</GameContext.Provider> );
return (
<>
<Configuration />
<ActiveGame />
<Results />
</>
); );
}; };

View File

@@ -1,5 +1,6 @@
import { createLatest } from "@solid-primitives/memo";
import { Observable, Property, Stream } from "kefir"; 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"; import { createStore } from "solid-js/store";
declare global { declare global {
@@ -63,3 +64,13 @@ export const extractProperty =
(o) => (o as { [K in P]: any })[property] as ExtractPropertyType<T, P> (o) => (o as { [K in P]: any })[property] as ExtractPropertyType<T, P>
) )
.toProperty(); .toProperty();
export const createSynced = <T>(p: {
ws: Stream<T, any>;
sendWs: (o: T) => void;
}) => {
const [local, setLocal] = createSignal<T>();
const remote = createObservable(p.ws.toProperty());
createEffect(() => local() !== undefined && p.sendWs(local()!));
return [createLatest([local, remote]), setLocal] as const;
};

View File

@@ -26,6 +26,9 @@ export const WsOut = t.Union([
export type TWsOut = typeof WsOut.static; export type TWsOut = typeof WsOut.static;
export const WsIn = t.Union([ export const WsIn = t.Union([
t.Object({ name: t.String() }), 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({ ready: t.Boolean() }),
t.Object({ action: t.Any() }), t.Object({ action: t.Any() }),
t.Object({ quit: t.Literal(true) }), t.Object({ quit: t.Literal(true) }),
@@ -138,8 +141,14 @@ export const liveTable = <
}); });
}); });
const { name, ready, action, quit } = partition( const {
["name", "ready", "action", "quit"], name,
ready,
action,
quit,
gameConfig: clientGameConfigs,
} = partition(
["name", "ready", "action", "quit", "gameConfig"],
messages messages
) as unknown as { ) as unknown as {
// yuck // yuck
@@ -147,6 +156,7 @@ export const liveTable = <
ready: Observable<Attributed & { ready: boolean }, any>; ready: Observable<Attributed & { ready: boolean }, any>;
action: Observable<Attributed & { action: GameAction }, any>; action: Observable<Attributed & { action: GameAction }, any>;
quit: Observable<Attributed, any>; quit: Observable<Attributed, any>;
gameConfig: Observable<Attributed & { gameConfig: GameConfig }, any>;
}; };
const gameEnds = quit.map((_) => null); const gameEnds = quit.map((_) => null);
@@ -265,13 +275,19 @@ export const liveTable = <
players: [] as string[], players: [] as string[],
}, },
[ [
playersPresent.filterBy(gameIsActive.map((active) => !active)), playersPresent.filterBy(gameIsActive.thru(invert)),
(prev, players) => ({ (prev, players) => ({
...prev, ...prev,
players, 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<GameConfig, any> ) as unknown as Observable<GameConfig, any>
); );

14
pnpm-lock.yaml generated
View File

@@ -20,6 +20,9 @@ importers:
'@elysiajs/eden': '@elysiajs/eden':
specifier: ^1.3.2 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)) 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': '@solid-primitives/scheduled':
specifier: ^1.5.2 specifier: ^1.5.2
version: 1.5.2(solid-js@1.9.9) version: 1.5.2(solid-js@1.9.9)
@@ -593,6 +596,11 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'} 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': '@solid-primitives/scheduled@1.5.2':
resolution: {integrity: sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA==} resolution: {integrity: sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA==}
peerDependencies: peerDependencies:
@@ -2833,6 +2841,12 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {} '@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)': '@solid-primitives/scheduled@1.5.2(solid-js@1.9.9)':
dependencies: dependencies:
solid-js: 1.9.9 solid-js: 1.9.9