[wip] kefir cleanup
This commit is contained in:
@@ -5,12 +5,22 @@ import {
|
|||||||
SimpleConfiguration,
|
SimpleConfiguration,
|
||||||
SimpleGameState,
|
SimpleGameState,
|
||||||
} from "@games/shared/games/simple";
|
} from "@games/shared/games/simple";
|
||||||
import { human } from "./human";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import db from "./db";
|
import db from "./db";
|
||||||
import { liveTable, WsOut, WsIn } from "./table";
|
import { liveTable, WsOut, WsIn } from "./table";
|
||||||
import { Human } from "@prisma/client";
|
import { Human } from "@prisma/client";
|
||||||
import { combine } from "kefir";
|
import { combine } from "kefir";
|
||||||
|
import Bus from "kefir-bus";
|
||||||
|
import { Game } from "@games/shared/games";
|
||||||
|
|
||||||
|
export const WS = Bus<
|
||||||
|
{
|
||||||
|
type: "open" | "message" | "error" | "close";
|
||||||
|
humanKey: string;
|
||||||
|
tableKey: string;
|
||||||
|
},
|
||||||
|
unknown
|
||||||
|
>();
|
||||||
|
|
||||||
const api = new Elysia({ prefix: "/api" })
|
const api = new Elysia({ prefix: "/api" })
|
||||||
.post("/whoami", async ({ cookie: { token } }) => {
|
.post("/whoami", async ({ cookie: { token } }) => {
|
||||||
@@ -35,7 +45,14 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
|
|
||||||
return human.key;
|
return human.key;
|
||||||
})
|
})
|
||||||
.use(human)
|
.derive(async ({ cookie: { token }, status }) => {
|
||||||
|
const humanKey = await db.human
|
||||||
|
.findUnique({
|
||||||
|
where: { token: token.value },
|
||||||
|
})
|
||||||
|
.then((human) => human?.key);
|
||||||
|
return humanKey != null ? { humanKey } : status(401);
|
||||||
|
})
|
||||||
.post(
|
.post(
|
||||||
"/setName",
|
"/setName",
|
||||||
({ body: { name }, humanKey }) =>
|
({ body: { name }, humanKey }) =>
|
||||||
@@ -64,7 +81,6 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
return safeProfile;
|
return safeProfile;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.get("/games", () => [{ key: "simple", name: "simple" }])
|
|
||||||
.ws("/ws/:tableKey", {
|
.ws("/ws/:tableKey", {
|
||||||
async open({
|
async open({
|
||||||
data: {
|
data: {
|
||||||
@@ -73,11 +89,7 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
},
|
},
|
||||||
send,
|
send,
|
||||||
}) {
|
}) {
|
||||||
const table = liveTable<
|
const table = liveTable(tableKey);
|
||||||
SimpleConfiguration,
|
|
||||||
SimpleGameState,
|
|
||||||
SimpleAction
|
|
||||||
>(tableKey);
|
|
||||||
|
|
||||||
table.inputs.connectionChanges.emit({
|
table.inputs.connectionChanges.emit({
|
||||||
humanKey,
|
humanKey,
|
||||||
@@ -87,27 +99,24 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
table.outputs.playersPresent.onValue((players) =>
|
table.outputs.playersPresent.onValue((players) =>
|
||||||
send({ players })
|
send({ players })
|
||||||
);
|
);
|
||||||
|
|
||||||
table.outputs.playersReady
|
table.outputs.playersReady
|
||||||
.skipDuplicates()
|
.skipDuplicates()
|
||||||
.onValue((readys) => send({ playersReady: readys }));
|
.onValue((readys) => send({ playersReady: readys }));
|
||||||
|
|
||||||
combine(
|
combine(
|
||||||
[table.outputs.gameState],
|
[table.outputs.gameState],
|
||||||
[table.outputs.gameConfig],
|
[table.outputs.gameImpl],
|
||||||
(state, config) =>
|
(state, { game: Game }) =>
|
||||||
state &&
|
state && game.getView({ config, state, humanKey })
|
||||||
config &&
|
|
||||||
getSimplePlayerView(config, state, humanKey)
|
|
||||||
)
|
)
|
||||||
.toProperty()
|
.toProperty()
|
||||||
.onValue((view) => send({ view }));
|
.onValue((view) => send({ view }));
|
||||||
},
|
},
|
||||||
|
|
||||||
response: WsOut,
|
|
||||||
body: WsIn,
|
body: WsIn,
|
||||||
|
response: WsOut,
|
||||||
|
|
||||||
message(
|
message: (
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
humanKey,
|
humanKey,
|
||||||
@@ -115,21 +124,9 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
body
|
body
|
||||||
) {
|
) => WS.emit({ ...body, type: "message", humanKey, tableKey }),
|
||||||
const {
|
|
||||||
inputs: { readys, actions, quits },
|
|
||||||
} = liveTable(tableKey);
|
|
||||||
|
|
||||||
if ("ready" in body) {
|
close({
|
||||||
readys.emit({ humanKey, ...body });
|
|
||||||
} else if ("action" in body) {
|
|
||||||
actions.emit({ humanKey, ...body.action });
|
|
||||||
} else if ("quit" in body) {
|
|
||||||
quits.emit({ humanKey });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async close({
|
|
||||||
data: {
|
data: {
|
||||||
params: { tableKey },
|
params: { tableKey },
|
||||||
humanKey,
|
humanKey,
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import Elysia from "elysia";
|
|
||||||
import db from "./db";
|
|
||||||
|
|
||||||
export const human = new Elysia({ name: "human" })
|
|
||||||
.derive(async ({ cookie: { token }, status }) => {
|
|
||||||
const humanKey = await db.human
|
|
||||||
.findUnique({
|
|
||||||
where: { token: token.value },
|
|
||||||
})
|
|
||||||
.then((human) => human?.key);
|
|
||||||
return humanKey != null ? { humanKey } : status(401);
|
|
||||||
})
|
|
||||||
.as("scoped");
|
|
||||||
@@ -1,33 +1,28 @@
|
|||||||
import { cors } from "@elysiajs/cors";
|
import { cors } from "@elysiajs/cors";
|
||||||
import { staticPlugin } from "@elysiajs/static";
|
|
||||||
import { Elysia, env } from "elysia";
|
import { Elysia, env } from "elysia";
|
||||||
import api from "./api";
|
import api from "./api";
|
||||||
|
import staticFiles from "./static";
|
||||||
|
import * as log from "./logging";
|
||||||
|
|
||||||
const port = env.PORT || 5001;
|
const port = env.PORT || 5001;
|
||||||
|
|
||||||
const app = new Elysia()
|
new Elysia()
|
||||||
.use(
|
.use(
|
||||||
cors({
|
cors({
|
||||||
origin: ["http://localhost:3000", "https://games.drm.dev"],
|
origin: [
|
||||||
|
"http://localhost:3000", // dev
|
||||||
|
"https://games.drm.dev", // prod
|
||||||
|
],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
// .onRequest(({ request }) => {
|
|
||||||
// console.log(request.method, request.url);
|
.onRequest(({ request }) => log.log(request))
|
||||||
// })
|
.onError(({ error }) => log.err(error))
|
||||||
.onError(({ error }) => {
|
|
||||||
console.error(error);
|
|
||||||
return error;
|
|
||||||
})
|
|
||||||
.get("/ping", () => "pong")
|
.get("/ping", () => "pong")
|
||||||
.use(api)
|
.use(api)
|
||||||
.get("/*", () => Bun.file("./public/index.html"))
|
.use(staticFiles)
|
||||||
.use(
|
|
||||||
staticPlugin({
|
|
||||||
assets: "public",
|
|
||||||
prefix: "/",
|
|
||||||
alwaysStatic: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.listen(port);
|
.listen(port);
|
||||||
|
|
||||||
console.log("server started on", port);
|
log.log(`server started on ${port}`);
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { combine, pool } from "kefir";
|
||||||
|
import Bus from "kefir-bus";
|
||||||
|
|
||||||
|
export const LogPool = pool();
|
||||||
|
const LogBus = Bus();
|
||||||
|
|
||||||
|
const LogStream = combine([LogPool, LogBus]);
|
||||||
|
|
||||||
|
export const log = (value: unknown) => LogBus.emit(value);
|
||||||
|
export const err = (value: unknown) =>
|
||||||
|
LogBus.emitEvent({ type: "error", value });
|
||||||
|
|
||||||
|
LogPool.log();
|
||||||
|
|||||||
12
pkg/server/src/static.ts
Normal file
12
pkg/server/src/static.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import staticPlugin from "@elysiajs/static";
|
||||||
|
import Elysia from "elysia";
|
||||||
|
|
||||||
|
export default new Elysia()
|
||||||
|
.get("/*", () => Bun.file("./public/index.html"))
|
||||||
|
.use(
|
||||||
|
staticPlugin({
|
||||||
|
assets: "public",
|
||||||
|
prefix: "/",
|
||||||
|
alwaysStatic: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
import { t } from "elysia";
|
import { t } from "elysia";
|
||||||
import { combine, pool, Property } from "kefir";
|
import { combine, pool, Property } from "kefir";
|
||||||
import Bus, { type Bus as TBus } from "kefir-bus";
|
import Bus, { type Bus as TBus } from "kefir-bus";
|
||||||
import {
|
import { transform } from "@games/shared/kefir";
|
||||||
newSimpleGameState,
|
import GAMES, { Game, GameKey } from "@games/shared/games";
|
||||||
resolveSimpleAction,
|
|
||||||
SimpleAction,
|
|
||||||
SimpleConfiguration,
|
|
||||||
SimpleGameState,
|
|
||||||
} from "@games/shared/games/simple";
|
|
||||||
import { transform } from "./kefir-extension";
|
|
||||||
|
|
||||||
export const WsOut = t.Object({
|
export const WsOut = t.Object({
|
||||||
players: t.Optional(t.Array(t.String())),
|
players: t.Optional(t.Array(t.String())),
|
||||||
@@ -39,6 +33,7 @@ type TablePayload<GameConfig, GameState, GameAction> = {
|
|||||||
playersPresent: Property<string[], any>;
|
playersPresent: Property<string[], any>;
|
||||||
playersReady: Property<{ [key: string]: boolean } | null, any>;
|
playersReady: Property<{ [key: string]: boolean } | null, any>;
|
||||||
gameConfig: Property<GameConfig | null, any>;
|
gameConfig: Property<GameConfig | null, any>;
|
||||||
|
gameImpl: Property<Game, unknown>;
|
||||||
gameState: Property<GameState | null, any>;
|
gameState: Property<GameState | null, any>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -47,7 +42,13 @@ const tables: {
|
|||||||
[key: string]: TablePayload<unknown, unknown, unknown>;
|
[key: string]: TablePayload<unknown, unknown, unknown>;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
|
export const liveTable = <
|
||||||
|
GameConfig extends { game: string },
|
||||||
|
GameState,
|
||||||
|
GameAction
|
||||||
|
>(
|
||||||
|
key: string
|
||||||
|
) => {
|
||||||
if (!(key in tables)) {
|
if (!(key in tables)) {
|
||||||
const inputs: TablePayload<
|
const inputs: TablePayload<
|
||||||
GameConfig,
|
GameConfig,
|
||||||
@@ -124,58 +125,63 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
|
|||||||
game: string;
|
game: string;
|
||||||
players: string[];
|
players: string[];
|
||||||
},
|
},
|
||||||
never
|
any
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const gameConfig = gameConfigPool.toProperty();
|
const gameConfig = gameConfigPool.toProperty();
|
||||||
|
|
||||||
|
const gameImpl = gameConfig
|
||||||
|
.filter((cfg) => cfg.game in GAMES)
|
||||||
|
.map((config) => ({ config, game: GAMES[config.game as GameKey] }))
|
||||||
|
.toProperty();
|
||||||
|
|
||||||
const gameState = transform(
|
const gameState = transform(
|
||||||
null as SimpleGameState | null,
|
null as GameState | null,
|
||||||
[
|
[
|
||||||
combine([gameStarts], [gameConfigPool], (_, config) => config),
|
// initialize game state when started
|
||||||
(prev, startConfig: SimpleConfiguration) =>
|
gameImpl.sampledBy(gameStarts),
|
||||||
prev == null ? newSimpleGameState(startConfig) : prev,
|
(prev, { config, game }) =>
|
||||||
|
prev == null ? game.init(config) : prev,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
combine([actions], [gameConfigPool], (action, config) => ({
|
combine([actions], [gameImpl], (action, impl) => ({
|
||||||
action,
|
action,
|
||||||
config,
|
...impl,
|
||||||
})),
|
})),
|
||||||
(
|
(
|
||||||
prev,
|
prev,
|
||||||
evt: {
|
evt: {
|
||||||
action: Attributed & SimpleAction;
|
action: Attributed & GameAction;
|
||||||
config: SimpleConfiguration;
|
config: GameConfig;
|
||||||
|
game: Game;
|
||||||
}
|
}
|
||||||
) =>
|
) =>
|
||||||
prev != null
|
prev != null
|
||||||
? resolveSimpleAction({
|
? (evt.game.resolveAction({
|
||||||
config: evt.config,
|
config: evt.config,
|
||||||
state: prev,
|
state: prev,
|
||||||
action: evt.action,
|
action: evt.action,
|
||||||
humanKey: evt.action.humanKey,
|
}) as GameState)
|
||||||
})
|
|
||||||
: prev,
|
: prev,
|
||||||
],
|
],
|
||||||
[quits, () => null]
|
[quits, () => null]
|
||||||
).toProperty();
|
).toProperty();
|
||||||
gameState
|
|
||||||
.map((state) => JSON.stringify(state).substring(0, 10))
|
|
||||||
.log("gameState");
|
|
||||||
|
|
||||||
const gameIsActive = gameState
|
const gameIsActive = gameState
|
||||||
.map((gs) => gs != null)
|
.map((gs) => gs != null)
|
||||||
.skipDuplicates()
|
.skipDuplicates()
|
||||||
.toProperty()
|
.toProperty();
|
||||||
.log("gameIsActive");
|
|
||||||
|
|
||||||
gameConfigPool.plug(
|
gameConfigPool.plug(
|
||||||
playersPresent
|
transform(
|
||||||
.filterBy(gameIsActive.map((active) => !active))
|
{ game: "simple", players: [] as string[] },
|
||||||
.map((players) => ({
|
[
|
||||||
game: "simple",
|
playersPresent.filterBy(
|
||||||
players,
|
gameIsActive.map((active) => !active)
|
||||||
}))
|
),
|
||||||
|
(prev, players) => ({ ...prev, players }),
|
||||||
|
]
|
||||||
|
// TODO: Add player defined config changes
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
tables[key] = {
|
tables[key] = {
|
||||||
@@ -185,6 +191,7 @@ export const liveTable = <GameConfig, GameState, GameAction>(key: string) => {
|
|||||||
playersReady: playersReady.toProperty(),
|
playersReady: playersReady.toProperty(),
|
||||||
gameConfig: gameConfig as Property<unknown, any>,
|
gameConfig: gameConfig as Property<unknown, any>,
|
||||||
gameState: gameState as Property<unknown, any>,
|
gameState: gameState as Property<unknown, any>,
|
||||||
|
gameImpl,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import * as renaissance from "./renaissance";
|
|||||||
import simple from "./simple";
|
import simple from "./simple";
|
||||||
|
|
||||||
export type Game<
|
export type Game<
|
||||||
C extends { game: string },
|
C extends { game: string } = { game: string },
|
||||||
S,
|
S = unknown,
|
||||||
A,
|
A extends { humanKey: string } = { humanKey: string },
|
||||||
E extends { error: any },
|
E extends { error: any } = { error: any },
|
||||||
V
|
V = unknown
|
||||||
> = {
|
> = {
|
||||||
title: string;
|
title: string;
|
||||||
rules: string;
|
rules: string;
|
||||||
@@ -16,10 +16,10 @@ export type Game<
|
|||||||
resolveQuit: (p: { config: C; state: S; humanKey: string }) => S;
|
resolveQuit: (p: { config: C; state: S; humanKey: string }) => S;
|
||||||
};
|
};
|
||||||
|
|
||||||
const games = {
|
export const GAMES = {
|
||||||
// renaissance,
|
// renaissance,
|
||||||
simple,
|
simple,
|
||||||
} satisfies { [key: string]: Game<any, any, any, any, any> };
|
} satisfies { [key: string]: Game<any, any, any, any, any> };
|
||||||
export default games;
|
export default GAMES;
|
||||||
|
|
||||||
export type GameId = keyof typeof games;
|
export type GameKey = keyof typeof GAMES;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"name": "@games/shared",
|
"name": "@games/shared",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"kefir": "^3.8.8",
|
||||||
"object-hash": "^3.0.0"
|
"object-hash": "^3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/kefir": "^3.8.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2397
pnpm-lock.yaml
generated
2397
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user