102 lines
2.4 KiB
TypeScript
102 lines
2.4 KiB
TypeScript
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<GameState, GameAction> = {
|
|
inputs: {
|
|
presenceChanges: TBus<
|
|
{ humanKey: string; presence: "joined" | "left" },
|
|
never
|
|
>;
|
|
gameProposals: TBus<{ proposeGame: string }, never>;
|
|
gameStarts: TBus<{ startGame: true }, never>;
|
|
gameActions: TBus<GameAction, never>;
|
|
};
|
|
outputs: {
|
|
playersPresent: Property<string[], never>;
|
|
gameState: Property<GameState | null, never>;
|
|
};
|
|
};
|
|
|
|
const tables: {
|
|
[key: string]: TablePayload<unknown, unknown>;
|
|
} = {};
|
|
|
|
export const liveTable = <GameState, GameAction>(key: string) => {
|
|
if (!(key in tables)) {
|
|
const inputs: TablePayload<GameState, GameAction>["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<unknown, never>,
|
|
},
|
|
};
|
|
|
|
// 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<GameState, GameAction>;
|
|
};
|