Compare commits
3 Commits
0f015841ff
...
686529507e
| Author | SHA1 | Date | |
|---|---|---|---|
| 686529507e | |||
| aeb7d9174b | |||
| e5f432dc98 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "games",
|
"name": "games",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm --parallel dev",
|
"dev": "pnpm --parallel dev",
|
||||||
"build": "pnpm run -F client build",
|
"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 { treaty } from "@elysiajs/eden";
|
||||||
import { EdenWS } from "@elysiajs/eden/treaty";
|
|
||||||
import { fromEvents } from "kefir";
|
import { fromEvents } from "kefir";
|
||||||
|
import { type Api } from "@games/server/src/api";
|
||||||
|
|
||||||
const { api } = treaty<Api>(
|
const { api } = treaty<Api>(
|
||||||
import.meta.env.DEV ? "http://localhost:5001" : window.location.origin,
|
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 { createResource, lazy, Suspense } from "solid-js";
|
||||||
import { render } from "solid-js/web";
|
import { render } from "solid-js/web";
|
||||||
import "virtual:uno.css";
|
import "virtual:uno.css";
|
||||||
import pkg from "../package.json";
|
import pkg from "^/package.json";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
import api from "./api";
|
import api from "./api";
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import { mePromise } from "./profile";
|
import { mePromise } from "./profile";
|
||||||
|
|
||||||
const 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 { Clickable, Stylable } from "./toolbox";
|
||||||
import { Card } from "../../../shared/cards";
|
|
||||||
|
|
||||||
const cardToSvgFilename = (card: Card) => {
|
const cardToSvgFilename = (card: Card) => {
|
||||||
if (card.kind == "joker") {
|
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 { Accessor, createContext, For, useContext } from "solid-js";
|
||||||
import {
|
import type {
|
||||||
SimpleAction,
|
SimpleAction,
|
||||||
SimplePlayerView,
|
SimplePlayerView,
|
||||||
} from "../../../server/src/games/simple";
|
} from "@games/server/src/games/simple";
|
||||||
import { me, profile } from "../profile";
|
import { me, profile } from "~/profile";
|
||||||
import Hand from "./Hand";
|
import Hand from "./Hand";
|
||||||
import Pile from "./Pile";
|
import Pile from "./Pile";
|
||||||
import { TableContext } from "./Table";
|
import { TableContext } from "./Table";
|
||||||
|
import { Portal } from "solid-js/web";
|
||||||
|
import FannedHand from "./FannedHand";
|
||||||
|
|
||||||
export const GameContext = createContext<{
|
export const GameContext = createContext<{
|
||||||
view: Accessor<SimplePlayerView>;
|
view: Accessor<SimplePlayerView>;
|
||||||
@@ -40,6 +42,23 @@ export default () => {
|
|||||||
</span>{" "}
|
</span>{" "}
|
||||||
turn
|
turn
|
||||||
</div>
|
</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>
|
</GameContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, For } from "solid-js";
|
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 Card from "./Card";
|
||||||
import { Stylable } from "./toolbox";
|
import { Stylable } from "./toolbox";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createSignal, useContext } from "solid-js";
|
import { createSignal, useContext } from "solid-js";
|
||||||
import { playerColor, profile } from "../profile";
|
import { playerColor, profile } from "~/profile";
|
||||||
import { TableContext } from "./Table";
|
import { TableContext } from "./Table";
|
||||||
import { Stylable } from "./toolbox";
|
import { Stylable } from "./toolbox";
|
||||||
import { createObservable, createObservableWithInit } from "../fn";
|
import { createObservable, createObservableWithInit } from "~/fn";
|
||||||
|
import { GameContext } from "./Game";
|
||||||
|
|
||||||
export default (props: { playerKey: string } & Stylable) => {
|
export default (props: { playerKey: string } & Stylable) => {
|
||||||
const table = useContext(TableContext);
|
const table = useContext(TableContext);
|
||||||
@@ -13,8 +14,11 @@ export default (props: { playerKey: string } & Stylable) => {
|
|||||||
.thru((Evt) => createObservableWithInit(Evt, false)) ??
|
.thru((Evt) => createObservableWithInit(Evt, false)) ??
|
||||||
createSignal(false)[0];
|
createSignal(false)[0];
|
||||||
|
|
||||||
|
const game = useContext(GameContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
id={`player-${props.playerKey}`}
|
||||||
style={{
|
style={{
|
||||||
...props.style,
|
...props.style,
|
||||||
"background-color": playerColor(props.playerKey),
|
"background-color": playerColor(props.playerKey),
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
|
import type { TWsIn, TWsOut } from "@games/server/src/table";
|
||||||
|
import { fromPromise, Stream } from "kefir";
|
||||||
import {
|
import {
|
||||||
Accessor,
|
Accessor,
|
||||||
createContext,
|
createContext,
|
||||||
createEffect,
|
createEffect,
|
||||||
createResource,
|
|
||||||
createSignal,
|
createSignal,
|
||||||
For,
|
For,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
Show,
|
Show,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { TWsIn, TWsOut } from "../../../server/src/table";
|
import api, { fromWebsocket } from "~/api";
|
||||||
import api, { fromWebsocket } from "../api";
|
import { createObservable, createObservableWithInit, cx } from "~/fn";
|
||||||
import { createObservable, createObservableWithInit, cx } from "../fn";
|
import { me, mePromise } from "~/profile";
|
||||||
import { me } from "../profile";
|
|
||||||
import Game from "./Game";
|
import Game from "./Game";
|
||||||
import Player from "./Player";
|
import Player from "./Player";
|
||||||
import { fromPromise, Stream } from "kefir";
|
|
||||||
|
|
||||||
export const TableContext = createContext<{
|
export const TableContext = createContext<{
|
||||||
view: Accessor<any>;
|
view: Accessor<any>;
|
||||||
@@ -38,7 +37,7 @@ 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 !== undefined);
|
||||||
|
|
||||||
const players = createObservableWithInit<string[]>(
|
const players = createObservableWithInit<string[]>(
|
||||||
presenceEvents.map((evt) => evt.players!),
|
presenceEvents.map((evt) => evt.players!),
|
||||||
@@ -46,6 +45,15 @@ export default (props: { tableKey: string }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [ready, setReady] = createSignal(false);
|
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() }));
|
createEffect(() => sendWs({ ready: ready() }));
|
||||||
const view = createObservable(gameEvents.map((evt) => evt.view));
|
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 { A, useParams } from "@solidjs/router";
|
||||||
|
|
||||||
import Table from "../components/Table";
|
import Table from "~/components/Table";
|
||||||
import { Show } from "solid-js";
|
import { Show } from "solid-js";
|
||||||
import { me } from "../profile";
|
import { me } from "~/profile";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { tableKey } = useParams();
|
const { tableKey } = useParams();
|
||||||
@@ -10,9 +10,6 @@ export default () => {
|
|||||||
return (
|
return (
|
||||||
<Show when={me() != null}>
|
<Show when={me() != null}>
|
||||||
<Table tableKey={tableKey} />
|
<Table tableKey={tableKey} />
|
||||||
<A href={"/"} class="fixed tl m-4 px-2 py-1.5 button">
|
|
||||||
Back
|
|
||||||
</A>
|
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { A } from "@solidjs/router";
|
import { A } from "@solidjs/router";
|
||||||
import { createEffect, createResource, For } from "solid-js";
|
import { createEffect, createResource, For } from "solid-js";
|
||||||
import api from "../api";
|
import api from "~/api";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [games] = createResource(async () =>
|
const [games] = createResource(async () =>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
@@ -12,9 +13,10 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"],
|
"^/*": ["./*"],
|
||||||
"$/*": ["./"],
|
"@games/*": ["./packages/*"],
|
||||||
"@/*": ["./public/*"]
|
"$/*": ["./packages/client/*"],
|
||||||
|
"~/*": ["./packages/client/src/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import solidPlugin from "vite-plugin-solid";
|
import solidPlugin from "vite-plugin-solid";
|
||||||
import UnoCSS from "unocss/vite";
|
import UnoCSS from "unocss/vite";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [solidPlugin(), UnoCSS()],
|
plugins: [solidPlugin(), UnoCSS()],
|
||||||
@@ -8,4 +9,12 @@ export default defineConfig({
|
|||||||
outDir: "../server/public",
|
outDir: "../server/public",
|
||||||
emptyOutDir: true,
|
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({
|
export const WsOut = t.Object({
|
||||||
players: t.Optional(t.Array(t.String())),
|
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()),
|
view: t.Optional(t.Any()),
|
||||||
});
|
});
|
||||||
export type TWsOut = typeof WsOut.static;
|
export type TWsOut = typeof WsOut.static;
|
||||||
@@ -31,15 +31,15 @@ type TablePayload<GameConfig, GameState, GameAction> = {
|
|||||||
never
|
never
|
||||||
>;
|
>;
|
||||||
|
|
||||||
readys: TBus<Attributed & { ready: boolean }, never>;
|
readys: TBus<Attributed & { ready: boolean }, any>;
|
||||||
actions: TBus<Attributed & GameAction, never>;
|
actions: TBus<Attributed & GameAction, any>;
|
||||||
quits: TBus<Attributed, never>;
|
quits: TBus<Attributed, any>;
|
||||||
};
|
};
|
||||||
outputs: {
|
outputs: {
|
||||||
playersPresent: Property<string[], never>;
|
playersPresent: Property<string[], any>;
|
||||||
playersReady: Property<{ [key: string]: boolean }, unknown>;
|
playersReady: Property<{ [key: string]: boolean } | null, any>;
|
||||||
gameConfig: Property<GameConfig | null, never>;
|
gameConfig: Property<GameConfig | null, any>;
|
||||||
gameState: Property<GameState | null, never>;
|
gameState: Property<GameState | null, any>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
|
|||||||
quits: Bus(),
|
quits: Bus(),
|
||||||
};
|
};
|
||||||
const { connectionChanges, readys, actions, quits } = inputs;
|
const { connectionChanges, readys, actions, quits } = inputs;
|
||||||
|
quits.log("quits");
|
||||||
// =======
|
// =======
|
||||||
|
|
||||||
const playersPresent = connectionChanges
|
const playersPresent = connectionChanges
|
||||||
@@ -78,34 +79,45 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
|
|||||||
.map((counts) => Object.keys(counts))
|
.map((counts) => Object.keys(counts))
|
||||||
.toProperty();
|
.toProperty();
|
||||||
|
|
||||||
|
const gameEnds = quits.map((_) => null);
|
||||||
|
|
||||||
|
const gameStarts = pool<null, any>();
|
||||||
const playersReady = transform(
|
const playersReady = transform(
|
||||||
{} as { [key: string]: boolean },
|
null as { [key: string]: boolean } | null,
|
||||||
[
|
[
|
||||||
playersPresent,
|
playersPresent,
|
||||||
(prev, players: string[]) =>
|
(prev, players: string[]) =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
players.map((p) => [p, prev[p] ?? false])
|
players.map((p) => [p, prev?.[p] ?? false])
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
readys,
|
readys,
|
||||||
(prev, evt: { humanKey: string; ready: boolean }) =>
|
(prev, evt: { humanKey: string; ready: boolean }) =>
|
||||||
prev[evt.humanKey] != null
|
prev?.[evt.humanKey] != null
|
||||||
? { ...prev, [evt.humanKey]: evt.ready }
|
? { ...prev, [evt.humanKey]: evt.ready }
|
||||||
: prev,
|
: prev,
|
||||||
|
],
|
||||||
|
[gameStarts, () => null],
|
||||||
|
[
|
||||||
|
combine([gameEnds], [playersPresent], (_, players) => players),
|
||||||
|
(_, players: string[]) =>
|
||||||
|
Object.fromEntries(players.map((p) => [p, false])),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
.toProperty()
|
.toProperty()
|
||||||
.log("playersReady");
|
.log("playersReady");
|
||||||
|
|
||||||
const gameStarts = playersReady
|
gameStarts.plug(
|
||||||
.filter(
|
playersReady
|
||||||
(pr) =>
|
.filter(
|
||||||
Object.values(pr).length > 0 &&
|
(pr) =>
|
||||||
Object.values(pr).every((ready) => ready)
|
Object.values(pr ?? {}).length > 0 &&
|
||||||
)
|
Object.values(pr!).every((ready) => ready)
|
||||||
.map((_) => null)
|
)
|
||||||
.log("gameStarts");
|
.map((_) => null)
|
||||||
|
.log("gameStarts")
|
||||||
|
);
|
||||||
|
|
||||||
const gameConfigPool = pool<
|
const gameConfigPool = pool<
|
||||||
{
|
{
|
||||||
@@ -144,8 +156,12 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
|
|||||||
humanKey: evt.action.humanKey,
|
humanKey: evt.action.humanKey,
|
||||||
})
|
})
|
||||||
: prev,
|
: prev,
|
||||||
]
|
],
|
||||||
|
[quits, () => null]
|
||||||
).toProperty();
|
).toProperty();
|
||||||
|
gameState
|
||||||
|
.map((state) => JSON.stringify(state).substring(0, 10))
|
||||||
|
.log("gameState");
|
||||||
|
|
||||||
const gameIsActive = gameState
|
const gameIsActive = gameState
|
||||||
.map((gs) => gs != null)
|
.map((gs) => gs != null)
|
||||||
@@ -166,9 +182,9 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
|
|||||||
inputs,
|
inputs,
|
||||||
outputs: {
|
outputs: {
|
||||||
playersPresent,
|
playersPresent,
|
||||||
playersReady,
|
playersReady: playersReady.toProperty(),
|
||||||
gameConfig: gameConfig as Property<unknown, never>,
|
gameConfig: gameConfig as Property<unknown, any>,
|
||||||
gameState: gameState as Property<unknown, never>,
|
gameState: gameState as Property<unknown, any>,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user