112 lines
2.7 KiB
TypeScript
112 lines
2.7 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 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<GameState, GameAction> = {
|
|
inputs: {
|
|
connectionChanges: TBus<
|
|
Attributed & { presence: "joined" | "left" },
|
|
never
|
|
>;
|
|
gameProposals: TBus<{ proposeGame: string }, never>;
|
|
gameStarts: TBus<{ startGame: true }, never>;
|
|
gameActions: TBus<Attributed & 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"] = {
|
|
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<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>;
|
|
};
|