we did it
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "games",
|
"name": "games",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm --parallel dev",
|
"dev": "pnpm --parallel dev",
|
||||||
"build": "pnpm run -F client build",
|
"build": "pnpm run -F client build",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Accessor, createContext, For, useContext } from "solid-js";
|
|||||||
import type {
|
import type {
|
||||||
SimpleAction,
|
SimpleAction,
|
||||||
SimplePlayerView,
|
SimplePlayerView,
|
||||||
} from "@games/server/src/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";
|
||||||
import Pile from "./Pile";
|
import Pile from "./Pile";
|
||||||
@@ -44,7 +44,9 @@ export default () => {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="button fixed tl m-4 p-1"
|
class="button fixed tl m-4 p-1"
|
||||||
onClick={() => table.sendWs({ quit: true })}
|
onClick={() => {
|
||||||
|
table.sendWs({ quit: true });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Quit
|
Quit
|
||||||
</button>
|
</button>
|
||||||
@@ -54,10 +56,7 @@ export default () => {
|
|||||||
mount={document.getElementById(`player-${playerKey}`)!}
|
mount={document.getElementById(`player-${playerKey}`)!}
|
||||||
ref={(ref) => {
|
ref={(ref) => {
|
||||||
const midOffset =
|
const midOffset =
|
||||||
i() +
|
i() + 0.5 - Object.values(view().playerHandCounts).length / 2;
|
||||||
0.5 -
|
|
||||||
Object.values(view().playerHandCounts).length /
|
|
||||||
2;
|
|
||||||
|
|
||||||
ref.style = `position: absolute; display: flex; justify-content: center; top: 65%; transform: translate(${Math.abs(
|
ref.style = `position: absolute; display: flex; justify-content: center; top: 65%; transform: translate(${Math.abs(
|
||||||
midOffset * 0
|
midOffset * 0
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ export default (props: { tableKey: string }) => {
|
|||||||
);
|
);
|
||||||
onCleanup(() => wsPromise.then((ws) => ws.close()));
|
onCleanup(() => wsPromise.then((ws) => ws.close()));
|
||||||
|
|
||||||
const presenceEvents = wsEvents.filter((evt) => evt.players != 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 players = createObservableWithInit<string[]>(
|
const players = createObservableWithInit<string[]>(
|
||||||
presenceEvents.map((evt) => evt.players!),
|
presenceEvents.map((evt) => evt.playersPresent!),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -80,14 +80,13 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
body: WsIn,
|
body: WsIn,
|
||||||
response: WsOut,
|
response: WsOut,
|
||||||
|
|
||||||
open: ({
|
open({
|
||||||
data: {
|
data: {
|
||||||
params: { tableKey },
|
params: { tableKey },
|
||||||
humanKey,
|
humanKey,
|
||||||
},
|
},
|
||||||
send,
|
send,
|
||||||
}) => {
|
}) {
|
||||||
console.log("websocket opened");
|
|
||||||
const table = liveTable(tableKey);
|
const table = liveTable(tableKey);
|
||||||
|
|
||||||
table.inputs.connectionChanges.emit({
|
table.inputs.connectionChanges.emit({
|
||||||
@@ -103,7 +102,7 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
message: (
|
message(
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
humanKey,
|
humanKey,
|
||||||
@@ -111,20 +110,25 @@ const api = new Elysia({ prefix: "/api" })
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
body
|
body
|
||||||
) => liveTable(tableKey).inputs.messages.emit({ ...body, humanKey }),
|
) {
|
||||||
|
liveTable(tableKey).inputs.messages.emit({ ...body, humanKey });
|
||||||
|
},
|
||||||
|
|
||||||
close: ({
|
close({
|
||||||
data: {
|
data: {
|
||||||
params: { tableKey },
|
params: { tableKey },
|
||||||
humanKey,
|
humanKey,
|
||||||
},
|
},
|
||||||
}) =>
|
}) {
|
||||||
liveTable(tableKey).inputs.connectionChanges.emit({
|
liveTable(tableKey).inputs.connectionChanges.emit({
|
||||||
humanKey,
|
humanKey,
|
||||||
presence: "left",
|
presence: "left",
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
|
|
||||||
error: (error) => err(error),
|
error(error) {
|
||||||
|
err(error);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ new Elysia()
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
.onRequest(({ request }) => console.log(request.url))
|
|
||||||
.onError(({ error }) => console.error(error))
|
.onError(({ error }) => console.error(error))
|
||||||
|
|
||||||
.get("/ping", () => "pong")
|
.get("/ping", () => "pong")
|
||||||
@@ -25,4 +24,4 @@ new Elysia()
|
|||||||
|
|
||||||
.listen(port);
|
.listen(port);
|
||||||
|
|
||||||
log.log(`server started on ${port}`);
|
console.log(`server started on ${port}`);
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import Bus, { type Bus as TBus } from "kefir-bus";
|
|||||||
import { log } from "./logging";
|
import { log } from "./logging";
|
||||||
|
|
||||||
export const WsOut = t.Object({
|
export const WsOut = t.Object({
|
||||||
players: t.Optional(t.Array(t.String())),
|
playersPresent: t.Optional(t.Array(t.String())),
|
||||||
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()),
|
||||||
view: t.Optional(t.Any()),
|
view: t.Optional(t.Any()),
|
||||||
});
|
});
|
||||||
export type TWsOut = typeof WsOut.static;
|
export type TWsOut = typeof WsOut.static;
|
||||||
@@ -53,7 +54,7 @@ type TablePayload<
|
|||||||
};
|
};
|
||||||
player: {
|
player: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
view: Property<GameView, any>;
|
view: Property<GameView | null, any>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -69,7 +70,8 @@ export const liveTable = <
|
|||||||
players: string[];
|
players: string[];
|
||||||
},
|
},
|
||||||
GameState,
|
GameState,
|
||||||
GameAction extends Attributed
|
GameAction extends Attributed,
|
||||||
|
GameView
|
||||||
>(
|
>(
|
||||||
key: string
|
key: string
|
||||||
) => {
|
) => {
|
||||||
@@ -98,11 +100,9 @@ export const liveTable = <
|
|||||||
.map((counts) => Object.keys(counts))
|
.map((counts) => Object.keys(counts))
|
||||||
.toProperty();
|
.toProperty();
|
||||||
|
|
||||||
const playerStreams: TablePayload<
|
const playerStreams: {
|
||||||
GameConfig,
|
[key: string]: { view: Property<GameView | null, any> };
|
||||||
GameState,
|
} = {};
|
||||||
GameAction
|
|
||||||
>["outputs"]["player"] = {};
|
|
||||||
playersPresent
|
playersPresent
|
||||||
.map(set)
|
.map(set)
|
||||||
.slidingWindow(2, 2)
|
.slidingWindow(2, 2)
|
||||||
@@ -110,7 +110,12 @@ export const liveTable = <
|
|||||||
.onValue(({ added, removed }) => {
|
.onValue(({ added, removed }) => {
|
||||||
added.forEach((p) => {
|
added.forEach((p) => {
|
||||||
playerStreams[p] = {
|
playerStreams[p] = {
|
||||||
view: Bus(),
|
view: combine([gameState], [gameImpl], (a, b) => [a, b] as const)
|
||||||
|
.map(
|
||||||
|
([state, game]) =>
|
||||||
|
state && (game.getView({ state, humanKey: p }) as GameView)
|
||||||
|
)
|
||||||
|
.toProperty(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
removed.forEach((p) => {
|
removed.forEach((p) => {
|
||||||
@@ -135,12 +140,12 @@ export const liveTable = <
|
|||||||
[key: string]: boolean;
|
[key: string]: boolean;
|
||||||
} | null,
|
} | null,
|
||||||
[
|
[
|
||||||
playersPresent,
|
playersPresent, // TODO: filter to only outside active games
|
||||||
(prev, players: ValueWithin<typeof playersPresent>) =>
|
(prev, players: ValueWithin<typeof playersPresent>) =>
|
||||||
Object.fromEntries(players.map((p) => [p, prev?.[p] ?? false])),
|
Object.fromEntries(players.map((p) => [p, prev?.[p] ?? false])),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
ready,
|
ready, // TODO: filter to only outside active games
|
||||||
(prev, evt: ValueWithin<typeof ready>) =>
|
(prev, evt: ValueWithin<typeof ready>) =>
|
||||||
prev?.[evt.humanKey] != null
|
prev?.[evt.humanKey] != null
|
||||||
? {
|
? {
|
||||||
@@ -155,9 +160,7 @@ export const liveTable = <
|
|||||||
(_, players: ValueWithin<typeof playersPresent>) =>
|
(_, players: ValueWithin<typeof playersPresent>) =>
|
||||||
Object.fromEntries(players.map((p) => [p, false])),
|
Object.fromEntries(players.map((p) => [p, false])),
|
||||||
]
|
]
|
||||||
)
|
).toProperty();
|
||||||
.toProperty()
|
|
||||||
.log("playersReady");
|
|
||||||
|
|
||||||
const gameStarts = playersReady
|
const gameStarts = playersReady
|
||||||
.filter(
|
.filter(
|
||||||
@@ -165,16 +168,9 @@ export const liveTable = <
|
|||||||
Object.values(pr ?? {}).length > 0 &&
|
Object.values(pr ?? {}).length > 0 &&
|
||||||
Object.values(pr!).every((ready) => ready)
|
Object.values(pr!).every((ready) => ready)
|
||||||
)
|
)
|
||||||
.map((_) => null)
|
.map((_) => null);
|
||||||
.log("gameStarts");
|
|
||||||
|
|
||||||
const gameConfigPool = pool<
|
const gameConfigPool = pool<GameConfig, any>();
|
||||||
{
|
|
||||||
game: GameKey;
|
|
||||||
players: string[];
|
|
||||||
},
|
|
||||||
any
|
|
||||||
>();
|
|
||||||
const gameConfig = gameConfigPool.toProperty();
|
const gameConfig = gameConfigPool.toProperty();
|
||||||
|
|
||||||
const gameImpl = gameConfig
|
const gameImpl = gameConfig
|
||||||
@@ -191,24 +187,19 @@ export const liveTable = <
|
|||||||
prev || (game.init() as GameState),
|
prev || (game.init() as GameState),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
combine([action], [gameImpl], (action, impl) => ({
|
combine([action], [gameImpl], (act, game) => [act, game] as const),
|
||||||
action,
|
|
||||||
...impl,
|
|
||||||
})),
|
|
||||||
(
|
(
|
||||||
prev,
|
prev,
|
||||||
{
|
[{ action, humanKey }, game]: [
|
||||||
game,
|
Attributed & { action: GameAction },
|
||||||
action,
|
Game
|
||||||
}: {
|
]
|
||||||
game: Game;
|
|
||||||
action: Attributed & GameAction;
|
|
||||||
}
|
|
||||||
) =>
|
) =>
|
||||||
prev &&
|
prev &&
|
||||||
(game.resolveAction({
|
(game.resolveAction({
|
||||||
state: prev,
|
state: prev,
|
||||||
action,
|
action,
|
||||||
|
humanKey,
|
||||||
}) as GameState),
|
}) as GameState),
|
||||||
],
|
],
|
||||||
[quit, () => null]
|
[quit, () => null]
|
||||||
@@ -233,7 +224,7 @@ export const liveTable = <
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
// TODO: Add player defined config changes
|
// TODO: Add player defined config changes
|
||||||
)
|
) as unknown as Observable<GameConfig, any>
|
||||||
);
|
);
|
||||||
|
|
||||||
tables[key] = {
|
tables[key] = {
|
||||||
@@ -241,10 +232,10 @@ export const liveTable = <
|
|||||||
outputs: {
|
outputs: {
|
||||||
global: {
|
global: {
|
||||||
playersPresent,
|
playersPresent,
|
||||||
playersReady: playersReady.toProperty(),
|
playersReady,
|
||||||
gameConfig: gameConfig as Property<unknown, any>,
|
gameConfig,
|
||||||
},
|
},
|
||||||
player: {},
|
player: playerStreams,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,24 +2,24 @@ import simple from "./simple";
|
|||||||
|
|
||||||
export type Game<
|
export type Game<
|
||||||
S = unknown,
|
S = unknown,
|
||||||
A extends { humanKey: string } = { humanKey: string },
|
A = unknown,
|
||||||
E extends { error: any } = { error: any },
|
E extends { error: any } = { error: any },
|
||||||
V = unknown
|
V = unknown
|
||||||
> = {
|
> = {
|
||||||
title: string;
|
title: string;
|
||||||
rules: string;
|
rules: string;
|
||||||
init: () => S;
|
init: () => S;
|
||||||
resolveAction: (p: { state: S; action: A }) => 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GAMES = {
|
export const GAMES: {
|
||||||
|
[key: string]: (config: { game: string; players: string[] }) => Game;
|
||||||
|
} = {
|
||||||
// renaissance,
|
// renaissance,
|
||||||
simple,
|
simple,
|
||||||
} satisfies {
|
|
||||||
[key: string]: (config: { game: string; players: string[] }) => Game;
|
|
||||||
};
|
};
|
||||||
export default GAMES;
|
export default GAMES;
|
||||||
|
|
||||||
export type GameKey = keyof typeof GAMES;
|
export type GameKey = string;
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ export type SimplePlayerView = {
|
|||||||
myHand: Hand<Card>;
|
myHand: Hand<Card>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SimpleAction = { humanKey: string } & (
|
export type SimpleAction = { type: "draw" } | { type: "discard"; card: Card };
|
||||||
| { type: "draw" }
|
|
||||||
| { type: "discard"; card: Card }
|
|
||||||
);
|
|
||||||
|
|
||||||
export const newSimpleGameState = (
|
export const newSimpleGameState = (
|
||||||
config: SimpleConfiguration
|
config: SimpleConfiguration
|
||||||
@@ -34,9 +31,7 @@ export const newSimpleGameState = (
|
|||||||
return {
|
return {
|
||||||
deck: shuffle(newDeck()),
|
deck: shuffle(newDeck()),
|
||||||
turnIdx: 0,
|
turnIdx: 0,
|
||||||
playerHands: Object.fromEntries(
|
playerHands: Object.fromEntries(players.map((humanKey) => [humanKey, []])),
|
||||||
players.map((humanKey) => [humanKey, []])
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,12 +54,13 @@ export const resolveSimpleAction = ({
|
|||||||
config,
|
config,
|
||||||
state,
|
state,
|
||||||
action,
|
action,
|
||||||
|
humanKey,
|
||||||
}: {
|
}: {
|
||||||
config: SimpleConfiguration;
|
config: SimpleConfiguration;
|
||||||
state: SimpleGameState;
|
state: SimpleGameState;
|
||||||
action: SimpleAction;
|
action: SimpleAction;
|
||||||
|
humanKey: string;
|
||||||
}): SimpleGameState => {
|
}): SimpleGameState => {
|
||||||
const { humanKey } = action;
|
|
||||||
const playerHand = state.playerHands[humanKey];
|
const playerHand = state.playerHands[humanKey];
|
||||||
if (playerHand == null) {
|
if (playerHand == null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
Reference in New Issue
Block a user