we did it

This commit is contained in:
2025-08-30 22:30:31 -04:00
parent 01a12ec58a
commit 11f21221ee
8 changed files with 61 additions and 72 deletions

View File

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

View File

@@ -2,7 +2,7 @@ import { Accessor, createContext, For, useContext } from "solid-js";
import type { import type {
SimpleAction, SimpleAction,
SimplePlayerView, SimplePlayerView,
} from "@games/server/src/games/simple"; } from "@games/shared/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";
@@ -44,7 +44,9 @@ export default () => {
</div> </div>
<button <button
class="button fixed tl m-4 p-1" class="button fixed tl m-4 p-1"
onClick={() => table.sendWs({ quit: true })} onClick={() => {
table.sendWs({ quit: true });
}}
> >
Quit Quit
</button> </button>
@@ -54,10 +56,7 @@ export default () => {
mount={document.getElementById(`player-${playerKey}`)!} mount={document.getElementById(`player-${playerKey}`)!}
ref={(ref) => { ref={(ref) => {
const midOffset = const midOffset =
i() + i() + 0.5 - Object.values(view().playerHandCounts).length / 2;
0.5 -
Object.values(view().playerHandCounts).length /
2;
ref.style = `position: absolute; display: flex; justify-content: center; top: 65%; transform: translate(${Math.abs( ref.style = `position: absolute; display: flex; justify-content: center; top: 65%; transform: translate(${Math.abs(
midOffset * 0 midOffset * 0

View File

@@ -37,11 +37,11 @@ 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.playersPresent != null);
const gameEvents = wsEvents.filter((evt) => evt.view !== undefined); 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.playersPresent!),
[] []
); );

View File

@@ -80,14 +80,13 @@ const api = new Elysia({ prefix: "/api" })
body: WsIn, body: WsIn,
response: WsOut, response: WsOut,
open: ({ open({
data: { data: {
params: { tableKey }, params: { tableKey },
humanKey, humanKey,
}, },
send, send,
}) => { }) {
console.log("websocket opened");
const table = liveTable(tableKey); const table = liveTable(tableKey);
table.inputs.connectionChanges.emit({ table.inputs.connectionChanges.emit({
@@ -103,7 +102,7 @@ const api = new Elysia({ prefix: "/api" })
); );
}, },
message: ( message(
{ {
data: { data: {
humanKey, humanKey,
@@ -111,20 +110,25 @@ const api = new Elysia({ prefix: "/api" })
}, },
}, },
body body
) => liveTable(tableKey).inputs.messages.emit({ ...body, humanKey }), ) {
liveTable(tableKey).inputs.messages.emit({ ...body, humanKey });
},
close: ({ close({
data: { data: {
params: { tableKey }, params: { tableKey },
humanKey, humanKey,
}, },
}) => }) {
liveTable(tableKey).inputs.connectionChanges.emit({ liveTable(tableKey).inputs.connectionChanges.emit({
humanKey, humanKey,
presence: "left", presence: "left",
}), });
},
error: (error) => err(error), error(error) {
err(error);
},
}); });
export default api; export default api;

View File

@@ -16,7 +16,6 @@ new Elysia()
}) })
) )
.onRequest(({ request }) => console.log(request.url))
.onError(({ error }) => console.error(error)) .onError(({ error }) => console.error(error))
.get("/ping", () => "pong") .get("/ping", () => "pong")
@@ -25,4 +24,4 @@ new Elysia()
.listen(port); .listen(port);
log.log(`server started on ${port}`); console.log(`server started on ${port}`);

View File

