[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

@@ -11,6 +11,4 @@ const { api } = treaty<Api>(
export default api;
export const fromWebsocket = <T>(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);

View File

@@ -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 (
<For each={Array(props.handCount)}>
{(_, i) => {
const midOffset = i() + 0.5 - props.handCount / 2;
return (
<Card
face="down"
scale={0.4}
style={{
"margin-left": "-12px",
"margin-right": "-12px",
transform: `translate(0px, ${Math.pow(
Math.abs(midOffset),
2
)}px) rotate(${midOffset * 0.12}rad)`,
"min-width": "40px",
"box-shadow": "-4px 4px 6px rgba(0, 0, 0, 0.6)",
}}
/>
);
}}
</For>
<div class={props.class} style={props.style}>
<For each={Array(props.handCount)}>
{(_, i) => {
const midOffset = i() + 0.5 - props.handCount / 2;
return (
<Card
face="down"
scale={0.4}
style={{
"margin-left": "-12px",
"margin-right": "-12px",
transform: `translate(0px, ${Math.pow(
Math.abs(midOffset),
2
)}px) rotate(${midOffset * 0.12}rad)`,
"min-width": "40px",
"box-shadow": "-4px 4px 6px rgba(0, 0, 0, 0.6)",
}}
/>
);
}}
</For>
</div>
);
};

View File

@@ -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";

View File

@@ -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<TWsOut, any>;
sendWs: (msg: TWsIn) => void;
tableRef: HTMLDivElement;
gameConfig: Accessor<any>;
setGameConfig: Setter<any>;
players: PlayerStore;
setPlayers: SetStoreFunction<PlayerStore>;
@@ -42,19 +42,10 @@ export const TableContext = createContext<{
}>();
export default (props: { tableKey: string }) => {
// #region Websocket Setup
const wsPromise = new Promise<
ReturnType<ReturnType<typeof api.ws>["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<TWsOut>(ws)
);
onCleanup(() => wsPromise.then((ws) => ws.close()));
// #region Websocket declaration
const wsEvents = pool<TWsOut, any>();
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<any, any>,
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<TWsOut>(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 (
<TableContext.Provider
value={{
wsEvents,
sendWs,
tableRef,
players,
setPlayers,
gameConfig,
setGameConfig,
view,
}}
>
@@ -149,7 +154,7 @@ export default (props: { tableKey: string }) => {
{/* The table body itself */}
<div
id="table"
ref={tableRef}
class={cx(
"fixed",

View File

@@ -1,114 +1,152 @@
import { Accessor, createContext, For, Show, useContext } from "solid-js";
import type {
SimpleAction,
SimplePlayerView,
SimpleResult,
} 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 { createObservable, extractProperty } from "../../fn";
import FannedHand from "../FannedHand";
import Hand from "../Hand";
import Pile from "../Pile";
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 () => {
const table = useContext(TableContext)!;
const view = table.view as Accessor<SimplePlayerView>;
createEffect(() => console.log(table.gameConfig()));
const Configuration = () => (
<Show when={view() == null}>
<Portal mount={table.tableRef}>
<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,
})
}
/>
<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}>
{/* Main pile in the middle of the table */}
<Pile
count={view().deckCount}
scale={0.8}
class="cursor-pointer fixed center"
onClick={() => submitAction({ type: "draw" })}
/>
{/* Your own hand */}
<Hand
class="fixed bc"
hand={view().myHand}
onClickCard={(card) => submitAction({ type: "discard", card })}
/>
{/* Other players' hands */}
<For
each={Object.entries(view().playerHandCounts).filter(
([key, _]) => key in table.players
)}
>
{([playerKey, handCount], i) => (
<Portal
mount={table.players[playerKey].ref}
ref={(ref) => {
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%;`;
}}
>
<FannedHand handCount={handCount} />
</Portal>
)}
</For>
{/* Turn indicator */}
<div
class="absolute tc text-align-center"
style={{
"background-color":
view().playerTurn == me() ? "var(--yellow)" : "transparent",
color: view().playerTurn == me() ? "var(--dark)" : "var(--light)",
}}
>
It's{" "}
<span class="font-bold">
{view().playerTurn == me()
? "your"
: table.players[view().playerTurn].name + "'s"}
</span>{" "}
turn
</div>
{/* Quit button */}
<button
class="button fixed tl m-4 p-1"
onClick={() => {
table.sendWs({ quit: true });
}}
>
Quit
</button>
</Show>
);
const results = table.wsEvents
.thru(extractProperty("results"))
.thru(createObservable);
const Results = () => (
<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">
{table.players[results()!].name} won!
</span>
</Show>
);
return (
<GameContext.Provider value={{ view, submitAction }}>
{/* Configuration */}
<Show when={view() == null}>
<div>Configuration!</div>
</Show>
{/* Active game */}
<Show when={view() != null}>
{/* Main pile in the middle of the table */}
<Pile
count={view().deckCount}
scale={0.8}
class="cursor-pointer fixed center"
onClick={() => submitAction({ type: "draw" })}
/>
{/* Your own hand */}
<Hand
class="fixed bc"
hand={view().myHand}
onClickCard={(card) => submitAction({ type: "discard", card })}
/>
{/* Other players' hands */}
<For
each={Object.entries(view().playerHandCounts).filter(
([key, _]) => key in table.players
)}
>
{([playerKey, handCount], i) => (
<Portal
mount={table.players[playerKey].ref}
ref={(ref) => {
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%;`;
}}
>
<FannedHand handCount={handCount} />
</Portal>
)}
</For>
{/* Turn indicator */}
<div
class="absolute tc text-align-center"
style={{
"background-color":
view().playerTurn == me() ? "var(--yellow)" : "transparent",
color: view().playerTurn == me() ? "var(--dark)" : "var(--light)",
}}
>
It's{" "}
<span class="font-bold">
{view().playerTurn == me()
? "your"
: table.players[view().playerTurn].name + "'s"}
</span>{" "}
turn
</div>
{/* Quit button */}
<button
class="button fixed tl m-4 p-1"
onClick={() => {
table.sendWs({ quit: true });
}}
>
Quit
</button>
</Show>
{/* Results */}
<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">
{table.players[results()!].name} won!
</span>
</Show>
</GameContext.Provider>
<>
<Configuration />
<ActiveGame />
<Results />
</>
);
};

View File

@@ -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<T, P>
)
.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;
};