when in doubt make it a property I guess

This commit is contained in:
2025-08-25 22:34:33 -04:00
parent 6c45e7b114
commit 0f015841ff
6 changed files with 91 additions and 53 deletions

View File

@@ -2,11 +2,11 @@ import { Accessor, createContext, useContext } from "solid-js";
import { import {
SimpleAction, SimpleAction,
SimplePlayerView, SimplePlayerView,
vSimpleGameState,
} from "../../../server/src/games/simple"; } from "../../../server/src/games/simple";
import { me, profile } from "../profile";
import Hand from "./Hand";
import Pile from "./Pile"; import Pile from "./Pile";
import { TableContext } from "./Table"; import { TableContext } from "./Table";
import Hand from "./Hand";
export const GameContext = createContext<{ export const GameContext = createContext<{
view: Accessor<SimplePlayerView>; view: Accessor<SimplePlayerView>;
@@ -31,6 +31,15 @@ export default () => {
hand={view().myHand} hand={view().myHand}
onClickCard={(card) => submitAction({ type: "discard", card })} onClickCard={(card) => submitAction({ type: "discard", card })}
/> />
<div class="absolute tc text-align-center">
It's{" "}
<span class="font-bold">
{view().playerTurn == me()
? "your"
: profile(view().playerTurn)()?.name + "'s"}
</span>{" "}
turn
</div>
</GameContext.Provider> </GameContext.Provider>
); );
}; };

View File