@@ -13,8 +13,9 @@ import Bus, { type Bus as TBus } from "kefir-bus";
import { log } from "./logging"; import { log } from "./logging";
export const WsOut = t.Object({ export const WsOut = t.Object({
players: t.Optional(t.Array(t.String())), playersPresent: t.Optional(t.Array(t.String())),
playersReady: t.Optional(t.Nullable(t.Record(t.String(), t.Boolean()))), playersReady: t.Optional(t.Nullable(t.Record(t.String(), t.Boolean()))),
gameConfig: t.Optional(t.Any()),
view: t.Optional(t.Any()), view: t.Optional(t.Any()),
}); });
export type TWsOut = typeof WsOut.static; export type TWsOut = typeof WsOut.static;
@@ -53,7 +54,7 @@ type TablePayload<
}; };
player: { player: {
[key: string]: { [key: string]: {
view: Property<GameView, any>; view: Property<GameView | null, any>;
}; };
}; };
}; };
@@ -69,7 +70,8 @@ export const liveTable = <
players: string[]; players: string[];
}, },
GameState, GameState,
GameAction extends Attributed GameAction extends Attributed,
GameView
>( >(
key: string key: string
) => { ) => {
@@ -98,11 +100,9 @@ export const liveTable = <
.map((counts) => Object.keys(counts)) .map((counts) => Object.keys(counts))
.toProperty(); .toProperty();
const playerStreams: TablePayload< const playerStreams: {
GameConfig, [key: string]: { view: Property<GameView | null, any> };
GameState, } = {};
GameAction
>["outputs"]["player"] = {};
playersPresent playersPresent
.map(set) .map(set)
.slidingWindow(2, 2) .slidingWindow(2, 2)
@@ -110,7 +110,12 @@ export const liveTable = <
.onValue(({ added, removed }) => { .onValue(({ added, removed }) => {
added.forEach((p) => { added.forEach((p) => {
playerStreams[p] = { playerStreams[p] = {
view: Bus(), view: combine([gameState], [gameImpl], (a, b) => [a, b] as const)
.map(
([state, game]) =>
state && (game.getView({ state, humanKey: p }) as GameView)
)
.toProperty(),
}; };
}); });
removed.forEach((p) => { removed.forEach((p) => {
@@ -135,12 +140,12 @@ export const liveTable = <
[key: string]: boolean; [key: string]: boolean;
} | null, } | null,
[ [
playersPresent, playersPresent, // TODO: filter to only outside active games
(prev, players: ValueWithin<typeof playersPresent>) => (prev, players: ValueWithin<typeof playersPresent>) =>
Object.fromEntries(players.map((p) => [p, prev?.[p] ?? false])), Object.fromEntries(players.map((p) => [p, prev?.[p] ?? false])),
], ],
[ [
ready, ready, // TODO: filter to only outside active games
(prev, evt: ValueWithin<typeof ready>) => (prev, evt: ValueWithin<typeof ready>) =>
prev?.[evt.humanKey] != null prev?.[evt.humanKey] != null
? { ? {
@@ -155,9 +160,7 @@ export const liveTable = <
(_, players: ValueWithin<typeof playersPresent>) => (_, players: ValueWithin<typeof playersPresent>) =>
Object.fromEntries(players.map((p) => [p, false])), Object.fromEntries(players.map((p) => [p, false])),
] ]
) ).toProperty();
.toProperty()
.log("playersReady");
const gameStarts = playersReady const gameStarts = playersReady
.filter( .filter(
@@ -165,16 +168,9 @@ export const liveTable = <
Object.values(pr ?? {}).length > 0 && Object.values(pr ?? {}).length > 0 &&
Object.values(pr!).every((ready) => ready) Object.values(pr!).every((ready) => ready)
) )
.map((_) => null) .map((_) => null);
.log("gameStarts");
const gameConfigPool = pool< const gameConfigPool = pool<GameConfig, any>();
{
game: GameKey;
players: string[];
},
any
>();
const gameConfig = gameConfigPool.toProperty(); const gameConfig = gameConfigPool.toProperty();
const gameImpl = gameConfig const gameImpl = gameConfig
@@ -191,24 +187,19 @@ export const liveTable = <
prev || (game.init() as GameState), prev || (game.init() as GameState),
], ],
[ [
combine([action], [gameImpl], (action, impl) => ({ combine([action], [gameImpl], (act, game) => [act, game] as const),
action,
...impl,
})),
( (
prev, prev,
{ [{ action, humanKey }, game]: [
game, Attributed & { action: GameAction },
action, Game
}: { ]
game: Game;
action: Attributed & GameAction;
}
) => ) =>
prev && prev &&
(game.resolveAction({ (game.resolveAction({
state: prev, state: prev,
action, action,
humanKey,
}) as GameState), }) as GameState),
], ],
[quit, () => null] [quit, () => null]
@@ -233,7 +224,7 @@ export const liveTable = <
}), }),
] ]
// TODO: Add player defined config changes // TODO: Add player defined config changes
) ) as unknown as Observable<GameConfig, any>
); );
tables[key] = { tables[key] = {
@@ -241,10 +232,10 @@ export const liveTable = <
outputs: { outputs: {
global: { global: {
playersPresent, playersPresent,
playersReady: playersReady.toProperty(), playersReady,
gameConfig: gameConfig as Property<unknown, any>, gameConfig,
}, },
player: {}, player: playerStreams,
}, },
}; };

