import { t } from "elysia"; import { combine, Property } from "kefir"; import Bus, { type Bus as TBus } from "kefir-bus"; import { newGame, resolveAction, SimpleAction, SimpleGameState, } from "./games/simple"; import { transform } from "./kefir-extension"; export const WsOut = t.Object({ players: t.Optional(t.Array(t.String())), view: t.Optional(t.Any()), }); export type TWsOut = typeof WsOut.static; export const WsIn = t.Union([ t.Object({ proposeGame: t.String() }), t.Object({ startGame: t.Literal(true) }), t.Object({ action: t.Any() }), ]); export type TWsIn = typeof WsIn.static; type Attributed = { humanKey: string }; type TablePayload = { inputs: { connectionChanges: TBus< Attributed & { presence: "joined" | "left" }, never >; gameProposals: TBus<{ proposeGame: string }, never>; gameStarts: TBus<{ startGame: true }, never>; gameActions: TBus; }; outputs: { playersPresent: Property; gameState: Property; }; }; const tables: { [key: string]: TablePayload; } = {}; export const liveTable = (key: string) => { if (!(key in tables)) { const inputs: TablePayload["inputs"] = { connectionChanges: Bus(), gameProposals: Bus(), gameStarts: Bus(), gameActions: Bus(), }; const { connectionChanges, gameProposals, gameStarts, gameActions } = inputs; // ======= const playerConnectionCounts = connectionChanges.scan((prev, evt) => { if (evt.presence == "left" && prev[evt.humanKey] == 1) { const { [evt.humanKey]: _, ...rest } = prev; return rest; } return { ...prev, [evt.humanKey]: (prev[evt.humanKey] ?? 0) + (evt.presence == "joined" ? 1 : -1), }; }, {} as { [key: string]: number }); const playersPresent = playerConnectionCounts.map((counts) => Object.keys(counts) ); const gameState = transform( null as SimpleGameState | null, [ combine([gameStarts], [playersPresent], (evt, players) => ({ ...evt, players, })), (prev, evt: { players: string[] }) => prev == null ? (newGame(evt.players) as SimpleGameState) : prev, ], [ gameActions, (prev, evt: Attributed & SimpleAction) => prev != null ? resolveAction(prev, evt.humanKey, evt) : prev, ] ).toProperty(); tables[key] = { inputs, outputs: { playersPresent, gameState: gameState as Property, }, }; // cleanup tables[key].outputs.playersPresent .slidingWindow(2) .filter(([prev, curr]) => prev.length > 0 && curr.length == 0) .onValue((_) => { delete tables[key]; }); } return tables[key] as TablePayload; };