really solid
This commit is contained in:
@@ -1,29 +1,21 @@
|
|||||||
import { createSignal, onMount, useContext } from "solid-js";
|
import { createSignal, onMount, useContext } from "solid-js";
|
||||||
import { createObservableWithInit } from "~/fn";
|
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";
|
||||||
|
|
||||||
export default (props: { playerKey: string } & Stylable) => {
|
export default (props: { playerKey: string } & Stylable) => {
|
||||||
const table = useContext(TableContext);
|
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"));
|
onMount(() => console.log("Player mounted"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={(e) => {
|
ref={(e) => table?.setPlayers(props.playerKey, "ref", e)}
|
||||||
if (table != null) table.playerRefs[props.playerKey] = e;
|
|
||||||
}}
|
|
||||||
style={{
|
style={{
|
||||||
...props.style,
|
...props.style,
|
||||||
"background-color": playerColor(props.playerKey),
|
"background-color": playerColor(props.playerKey),
|
||||||
...(playerReady() && table?.view() == null
|
...(table?.view() == null && table?.players[props.playerKey].ready
|
||||||
? {
|
? {
|
||||||
border: "10px solid green",
|
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`}
|
class={`${props.class} w-20 h-20 rounded-full flex justify-center items-center`}
|
||||||
>
|
>
|
||||||
<p style={{ "font-size": "1em" }}>
|
<p class="font-[1em] text-align-center">
|
||||||
{table?.playerNames[props.playerKey]}
|
{table?.players[props.playerKey].name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
onCleanup,
|
onCleanup,
|
||||||
Show,
|
Show,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { 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 {
|
||||||
@@ -24,16 +24,25 @@ import { me, mePromise, name } from "~/profile";
|
|||||||
import GAMES from "./games";
|
import GAMES from "./games";
|
||||||
import Player from "./Player";
|
import Player from "./Player";
|
||||||
|
|
||||||
|
type PlayerStore = Store<{
|
||||||
|
[key: string]: {
|
||||||
|
name: string;
|
||||||
|
ready: boolean;
|
||||||
|
ref?: HTMLDivElement;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
export const TableContext = createContext<{
|
export const TableContext = createContext<{
|
||||||
view: Accessor<any>;
|
|
||||||
sendWs: (msg: TWsIn) => void;
|
|
||||||
wsEvents: Stream<TWsOut, any>;
|
wsEvents: Stream<TWsOut, any>;
|
||||||
playerNames: Store<{ [key: string]: string }>;
|
sendWs: (msg: TWsIn) => void;
|
||||||
players: Accessor<string[]>;
|
|
||||||
playerRefs: { [key: string]: HTMLDivElement };
|
players: PlayerStore;
|
||||||
|
setPlayers: SetStoreFunction<PlayerStore>;
|
||||||
|
|
||||||
|
view: Accessor<any>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
export default (props: { tableKey: string }) => {
|
export default (props: { tableKey: string }) => {
|
||||||
|
// #region Websocket Setup
|
||||||
const wsPromise = new Promise<
|
const wsPromise = new Promise<
|
||||||
ReturnType<ReturnType<typeof api.ws>["subscribe"]>
|
ReturnType<ReturnType<typeof api.ws>["subscribe"]>
|
||||||
>((res) => {
|
>((res) => {
|
||||||
@@ -46,51 +55,82 @@ export default (props: { tableKey: string }) => {
|
|||||||
fromWebsocket<TWsOut>(ws)
|
fromWebsocket<TWsOut>(ws)
|
||||||
);
|
);
|
||||||
onCleanup(() => wsPromise.then((ws) => ws.close()));
|
onCleanup(() => wsPromise.then((ws) => ws.close()));
|
||||||
|
// #endregion
|
||||||
|
|
||||||
const gameConfig = extractProperty(wsEvents, "gameConfig").thru(
|
// #region inbound table properties
|
||||||
createObservable
|
const [players, setPlayers] = createStore<PlayerStore>({});
|
||||||
);
|
const playerKeys = () => Object.keys(players);
|
||||||
const view = extractProperty(wsEvents, "view").thru(createObservable);
|
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(
|
wsEvents
|
||||||
createObservable
|
.thru(extractProperty("playerNames"))
|
||||||
);
|
.onValue((P) =>
|
||||||
|
Object.entries(P).map(([player, name]) =>
|
||||||
|
setPlayers(player, "name", name)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const playerNames = createObservableStore(
|
wsEvents
|
||||||
extractProperty(wsEvents, "playerNames"),
|
.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);
|
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({ ready: ready() }));
|
||||||
|
|
||||||
createEffect(() => sendWs({ name: name() }));
|
createEffect(() => sendWs({ name: name() }));
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
const GamePicker = () => {
|
||||||
|
return (
|
||||||
|
<div class="absolute tc mt-8 flex gap-4">
|
||||||
|
<select>
|
||||||
|
<For each={Object.entries(games)}>
|
||||||
|
{([gameId]) => <option value={gameId}>{gameId}</option>}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
<button onClick={() => setReady((prev) => !prev)} class="button p-1 ">
|
||||||
|
{ready() ? "Not Ready" : "Ready"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContext.Provider
|
<TableContext.Provider
|
||||||
value={{
|
value={{
|
||||||
sendWs,
|
|
||||||
wsEvents,
|
wsEvents,
|
||||||
view,
|
sendWs,
|
||||||
playerNames,
|
|
||||||
players,
|
players,
|
||||||
playerRefs: {},
|
setPlayers,
|
||||||
|
view,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Player avatars around the table */}
|
{/* Player avatars around the table */}
|
||||||
<div class="flex justify-around p-t-14">
|
<div class="flex justify-around p-t-14">
|
||||||
<For each={players().filter((p) => p != me())}>
|
<For each={playerKeys().filter((p) => p != me())}>
|
||||||
{(player, i) => {
|
{(player, i) => {
|
||||||
const verticalOffset = () => {
|
const verticalOffset = () => {
|
||||||
const N = players().length - 1;
|
const N = playerKeys().length - 1;
|
||||||
const x = Math.abs((2 * i() + 1) / (N * 2) - 0.5);
|
const x = Math.abs((2 * i() + 1) / (N * 2) - 0.5);
|
||||||
const y = Math.sqrt(1 - x * x);
|
const y = Math.sqrt(1 - x * x);
|
||||||
return 1 - y;
|
return 1 - y;
|
||||||
@@ -131,19 +171,7 @@ export default (props: { tableKey: string }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={view() == null}>
|
<Show when={view() == null}>
|
||||||
<div class="absolute tc mt-8 flex gap-4">
|
<GamePicker />
|
||||||
<select>
|
|
||||||
<For each={Object.entries(games)}>
|
|
||||||
{([gameId, game]) => <option value={gameId}>{gameId}</option>}
|
|
||||||
</For>
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
onClick={() => setReady((prev) => !prev)}
|
|
||||||
class="button p-1 "
|
|
||||||
>
|
|
||||||
{ready() ? "Not Ready" : "Ready"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ 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 results = (
|
|
||||||
extractProperty(table.wsEvents, "results") as Property<string, any>
|
|
||||||
).thru(createObservable);
|
|
||||||
const submitAction = (action: SimpleAction) => table.sendWs({ action });
|
const submitAction = (action: SimpleAction) => table.sendWs({ action });
|
||||||
|
|
||||||
|
const results = table.wsEvents
|
||||||
|
.thru(extractProperty("results"))
|
||||||
|
.thru(createObservable);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GameContext.Provider value={{ view, submitAction }}>
|
<GameContext.Provider value={{ view, submitAction }}>
|
||||||
{/* Configuration */}
|
{/* Configuration */}
|
||||||
@@ -53,13 +54,13 @@ export default () => {
|
|||||||
|
|
||||||
{/* Other players' hands */}
|
{/* Other players' hands */}
|
||||||
<For
|
<For
|
||||||
each={Object.entries(view().playerHandCounts).filter(([key, _]) =>
|
each={Object.entries(view().playerHandCounts).filter(
|
||||||
table.players().includes(key)
|
([key, _]) => key in table.players
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{([playerKey, handCount], i) => (
|
{([playerKey, handCount], i) => (
|
||||||
<Portal
|
<Portal
|
||||||
mount={table.playerRefs[playerKey]}
|
mount={table.players[playerKey].ref}
|
||||||
ref={(ref) => {
|
ref={(ref) => {
|
||||||
console.log("Setting hand ref");
|
console.log("Setting hand ref");
|
||||||
const midOffset =
|
const midOffset =
|
||||||
@@ -86,7 +87,7 @@ export default () => {
|
|||||||
<span class="font-bold">
|
<span class="font-bold">
|
||||||
{view().playerTurn == me()
|
{view().playerTurn == me()
|
||||||
? "your"
|
? "your"
|
||||||
: table.playerNames[view().playerTurn] + "'s"}
|
: table.players[view().playerTurn].name + "'s"}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
turn
|
turn
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +106,7 @@ export default () => {
|
|||||||
{/* Results */}
|
{/* 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.playerNames[results()!]} won!
|
{table.players[results()!].name} won!
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</GameContext.Provider>
|
</GameContext.Provider>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Observable, Property } from "kefir";
|
import { Observable, Property, Stream } from "kefir";
|
||||||
import { Accessor, createSignal } from "solid-js";
|
import { Accessor, createSignal } from "solid-js";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
|
|
||||||
@@ -39,21 +39,27 @@ export const createObservableWithInit = <T>(
|
|||||||
|
|
||||||
export const cx = (...classes: string[]) => classes.join(" ");
|
export const cx = (...classes: string[]) => classes.join(" ");
|
||||||
|
|
||||||
export const createObservableStore = <T extends object = {}>(
|
export const createObservableStore =
|
||||||
obs: Observable<T, any>,
|
<T extends object = {}>(init: T) =>
|
||||||
init: T
|
(obs: Observable<T, any>) => {
|
||||||
) => {
|
const [store, setStore] = createStore<T>(init);
|
||||||
const [store, setStore] = createStore<T>(init);
|
obs.onValue((val) => setStore(val));
|
||||||
obs.onValue((val) => setStore(val));
|
return store;
|
||||||
return store;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
type UnionKeys<T> = T extends any ? keyof T : never;
|
type UnionKeys<T> = T extends any ? keyof T : never;
|
||||||
export const extractProperty = <T extends object, P extends UnionKeys<T>>(
|
type ExtractPropertyType<T, P extends string | number | symbol> = T extends {
|
||||||
obs: Observable<T, any>,
|
[K in P]: any;
|
||||||
property: P
|
}
|
||||||
): Property<T[P], any> =>
|
? T[P]
|
||||||
obs
|
: never;
|
||||||
.filter((o) => property in o)
|
|
||||||
.map((o) => o[property]!)
|
export const extractProperty =
|
||||||
.toProperty();
|
<T extends object, P extends UnionKeys<T>>(property: P) =>
|
||||||
|
(obs: Observable<T, any>): Property<ExtractPropertyType<T, P>, any> =>
|
||||||
|
obs
|
||||||
|
.filter((o) => property in o)
|
||||||
|
.map(
|
||||||
|
(o) => (o as { [K in P]: any })[property] as ExtractPropertyType<T, P>
|
||||||
|
)
|
||||||
|
.toProperty();
|
||||||
|
|||||||
@@ -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 { 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 mePromise = api.whoami.post().then((r) => r.data);
|
||||||
export const [me] = createResource(() => mePromise);
|
export const [me] = createResource(() => mePromise);
|
||||||
createEffect(() => console.log(me()));
|
|
||||||
|
|
||||||
export const playerColor = (humanKey: string) =>
|
export const playerColor = (humanKey: string) =>
|
||||||
"#" + hash(humanKey).substring(0, 6);
|
"#" + hash(humanKey).substring(0, 6);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { log } from "./logging";
|
|||||||
export const WsOut = t.Union([
|
export const WsOut = t.Union([
|
||||||
t.Object({ playersPresent: t.Array(t.String()) }),
|
t.Object({ playersPresent: t.Array(t.String()) }),
|
||||||
t.Object({ playerNames: t.Record(t.String(), 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({
|
t.Object({
|
||||||
gameConfig: t.Object({ game: t.String(), players: t.Array(t.String()) }),
|
gameConfig: t.Object({ game: t.String(), players: t.Array(t.String()) }),
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user