View File

@@ -2,24 +2,24 @@ import simple from "./simple";
export type Game< export type Game<
S = unknown, S = unknown,
A extends { humanKey: string } = { humanKey: string }, A = unknown,
E extends { error: any } = { error: any }, E extends { error: any } = { error: any },
V = unknown V = unknown
> = { > = {
title: string; title: string;
rules: string; rules: string;
init: () => S; init: () => S;
resolveAction: (p: { state: S; action: A }) => S | E; resolveAction: (p: { state: S; action: A; humanKey: string }) => S | E;
getView: (p: { state: S; humanKey: string }) => V; getView: (p: { state: S; humanKey: string }) => V;
resolveQuit: (p: { state: S; humanKey: string }) => S; resolveQuit: (p: { state: S; humanKey: string }) => S;
}; };
export const GAMES = { export const GAMES: {
[key: string]: (config: { game: string; players: string[] }) => Game;
} = {
// renaissance, // renaissance,
simple, simple,
} satisfies {
[key: string]: (config: { game: string; players: string[] }) => Game;
}; };
export default GAMES; export default GAMES;
export type GameKey = keyof typeof GAMES; export type GameKey = string;

View File

@@ -22,10 +22,7 @@ export type SimplePlayerView = {
myHand: Hand<Card>; myHand: Hand<Card>;
}; };
export type SimpleAction = { humanKey: string } & ( export type SimpleAction = { type: "draw" } | { type: "discard"; card: Card };
| { type: "draw" }
| { type: "discard"; card: Card }
);
export const newSimpleGameState = ( export const newSimpleGameState = (
config: SimpleConfiguration config: SimpleConfiguration
@@ -34,9 +31,7 @@ export const newSimpleGameState = (
return { return {
deck: shuffle(newDeck()), deck: shuffle(newDeck()),
turnIdx: 0, turnIdx: 0,
playerHands: Object.fromEntries( playerHands: Object.fromEntries(players.map((humanKey) => [humanKey, []])),
players.map((humanKey) => [humanKey, []])
),
}; };
}; };
@@ -59,12 +54,13 @@ export const resolveSimpleAction = ({
config, config,
state, state,
action, action,
humanKey,
}: { }: {
config: SimpleConfiguration; config: SimpleConfiguration;
state: SimpleGameState; state: SimpleGameState;
action: SimpleAction; action: SimpleAction;
humanKey: string;
}): SimpleGameState => { }): SimpleGameState => {
const { humanKey } = action;
const playerHand = state.playerHands[humanKey]; const playerHand = state.playerHands[humanKey];
if (playerHand == null) { if (playerHand == null) {
throw new Error( throw new Error(