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",
"type": "module",
"version": "0.0.5",
"version": "0.0.6",
"scripts": {
"dev": "pnpm --parallel dev",
"build": "pnpm run -F client build",

View File

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

View File

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

View File

@@ -16,7 +16,6 @@ new Elysia()
})
)
.onRequest(({ request }) => console.log(request.url))
.onError(({ error }) => console.error(error))
.get("/ping", () => "pong")
@@ -25,4 +24,4 @@ new Elysia()
.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";
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()))),
gameConfig: t.Optional(t.Any()),
view: t.Optional(t.Any()),
});
export type TWsOut = typeof WsOut.static;
@@ -53,7 +54,7 @@ type TablePayload<
};
player: {
[key: string]: {
view: Property<GameView, any>;
view: Property<GameView | null, any>;
};
};
};
@@ -69,7 +70,8 @@ export const liveTable = <
players: string[];
},
GameState,
GameAction extends Attributed
GameAction extends Attributed,
GameView
>(
key: string
) => {
@@ -98,11 +100,9 @@ export const liveTable = <
.map((counts) => Object.keys(counts))
.toProperty();
const playerStreams: TablePayload<
GameConfig,
GameState,
GameAction
>["outputs"]["player"] = {};
const playerStreams: {
[key: string]: { view: Property<GameView | null, any> };
} = {};
playersPresent
.map(set)
.slidingWindow(2, 2)
@@ -110,7 +110,12 @@ export const liveTable = <
.onValue(({ added, removed }) => {
added.forEach((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) => {
@@ -135,12 +140,12 @@ export const liveTable = <
[key: string]: boolean;
} | null,
[
playersPresent,
playersPresent, // TODO: filter to only outside active games
(prev, players: ValueWithin<typeof playersPresent>) =>
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.humanKey] != null
? {
@@ -155,9 +160,7 @@ export const liveTable = <
(_, players: ValueWithin<typeof playersPresent>) =>
Object.fromEntries(players.map((p) => [p, false])),
]
)
.toProperty()
.log("playersReady");
).toProperty();
const gameStarts = playersReady
.filter(
@@ -165,16 +168,9 @@ export const liveTable = <
Object.values(pr ?? {}).length > 0 &&
Object.values(pr!).every((ready) => ready)
)
.map((_) => null)
.log("gameStarts");
.map((_) => null);
const gameConfigPool = pool<
{
game: GameKey;
players: string[];
},
any
>();
const gameConfigPool = pool<GameConfig, any>();
const gameConfig = gameConfigPool.toProperty();
const gameImpl = gameConfig
@@ -191,24 +187,19 @@ export const liveTable = <
prev || (game.init() as GameState),
],
[
combine([action], [gameImpl], (action, impl) => ({
action,
...impl,
})),
combine([action], [gameImpl], (act, game) => [act, game] as const),
(
prev,
{
game,
action,
}: {
game: Game;
action: Attributed & GameAction;
}
[{ action, humanKey }, game]: [
Attributed & { action: GameAction },
Game
]
) =>
prev &&
(game.resolveAction({
state: prev,
action,
humanKey,
}) as GameState),
],
[quit, () => null]
@@ -233,7 +224,7 @@ export const liveTable = <
}),
]
// TODO: Add player defined config changes
)
) as unknown as Observable<GameConfig, any>
);
tables[key] = {
@@ -241,10 +232,10 @@ export const liveTable = <
outputs: {
global: {
playersPresent,
playersReady: playersReady.toProperty(),
gameConfig: gameConfig as Property<unknown, any>,
playersReady,
gameConfig,
},
player: {},
player: playerStreams,
},
};

View File

@@ -2,24 +2,24 @@ import simple from "./simple";
export type Game<
S = unknown,
A extends { humanKey: string } = { humanKey: string },
A = unknown,
E extends { error: any } = { error: any },
V = unknown
> = {
title: string;
rules: string;
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;
resolveQuit: (p: { state: S; humanKey: string }) => S;
};
export const GAMES = {
export const GAMES: {
[key: string]: (config: { game: string; players: string[] }) => Game;
} = {
// renaissance,
simple,
} satisfies {
[key: string]: (config: { game: string; players: string[] }) => Game;
};
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>;
};
export type SimpleAction = { humanKey: string } & (
| { type: "draw" }
| { type: "discard"; card: Card }
);
export type SimpleAction = { type: "draw" } | { type: "discard"; card: Card };
export const newSimpleGameState = (
config: SimpleConfiguration
@@ -34,9 +31,7 @@ export const newSimpleGameState = (
return {
deck: shuffle(newDeck()),
turnIdx: 0,
playerHands: Object.fromEntries(
players.map((humanKey) => [humanKey, []])
),
playerHands: Object.fromEntries(players.map((humanKey) => [humanKey, []])),
};
};
@@ -59,12 +54,13 @@ export const resolveSimpleAction = ({
config,
state,
action,
humanKey,
}: {
config: SimpleConfiguration;
state: SimpleGameState;
action: SimpleAction;
humanKey: string;
}): SimpleGameState => {
const { humanKey } = action;
const playerHand = state.playerHands[humanKey];
if (playerHand == null) {
throw new Error(