Compare commits

..

3 Commits

Author SHA1 Message Date
686529507e basic hand viewing 2025-08-26 21:42:44 -04:00
aeb7d9174b quitting working but suspiciously 2025-08-26 19:13:09 -04:00
e5f432dc98 housekeeping 2025-08-26 18:37:27 -04:00
17 changed files with 127 additions and 103 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "games",
"type": "module",
"version": "0.0.4",
"version": "0.0.5",
"scripts": {
"dev": "pnpm --parallel dev",
"build": "pnpm run -F client build",

View File

@@ -1,8 +1,6 @@
import { createResource } from "solid-js";
import { type Api } from "../../server/src/api";
import { treaty } from "@elysiajs/eden";
import { EdenWS } from "@elysiajs/eden/treaty";
import { fromEvents } from "kefir";
import { type Api } from "@games/server/src/api";
const { api } = treaty<Api>(
import.meta.env.DEV ? "http://localhost:5001" : window.location.origin,

View File

@@ -2,10 +2,9 @@ import { Route, Router } from "@solidjs/router";
import { createResource, lazy, Suspense } from "solid-js";
import { render } from "solid-js/web";
import "virtual:uno.css";
import pkg from "../package.json";
import pkg from "^/package.json";
import "./style.css";
import api from "./api";
import Cookies from "js-cookie";
import { mePromise } from "./profile";
const Profile = () => {

View File

@@ -1,7 +1,7 @@
import { Component, createResource, JSX, Suspense } from "solid-js";
import { Component, Suspense } from "solid-js";
import type { Card } from "@games/shared/cards";
import { Clickable, Stylable } from "./toolbox";
import { Card } from "../../../shared/cards";
const cardToSvgFilename = (card: Card) => {
if (card.kind == "joker") {

View File

@@ -0,0 +1,21 @@
import type { Hand } from "@games/shared/cards";
import { For } from "solid-js";
import Card from "./Card";
export default (props: { handCount: number }) => {
return (
<div class="flex">
<For each={Array(props.handCount)}>
{() => (
<Card
face="down"
style={{
"margin-left": "-15px",
transform: "translate(0, 40px)",
}}
/>
)}
</For>
</div>
);
};

View File

@@ -1,12 +1,14 @@
import { Accessor, createContext, useContext } from "solid-js";
import {
import { Accessor, createContext, For, useContext } from "solid-js";
import type {
SimpleAction,
SimplePlayerView,
} from "../../../server/src/games/simple";
import { me, profile } from "../profile";
} from "@games/server/src/games/simple";
import { me, profile } from "~/profile";
import Hand from "./Hand";
import Pile from "./Pile";
import { TableContext } from "./Table";
import { Portal } from "solid-js/web";
import FannedHand from "./FannedHand";
export const GameContext = createContext<{
view: Accessor<SimplePlayerView>;
@@ -40,6 +42,23 @@ export default () => {
</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)}>
{([playerKey, handCount]) => (
<Portal
mount={document.getElementById(`player-${playerKey}`)!}
>
<div class="absolute center">
<FannedHand handCount={handCount} />
</div>
</Portal>
)}
</For>
</GameContext.Provider>
);
};

View File

@@ -1,5 +1,5 @@
import { Component, For } from "solid-js";
import type { Card as TCard, Hand as THand } from "../../../shared/cards";
import type { Card as TCard, Hand as THand } from "@games/shared/cards";
import Card from "./Card";
import { Stylable } from "./toolbox";

View File

@@ -1,8 +1,9 @@
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 { createObservable, createObservableWithInit } from "../fn";
import { createObservable, createObservableWithInit } from "~/fn";
import { GameContext } from "./Game";
export default (props: { playerKey: string } & Stylable) => {
const table = useContext(TableContext);
@@ -13,8 +14,11 @@ export default (props: { playerKey: string } & Stylable) => {
.thru((Evt) => createObservableWithInit(Evt, false)) ??
createSignal(false)[0];
const game = useContext(GameContext);
return (
<div
id={`player-${props.playerKey}`}
style={{
...props.style,
"background-color": playerColor(props.playerKey),

View File

@@ -1,20 +1,19 @@
import type { TWsIn, TWsOut } from "@games/server/src/table";
import { fromPromise, Stream } from "kefir";
import {
Accessor,
createContext,
createEffect,
createResource,
createSignal,
For,
onCleanup,
Show,
} from "solid-js";
import { TWsIn, TWsOut } from "../../../server/src/table";
import api, { fromWebsocket } from "../api";
import { createObservable, createObservableWithInit, cx } from "../fn";
import { me } from "../profile";
import api, { fromWebsocket } from "~/api";
import { createObservable, createObservableWithInit, cx } from "~/fn";
import { me, mePromise } from "~/profile";
import Game from "./Game";
import Player from "./Player";
import { fromPromise, Stream } from "kefir";
export const TableContext = createContext<{
view: Accessor<any>;
@@ -38,7 +37,7 @@ 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 gameEvents = wsEvents.filter((evt) => evt.view !== undefined);
const players = createObservableWithInit<string[]>(
presenceEvents.map((evt) => evt.players!),
@@ -46,6 +45,15 @@ export default (props: { tableKey: string }) => {
);
const [ready, setReady] = createSignal(false);
mePromise.then(
(me) =>
me &&
wsEvents
.filter((evt) => evt.playersReady !== undefined)
.map((evt) => evt.playersReady?.[me] ?? false)
.onValue(setReady)
);
createEffect(() => sendWs({ ready: ready() }));
const view = createObservable(gameEvents.map((evt) => evt.view));

View File

@@ -1,16 +0,0 @@
import { A, useParams } from "@solidjs/router";
import Game from "../../components/Game";
export default () => {
const params = useParams<{ game: string; instance: string }>();
return (
<>
<Game instanceId={params.instance} />
<A href={`/${params.game}`} class="fixed tl m-4 px-2 py-1.5 button">
Back
</A>
</>
);
};

View File

@@ -1,33 +0,0 @@
import { A, useParams } from "@solidjs/router";
import { createEffect, createResource, For, Suspense } from "solid-js";
import api from "../../api";
export default () => {
const param = useParams<{ game: string }>();
const [instances, { refetch }] = createResource(
() => param.game,
async () => api.instances.get({ query: param }).then((res) => res.data)
);
return (
<Suspense>
<div style={{ padding: "20px" }}>
<p class="text-[40px]">{param.game}</p>
<button class="px-2 py-1.5 m-4 button">New Game</button>
<ul>
<For each={instances() ?? []}>
{(instance) => (
<li>
<A href={`/${param.game}/${instance.id}`}>
{instance.id}
</A>
</li>
)}
</For>
</ul>
</div>
</Suspense>
);
};

View File

@@ -1,8 +1,8 @@
import { A, useParams } from "@solidjs/router";
import Table from "../components/Table";
import Table from "~/components/Table";
import { Show } from "solid-js";
import { me } from "../profile";
import { me } from "~/profile";
export default () => {
const { tableKey } = useParams();
@@ -10,9 +10,6 @@ export default () => {
return (
<Show when={me() != null}>
<Table tableKey={tableKey} />
<A href={"/"} class="fixed tl m-4 px-2 py-1.5 button">
Back
</A>
</Show>
);
};

View File

@@ -1,6 +1,6 @@
import { A } from "@solidjs/router";
import { createEffect, createResource, For } from "solid-js";
import api from "../api";
import api from "~/api";
export default () => {
const [games] = createResource(async () =>

View File

@@ -1,4 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
@@ -12,9 +13,10 @@
"noEmit": true,
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"],
"$/*": ["./"],
"@/*": ["./public/*"]
"^/*": ["./*"],
"@games/*": ["./packages/*"],
"$/*": ["./packages/client/*"],
"~/*": ["./packages/client/src/*"]
}
}
}

View File

@@ -1,6 +1,7 @@
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";
import UnoCSS from "unocss/vite";
import path from "path";
export default defineConfig({
plugins: [solidPlugin(), UnoCSS()],
@@ -8,4 +9,12 @@ export default defineConfig({
outDir: "../server/public",
emptyOutDir: true,
},
resolve: {
alias: {
"^": path.resolve(__dirname, "../../"),
"@games": path.resolve(__dirname, "../"),
$: path.resolve(__dirname, "./"),
"~": path.resolve(__dirname, "./src"),
},
},
});

View File

View File

@@ -12,7 +12,7 @@ import { transform } from "./kefir-extension";
export const WsOut = t.Object({
players: t.Optional(t.Array(t.String())),
playersReady: t.Optional(t.Record(t.String(), t.Boolean())),
playersReady: t.Optional(t.Nullable(t.Record(t.String(), t.Boolean()))),
view: t.Optional(t.Any()),
});
export type TWsOut = typeof WsOut.static;
@@ -31,15 +31,15 @@ type TablePayload<GameConfig, GameState, GameAction> = {
never
>;
readys: TBus<Attributed & { ready: boolean }, never>;
actions: TBus<Attributed & GameAction, never>;
quits: TBus<Attributed, never>;
readys: TBus<Attributed & { ready: boolean }, any>;
actions: TBus<Attributed & GameAction, any>;
quits: TBus<Attributed, any>;
};
outputs: {
playersPresent: Property<string[], never>;
playersReady: Property<{ [key: string]: boolean }, unknown>;
gameConfig: Property<GameConfig | null, never>;
gameState: Property<GameState | null, never>;
playersPresent: Property<string[], any>;
playersReady: Property<{ [key: string]: boolean } | null, any>;
gameConfig: Property<GameConfig | null, any>;
gameState: Property<GameState | null, any>;
};
};
@@ -60,6 +60,7 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
quits: Bus(),
};
const { connectionChanges, readys, actions, quits } = inputs;
quits.log("quits");
// =======
const playersPresent = connectionChanges
@@ -78,34 +79,45 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
.map((counts) => Object.keys(counts))
.toProperty();
const gameEnds = quits.map((_) => null);
const gameStarts = pool<null, any>();
const playersReady = transform(
{} as { [key: string]: boolean },
null as { [key: string]: boolean } | null,
[
playersPresent,
(prev, players: string[]) =>
Object.fromEntries(
players.map((p) => [p, prev[p] ?? false])
players.map((p) => [p, prev?.[p] ?? false])
),
],
[
readys,
(prev, evt: { humanKey: string; ready: boolean }) =>
prev[evt.humanKey] != null
prev?.[evt.humanKey] != null
? { ...prev, [evt.humanKey]: evt.ready }
: prev,
],
[gameStarts, () => null],
[
combine([gameEnds], [playersPresent], (_, players) => players),
(_, players: string[]) =>
Object.fromEntries(players.map((p) => [p, false])),
]
)
.toProperty()
.log("playersReady");
const gameStarts = playersReady
gameStarts.plug(
playersReady
.filter(
(pr) =>
Object.values(pr).length > 0 &&
Object.values(pr).every((ready) => ready)
Object.values(pr ?? {}).length > 0 &&
Object.values(pr!).every((ready) => ready)
)
.map((_) => null)
.log("gameStarts");
.log("gameStarts")
);
const gameConfigPool = pool<
{
@@ -144,8 +156,12 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
humanKey: evt.action.humanKey,
})
: prev,
]
],
[quits, () => null]
).toProperty();
gameState
.map((state) => JSON.stringify(state).substring(0, 10))
.log("gameState");
const gameIsActive = gameState
.map((gs) => gs != null)
@@ -166,9 +182,9 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
inputs,
outputs: {
playersPresent,
playersReady,
gameConfig: gameConfig as Property<unknown, never>,
gameState: gameState as Property<unknown, never>,
playersReady: playersReady.toProperty(),
gameConfig: gameConfig as Property<unknown, any>,
gameState: gameState as Property<unknown, any>,
},
};