end to end!
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "games",
|
"name": "games",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.2",
|
"version": "0.0.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm --parallel dev",
|
"dev": "pnpm --parallel dev",
|
||||||
"build": "pnpm run -F client build",
|
"build": "pnpm run -F client build",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@games/client",
|
"name": "@games/client",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.3",
|
"version": "0.0.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 3000",
|
"dev": "vite --port 3000",
|
||||||
"build": "vite build"
|
"build": "vite build"
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"@solid-primitives/scheduled": "^1.5.2",
|
"@solid-primitives/scheduled": "^1.5.2",
|
||||||
"@solidjs/router": "^0.15.3",
|
"@solidjs/router": "^0.15.3",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
"kefir": "^3.8.8",
|
||||||
"kefir-bus": "^2.3.1",
|
"kefir-bus": "^2.3.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"solid-js": "^1.9.5"
|
"solid-js": "^1.9.5"
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ const { api } = treaty<Api>(
|
|||||||
export default api;
|
export default api;
|
||||||
|
|
||||||
export const fromWebsocket = <T>(ws: any) =>
|
export const fromWebsocket = <T>(ws: any) =>
|
||||||
fromEvents<T, never>(ws, "message");
|
fromEvents(ws, "message").map(
|
||||||
|
(evt) => (evt as unknown as { data: T }).data
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,25 +1,36 @@
|
|||||||
import { Accessor, createContext } from "solid-js";
|
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 Pile from "./Pile";
|
||||||
|
import { TableContext } from "./Table";
|
||||||
|
import Hand from "./Hand";
|
||||||
|
|
||||||
export const GameContext = createContext<{
|
export const GameContext = createContext<{
|
||||||
view: Accessor<SimplePlayerView | undefined>;
|
view: Accessor<SimplePlayerView>;
|
||||||
submitAction: (action: SimpleAction) => Promise<any>;
|
submitAction: (action: SimpleAction) => any;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return (
|
const table = useContext(TableContext)!;
|
||||||
<>
|
const view = table.view as Accessor<SimplePlayerView>;
|
||||||
Game started!
|
const submitAction = (action: SimpleAction) => table.sendWs({ action });
|
||||||
{/* <Pile
|
|
||||||
count={view()!.deckCount}
|
|
||||||
class="cursor-pointer fixed center"
|
|
||||||
onClick={() => submitAction({ type: "draw" })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Hand class="fixed bc" hand={view()!.myHand} /> */}
|
return (
|
||||||
</>
|
<GameContext.Provider value={{ view, submitAction }}>
|
||||||
|
<Pile
|
||||||
|
count={view().deckCount}
|
||||||
|
class="cursor-pointer fixed center"
|
||||||
|
onClick={() => submitAction({ type: "draw" })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Hand
|
||||||
|
class="fixed bc"
|
||||||
|
hand={view().myHand}
|
||||||
|
onClickCard={(card) => submitAction({ type: "discard", card })}
|
||||||
|
/>
|
||||||
|
</GameContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import { Component, For, useContext } from "solid-js";
|
import { Component, For, useContext } from "solid-js";
|
||||||
import Card from "./Card";
|
import Card from "./Card";
|
||||||
import { Hand } from "../../../shared/cards";
|
import type { Hand as THand, Card as TCard } from "../../../shared/cards";
|
||||||
import { GameContext } from "./Game";
|
import { GameContext } from "./Game";
|
||||||
import { produce } from "solid-js/store";
|
import { produce } from "solid-js/store";
|
||||||
import { Stylable } from "./toolbox";
|
import { Stylable } from "./toolbox";
|
||||||
|
|
||||||
export default ((props) => {
|
export default ((props) => {
|
||||||
const { submitAction, view } = useContext(GameContext)!;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={"hand " + props.class} style={props.style}>
|
<div class={"hand " + props.class} style={props.style}>
|
||||||
<For each={props.hand}>
|
<For each={props.hand}>
|
||||||
{(card) => (
|
{(card, i) => (
|
||||||
<Card
|
<Card
|
||||||
card={card}
|
card={card}
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={() => submitAction({ type: "discard", card })}
|
onClick={() => props.onClickCard?.(card, i())}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}) satisfies Component<{ hand: Hand } & Stylable>;
|
}) satisfies Component<
|
||||||
|
{ hand: THand; onClickCard?: (card: TCard, i: number) => any } & Stylable
|
||||||
|
>;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { playerColor, profile } from "../profile";
|
||||||
|
import { Stylable } from "./toolbox";
|
||||||
|
|
||||||
|
export default (props: { playerKey: string } & Stylable) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...props.style,
|
||||||
|
"background-color": playerColor(props.playerKey),
|
||||||
|
}}
|
||||||
|
class={`${props.class} w-20 h-20 rounded-full flex justify-center items-center`}
|
||||||
|
>
|
||||||
|
<p style={{ "font-size": "1em" }}>
|
||||||
|
{profile(props.playerKey)()?.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Accessor,
|
Accessor,
|
||||||
createContext,
|
createContext,
|
||||||
|
createEffect,
|
||||||
createSignal,
|
createSignal,
|
||||||
For,
|
For,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
@@ -14,6 +15,8 @@ import Bus from "kefir-bus";
|
|||||||
import { createObservable, createObservableWithInit, WSEvent } from "../fn";
|
import { createObservable, createObservableWithInit, WSEvent } from "../fn";
|
||||||
import { EdenWS } from "@elysiajs/eden/treaty";
|
import { EdenWS } from "@elysiajs/eden/treaty";
|
||||||
import { TWsIn, TWsOut } from "../../../server/src/table";
|
import { TWsIn, TWsOut } from "../../../server/src/table";
|
||||||
|
import Player from "./Player";
|
||||||
|
import Game from "./Game";
|
||||||
|
|
||||||
export const TableContext = createContext<{
|
export const TableContext = createContext<{
|
||||||
players: Accessor<string[]>;
|
players: Accessor<string[]>;
|
||||||
@@ -27,18 +30,20 @@ export default (props: { tableKey: string }) => {
|
|||||||
onCleanup(() => ws.close());
|
onCleanup(() => 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[]>(
|
||||||
presenceEvents.map((evt) => evt.players!),
|
presenceEvents.map((evt) => evt.players!),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const view = createObservable(gameEvents.map((evt) => evt.view));
|
const view = createObservable(gameEvents.map((evt) => evt.view));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContext.Provider
|
<TableContext.Provider
|
||||||
value={{
|
value={{
|
||||||
sendWs: ws.send,
|
sendWs: (evt) => ws.send(evt),
|
||||||
view,
|
view,
|
||||||
players,
|
players,
|
||||||
}}
|
}}
|
||||||
@@ -53,19 +58,14 @@ export default (props: { tableKey: string }) => {
|
|||||||
return 1 - y;
|
return 1 - y;
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div
|
<Player
|
||||||
|
playerKey={player}
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(0, ${
|
transform: `translate(0, ${
|
||||||
verticalOffset() * 150
|
verticalOffset() * 150
|
||||||
}vh)`,
|
}vh)`,
|
||||||
"background-color": playerColor(player),
|
|
||||||
}}
|
}}
|
||||||
class="w-20 h-20 rounded-full flex justify-center items-center"
|
/>
|
||||||
>
|
|
||||||
<p style={{ "font-size": "1em" }}>
|
|
||||||
{profile(player)()?.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
@@ -89,7 +89,7 @@ export default (props: { tableKey: string }) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={view() != null}>
|
<Show when={view() != null}>
|
||||||
<div>Game started!</div>
|
<Game />
|
||||||
</Show>
|
</Show>
|
||||||
</TableContext.Provider>
|
</TableContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
1
packages/client/src/global.d.ts
vendored
Normal file
1
packages/client/src/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="@solidjs/start/env" />
|
||||||
@@ -62,31 +62,6 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
)
|
)
|
||||||
.get("/games", () => [{ key: "simple", name: "simple" }])
|
.get("/games", () => [{ key: "simple", name: "simple" }])
|
||||||
.ws("/ws/:tableKey", {
|
.ws("/ws/:tableKey", {
|
||||||
response: WsOut,
|
|
||||||
body: WsIn,
|
|
||||||
|
|
||||||
message(
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
humanKey,
|
|
||||||
params: { tableKey },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
inputs: { gameProposals, gameStarts, gameActions },
|
|
||||||
} = liveTable(tableKey);
|
|
||||||
|
|
||||||
if ("proposeGame" in body) {
|
|
||||||
gameProposals.emit(body);
|
|
||||||
} else if ("startGame" in body) {
|
|
||||||
gameStarts.emit(body);
|
|
||||||
} else if ("action" in body) {
|
|
||||||
gameActions.emit(body);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async open({
|
async open({
|
||||||
data: {
|
data: {
|
||||||
params: { tableKey },
|
params: { tableKey },
|
||||||
@@ -108,6 +83,32 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
);
|
);
|
||||||
table.inputs.presenceChanges.emit({ humanKey, presence: "joined" });
|
table.inputs.presenceChanges.emit({ humanKey, presence: "joined" });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
response: WsOut,
|
||||||
|
body: WsIn,
|
||||||
|
|
||||||
|
message(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
humanKey,
|
||||||
|
params: { tableKey },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
body
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
inputs: { gameProposals, gameStarts, gameActions },
|
||||||
|
} = liveTable(tableKey);
|
||||||
|
|
||||||
|
if ("proposeGame" in body) {
|
||||||
|
gameProposals.emit(body);
|
||||||
|
} else if ("startGame" in body) {
|
||||||
|
gameStarts.emit(body);
|
||||||
|
} else if ("action" in body) {
|
||||||
|
gameActions.emit({ humanKey, ...body.action });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async close({
|
async close({
|
||||||
data: {
|
data: {
|
||||||
params: { tableKey },
|
params: { tableKey },
|
||||||
|
|||||||
@@ -71,9 +71,16 @@ export const resolveAction = (
|
|||||||
humanId: string,
|
humanId: string,
|
||||||
action: SimpleAction
|
action: SimpleAction
|
||||||
): SimpleGameState => {
|
): SimpleGameState => {
|
||||||
|
console.log("attempting to resolve action", JSON.stringify(action));
|
||||||
|
if (!(humanId in state.players)) {
|
||||||
|
throw Error(
|
||||||
|
`${humanId} is not a player in this game; they cannot perform actions`
|
||||||
|
);
|
||||||
|
}
|
||||||
const playerHand = state.players[humanId];
|
const playerHand = state.players[humanId];
|
||||||
if (action.type == "draw") {
|
if (action.type == "draw") {
|
||||||
const [drawn, ...rest] = state.deck;
|
const [drawn, ...rest] = state.deck;
|
||||||
|
console.log("drew card", JSON.stringify(drawn));
|
||||||
return {
|
return {
|
||||||
deck: rest,
|
deck: rest,
|
||||||
players: {
|
players: {
|
||||||
|
|||||||
@@ -21,15 +21,16 @@ export const WsIn = t.Union([
|
|||||||
]);
|
]);
|
||||||
export type TWsIn = typeof WsIn.static;
|
export type TWsIn = typeof WsIn.static;
|
||||||
|
|
||||||
|
type Attributed = { humanKey: string };
|
||||||
type TablePayload<GameState, GameAction> = {
|
type TablePayload<GameState, GameAction> = {
|
||||||
inputs: {
|
inputs: {
|
||||||
presenceChanges: TBus<
|
presenceChanges: TBus<
|
||||||
{ humanKey: string; presence: "joined" | "left" },
|
Attributed & { presence: "joined" | "left" },
|
||||||
never
|
never
|
||||||
>;
|
>;
|
||||||
gameProposals: TBus<{ proposeGame: string }, never>;
|
gameProposals: TBus<{ proposeGame: string }, never>;
|
||||||
gameStarts: TBus<{ startGame: true }, never>;
|
gameStarts: TBus<{ startGame: true }, never>;
|
||||||
gameActions: TBus<GameAction, never>;
|
gameActions: TBus<Attributed & GameAction, never>;
|
||||||
};
|
};
|
||||||
outputs: {
|
outputs: {
|
||||||
playersPresent: Property<string[], never>;
|
playersPresent: Property<string[], never>;
|
||||||
@@ -63,26 +64,22 @@ export const liveTable = <GameState, GameAction>(key: string) => {
|
|||||||
}, [] as string[]);
|
}, [] as string[]);
|
||||||
|
|
||||||
const gameState = transform(
|
const gameState = transform(
|
||||||
null as GameState | null,
|
null as SimpleGameState | null,
|
||||||
[
|
[
|
||||||
gameStarts.thru((Evt) =>
|
combine([gameStarts], [playersPresent], (evt, players) => ({
|
||||||
combine([Evt], [playersPresent], (evt, players) => ({
|
...evt,
|
||||||
...evt,
|
players,
|
||||||
players,
|
})),
|
||||||
}))
|
|
||||||
),
|
|
||||||
(prev, evt: { players: string[] }) =>
|
(prev, evt: { players: string[] }) =>
|
||||||
prev == null ? (newGame(evt.players) as GameState) : prev,
|
prev == null
|
||||||
|
? (newGame(evt.players) as SimpleGameState)
|
||||||
|
: prev,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
gameActions,
|
gameActions,
|
||||||
(prev, evt: GameAction) =>
|
(prev, evt: Attributed & SimpleAction) =>
|
||||||
prev != null
|
prev != null
|
||||||
? (resolveAction(
|
? resolveAction(prev, evt.humanKey, evt)
|
||||||
prev as unknown as SimpleGameState,
|
|
||||||
"evt",
|
|
||||||
evt as SimpleAction
|
|
||||||
) as GameState)
|
|
||||||
: prev,
|
: prev,
|
||||||
]
|
]
|
||||||
).toProperty();
|
).toProperty();
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
|||||||
js-cookie:
|
js-cookie:
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
|
kefir:
|
||||||
|
specifier: ^3.8.8
|
||||||
|
version: 3.8.8
|
||||||
kefir-bus:
|
kefir-bus:
|
||||||
specifier: ^2.3.1
|
specifier: ^2.3.1
|
||||||
version: 2.3.1(kefir@3.8.8)
|
version: 2.3.1(kefir@3.8.8)
|
||||||
|
|||||||
Reference in New Issue
Block a user