lots of necessary plumbing
This commit is contained in:
@@ -2,91 +2,135 @@ import { t } from "elysia";
|
||||
import { combine, Property } from "kefir";
|
||||
import Bus, { type Bus as TBus } from "kefir-bus";
|
||||
import {
|
||||
newGame,
|
||||
resolveAction,
|
||||
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.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({ 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<GameState, GameAction> = {
|
||||
type TablePayload<GameConfig, 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>;
|
||||
|
||||
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>;
|
||||
[key: string]: TablePayload<unknown, unknown, unknown>;
|
||||
} = {};
|
||||
|
||||
export const liveTable = <GameState, GameAction>(key: string) => {
|
||||
export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
|
||||
if (!(key in tables)) {
|
||||
const inputs: TablePayload<GameState, GameAction>["inputs"] = {
|
||||
const inputs: TablePayload<
|
||||
GameConfig,
|
||||
GameState,
|
||||
GameAction
|
||||
>["inputs"] = {
|
||||
connectionChanges: Bus(),
|
||||
gameProposals: Bus(),
|
||||
gameStarts: Bus(),
|
||||
gameActions: Bus(),
|
||||
readys: Bus(),
|
||||
actions: Bus(),
|
||||
quits: Bus(),
|
||||
};
|
||||
const { connectionChanges, gameProposals, gameStarts, gameActions } =
|
||||
inputs;
|
||||
const { connectionChanges, readys, actions, quits } = 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 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).every((ready) => ready))
|
||||
.map((_) => null);
|
||||
|
||||
const gameConfig = playersPresent.map((players) => ({
|
||||
game: "simple",
|
||||
players,
|
||||
}));
|
||||
|
||||
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,
|
||||
combine([gameStarts], [gameConfig], (_, config) => config),
|
||||
(prev, startConfig: SimpleConfiguration) =>
|
||||
prev == null ? newSimpleGameState(startConfig) : prev,
|
||||
],
|
||||
[
|
||||
gameActions,
|
||||
(prev, evt: Attributed & SimpleAction) =>
|
||||
combine([actions], [gameConfig], (action, config) => ({
|
||||
action,
|
||||
config,
|
||||
})),
|
||||
(
|
||||
prev,
|
||||
evt: {
|
||||
action: Attributed & SimpleAction;
|
||||
config: SimpleConfiguration;
|
||||
}
|
||||
) =>
|
||||
prev != null
|
||||
? resolveAction(prev, evt.humanKey, evt)
|
||||
? resolveSimpleAction({
|
||||
config: evt.config,
|
||||
state: prev,
|
||||
action: evt.action,
|
||||
humanKey: evt.action.humanKey,
|
||||
})
|
||||
: prev,
|
||||
]
|
||||
).toProperty();
|
||||
@@ -95,17 +139,19 @@ export const liveTable = <GameState, GameAction>(key: string) => {
|
||||
inputs,
|
||||
outputs: {
|
||||
playersPresent,
|
||||
playersReady,
|
||||
gameConfig: gameConfig as Property<unknown, never>,
|
||||
gameState: gameState as Property<unknown, never>,
|
||||
},
|
||||
};
|
||||
|
||||
// cleanup
|
||||
tables[key].outputs.playersPresent
|
||||
.slidingWindow(2)
|
||||
.filter(([prev, curr]) => prev.length > 0 && curr.length == 0)
|
||||
.debounce(30000)
|
||||
.filter((players) => players.length === 0)
|
||||
.onValue((_) => {
|
||||
delete tables[key];
|
||||
});
|
||||
}
|
||||
return tables[key] as TablePayload<GameState, GameAction>;
|
||||
return tables[key] as TablePayload<GameConfig, GameState, GameAction>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user