result states
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "games",
|
||||
"type": "module",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"scripts": {
|
||||
"dev": "pnpm --parallel dev",
|
||||
"build": "pnpm run -F client build",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Accessor, createContext, For, useContext } from "solid-js";
|
||||
import type {
|
||||
SimpleAction,
|
||||
SimplePlayerView,
|
||||
SimpleResult,
|
||||
} from "@games/shared/games/simple";
|
||||
import { me, profile } from "~/profile";
|
||||
import Hand from "./Hand";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TWsIn, TWsOut } from "@games/server/src/table";
|
||||
import { fromPromise, Stream } from "kefir";
|
||||
import { fromPromise, merge, Stream } from "kefir";
|
||||
import {
|
||||
Accessor,
|
||||
createContext,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "solid-js";
|
||||
import api, { fromWebsocket } from "~/api";
|
||||
import { createObservable, createObservableWithInit, cx } from "~/fn";
|
||||
import { me, mePromise } from "~/profile";
|
||||
import { me, mePromise, profile } from "~/profile";
|
||||
import Game from "./Game";
|
||||
import Player from "./Player";
|
||||
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 gameEvents = wsEvents.filter((evt) => evt.view !== undefined);
|
||||
const resultEvents = wsEvents.filter((evt) => evt.results !== undefined);
|
||||
|
||||
const players = createObservableWithInit<string[]>(
|
||||
presenceEvents.map((evt) => evt.playersPresent!),
|
||||
@@ -57,6 +58,14 @@ export default (props: { tableKey: string }) => {
|
||||
|
||||
createEffect(() => sendWs({ ready: ready() }));
|
||||
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 (
|
||||
<TableContext.Provider
|
||||
@@ -127,6 +136,11 @@ export default (props: { tableKey: string }) => {
|
||||
<Show when={view() != null}>
|
||||
<Game />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
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();
|
||||
.toProperty()
|
||||
);
|
||||
|
||||
gameConfigPool.plug(
|
||||
multiScan(
|
||||
@@ -234,6 +269,7 @@ export const liveTable = <
|
||||
playersPresent,
|
||||
playersReady,
|
||||
gameConfig,
|
||||
results,
|
||||
},
|
||||
player: playerStreams,
|
||||
},
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import simple from "./simple";
|
||||
|
||||
export type Game<
|
||||
S = unknown,
|
||||
A = unknown,
|
||||
E extends { error: any } = { error: any },
|
||||
V = unknown
|
||||
S = unknown, // state
|
||||
A = unknown, // action
|
||||
E extends { error: any } = { error: any }, // error
|
||||
V = unknown, // view
|
||||
R = unknown // results
|
||||
> = {
|
||||
title: string;
|
||||
rules: string;
|
||||
@@ -12,6 +13,7 @@ export type Game<
|
||||
resolveAction: (p: { state: S; action: A; humanKey: string }) => S | E;
|
||||
getView: (p: { state: S; humanKey: string }) => V;
|
||||
resolveQuit: (p: { state: S; humanKey: string }) => S;
|
||||
getResult: (state: S) => R | undefined;
|
||||
};
|
||||
|
||||
export const GAMES: {
|
||||
|
||||
@@ -101,6 +101,8 @@ export const resolveSimpleAction = ({
|
||||
}
|
||||
};
|
||||
|
||||
export type SimpleResult = string;
|
||||
|
||||
type SimpleError = { error: "whoops!" };
|
||||
|
||||
export default (config: SimpleConfiguration) =>
|
||||
@@ -112,9 +114,14 @@ export default (config: SimpleConfiguration) =>
|
||||
getView: ({ state, humanKey }) =>
|
||||
getSimplePlayerView(config, state, humanKey),
|
||||
resolveQuit: () => null,
|
||||
getResult: (state) =>
|
||||
Object.entries(state.playerHands).find(
|
||||
([_, hand]) => hand.length === 2
|
||||
)?.[0],
|
||||
} satisfies Game<
|
||||
SimpleGameState,
|
||||
SimpleAction,
|
||||
SimpleError,
|
||||
SimplePlayerView
|
||||
SimplePlayerView,
|
||||
SimpleResult
|
||||
>);
|
||||
|
||||
@@ -50,3 +50,5 @@ export const setDiff = <T>(
|
||||
});
|
||||
|
||||
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