[wip] configs are synced but gameplay is broken
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
14
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user