From 0f015841ff7df62123a161fcee3afe12437e67f3 Mon Sep 17 00:00:00 2001 From: Daniel McCrystal Date: Mon, 25 Aug 2025 22:34:33 -0400 Subject: [PATCH] when in doubt make it a property I guess --- packages/client/src/components/Game.tsx | 13 +++++- packages/client/src/components/Hand.tsx | 6 +-- packages/client/src/components/Player.tsx | 16 +++++++ packages/client/src/components/Table.tsx | 8 ++-- packages/server/src/api.ts | 57 ++++++++++------------- packages/server/src/table.ts | 44 +++++++++++++---- 6 files changed, 91 insertions(+), 53 deletions(-) diff --git a/packages/client/src/components/Game.tsx b/packages/client/src/components/Game.tsx index 3efc0a9..09cd1e7 100644 --- a/packages/client/src/components/Game.tsx +++ b/packages/client/src/components/Game.tsx @@ -2,11 +2,11 @@ import { Accessor, createContext, useContext } from "solid-js"; import { SimpleAction, SimplePlayerView, - vSimpleGameState, } from "../../../server/src/games/simple"; +import { me, profile } from "../profile"; +import Hand from "./Hand"; import Pile from "./Pile"; import { TableContext } from "./Table"; -import Hand from "./Hand"; export const GameContext = createContext<{ view: Accessor; @@ -31,6 +31,15 @@ export default () => { hand={view().myHand} onClickCard={(card) => submitAction({ type: "discard", card })} /> +
+ It's{" "} + + {view().playerTurn == me() + ? "your" + : profile(view().playerTurn)()?.name + "'s"} + {" "} + turn +
); }; diff --git a/packages/client/src/components/Hand.tsx b/packages/client/src/components/Hand.tsx index ef89a33..3fc2a8b 100644 --- a/packages/client/src/components/Hand.tsx +++ b/packages/client/src/components/Hand.tsx @@ -1,8 +1,6 @@ -import { Component, For, useContext } from "solid-js"; +import { Component, For } from "solid-js"; +import type { Card as TCard, Hand as THand } from "../../../shared/cards"; import Card from "./Card"; -import type { Hand as THand, Card as TCard } from "../../../shared/cards"; -import { GameContext } from "./Game"; -import { produce } from "solid-js/store"; import { Stylable } from "./toolbox"; export default ((props) => { diff --git a/packages/client/src/components/Player.tsx b/packages/client/src/components/Player.tsx index 80a34c2..4295837 100644 --- a/packages/client/src/components/Player.tsx +++ b/packages/client/src/components/Player.tsx @@ -1,12 +1,28 @@ +import { createSignal, useContext } from "solid-js"; import { playerColor, profile } from "../profile"; +import { TableContext } from "./Table"; import { Stylable } from "./toolbox"; +import { createObservable, createObservableWithInit } from "../fn"; 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]; + return (
diff --git a/packages/client/src/components/Table.tsx b/packages/client/src/components/Table.tsx index 223d6a8..0fab01d 100644 --- a/packages/client/src/components/Table.tsx +++ b/packages/client/src/components/Table.tsx @@ -14,12 +14,12 @@ import { createObservable, createObservableWithInit, cx } from "../fn"; import { me } from "../profile"; import Game from "./Game"; import Player from "./Player"; -import { fromPromise } from "kefir"; +import { fromPromise, Stream } from "kefir"; export const TableContext = createContext<{ - players: Accessor; view: Accessor; sendWs: (msg: TWsIn) => void; + wsEvents: Stream; }>(); export default (props: { tableKey: string }) => { @@ -29,7 +29,6 @@ export default (props: { tableKey: string }) => { const ws = api.ws(props).subscribe(); ws.on("open", () => res(ws)); ws.on("error", () => res(ws)); - ws.on("close", () => res(ws)); }); const sendWs = (msg: TWsIn) => wsPromise.then((ws) => ws.send(msg)); @@ -39,7 +38,6 @@ export default (props: { tableKey: string }) => { onCleanup(() => wsPromise.then((ws) => ws.close())); const presenceEvents = wsEvents.filter((evt) => evt.players != null); - const gameEvents = wsEvents.filter((evt) => evt.view != null); const players = createObservableWithInit( @@ -55,8 +53,8 @@ export default (props: { tableKey: string }) => {
diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index fc08eaf..d13c423 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -74,38 +74,35 @@ const api = new Elysia({ prefix: "/api" }) }, send, }) { - console.log(humanKey, "connected"); - try { - const table = liveTable< - SimpleConfiguration, - SimpleGameState, - SimpleAction - >(tableKey); + const table = liveTable< + SimpleConfiguration, + SimpleGameState, + SimpleAction + >(tableKey); - table.outputs.playersPresent.onValue((players) => - send({ players }) - ); + table.inputs.connectionChanges.emit({ + humanKey, + presence: "joined", + }); - table.outputs.playersReady.onValue((readys) => - send({ playersReady: readys }) - ); + table.outputs.playersPresent.onValue((players) => + send({ players }) + ); - combine( - [table.outputs.gameState], - [table.outputs.gameConfig], - (state, config) => - state && - config && - getSimplePlayerView(config, state, humanKey) - ).onValue((view) => send({ view })); + table.outputs.playersReady.onValue((readys) => + send({ playersReady: readys }) + ); - table.inputs.connectionChanges.emit({ - humanKey, - presence: "joined", - }); - } catch (err) { - console.error(err); - } + combine( + [table.outputs.gameState], + [table.outputs.gameConfig], + (state, config) => + state && + config && + getSimplePlayerView(config, state, humanKey) + ) + .toProperty() + .onValue((view) => send({ view })); }, response: WsOut, @@ -144,10 +141,6 @@ const api = new Elysia({ prefix: "/api" }) presence: "left", }); }, - - // error(err) { - // console.error("ERROR IN WEBSOCKET", JSON.stringify(err, null, 2)); - // }, }); export default api; diff --git a/packages/server/src/table.ts b/packages/server/src/table.ts index a6044a9..2d7409a 100644 --- a/packages/server/src/table.ts +++ b/packages/server/src/table.ts @@ -1,5 +1,5 @@ import { t } from "elysia"; -import { combine, Property } from "kefir"; +import { combine, pool, Property } from "kefir"; import Bus, { type Bus as TBus } from "kefir-bus"; import { newSimpleGameState, @@ -75,7 +75,8 @@ export const liveTable = (key: string) => { (evt.presence == "joined" ? 1 : -1), }; }, {} as { [key: string]: number }) - .map((counts) => Object.keys(counts)); + .map((counts) => Object.keys(counts)) + .toProperty(); const playersReady = transform( {} as { [key: string]: boolean }, @@ -93,7 +94,9 @@ export const liveTable = (key: string) => { ? { ...prev, [evt.humanKey]: evt.ready } : prev, ] - ); + ) + .toProperty() + .log("playersReady"); const gameStarts = playersReady .filter( @@ -101,22 +104,28 @@ export const liveTable = (key: string) => { Object.values(pr).length > 0 && Object.values(pr).every((ready) => ready) ) - .map((_) => null); + .map((_) => null) + .log("gameStarts"); - const gameConfig = playersPresent.map((players) => ({ - game: "simple", - players, - })); + const gameConfigPool = pool< + { + game: string; + players: string[]; + }, + never + >(); + + const gameConfig = gameConfigPool.toProperty(); const gameState = transform( null as SimpleGameState | null, [ - combine([gameStarts], [gameConfig], (_, config) => config), + combine([gameStarts], [gameConfigPool], (_, config) => config), (prev, startConfig: SimpleConfiguration) => prev == null ? newSimpleGameState(startConfig) : prev, ], [ - combine([actions], [gameConfig], (action, config) => ({ + combine([actions], [gameConfigPool], (action, config) => ({ action, config, })), @@ -138,6 +147,21 @@ export const liveTable = (key: string) => { ] ).toProperty(); + const gameIsActive = gameState + .map((gs) => gs != null) + .skipDuplicates() + .toProperty() + .log("gameIsActive"); + + gameConfigPool.plug( + playersPresent + .filterBy(gameIsActive.map((active) => !active)) + .map((players) => ({ + game: "simple", + players, + })) + ); + tables[key] = { inputs, outputs: {