@@ -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 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"; import { Stylable } from "./toolbox";
export default ((props) => { export default ((props) => {

View File

@@ -1,12 +1,28 @@
import { createSignal, useContext } from "solid-js";
import { playerColor, profile } from "../profile"; import { playerColor, profile } from "../profile";
import { TableContext } from "./Table";
import { Stylable } from "./toolbox"; import { Stylable } from "./toolbox";
import { createObservable, createObservableWithInit } from "../fn";
export default (props: { playerKey: string } & Stylable) => { 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 ( return (
<div <div
style={{ style={{
...props.style, ...props.style,
"background-color": playerColor(props.playerKey), "background-color": playerColor(props.playerKey),
...(playerReady() && table?.view() == null
? {
border: "10px solid green",
}
: {}),
}} }}
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`}
> >

View File

@@ -14,12 +14,12 @@ import { createObservable, createObservableWithInit, cx } from "../fn";
import { me } from "../profile"; import { me } from "../profile";
import Game from "./Game"; import Game from "./Game";
import Player from "./Player"; import Player from "./Player";
import { fromPromise } from "kefir"; import { fromPromise, Stream } from "kefir";
export const TableContext = createContext<{ export const TableContext = createContext<{
players: Accessor<string[]>;
view: Accessor<any>; view: Accessor<any>;
sendWs: (msg: TWsIn) => void; sendWs: (msg: TWsIn) => void;
wsEvents: Stream<TWsOut, any>;
}>(); }>();
export default (props: { tableKey: string }) => { export default (props: { tableKey: string }) => {
@@ -29,7 +29,6 @@ export default (props: { tableKey: string }) => {
const ws = api.ws(props).subscribe(); const ws = api.ws(props).subscribe();
ws.on("open", () => res(ws)); ws.on("open", () => res(ws));
ws.on("error", () => res(ws)); ws.on("error", () => res(ws));
ws.on("close", () => res(ws));
}); });
const sendWs = (msg: TWsIn) => wsPromise.then((ws) => ws.send(msg)); 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())); onCleanup(() => wsPromise.then((ws) => ws.close()));
const presenceEvents = wsEvents.filter((evt) => evt.players != null); const presenceEvents = wsEvents.filter((evt) => evt.players != null);
const gameEvents = wsEvents.filter((evt) => evt.view != null); const gameEvents = wsEvents.filter((evt) => evt.view != null);
const players = createObservableWithInit<string[]>( const players = createObservableWithInit<string[]>(
@@ -55,8 +53,8 @@ export default (props: { tableKey: string }) => {
<TableContext.Provider <TableContext.Provider
value={{ value={{
sendWs, sendWs,
wsEvents,
view, view,
players,
}} }}
> >
<div class="flex justify-around p-t-10"> <div class="flex justify-around p-t-10">

View File

@@ -74,38 +74,35 @@ const api = new Elysia({ prefix: "/api" })
}, },
send, send,
}) { }) {
console.log(humanKey, "connected"); const table = liveTable<
try { SimpleConfiguration,
const table = liveTable< SimpleGameState,
SimpleConfiguration, SimpleAction
SimpleGameState, >(tableKey);
SimpleAction
>(tableKey);
table.outputs.playersPresent.onValue((players) => table.inputs.connectionChanges.emit({
send({ players }) humanKey,
); presence: "joined",
});
table.outputs.playersReady.onValue((readys) => table.outputs.playersPresent.onValue((players) =>
send({ playersReady: readys }) send({ players })
); );
combine( table.outputs.playersReady.onValue((readys) =>
[table.outputs.gameState], send({ playersReady: readys })
[table.outputs.gameConfig], );
(state, config) =>
state &&
config &&
getSimplePlayerView(config, state, humanKey)
).onValue((view) => send({ view }));
table.inputs.connectionChanges.emit({ combine(
humanKey, [table.outputs.gameState],
presence: "joined", [table.outputs.gameConfig],
}); (state, config) =>
} catch (err) { state &&
console.error(err); config &&
} getSimplePlayerView(config, state, humanKey)
)
.toProperty()
.onValue((view) => send({ view }));
}, },
response: WsOut, response: WsOut,
@@ -144,10 +141,6 @@ const api = new Elysia({ prefix: "/api" })
presence: "left", presence: "left",
}); });
}, },
// error(err) {
// console.error("ERROR IN WEBSOCKET", JSON.stringify(err, null, 2));
// },
}); });
export default api; export default api;

View File

@@ -1,5 +1,5 @@
import { t } from "elysia"; 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 Bus, { type Bus as TBus } from "kefir-bus";
import { import {
newSimpleGameState, newSimpleGameState,
@@ -75,7 +75,8 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
(evt.presence == "joined" ? 1 : -1), (evt.presence == "joined" ? 1 : -1),
}; };
}, {} as { [key: string]: number }) }, {} as { [key: string]: number })
.map((counts) => Object.keys(counts)); .map((counts) => Object.keys(counts))
.toProperty();
const playersReady = transform( const playersReady = transform(
{} as { [key: string]: boolean }, {} as { [key: string]: boolean },
@@ -93,7 +94,9 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
? { ...prev, [evt.humanKey]: evt.ready } ? { ...prev, [evt.humanKey]: evt.ready }
: prev, : prev,
] ]
); )
.toProperty()
.log("playersReady");
const gameStarts = playersReady const gameStarts = playersReady
.filter( .filter(
@@ -101,22 +104,28 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
Object.values(pr).length > 0 && Object.values(pr).length > 0 &&
Object.values(pr).every((ready) => ready) Object.values(pr).every((ready) => ready)
) )
.map((_) => null); .map((_) => null)
.log("gameStarts");
const gameConfig = playersPresent.map((players) => ({ const gameConfigPool = pool<
game: "simple", {
players, game: string;
})); players: string[];
},
never
>();
const gameConfig = gameConfigPool.toProperty();
const gameState = transform( const gameState = transform(
null as SimpleGameState | null, null as SimpleGameState | null,
[ [
combine([gameStarts], [gameConfig], (_, config) => config), combine([gameStarts], [gameConfigPool], (_, config) => config),
(prev, startConfig: SimpleConfiguration) => (prev, startConfig: SimpleConfiguration) =>
prev == null ? newSimpleGameState(startConfig) : prev, prev == null ? newSimpleGameState(startConfig) : prev,
], ],
[ [
combine([actions], [gameConfig], (action, config) => ({ combine([actions], [gameConfigPool], (action, config) => ({
action, action,
config, config,
})), })),
@@ -138,6 +147,21 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
] ]
).toProperty(); ).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] = { tables[key] = {
inputs, inputs,
outputs: { outputs: {