result states

This commit is contained in:
2025-08-31 22:23:30 -04:00
parent 0ea16ead64
commit b433a26fc6
7 changed files with 82 additions and 20 deletions

View File

@@ -1,5 +1,6 @@
import GAMES, { Game, GameKey } from "@games/shared/games";
import {
invert,
isEmpty,
multiScan,
partition,
@@ -8,7 +9,7 @@ import {
ValueWithin,
} from "@games/shared/kefirs";
import { t } from "elysia";
import { combine, Observable, pool, Property } from "kefir";
import { combine, constant, merge, Observable, pool, Property } from "kefir";
import Bus, { type Bus as TBus } from "kefir-bus";
import { log } from "./logging";
@@ -17,6 +18,7 @@ export const WsOut = t.Object({
playersReady: t.Optional(t.Nullable(t.Record(t.String(), t.Boolean()))),
gameConfig: t.Optional(t.Any()),
view: t.Optional(t.Any()),
results: t.Optional(t.Any()),
});
export type TWsOut = typeof WsOut.static;
export const WsIn = t.Union([
@@ -30,7 +32,8 @@ type Attributed = { humanKey: string };
type TablePayload<
GameConfig = unknown,
GameView = unknown,
GameAction = unknown
GameAction = unknown,
GameResult = unknown
> = {
inputs: {
connectionChanges: TBus<
@@ -51,6 +54,7 @@ type TablePayload<
any
>;
gameConfig: Property<GameConfig | null, any>;
results: Property<GameResult, any>;
};
player: {
[key: string]: {
@@ -71,12 +75,18 @@ export const liveTable = <
},
GameState,
GameAction extends Attributed,
GameView
GameView,
GameResult
>(
key: string
) => {
if (!(key in tables)) {
const inputs: TablePayload<GameConfig, GameState, GameAction>["inputs"] = {
const inputs: TablePayload<
GameConfig,
GameState,
GameAction,
GameResult
>["inputs"] = {
connectionChanges: Bus(),
messages: Bus(),
};
@@ -110,7 +120,7 @@ export const liveTable = <
.onValue(({ added, removed }) => {
added.forEach((p) => {
playerStreams[p] = {
view: combine([gameState], [gameImpl], (a, b) => [a, b] as const)
view: withGame(gameState)
.map(
([state, game]) =>
state && (game.getView({ state, humanKey: p }) as GameView)
@@ -178,16 +188,29 @@ export const liveTable = <
.map((config) => GAMES[config.game as GameKey](config))
.toProperty();
const withGame = <T>(obs: Observable<T, any>) =>
combine([obs], [gameImpl], (o, game) => [o, game] as const);
const resultsPool = pool<GameResult | null, any>();
const results = merge([constant(null), resultsPool]).toProperty();
const gameIsActivePool = pool<boolean, any>();
const gameIsActive = merge([
constant(false),
gameIsActivePool,
]).toProperty();
const gameState = multiScan(
null as GameState | null,
[
// initialize game state when started
gameImpl.sampledBy(gameStarts),
gameImpl.sampledBy(gameStarts).filterBy(invert(gameIsActive)),
(prev, game: ValueWithin<typeof gameImpl>) =>
prev || (game.init() as GameState),
],
[
combine([action], [gameImpl], (act, game) => [act, game] as const),
// handle actions from players
action.filterBy(gameIsActive).thru(withGame),
(
prev,
[{ action, humanKey }, game]: [
@@ -202,13 +225,25 @@ export const liveTable = <
humanKey,
}) as GameState),
],
[quit, () => null]
[results.filterBy(gameIsActive), () => null], // handle game ending criteria
[quit, () => null] // handle players leaving the room
).toProperty();
const gameIsActive = gameState
.map((gs) => gs != null)
.skipDuplicates()
.toProperty();
resultsPool.plug(
gameState
.filter((state) => state != null)
.thru(withGame)
.map(([state, game]) => game.getResult(state) as unknown as GameResult)
.filter((result) => result != null)
.merge(quit.map(() => null))
);
gameIsActivePool.plug(
combine([gameState, results])
.map(([state, result]) => state != null && result == null)
.skipDuplicates()
.toProperty()
);
gameConfigPool.plug(
multiScan(
@@ -234,6 +269,7 @@ export const liveTable = <
playersPresent,
playersReady,
gameConfig,
results,
},
player: playerStreams,
},