better plumbing for games frontend
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import { createSignal, onMount, useContext } from "solid-js";
|
||||
import { createObservableWithInit } from "~/fn";
|
||||
import { playerColor } from "~/profile";
|
||||
import { TableContext } from "./Table";
|
||||
import { Stylable } from "./toolbox";
|
||||
import { createObservable, createObservableWithInit } from "~/fn";
|
||||
import { GameContext } from "./Game";
|
||||
|
||||
export default (props: { playerKey: string } & Stylable) => {
|
||||
const table = useContext(TableContext);
|
||||
@@ -14,8 +13,6 @@ export default (props: { playerKey: string } & Stylable) => {
|
||||
.thru((Evt) => createObservableWithInit(Evt, false)) ??
|
||||
createSignal(false)[0];
|
||||
|
||||
const game = useContext(GameContext);
|
||||
|
||||
onMount(() => console.log("Player mounted"));
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TWsIn, TWsOut } from "@games/server/src/table";
|
||||
import { fromPromise, merge, Stream } from "kefir";
|
||||
import games from "@games/shared/games/index";
|
||||
import { fromPromise, Stream } from "kefir";
|
||||
import {
|
||||
Accessor,
|
||||
createContext,
|
||||
@@ -9,19 +10,19 @@ import {
|
||||
onCleanup,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import { 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 } from "~/profile";
|
||||
import Simple from "./games/simple";
|
||||
import { me, mePromise, name } from "~/profile";
|
||||
import GAMES from "./games";
|
||||
import Player from "./Player";
|
||||
import games from "@games/shared/games/index";
|
||||
import { createStore, Store } from "solid-js/store";
|
||||
import { name } from "~/profile";
|
||||
|
||||
export const TableContext = createContext<{
|
||||
view: Accessor<any>;
|
||||
@@ -49,6 +50,11 @@ export default (props: { tableKey: string }) => {
|
||||
const presenceEvents = wsEvents.filter(
|
||||
(evt) => evt.playersPresent !== undefined
|
||||
);
|
||||
|
||||
const gameConfig = extractProperty(wsEvents, "gameConfig").thru(
|
||||
createObservable
|
||||
);
|
||||
|
||||
const gameEvents = wsEvents.filter((evt) => evt.view !== undefined);
|
||||
const resultEvents = wsEvents.filter((evt) => evt.results !== undefined);
|
||||
|
||||
@@ -77,16 +83,8 @@ export default (props: { tableKey: string }) => {
|
||||
createEffect(() => sendWs({ ready: ready() }));
|
||||
createEffect(() => sendWs({ name: name() }));
|
||||
const viewProp = gameEvents.map((evt) => evt.view).toProperty();
|
||||
viewProp.log();
|
||||
|
||||
const view = createObservable(viewProp);
|
||||
const results = createObservable<string>(
|
||||
merge([
|
||||
gameEvents
|
||||
.filter((evt) => "view" in evt && evt.view == null)
|
||||
.map(() => undefined),
|
||||
resultEvents.map((evt) => evt.results),
|
||||
])
|
||||
);
|
||||
|
||||
return (
|
||||
<TableContext.Provider
|
||||
@@ -98,6 +96,7 @@ export default (props: { tableKey: string }) => {
|
||||
players,
|
||||
}}
|
||||
>
|
||||
{/* Player avatars around the table */}
|
||||
<div class="flex justify-around p-t-14">
|
||||
<For each={players().filter((p) => p != me())}>
|
||||
{(player, i) => {
|
||||
@@ -118,6 +117,8 @@ export default (props: { tableKey: string }) => {
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* The table body itself */}
|
||||
<div
|
||||
id="table"
|
||||
class={cx(
|
||||
@@ -156,14 +157,15 @@ export default (props: { tableKey: string }) => {
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={view() != null}>
|
||||
<Simple />
|
||||
</Show>
|
||||
<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">
|
||||
{playerNames[results()!]} won!
|
||||
</span>
|
||||
</Show>
|
||||
|
||||
{/* The game being played */}
|
||||
<Dynamic
|
||||
component={
|
||||
gameConfig()?.game ?? "" in GAMES
|
||||
? GAMES[gameConfig()!.game as keyof typeof GAMES]
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</TableContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
5
pkg/client/src/components/games/index.ts
Normal file
5
pkg/client/src/components/games/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import simple from "./simple";
|
||||
|
||||
export default {
|
||||
simple,
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Accessor, createContext, For, useContext } from "solid-js";
|
||||
import { Accessor, createContext, For, Show, useContext } from "solid-js";
|
||||
import type {
|
||||
SimpleAction,
|
||||
SimplePlayerView,
|
||||
@@ -10,6 +10,8 @@ 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>;
|
||||
@@ -19,66 +21,93 @@ export const GameContext = createContext<{
|
||||
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 });
|
||||
|
||||
return (
|
||||
<GameContext.Provider value={{ view, submitAction }}>
|
||||
<Pile
|
||||
count={view().deckCount}
|
||||
scale={0.8}
|
||||
class="cursor-pointer fixed center"
|
||||
onClick={() => submitAction({ type: "draw" })}
|
||||
/>
|
||||
{/* Configuration */}
|
||||
<Show when={view() == null}>
|
||||
<div>Configuration!</div>
|
||||
</Show>
|
||||
|
||||
<Hand
|
||||
class="fixed bc"
|
||||
hand={view().myHand}
|
||||
onClickCard={(card) => submitAction({ type: "discard", card })}
|
||||
/>
|
||||
<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.playerNames[view().playerTurn] + "'s"}
|
||||
</span>{" "}
|
||||
turn
|
||||
</div>
|
||||
<button
|
||||
class="button fixed tl m-4 p-1"
|
||||
onClick={() => {
|
||||
table.sendWs({ quit: true });
|
||||
}}
|
||||
>
|
||||
Quit
|
||||
</button>
|
||||
<For
|
||||
each={Object.entries(view().playerHandCounts).filter(([key, _]) =>
|
||||
table.players().includes(key)
|
||||
)}
|
||||
>
|
||||
{([playerKey, handCount], i) => (
|
||||
<Portal
|
||||
mount={document.getElementById(`player-${playerKey}`)!}
|
||||
ref={(ref) => {
|
||||
console.log("Setting hand ref");
|
||||
const midOffset =
|
||||
i() + 0.5 - Object.values(view().playerHandCounts).length / 2;
|
||||
{/* 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" })}
|
||||
/>
|
||||
|
||||
ref.style = `position: absolute; display: flex; justify-content: center; top: 65%;`;
|
||||
}}
|
||||
>
|
||||
<FannedHand handCount={handCount} />
|
||||
</Portal>
|
||||
)}
|
||||
</For>
|
||||
{/* 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, _]) =>
|
||||
table.players().includes(key)
|
||||
)}
|
||||
>
|
||||
{([playerKey, handCount], i) => (
|
||||
<Portal
|
||||
mount={document.getElementById(`player-${playerKey}`)!}
|
||||
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.playerNames[view().playerTurn] + "'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.playerNames[results()!]} won!
|
||||
</span>
|
||||
</Show>
|
||||
</GameContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Observable } from "kefir";
|
||||
import { Observable, Property } from "kefir";
|
||||
import { Accessor, createSignal } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
|
||||
@@ -47,3 +47,15 @@ export const createObservableStore = <T extends object = {}>(
|
||||
obs.onValue((val) => setStore(val));
|
||||
return store;
|
||||
};
|
||||
|
||||
export const extractProperty = <
|
||||
P extends string,
|
||||
T extends { [key in P]?: any }
|
||||
>(
|
||||
obs: Observable<T, any>,
|
||||
property: P
|
||||
): Property<T[P], any> =>
|
||||
obs
|
||||
.filter((o) => property in o)
|
||||
.map((o) => o[property]!)
|
||||
.toProperty();
|
||||
|
||||
@@ -17,7 +17,9 @@ export const WsOut = t.Object({
|
||||
playersPresent: t.Optional(t.Array(t.String())),
|
||||
playerNames: t.Optional(t.Record(t.String(), t.String())),
|
||||
playersReady: t.Optional(t.Nullable(t.Record(t.String(), t.Boolean()))),
|
||||
gameConfig: t.Optional(t.Any()),
|
||||
gameConfig: t.Optional(
|
||||
t.Object({ game: t.String(), players: t.Array(t.String()) })
|
||||
),
|
||||
view: t.Optional(t.Any()),
|
||||
results: t.Optional(t.Any()),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user