result states
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "games",
|
"name": "games",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.6",
|
"version": "0.0.7",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm --parallel dev",
|
"dev": "pnpm --parallel dev",
|
||||||
"build": "pnpm run -F client build",
|
"build": "pnpm run -F client build",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Accessor, createContext, For, useContext } from "solid-js";
|
|||||||
import type {
|
import type {
|
||||||
SimpleAction,
|
SimpleAction,
|
||||||
SimplePlayerView,
|
SimplePlayerView,
|
||||||
|
SimpleResult,
|
||||||
} from "@games/shared/games/simple";
|
} from "@games/shared/games/simple";
|
||||||
import { me, profile } from "~/profile";
|
import { me, profile } from "~/profile";
|
||||||
import Hand from "./Hand";
|
import Hand from "./Hand";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { TWsIn, TWsOut } from "@games/server/src/table";
|
import type { TWsIn, TWsOut } from "@games/server/src/table";
|
||||||
import { fromPromise, Stream } from "kefir";
|
import { fromPromise, merge, Stream } from "kefir";
|
||||||
import {
|
import {
|
||||||
Accessor,
|
Accessor,
|
||||||
createContext,
|
createContext,
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import api, { fromWebsocket } from "~/api";
|
import api, { fromWebsocket } from "~/api";
|
||||||
import { createObservable, createObservableWithInit, cx } from "~/fn";
|
import { createObservable, createObservableWithInit, cx } from "~/fn";
|
||||||
import { me, mePromise } from "~/profile";
|
import { me, mePromise, profile } from "~/profile";
|
||||||
import Game from "./Game";
|
import Game from "./Game";
|
||||||
import Player from "./Player";
|
import Player from "./Player";
|
||||||
import games from "@games/shared/games/index";
|
import games from "@games/shared/games/index";
|
||||||
@@ -39,6 +39,7 @@ export default (props: { tableKey: string }) => {
|
|||||||
|
|
||||||
const presenceEvents = wsEvents.filter((evt) => evt.playersPresent != null);
|
const presenceEvents = wsEvents.filter((evt) => evt.playersPresent != null);
|
||||||
const gameEvents = wsEvents.filter((evt) => evt.view !== undefined);
|
const gameEvents = wsEvents.filter((evt) => evt.view !== undefined);
|
||||||
|
const resultEvents = wsEvents.filter((evt) => evt.results !== undefined);
|
||||||
|
|
||||||
const players = createObservableWithInit<string[]>(
|
const players = createObservableWithInit<string[]>(
|
||||||
presenceEvents.map((evt) => evt.playersPresent!),
|
presenceEvents.map((evt) => evt.playersPresent!),
|
||||||
@@ -57,6 +58,14 @@ export default (props: { tableKey: string }) => {
|
|||||||
|
|
||||||
createEffect(() => sendWs({ ready: ready() }));
|
createEffect(() => sendWs({ ready: ready() }));
|
||||||
const view = createObservable(gameEvents.map((evt) => evt.view));
|
const view = createObservable(gameEvents.map((evt) => evt.view));
|
||||||
|
const results = createObservable(
|
||||||
|
merge([
|
||||||
|
gameEvents
|
||||||
|
.filter((evt) => "view" in evt && evt.view == null)
|
||||||
|
.map(() => undefined),
|
||||||
|
resultEvents.map((evt) => evt.results),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContext.Provider
|
<TableContext.Provider
|
||||||
@@ -127,6 +136,11 @@ export default (props: { tableKey: string }) => {
|
|||||||
<Show when={view() != null}>
|
<Show when={view() != null}>
|
||||||
<Game />
|
<Game />
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={results() != null}>
|
||||||
|
<span class="bg-[var(--light)] text-[var(--dark)] rounded-[24px] border-2 border-[var(--dark)] absolute center p-4 shadow-lg text-[4em] text-center">
|
||||||
|
{profile(results())()?.name} won!
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
</TableContext.Provider>
|
</TableContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import GAMES, { Game, GameKey } from "@games/shared/games";
|
import GAMES, { Game, GameKey } from "@games/shared/games";
|
||||||
import {
|
import {
|
||||||
|
invert,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
multiScan,
|
multiScan,
|
||||||
partition,
|
partition,
|
||||||
@@ -8,7 +9,7 @@ import {
|
|||||||
ValueWithin,
|
ValueWithin,
|
||||||
} from "@games/shared/kefirs";
|
} from "@games/shared/kefirs";
|
||||||
import { t } from "elysia";
|
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 Bus, { type Bus as TBus } from "kefir-bus";
|
||||||
import { log } from "./logging";
|
import { log } from "./logging";
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ export const WsOut = t.Object({
|
|||||||
playersReady: t.Optional(t.Nullable(t.Record(t.String(), t.Boolean()))),
|
playersReady: t.Optional(t.Nullable(t.Record(t.String(), t.Boolean()))),
|
||||||
gameConfig: t.Optional(t.Any()),
|
gameConfig: t.Optional(t.Any()),
|
||||||
view: t.Optional(t.Any()),
|
view: t.Optional(t.Any()),
|
||||||
|
results: t.Optional(t.Any()),
|
||||||
});
|
});
|
||||||
export type TWsOut = typeof WsOut.static;
|
export type TWsOut = typeof WsOut.static;
|
||||||
export const WsIn = t.Union([
|
export const WsIn = t.Union([
|
||||||
@@ -30,7 +32,8 @@ type Attributed = { humanKey: string };
|
|||||||
type TablePayload<
|
type TablePayload<
|
||||||
GameConfig = unknown,
|
GameConfig = unknown,
|
||||||
GameView = unknown,
|
GameView = unknown,
|
||||||
GameAction = unknown
|
GameAction = unknown,
|
||||||
|
GameResult = unknown
|
||||||
> = {
|
> = {
|
||||||
inputs: {
|
inputs: {
|
||||||
connectionChanges: TBus<
|
connectionChanges: TBus<
|
||||||
@@ -51,6 +54,7 @@ type TablePayload<
|
|||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
gameConfig: Property<GameConfig | null, any>;
|
gameConfig: Property<GameConfig | null, any>;
|
||||||
|
results: Property<GameResult, any>;
|
||||||
};
|
};
|
||||||
player: {
|
player: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
@@ -71,12 +75,18 @@ export const liveTable = <
|
|||||||
},
|
},
|
||||||
GameState,
|
GameState,
|
||||||
GameAction extends Attributed,
|
GameAction extends Attributed,
|
||||||
GameView
|
GameView,
|
||||||
|
GameResult
|
||||||
>(
|
>(
|
||||||
key: string
|
key: string
|
||||||
) => {
|
) => {
|
||||||
if (!(key in tables)) {
|
if (!(key in tables)) {
|
||||||
const inputs: TablePayload<GameConfig, GameState, GameAction>["inputs"] = {
|
const inputs: TablePayload<
|
||||||
|
GameConfig,
|
||||||
|
GameState,
|
||||||
|
GameAction,
|
||||||
|
GameResult
|
||||||
|
>["inputs"] = {
|
||||||
connectionChanges: Bus(),
|
connectionChanges: Bus(),
|
||||||
messages: Bus(),
|
messages: Bus(),
|
||||||
};
|
};
|
||||||
@@ -110,7 +120,7 @@ export const liveTable = <
|
|||||||
.onValue(({ added, removed }) => {
|
.onValue(({ added, removed }) => {
|
||||||
added.forEach((p) => {
|
added.forEach((p) => {
|
||||||
playerStreams[p] = {
|
playerStreams[p] = {
|
||||||
view: combine([gameState], [gameImpl], (a, b) => [a, b] as const)
|
view: withGame(gameState)
|
||||||
.map(
|
.map(
|
||||||
([state, game]) =>
|
([state, game]) =>
|
||||||
state && (game.getView({ state, humanKey: p }) as GameView)
|
state && (game.getView({ state, humanKey: p }) as GameView)
|
||||||
@@ -178,16 +188,29 @@ export const liveTable = <
|
|||||||
.map((config) => GAMES[config.game as GameKey](config))
|
.map((config) => GAMES[config.game as GameKey](config))
|
||||||
.toProperty();
|
.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(
|
const gameState = multiScan(
|
||||||
null as GameState | null,
|
null as GameState | null,
|
||||||
[
|
[
|
||||||
// initialize game state when started
|
// initialize game state when started
|
||||||
gameImpl.sampledBy(gameStarts),
|
gameImpl.sampledBy(gameStarts).filterBy(invert(gameIsActive)),
|
||||||
(prev, game: ValueWithin<typeof gameImpl>) =>
|
(prev, game: ValueWithin<typeof gameImpl>) =>
|
||||||
prev || (game.init() as GameState),
|
prev || (game.init() as GameState),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
combine([action], [gameImpl], (act, game) => [act, game] as const),
|
// handle actions from players
|
||||||
|
action.filterBy(gameIsActive).thru(withGame),
|
||||||
(
|
(
|
||||||
prev,
|
prev,
|
||||||
[{ action, humanKey }, game]: [
|
[{ action, humanKey }, game]: [
|
||||||
@@ -202,13 +225,25 @@ export const liveTable = <
|
|||||||
humanKey,
|
humanKey,
|
||||||
}) as GameState),
|
}) as GameState),
|
||||||
],
|
],
|
||||||
[quit, () => null]
|
[results.filterBy(gameIsActive), () => null], // handle game ending criteria
|
||||||
|
[quit, () => null] // handle players leaving the room
|
||||||
).toProperty();
|
).toProperty();
|
||||||
|
|
||||||
const gameIsActive = gameState
|
resultsPool.plug(
|
||||||
.map((gs) => gs != null)
|
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()
|
.skipDuplicates()
|
||||||
.toProperty();
|
.toProperty()
|
||||||
|
);
|
||||||
|
|
||||||
gameConfigPool.plug(
|
gameConfigPool.plug(
|
||||||
multiScan(
|
multiScan(
|
||||||
@@ -234,6 +269,7 @@ export const liveTable = <
|
|||||||
playersPresent,
|
playersPresent,
|
||||||
playersReady,
|
playersReady,
|
||||||
gameConfig,
|
gameConfig,
|
||||||
|
results,
|
||||||
},
|
},
|
||||||
player: playerStreams,
|
player: playerStreams,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import simple from "./simple";
|
import simple from "./simple";
|
||||||
|
|
||||||
export type Game<
|
export type Game<
|
||||||
S = unknown,
|
S = unknown, // state
|
||||||
A = unknown,
|
A = unknown, // action
|
||||||
E extends { error: any } = { error: any },
|
E extends { error: any } = { error: any }, // error
|
||||||
V = unknown
|
V = unknown, // view
|
||||||
|
R = unknown // results
|
||||||
> = {
|
> = {
|
||||||
title: string;
|
title: string;
|
||||||
rules: string;
|
rules: string;
|
||||||
@@ -12,6 +13,7 @@ export type Game<
|
|||||||
resolveAction: (p: { state: S; action: A; humanKey: string }) => S | E;
|
resolveAction: (p: { state: S; action: A; humanKey: string }) => S | E;
|
||||||
getView: (p: { state: S; humanKey: string }) => V;
|
getView: (p: { state: S; humanKey: string }) => V;
|
||||||
resolveQuit: (p: { state: S; humanKey: string }) => S;
|
resolveQuit: (p: { state: S; humanKey: string }) => S;
|
||||||
|
getResult: (state: S) => R | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GAMES: {
|
export const GAMES: {
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ export const resolveSimpleAction = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SimpleResult = string;
|
||||||
|
|
||||||
type SimpleError = { error: "whoops!" };
|
type SimpleError = { error: "whoops!" };
|
||||||
|
|
||||||
export default (config: SimpleConfiguration) =>
|
export default (config: SimpleConfiguration) =>
|
||||||
@@ -112,9 +114,14 @@ export default (config: SimpleConfiguration) =>
|
|||||||
getView: ({ state, humanKey }) =>
|
getView: ({ state, humanKey }) =>
|
||||||
getSimplePlayerView(config, state, humanKey),
|
getSimplePlayerView(config, state, humanKey),
|
||||||
resolveQuit: () => null,
|
resolveQuit: () => null,
|
||||||
|
getResult: (state) =>
|
||||||
|
Object.entries(state.playerHands).find(
|
||||||
|
([_, hand]) => hand.length === 2
|
||||||
|
)?.[0],
|
||||||
} satisfies Game<
|
} satisfies Game<
|
||||||
SimpleGameState,
|
SimpleGameState,
|
||||||
SimpleAction,
|
SimpleAction,
|
||||||
SimpleError,
|
SimpleError,
|
||||||
SimplePlayerView
|
SimplePlayerView,
|
||||||
|
SimpleResult
|
||||||
>);
|
>);
|
||||||
|
|||||||
@@ -50,3 +50,5 @@ export const setDiff = <T>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const set = <T>(arr: T[]) => new Set<T>(arr);
|
export const set = <T>(arr: T[]) => new Set<T>(arr);
|
||||||
|
|
||||||
|
export const invert = <E>(obs: Observable<boolean, E>) => obs.map((o) => !o);
|
||||||
|
|||||||
Reference in New Issue
Block a user