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 WebsocketIncomingMessage = t.Union([ t.Object({ proposeGame: t.String() }), t.Object({ startGame: t.Literal(true) }), t.Object({ action: t.Any() }), ]); type TablePayload = { inputs: { presenceChanges: TBus< { humanKey: string; 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"] = { presenceChanges: Bus(), gameProposals: Bus(), gameStarts: Bus(), gameActions: Bus(), }; const { presenceChanges, gameProposals, gameStarts, gameActions } = inputs; // ======= const playersPresent = presenceChanges.scan((prev, evt) => { if (evt.presence == "joined") { prev.push(evt.humanKey); } else if (evt.presence == "left") { prev.splice(prev.indexOf(evt.humanKey), 1); } return prev; }, [] as string[]); const gameState = transform( null as GameState | null, [ gameStarts.thru((Evt) => combine([Evt], [playersPresent], (evt, players) => ({ ...evt, players, })) ), (prev, evt: { players: string[] }) => prev == null ? (newGame(evt.players) as GameState) : prev, ], [ gameActions, (prev, evt: GameAction) => prev != null ? (resolveAction( prev as unknown as SimpleGameState, "evt", evt as SimpleAction ) as GameState) : 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; };