[wip] configs are synced but gameplay is broken
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/eden": "^1.3.2",
|
||||
"@solid-primitives/memo": "^1.4.3",
|
||||
"@solid-primitives/scheduled": "^1.5.2",
|
||||
"@solid-primitives/storage": "^4.3.3",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -26,6 +26,9 @@ export const WsOut = t.Union([
|
||||
export type TWsOut = typeof WsOut.static;
|
||||
export const WsIn = t.Union([
|
||||
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({ action: t.Any() }),
|
||||
t.Object({ quit: t.Literal(true) }),
|
||||
@@ -138,8 +141,14 @@ export const liveTable = <
|
||||
});
|
||||
});
|
||||
|
||||
const { name, ready, action, quit } = partition(
|
||||
["name", "ready", "action", "quit"],
|
||||
const {
|
||||
name,
|
||||
ready,
|
||||
action,
|
||||
quit,
|
||||
gameConfig: clientGameConfigs,
|
||||
} = partition(
|
||||
["name", "ready", "action", "quit", "gameConfig"],
|
||||
messages
|
||||
) as unknown as {
|
||||
// yuck
|
||||
@@ -147,6 +156,7 @@ export const liveTable = <
|
||||
ready: Observable<Attributed & { ready: boolean }, any>;
|
||||
action: Observable<Attributed & { action: GameAction }, any>;
|
||||
quit: Observable<Attributed, any>;
|
||||
gameConfig: Observable<Attributed & { gameConfig: GameConfig }, any>;
|
||||
};
|
||||
|
||||
const gameEnds = quit.map((_) => null);
|
||||
@@ -265,13 +275,19 @@ export const liveTable = <
|
||||
players: [] as string[],
|
||||
},
|
||||
[
|
||||
playersPresent.filterBy(gameIsActive.map((active) => !active)),
|
||||
playersPresent.filterBy(gameIsActive.thru(invert)),
|
||||
(prev, players) => ({
|
||||
...prev,
|
||||
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>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user