163 lines
3.9 KiB
TypeScript
163 lines
3.9 KiB
TypeScript
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<GameConfig, GameState, GameAction> = {
|
|
inputs: {
|
|
connectionChanges: TBus<
|
|
Attributed & { presence: "joined" | "left" },
|
|
never
|
|
>;
|
|
|
|
readys: TBus<Attributed & { ready: boolean }, never>;
|
|
actions: TBus<Attributed & GameAction, never>;
|
|
quits: TBus<Attributed, never>;
|
|
};
|
|
outputs: {
|
|
playersPresent: Property<string[], never>;
|
|
playersReady: Property<{ [key: string]: boolean }, unknown>;
|
|
gameConfig: Property<GameConfig | null, never>;
|
|
gameState: Property<GameState | null, never>;
|
|
};
|
|
};
|
|
|
|
const tables: {
|
|
[key: string]: TablePayload<unknown, unknown, unknown>;
|
|
} = {};
|
|
|
|
export const liveTable = <GameConfig, GameState, GameAction>(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<unknown, never>,
|
|
gameState: gameState as Property<unknown, never>,
|
|
},
|
|
};
|
|
|
|
// 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<GameConfig, GameState, GameAction>;
|
|
};
|