import { t } from "elysia"; import { combine, Property } from "kefir"; import Bus, { type Bus as TBus } from "kefir-bus"; import { newSimpleGameState, resolveSimpleAction, SimpleAction, SimpleConfiguration, SimpleGameState, } from "./games/simple"; import { transform } from "./kefir-extension"; export const WsOut = t.Object({ players: t.Optional(t.Array(t.String())), playersReady: t.Optional(t.Record(t.String(), t.Boolean())), view: t.Optional(t.Any()), }); export type TWsOut = typeof WsOut.static; export const WsIn = t.Union([ t.Object({ ready: t.Boolean() }), t.Object({ action: t.Any() }), t.Object({ quit: t.Literal(true) }), ]); export type TWsIn = typeof WsIn.static; type Attributed = { humanKey: string }; type TablePayload = { inputs: { connectionChanges: TBus< Attributed & { presence: "joined" | "left" }, never >; readys: TBus; actions: TBus; quits: TBus; }; outputs: { playersPresent: Property; playersReady: Property<{ [key: string]: boolean }, unknown>; gameConfig: Property; gameState: Property; }; }; const tables: { [key: string]: TablePayload; } = {}; export const liveTable = (key: string) => { if (!(key in tables)) { const inputs: TablePayload< GameConfig, GameState, GameAction >["inputs"] = { connectionChanges: Bus(), readys: Bus(), actions: Bus(), quits: Bus(), }; const { connectionChanges, readys, actions, quits } = inputs; // ======= const playersPresent = 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 }) .map((counts) => Object.keys(counts)); const playersReady = transform( {} as { [key: string]: boolean }, [ playersPresent, (prev, players: string[]) => Object.fromEntries( players.map((p) => [p, prev[p] ?? false]) ), ], [ readys, (prev, evt: { humanKey: string; ready: boolean }) => prev[evt.humanKey] != null ? { ...prev, [evt.humanKey]: evt.ready } : prev, ] ); const gameStarts = playersReady .filter( (pr) => Object.values(pr).length > 0 && Object.values(pr).every((ready) => ready) ) .map((_) => null); const gameConfig = playersPresent.map((players) => ({ game: "simple", players, })); const gameState = transform( null as SimpleGameState | null, [ combine([gameStarts], [gameConfig], (_, config) => config), (prev, startConfig: SimpleConfiguration) => prev == null ? newSimpleGameState(startConfig) : prev, ], [ combine([actions], [gameConfig], (action, config) => ({ action, config, })), ( prev, evt: { action: Attributed & SimpleAction; config: SimpleConfiguration; } ) => prev != null ? resolveSimpleAction({ config: evt.config, state: prev, action: evt.action, humanKey: evt.action.humanKey, }) : prev, ] ).toProperty(); tables[key] = { inputs, outputs: { playersPresent, playersReady, gameConfig: gameConfig as Property, gameState: gameState as Property, }, }; // cleanup tables[key].outputs.playersPresent .debounce(30000, { immediate: false }) .filter((players) => players.length === 0) .skip(1) .onValue((_) => { console.log("DELETING LIVE TABLE"); delete tables[key]; }); } return tables[key] as TablePayload; };