deep in kefir lore
This commit is contained in:
@@ -10,7 +10,14 @@
|
|||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"object-hash": "^3.0.0"
|
"object-hash": "^3.0.0"
|
||||||
}
|
},
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@parcel/watcher",
|
||||||
|
"@prisma/client",
|
||||||
|
"@prisma/engines",
|
||||||
|
"esbuild",
|
||||||
|
"prisma"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/object-hash": "^3.0.6"
|
"@types/object-hash": "^3.0.6"
|
||||||
|
|||||||
@@ -1,95 +1,25 @@
|
|||||||
|
import { Accessor, createContext } from "solid-js";
|
||||||
import {
|
import {
|
||||||
Accessor,
|
SimpleAction,
|
||||||
createContext,
|
SimplePlayerView,
|
||||||
createEffect,
|
|
||||||
createResource,
|
|
||||||
createSignal,
|
|
||||||
For,
|
|
||||||
onCleanup,
|
|
||||||
Resource,
|
|
||||||
ResourceReturn,
|
|
||||||
Show,
|
|
||||||
untrack,
|
|
||||||
} from "solid-js";
|
|
||||||
import {
|
|
||||||
GameState,
|
|
||||||
Action,
|
|
||||||
vGameState,
|
|
||||||
PlayerView,
|
|
||||||
} from "../../../server/src/games/simple";
|
} from "../../../server/src/games/simple";
|
||||||
import api from "../api";
|
|
||||||
import Hand from "./Hand";
|
|
||||||
import Pile from "./Pile";
|
|
||||||
import { ApiType } from "../fn";
|
|
||||||
import { createStore } from "solid-js/store";
|
|
||||||
|
|
||||||
export const GameContext = createContext<{
|
export const GameContext = createContext<{
|
||||||
view: Accessor<PlayerView | undefined>;
|
view: Accessor<SimplePlayerView | undefined>;
|
||||||
submitAction: (action: Action) => Promise<any>;
|
submitAction: (action: SimpleAction) => Promise<any>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const [playerProfiles, setPlayerProfiles] = createStore<
|
export default () => {
|
||||||
Record<string, Resource<ApiType<typeof api.profile.get>>>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
export default (props: { tableKey: string }) => {
|
|
||||||
const [view, setView] = createSignal<PlayerView>();
|
|
||||||
const [players, setPlayers] = createSignal<string[]>([]);
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
players().forEach((player) => {
|
|
||||||
if (!untrack(() => playerProfiles[player])) {
|
|
||||||
const [playerProfile] = createResource(() =>
|
|
||||||
api.profile
|
|
||||||
.get({ query: { otherHumanKey: player } })
|
|
||||||
.then((r) => r.data)
|
|
||||||
);
|
|
||||||
setPlayerProfiles((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[player]: playerProfile,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const ws = api(props).subscribe;
|
|
||||||
onCleanup(() => ws.close());
|
|
||||||
ws.on("message", (evt) => {
|
|
||||||
if (evt.data.players) {
|
|
||||||
setPlayers(evt.data.players);
|
|
||||||
}
|
|
||||||
if (evt.data.view) {
|
|
||||||
setView(evt.data.view);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const submitAction = (action: Action) => api.simple(props).post({ action });
|
|
||||||
|
|
||||||
const Lobby = () => {
|
|
||||||
return (
|
return (
|
||||||
<div class="fixed tc mt-20 flex flex-col items-center">
|
<>
|
||||||
<button class="button p-1 m-10">Start Game!</button>
|
Game started!
|
||||||
<For each={players()}>
|
{/* <Pile
|
||||||
{(player) => (
|
|
||||||
<p style={{ "font-size": "2em" }}>
|
|
||||||
{playerProfiles[player]?.()?.name}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<GameContext.Provider value={{ view, submitAction }}>
|
|
||||||
<Show when={view() != undefined} fallback={<Lobby />}>
|
|
||||||
<Pile
|
|
||||||
count={view()!.deckCount}
|
count={view()!.deckCount}
|
||||||
class="cursor-pointer fixed center"
|
class="cursor-pointer fixed center"
|
||||||
onClick={() => submitAction({ type: "draw" })}
|
onClick={() => submitAction({ type: "draw" })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Hand class="fixed bc" hand={view()!.myHand} />
|
<Hand class="fixed bc" hand={view()!.myHand} /> */}
|
||||||
</Show>
|
</>
|
||||||
</GameContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,21 +7,13 @@ import {
|
|||||||
For,
|
For,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
Resource,
|
Resource,
|
||||||
ResourceReturn,
|
|
||||||
Show,
|
Show,
|
||||||
untrack,
|
untrack,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import {
|
|
||||||
GameState,
|
|
||||||
Action,
|
|
||||||
vGameState,
|
|
||||||
PlayerView,
|
|
||||||
} from "../../../server/src/games/simple";
|
|
||||||
import api, { me } from "../api";
|
|
||||||
import Hand from "./Hand";
|
|
||||||
import Pile from "./Pile";
|
|
||||||
import { ApiType } from "../fn";
|
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
|
import { SimplePlayerView } from "../../../server/src/games/simple";
|
||||||
|
import api, { me } from "../api";
|
||||||
|
import { ApiType } from "../fn";
|
||||||
import Game from "./Game";
|
import Game from "./Game";
|
||||||
|
|
||||||
const [playerProfiles, setPlayerProfiles] = createStore<
|
const [playerProfiles, setPlayerProfiles] = createStore<
|
||||||
@@ -30,15 +22,15 @@ const [playerProfiles, setPlayerProfiles] = createStore<
|
|||||||
|
|
||||||
export const TableContext = createContext<{
|
export const TableContext = createContext<{
|
||||||
players: Accessor<string[]>;
|
players: Accessor<string[]>;
|
||||||
view: Accessor<PlayerView | undefined>;
|
view: Accessor<SimplePlayerView | undefined>;
|
||||||
// submitAction: (action: Action) => Promise<any>;
|
// submitAction: (action: Action) => Promise<any>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
export default (props: { tableKey: string }) => {
|
export default (props: { tableKey: string }) => {
|
||||||
const [players, setPlayers] = createSignal<string[]>([]);
|
const [players, setPlayers] = createSignal<string[]>([]);
|
||||||
const [view, setView] = createSignal<PlayerView>();
|
const [view, setView] = createSignal<SimplePlayerView>();
|
||||||
|
|
||||||
const ws = api(props).subscribe();
|
const ws = api.ws(props).subscribe();
|
||||||
onCleanup(() => ws.close());
|
onCleanup(() => ws.close());
|
||||||
|
|
||||||
ws.on("message", (evt) => {
|
ws.on("message", (evt) => {
|
||||||
@@ -81,8 +73,8 @@ export default (props: { tableKey: string }) => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(0, ${
|
transform: `translate(0, ${
|
||||||
verticalOffset() * 1500
|
verticalOffset() * 150
|
||||||
}px)`,
|
}vh)`,
|
||||||
}}
|
}}
|
||||||
class="w-20 h-20 rounded-full bg-red-900 flex justify-center items-center"
|
class="w-20 h-20 rounded-full bg-red-900 flex justify-center items-center"
|
||||||
>
|
>
|
||||||
@@ -103,7 +95,12 @@ export default (props: { tableKey: string }) => {
|
|||||||
>
|
>
|
||||||
<Show when={view() == null}>
|
<Show when={view() == null}>
|
||||||
<div class="absolute center">
|
<div class="absolute center">
|
||||||
<button class="button p-1 ">Start Game!</button>
|
<button
|
||||||
|
onClick={() => ws.send({ startGame: true })}
|
||||||
|
class="button p-1 "
|
||||||
|
>
|
||||||
|
Start Game!
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,12 +15,7 @@ export default () => {
|
|||||||
<Suspense>
|
<Suspense>
|
||||||
<div style={{ padding: "20px" }}>
|
<div style={{ padding: "20px" }}>
|
||||||
<p class="text-[40px]">{param.game}</p>
|
<p class="text-[40px]">{param.game}</p>
|
||||||
<button
|
<button class="px-2 py-1.5 m-4 button">New Game</button>
|
||||||
class="px-2 py-1.5 m-4 button"
|
|
||||||
onClick={() => api.simple.newGame.post().then(refetch)}
|
|
||||||
>
|
|
||||||
New Game
|
|
||||||
</button>
|
|
||||||
<ul>
|
<ul>
|
||||||
<For each={instances() ?? []}>
|
<For each={instances() ?? []}>
|
||||||
{(instance) => (
|
{(instance) => (
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
import { Action, GameState, getKnowledge, getView } from "./games/simple";
|
import {
|
||||||
|
SimpleAction,
|
||||||
|
SimpleGameState,
|
||||||
|
getKnowledge,
|
||||||
|
getView,
|
||||||
|
} from "./games/simple";
|
||||||
import { human } from "./human";
|
import { human } from "./human";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import db from "./db";
|
import db from "./db";
|
||||||
import { liveTable } from "./table";
|
import { liveTable, WebsocketIncomingMessage } from "./table";
|
||||||
|
|
||||||
const api = new Elysia({ prefix: "/api" })
|
const api = new Elysia({ prefix: "/api" })
|
||||||
.post("/whoami", async ({ cookie: { token } }) => {
|
.post("/whoami", async ({ cookie: { token } }) => {
|
||||||
@@ -56,11 +61,34 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.get("/games", () => [{ key: "simple", name: "simple" }])
|
.get("/games", () => [{ key: "simple", name: "simple" }])
|
||||||
.ws("/:tableKey", {
|
.ws("/ws/:tableKey", {
|
||||||
response: t.Object({
|
response: t.Object({
|
||||||
players: t.Optional(t.Array(t.String())),
|
players: t.Optional(t.Array(t.String())),
|
||||||
view: t.Optional(t.Any()),
|
view: t.Optional(t.Any()),
|
||||||
}),
|
}),
|
||||||
|
body: WebsocketIncomingMessage,
|
||||||
|
|
||||||
|
message(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
humanKey,
|
||||||
|
params: { tableKey },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
body
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
inputs: { gameProposals, gameStarts, gameActions },
|
||||||
|
} = liveTable(tableKey);
|
||||||
|
|
||||||
|
if ("proposeGame" in body) {
|
||||||
|
gameProposals.emit(body);
|
||||||
|
} else if ("startGame" in body) {
|
||||||
|
gameStarts.emit(body);
|
||||||
|
} else if ("action" in body) {
|
||||||
|
gameActions.emit(body);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async open({
|
async open({
|
||||||
data: {
|
data: {
|
||||||
@@ -69,7 +97,7 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
},
|
},
|
||||||
send,
|
send,
|
||||||
}) {
|
}) {
|
||||||
const table = liveTable<GameState, Action>(tableKey);
|
const table = liveTable<SimpleGameState, SimpleAction>(tableKey);
|
||||||
|
|
||||||
table.outputs.playersPresent.onValue((players) =>
|
table.outputs.playersPresent.onValue((players) =>
|
||||||
send({ players })
|
send({ players })
|
||||||
@@ -81,8 +109,7 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
getView(getKnowledge(gameState, humanKey), humanKey),
|
getView(getKnowledge(gameState, humanKey), humanKey),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
table.inputs.presenceChanges.emit({ humanKey, presence: "joined" });
|
||||||
table.input.emit({ humanKey, presence: "joined" });
|
|
||||||
},
|
},
|
||||||
async close({
|
async close({
|
||||||
data: {
|
data: {
|
||||||
@@ -90,7 +117,7 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
humanKey,
|
humanKey,
|
||||||
},
|
},
|
||||||
}) {
|
}) {
|
||||||
liveTable(tableKey).input.emit({
|
liveTable(tableKey).inputs.presenceChanges.emit({
|
||||||
humanKey,
|
humanKey,
|
||||||
presence: "left",
|
presence: "left",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
import { Card, Hand, newDeck, Pile, shuffle, vCard } from "@games/shared/cards";
|
import { Card, Hand, newDeck, Pile, shuffle, vCard } from "@games/shared/cards";
|
||||||
import { heq } from "@games/shared/utils";
|
import { heq } from "@games/shared/utils";
|
||||||
import { Elysia, t } from "elysia";
|
|
||||||
import db from "../db";
|
|
||||||
import { human } from "../human";
|
|
||||||
import { ElysiaWS } from "elysia/dist/ws";
|
|
||||||
import K, { Property, Stream } from "kefir";
|
|
||||||
import Bus, { type Bus as TBus } from "kefir-bus";
|
|
||||||
import { Prisma, Instance } from "@prisma/client";
|
|
||||||
import type { XOR } from "ts-xor";
|
|
||||||
import { liveTable } from "../table";
|
|
||||||
|
|
||||||
// omniscient game state
|
// omniscient game state
|
||||||
export type GameState = {
|
export type SimpleGameState = {
|
||||||
prev?: {
|
prev?: {
|
||||||
action: Action;
|
action: SimpleAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
deck: Pile;
|
deck: Pile;
|
||||||
@@ -21,7 +12,7 @@ export type GameState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// a particular player's knowledge of the global game state
|
// a particular player's knowledge of the global game state
|
||||||
export type vGameState = {
|
export type vSimpleGameState = {
|
||||||
humanId: string;
|
humanId: string;
|
||||||
|
|
||||||
deck: Pile<vCard>;
|
deck: Pile<vCard>;
|
||||||
@@ -29,7 +20,7 @@ export type vGameState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// a particular player's point of view in the game
|
// a particular player's point of view in the game
|
||||||
export type PlayerView = {
|
export type SimplePlayerView = {
|
||||||
humanId: string;
|
humanId: string;
|
||||||
|
|
||||||
deckCount: number;
|
deckCount: number;
|
||||||
@@ -37,20 +28,20 @@ export type PlayerView = {
|
|||||||
myHand: Hand<Card>;
|
myHand: Hand<Card>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Action = { type: "draw" } | { type: "discard"; card: Card };
|
export type SimpleAction = { type: "draw" } | { type: "discard"; card: Card };
|
||||||
|
|
||||||
export const newGame = (players: string[]) => {
|
export const newGame = (players: string[]) => {
|
||||||
console.log("new game called with", JSON.stringify(players));
|
console.log("new game called with", JSON.stringify(players));
|
||||||
return {
|
return {
|
||||||
deck: shuffle(newDeck()),
|
deck: shuffle(newDeck()),
|
||||||
players: Object.fromEntries(players.map((humanId) => [humanId, []])),
|
players: Object.fromEntries(players.map((humanId) => [humanId, []])),
|
||||||
} as GameState;
|
} as SimpleGameState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getKnowledge = (
|
export const getKnowledge = (
|
||||||
state: GameState,
|
state: SimpleGameState,
|
||||||
humanId: string
|
humanId: string
|
||||||
): vGameState => ({
|
): vSimpleGameState => ({
|
||||||
humanId,
|
humanId,
|
||||||
deck: state.deck.map((_) => null),
|
deck: state.deck.map((_) => null),
|
||||||
players: Object.fromEntries(
|
players: Object.fromEntries(
|
||||||
@@ -61,7 +52,10 @@ export const getKnowledge = (
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getView = (state: vGameState, humanId: string): PlayerView => ({
|
export const getView = (
|
||||||
|
state: vSimpleGameState,
|
||||||
|
humanId: string
|
||||||
|
): SimplePlayerView => ({
|
||||||
humanId,
|
humanId,
|
||||||
deckCount: state.deck.length,
|
deckCount: state.deck.length,
|
||||||
myHand: state.players[humanId] as Hand,
|
myHand: state.players[humanId] as Hand,
|
||||||
@@ -73,10 +67,10 @@ export const getView = (state: vGameState, humanId: string): PlayerView => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const resolveAction = (
|
export const resolveAction = (
|
||||||
state: GameState,
|
state: SimpleGameState,
|
||||||
humanId: string,
|
humanId: string,
|
||||||
action: Action
|
action: SimpleAction
|
||||||
): GameState => {
|
): SimpleGameState => {
|
||||||
const playerHand = state.players[humanId];
|
const playerHand = state.players[humanId];
|
||||||
if (action.type == "draw") {
|
if (action.type == "draw") {
|
||||||
const [drawn, ...rest] = state.deck;
|
const [drawn, ...rest] = state.deck;
|
||||||
|
|||||||
29
packages/server/src/kefir-extension.ts
Normal file
29
packages/server/src/kefir-extension.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { merge, Observable } from "kefir";
|
||||||
|
|
||||||
|
export const transform = <
|
||||||
|
T,
|
||||||
|
Mutations extends [Observable<any, any>, (prev: T, evt: any) => T][]
|
||||||
|
>(
|
||||||
|
initValue: T,
|
||||||
|
...mutations: Mutations
|
||||||
|
): Observable<T, any> =>
|
||||||
|
merge(
|
||||||
|
mutations.map(([source, mutation]) =>
|
||||||
|
source.map((event) => ({ event, mutation }))
|
||||||
|
)
|
||||||
|
).scan((prev, { event, mutation }) => mutation(prev, event), initValue);
|
||||||
|
|
||||||
|
export const partition =
|
||||||
|
<C extends readonly [...string[]], T, E>(
|
||||||
|
classes: C,
|
||||||
|
partitionFn: (v: T) => C[number]
|
||||||
|
) =>
|
||||||
|
(obs: Observable<T, E>) => {
|
||||||
|
const assigned = obs.map((obj) => ({ obj, cls: partitionFn(obj) }));
|
||||||
|
return Object.fromEntries(
|
||||||
|
classes.map((C) => [
|
||||||
|
C,
|
||||||
|
assigned.filter(({ cls }) => cls == C).map(({ obj }) => obj),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,28 +1,34 @@
|
|||||||
import { XOR } from "ts-xor";
|
import { t } from "elysia";
|
||||||
|
import { combine, Property } from "kefir";
|
||||||
import Bus, { type Bus as TBus } from "kefir-bus";
|
import Bus, { type Bus as TBus } from "kefir-bus";
|
||||||
import { merge, Observable, Property, Stream } from "kefir";
|
import {
|
||||||
import { Game } from "@prisma/client";
|
newGame,
|
||||||
|
resolveAction,
|
||||||
|
SimpleAction,
|
||||||
|
SimpleGameState,
|
||||||
|
} from "./games/simple";
|
||||||
|
import { transform } from "./kefir-extension";
|
||||||
|
|
||||||
type TableInputEvent<GameAction> = { humanKey: string } & XOR<
|
export const WebsocketIncomingMessage = t.Union([
|
||||||
{ presence: "joined" | "left" },
|
t.Object({ proposeGame: t.String() }),
|
||||||
{ proposeGame: string },
|
t.Object({ startGame: t.Literal(true) }),
|
||||||
{ startGame: true },
|
t.Object({ action: t.Any() }),
|
||||||
{ action: GameAction }
|
]);
|
||||||
>;
|
|
||||||
|
|
||||||
type InMemoryTable<GameState> = {
|
type TablePayload<GameState, GameAction> = {
|
||||||
playersPresent: string[];
|
inputs: {
|
||||||
gameState: GameState | null;
|
presenceChanges: TBus<
|
||||||
};
|
{ humanKey: string; presence: "joined" | "left" },
|
||||||
|
never
|
||||||
type TableOutputEvents<GameState> = {
|
>;
|
||||||
|
gameProposals: TBus<{ proposeGame: string }, never>;
|
||||||
|
gameStarts: TBus<{ startGame: true }, never>;
|
||||||
|
gameActions: TBus<GameAction, never>;
|
||||||
|
};
|
||||||
|
outputs: {
|
||||||
playersPresent: Property<string[], never>;
|
playersPresent: Property<string[], never>;
|
||||||
gameState: Property<GameState | null, never>;
|
gameState: Property<GameState | null, never>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TablePayload<GameAction, GameState> = {
|
|
||||||
input: TBus<TableInputEvent<GameAction>, never>;
|
|
||||||
outputs: TableOutputEvents<GameState>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tables: {
|
const tables: {
|
||||||
@@ -31,26 +37,55 @@ const tables: {
|
|||||||
|
|
||||||
export const liveTable = <GameState, GameAction>(key: string) => {
|
export const liveTable = <GameState, GameAction>(key: string) => {
|
||||||
if (!(key in tables)) {
|
if (!(key in tables)) {
|
||||||
const inputEvents = Bus<TableInputEvent<GameAction>, never>();
|
const inputs: TablePayload<GameState, GameAction>["inputs"] = {
|
||||||
|
presenceChanges: Bus(),
|
||||||
|
gameProposals: Bus(),
|
||||||
|
gameStarts: Bus(),
|
||||||
|
gameActions: Bus(),
|
||||||
|
};
|
||||||
|
const { presenceChanges, gameProposals, gameStarts, gameActions } =
|
||||||
|
inputs;
|
||||||
|
|
||||||
tables[key] = {
|
// =======
|
||||||
input: inputEvents,
|
const playersPresent = presenceChanges.scan((prev, evt) => {
|
||||||
outputs: {
|
|
||||||
playersPresent: inputEvents
|
|
||||||
.filter((evt) => Boolean(evt.presence))
|
|
||||||
.scan((prev, evt) => {
|
|
||||||
if (evt.presence == "joined") {
|
if (evt.presence == "joined") {
|
||||||
prev.push(evt.humanKey);
|
prev.push(evt.humanKey);
|
||||||
} else if (evt.presence == "left") {
|
} else if (evt.presence == "left") {
|
||||||
prev.splice(prev.indexOf(evt.humanKey), 1);
|
prev.splice(prev.indexOf(evt.humanKey), 1);
|
||||||
}
|
}
|
||||||
return prev;
|
return prev;
|
||||||
}, [] as string[]),
|
}, [] as string[]);
|
||||||
gameState: inputEvents
|
|
||||||
.filter((evt) => Boolean(evt.startGame || evt.action))
|
const gameState = transform(
|
||||||
.scan((prev, evt) => {
|
null as GameState | null,
|
||||||
return prev;
|
[
|
||||||
}, null as GameState | null),
|
gameStarts.thru((Evt) =>
|
||||||
|
combine([Evt], [playersPresent], (evt, players) => ({
|
||||||
|
...evt,
|
||||||
|
players,
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
(prev, evt: { players: string[] }) =>
|
||||||
|
prev == null ? (newGame(evt.players) as GameState) : prev,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
gameActions,
|
||||||
|
(prev, evt: GameAction) =>
|
||||||
|
prev != null
|
||||||
|
? (resolveAction(
|
||||||
|
prev as unknown as SimpleGameState,
|
||||||
|
"evt",
|
||||||
|
evt as SimpleAction
|
||||||
|
) as GameState)
|
||||||
|
: prev,
|
||||||
|
]
|
||||||
|
).toProperty();
|
||||||
|
|
||||||
|
tables[key] = {
|
||||||
|
inputs,
|
||||||
|
outputs: {
|
||||||
|
playersPresent,
|
||||||
|
gameState: gameState as Property<unknown, never>,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,5 +97,5 @@ export const liveTable = <GameState, GameAction>(key: string) => {
|
|||||||
delete tables[key];
|
delete tables[key];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return tables[key] as TablePayload<GameAction, GameState>;
|
return tables[key] as TablePayload<GameState, GameAction>;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user