Compare commits
3 Commits
0f015841ff
...
686529507e
| Author | SHA1 | Date | |
|---|---|---|---|
| 686529507e | |||
| aeb7d9174b | |||
| e5f432dc98 |
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
21
packages/client/src/components/FannedHand.tsx
Normal file
21
packages/client/src/components/FannedHand.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 () =>
|
||||
|
||||
@@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
0
packages/server/src/logging.ts
Normal file
0
packages/server/src/logging.ts
Normal 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>,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user