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