diff --git a/notes/2025-09-03-224857.md b/notes/2025-09-03-224857.md deleted file mode 100644 index 61e80c5..0000000 --- a/notes/2025-09-03-224857.md +++ /dev/null @@ -1,5 +0,0 @@ -# 2025-09-03-224857 - -The backend has a pretty good abstraction of a `Game`, that it can swap in implemenations for. -The frontend is still mostly hardcoded to the test `simple` game. -Will need to refine the concept of `Game` to include the necessary components of displaying and interacting with a game. diff --git a/pkg/client/src/components/Player.tsx b/pkg/client/src/components/Player.tsx index fab7398..020d9ad 100644 --- a/pkg/client/src/components/Player.tsx +++ b/pkg/client/src/components/Player.tsx @@ -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 ( diff --git a/pkg/client/src/components/Table.tsx b/pkg/client/src/components/Table.tsx index 65fd7a5..6e5d320 100644 --- a/pkg/client/src/components/Table.tsx +++ b/pkg/client/src/components/Table.tsx @@ -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; @@ -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( - merge([ - gameEvents - .filter((evt) => "view" in evt && evt.view == null) - .map(() => undefined), - resultEvents.map((evt) => evt.results), - ]) - ); return ( { players, }} > + {/* Player avatars around the table */}
p != me())}> {(player, i) => { @@ -118,6 +117,8 @@ export default (props: { tableKey: string }) => { }}
+ + {/* The table body itself */}
{
- - - - - - {playerNames[results()!]} won! - - + + {/* The game being played */} +
); }; diff --git a/pkg/client/src/components/games/index.ts b/pkg/client/src/components/games/index.ts new file mode 100644 index 0000000..b91689a --- /dev/null +++ b/pkg/client/src/components/games/index.ts @@ -0,0 +1,5 @@ +import simple from "./simple"; + +export default { + simple, +}; diff --git a/pkg/client/src/components/games/simple.tsx b/pkg/client/src/components/games/simple.tsx index 3050f10..1a3c5ce 100644 --- a/pkg/client/src/components/games/simple.tsx +++ b/pkg/client/src/components/games/simple.tsx @@ -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; @@ -19,66 +21,93 @@ export const GameContext = createContext<{ export default () => { const table = useContext(TableContext)!; const view = table.view as Accessor; + + const results = ( + extractProperty(table.wsEvents, "results") as Property + ).thru(createObservable); const submitAction = (action: SimpleAction) => table.sendWs({ action }); return ( - submitAction({ type: "draw" })} - /> + {/* Configuration */} + +
Configuration!
+
- submitAction({ type: "discard", card })} - /> -
- It's{" "} - - {view().playerTurn == me() - ? "your" - : table.playerNames[view().playerTurn] + "'s"} - {" "} - turn -
- - - table.players().includes(key) - )} - > - {([playerKey, handCount], i) => ( - { - console.log("Setting hand ref"); - const midOffset = - i() + 0.5 - Object.values(view().playerHandCounts).length / 2; + {/* Active game */} + + {/* Main pile in the middle of the table */} + submitAction({ type: "draw" })} + /> - ref.style = `position: absolute; display: flex; justify-content: center; top: 65%;`; - }} - > - - - )} - + {/* Your own hand */} + submitAction({ type: "discard", card })} + /> + + {/* Other players' hands */} + + table.players().includes(key) + )} + > + {([playerKey, handCount], i) => ( + { + 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%;`; + }} + > + + + )} + + + {/* Turn indicator */} +
+ It's{" "} + + {view().playerTurn == me() + ? "your" + : table.playerNames[view().playerTurn] + "'s"} + {" "} + turn +
+ + {/* Quit button */} + + + + {/* Results */} + + + {table.playerNames[results()!]} won! + +
); }; diff --git a/pkg/client/src/fn.ts b/pkg/client/src/fn.ts index 069f7bb..086166f 100644 --- a/pkg/client/src/fn.ts +++ b/pkg/client/src/fn.ts @@ -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 = ( obs.onValue((val) => setStore(val)); return store; }; + +export const extractProperty = < + P extends string, + T extends { [key in P]?: any } +>( + obs: Observable, + property: P +): Property => + obs + .filter((o) => property in o) + .map((o) => o[property]!) + .toProperty(); diff --git a/pkg/server/src/table.ts b/pkg/server/src/table.ts index 72a5abc..f1835ee 100644 --- a/pkg/server/src/table.ts +++ b/pkg/server/src/table.ts @@ -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()), });