diff --git a/package.json b/package.json index 2ae47b1..dcbc4c7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "games", "type": "module", - "version": "0.0.2", + "version": "0.0.4", "scripts": { "dev": "pnpm --parallel dev", "build": "pnpm run -F client build", diff --git a/packages/client/package.json b/packages/client/package.json index f50e6e7..227f358 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,7 +1,7 @@ { "name": "@games/client", "type": "module", - "version": "0.0.3", + "version": "0.0.4", "scripts": { "dev": "vite --port 3000", "build": "vite build" @@ -11,6 +11,7 @@ "@solid-primitives/scheduled": "^1.5.2", "@solidjs/router": "^0.15.3", "js-cookie": "^3.0.5", + "kefir": "^3.8.8", "kefir-bus": "^2.3.1", "object-hash": "^3.0.0", "solid-js": "^1.9.5" diff --git a/packages/client/src/api.ts b/packages/client/src/api.ts index ea60fbc..526352c 100644 --- a/packages/client/src/api.ts +++ b/packages/client/src/api.ts @@ -13,4 +13,6 @@ const { api } = treaty( export default api; export const fromWebsocket = (ws: any) => - fromEvents(ws, "message"); + fromEvents(ws, "message").map( + (evt) => (evt as unknown as { data: T }).data + ); diff --git a/packages/client/src/components/Game.tsx b/packages/client/src/components/Game.tsx index 5503345..3efc0a9 100644 --- a/packages/client/src/components/Game.tsx +++ b/packages/client/src/components/Game.tsx @@ -1,25 +1,36 @@ -import { Accessor, createContext } from "solid-js"; +import { Accessor, createContext, useContext } from "solid-js"; import { SimpleAction, SimplePlayerView, + vSimpleGameState, } from "../../../server/src/games/simple"; +import Pile from "./Pile"; +import { TableContext } from "./Table"; +import Hand from "./Hand"; export const GameContext = createContext<{ - view: Accessor; - submitAction: (action: SimpleAction) => Promise; + view: Accessor; + submitAction: (action: SimpleAction) => any; }>(); export default () => { - return ( - <> - Game started! - {/* submitAction({ type: "draw" })} - /> + const table = useContext(TableContext)!; + const view = table.view as Accessor; + const submitAction = (action: SimpleAction) => table.sendWs({ action }); - */} - + return ( + + submitAction({ type: "draw" })} + /> + + submitAction({ type: "discard", card })} + /> + ); }; diff --git a/packages/client/src/components/Hand.tsx b/packages/client/src/components/Hand.tsx index 692ea43..ef89a33 100644 --- a/packages/client/src/components/Hand.tsx +++ b/packages/client/src/components/Hand.tsx @@ -1,26 +1,26 @@ import { Component, For, useContext } from "solid-js"; 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 { produce } from "solid-js/store"; import { Stylable } from "./toolbox"; export default ((props) => { - const { submitAction, view } = useContext(GameContext)!; - return (
- {(card) => ( + {(card, i) => ( submitAction({ type: "discard", card })} + onClick={() => props.onClickCard?.(card, i())} /> )}
); -}) satisfies Component<{ hand: Hand } & Stylable>; +}) satisfies Component< + { hand: THand; onClickCard?: (card: TCard, i: number) => any } & Stylable +>; diff --git a/packages/client/src/components/Player.tsx b/packages/client/src/components/Player.tsx index e69de29..80a34c2 100644 --- a/packages/client/src/components/Player.tsx +++ b/packages/client/src/components/Player.tsx @@ -0,0 +1,18 @@ +import { playerColor, profile } from "../profile"; +import { Stylable } from "./toolbox"; + +export default (props: { playerKey: string } & Stylable) => { + return ( +
+

+ {profile(props.playerKey)()?.name} +

+
+ ); +}; diff --git a/packages/client/src/components/Table.tsx b/packages/client/src/components/Table.tsx index 67dc607..c5fc83a 100644 --- a/packages/client/src/components/Table.tsx +++ b/packages/client/src/components/Table.tsx @@ -1,6 +1,7 @@ import { Accessor, createContext, + createEffect, createSignal, For, onCleanup, @@ -14,6 +15,8 @@ import Bus from "kefir-bus"; import { createObservable, createObservableWithInit, WSEvent } from "../fn"; import { EdenWS } from "@elysiajs/eden/treaty"; import { TWsIn, TWsOut } from "../../../server/src/table"; +import Player from "./Player"; +import Game from "./Game"; export const TableContext = createContext<{ players: Accessor; @@ -27,18 +30,20 @@ export default (props: { tableKey: string }) => { onCleanup(() => ws.close()); const presenceEvents = wsEvents.filter((evt) => evt.players != null); + const gameEvents = wsEvents.filter((evt) => evt.view != null); const players = createObservableWithInit( presenceEvents.map((evt) => evt.players!), [] ); + const view = createObservable(gameEvents.map((evt) => evt.view)); return ( ws.send(evt), view, players, }} @@ -53,19 +58,14 @@ export default (props: { tableKey: string }) => { return 1 - y; }; return ( -
-

- {profile(player)()?.name} -

-
+ /> ); }} @@ -89,7 +89,7 @@ export default (props: { tableKey: string }) => { -
Game started!
+
); diff --git a/packages/client/src/global.d.ts b/packages/client/src/global.d.ts new file mode 100644 index 0000000..dc6f10c --- /dev/null +++ b/packages/client/src/global.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index 2575a83..375b5b3 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -62,31 +62,6 @@ const api = new Elysia({ prefix: "/api" }) ) .get("/games", () => [{ key: "simple", name: "simple" }]) .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({ data: { params: { tableKey }, @@ -108,6 +83,32 @@ const api = new Elysia({ prefix: "/api" }) ); 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({ data: { params: { tableKey }, diff --git a/packages/server/src/games/simple.ts b/packages/server/src/games/simple.ts index 17a1c08..9320e61 100644 --- a/packages/server/src/games/simple.ts +++ b/packages/server/src/games/simple.ts @@ -71,9 +71,16 @@ export const resolveAction = ( humanId: string, action: SimpleAction ): 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]; if (action.type == "draw") { const [drawn, ...rest] = state.deck; + console.log("drew card", JSON.stringify(drawn)); return { deck: rest, players: { diff --git a/packages/server/src/table.ts b/packages/server/src/table.ts index 6ee460b..a5a884c 100644 --- a/packages/server/src/table.ts +++ b/packages/server/src/table.ts @@ -21,15 +21,16 @@ export const WsIn = t.Union([ ]); export type TWsIn = typeof WsIn.static; +type Attributed = { humanKey: string }; type TablePayload = { inputs: { presenceChanges: TBus< - { humanKey: string; presence: "joined" | "left" }, + Attributed & { presence: "joined" | "left" }, never >; gameProposals: TBus<{ proposeGame: string }, never>; gameStarts: TBus<{ startGame: true }, never>; - gameActions: TBus; + gameActions: TBus; }; outputs: { playersPresent: Property; @@ -63,26 +64,22 @@ export const liveTable = (key: string) => { }, [] as string[]); const gameState = transform( - null as GameState | null, + null as SimpleGameState | null, [ - gameStarts.thru((Evt) => - combine([Evt], [playersPresent], (evt, players) => ({ - ...evt, - players, - })) - ), + combine([gameStarts], [playersPresent], (evt, players) => ({ + ...evt, + players, + })), (prev, evt: { players: string[] }) => - prev == null ? (newGame(evt.players) as GameState) : prev, + prev == null + ? (newGame(evt.players) as SimpleGameState) + : prev, ], [ gameActions, - (prev, evt: GameAction) => + (prev, evt: Attributed & SimpleAction) => prev != null - ? (resolveAction( - prev as unknown as SimpleGameState, - "evt", - evt as SimpleAction - ) as GameState) + ? resolveAction(prev, evt.humanKey, evt) : prev, ] ).toProperty(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c24abf..055da3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: js-cookie: specifier: ^3.0.5 version: 3.0.5 + kefir: + specifier: ^3.8.8 + version: 3.8.8 kefir-bus: specifier: ^2.3.1 version: 2.3.1(kefir@3.8.8)