working e2e

This commit is contained in:
2025-08-10 12:40:02 -04:00
parent 5e8978c550
commit 32c516bf37
5 changed files with 67 additions and 55 deletions

View File

@@ -1,6 +1,7 @@
import { Component, createResource, JSX, Suspense } from "solid-js"; import { Component, createResource, JSX, Suspense } from "solid-js";
import { Card, newDeck } from "../types/cards";
import { Clickable, Stylable } from "./toolbox"; import { Clickable, Stylable } from "./toolbox";
import { Card } from "../../../shared/cards";
const cardToSvgFilename = (card: Card) => { const cardToSvgFilename = (card: Card) => {
if (card.kind == "joker") { if (card.kind == "joker") {
@@ -37,9 +38,13 @@ export default ((props) => {
</Suspense> </Suspense>
); );
}) satisfies Component< }) satisfies Component<
{ (
card: Card; | {
face?: "up" | "down"; card: Card;
} & Stylable & face?: "up";
}
| { card?: Card; face: "down" }
) &
Stylable &
Clickable Clickable
>; >;

View File

@@ -3,46 +3,45 @@ import {
GameState, GameState,
Action, Action,
vGameState, vGameState,
PlayerView,
} from "../../../server/src/games/simple"; } from "../../../server/src/games/simple";
import api from "../api"; import api from "../api";
import Hand from "./Hand"; import Hand from "./Hand";
import Pile from "./Pile"; import Pile from "./Pile";
export const GameContext = createContext<{ export const GameContext = createContext<{
gameState: Accessor<GameState | undefined>; view: Accessor<PlayerView | undefined>;
submitAction: (action: Action) => Promise<any>; submitAction: (action: Action) => Promise<any>;
}>(); }>();
export default (props: { instanceId: string }) => { export default (props: { instanceId: string }) => {
const [gameState, { mutate }] = createResource(() => const [view, { mutate }] = createResource(() =>
api api
.simple(props) .simple(props)
.get() .get()
.then((res) => res.data as GameState) .then((res) => res.data as PlayerView)
); );
const submitAction = (action: Action) => const submitAction = (action: Action) =>
api api
.simple(props) .simple(props)
.post({ action }) .post({ action })
.then((res) => res.status == 200 && mutate(res.data as vGameState)); .then((res) => res.status == 200 && mutate(res.data as PlayerView));
return ( return (
<GameContext.Provider value={{ gameState, submitAction }}> <GameContext.Provider value={{ view, submitAction }}>
<Show when={gameState.latest != undefined}> <Show when={view.latest != undefined}>
<div <div
class="full column center" class="full column center"
style={{ "row-gap": "20px", "font-size": "32px" }} style={{ "row-gap": "20px", "font-size": "32px" }}
> >
<div class="full center"> <div class="full center">
<Pile <Pile
pile={gameState.latest!.deck} count={view.latest!.deckCount}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
onClick={() => onClick={() => submitAction({ type: "draw" })}
api.simple({ instanceId: props.instanceId })
}
/> />
</div> </div>
<Hand hand={gameState.latest!.players["daniel"]} /> <Hand hand={view.latest!.myHand} />
</div> </div>
</Show> </Show>
</GameContext.Provider> </GameContext.Provider>

View File

@@ -5,7 +5,7 @@ import { GameContext } from "./Game";
import { produce } from "solid-js/store"; import { produce } from "solid-js/store";
export default ((props) => { export default ((props) => {
const { submitAction, gameState } = useContext(GameContext)!; const { submitAction, view } = useContext(GameContext)!;
return ( return (
<div <div

View File

@@ -1,7 +1,6 @@
import { Component, For, JSX } from "solid-js"; import { Component, For, JSX, Show } from "solid-js";
import Card from "./Card"; import Card from "./Card";
import { Pile } from "../types/cards";
import { type ComponentProps } from "solid-js";
import { Clickable, Stylable } from "./toolbox"; import { Clickable, Stylable } from "./toolbox";
export default ((props) => { export default ((props) => {
@@ -16,31 +15,14 @@ export default ((props) => {
}} }}
onClick={props.onClick} onClick={props.onClick}
> >
<For each={props.pile}> <Show when={props.count > 0}>
{(card, i) => ( <Card face="down" />
<Card </Show>
card={card}
face="down"
style={{
position: "absolute",
transform: `translate(${i() * 0.5}px, ${
i() * 0.2
}px)`,
// "z-index": 100 - i(),
border: `0.1px solid rgb(${
10 + i() + Math.random() * 50
}, ${10 + i() + Math.random() * 50}, ${
10 + i() + Math.random() * 50
});`,
}}
/>
)}
</For>
</div> </div>
); );
}) satisfies Component< }) satisfies Component<
{ {
pile: Pile; count: number;
} & Stylable & } & Stylable &
Clickable Clickable
>; >;

View File

@@ -47,7 +47,10 @@ export const newGame = (players: string[]) => {
} as GameState; } as GameState;
}; };
export const getKnowledge = (state: GameState, humanId: string) => ({ export const getKnowledge = (
state: GameState,
humanId: string
): vGameState => ({
humanId, humanId,
deck: state.deck.map((_) => null), deck: state.deck.map((_) => null),
players: Object.fromEntries( players: Object.fromEntries(
@@ -58,6 +61,17 @@ export const getKnowledge = (state: GameState, humanId: string) => ({
), ),
}); });
const getView = (state: vGameState, humanId: string): PlayerView => ({
humanId,
deckCount: state.deck.length,
myHand: state.players[humanId] as Hand,
playerHandCounts: Object.fromEntries(
Object.entries(state.players)
.filter(([id]) => id != humanId)
.map(([id, hand]) => [id, hand.length])
),
});
export const resolveAction = ( export const resolveAction = (
state: GameState, state: GameState,
humanId: string, humanId: string,
@@ -112,21 +126,31 @@ export const simpleApi = new Elysia({ prefix: "/simple" })
) )
.group("/:instanceId", (app) => .group("/:instanceId", (app) =>
app app
.get("/", ({ params: { instanceId } }) => .get(
prisma.instance "/",
.findUnique({ ({ params: { instanceId }, headers: { human: humanId } }) =>
where: { prisma.instance
id: instanceId, .findUnique({
}, where: {
}) id: instanceId,
.then((game) => game?.gameState) },
})
.then((game) =>
getView(
getKnowledge(
game!.gameState as GameState,
humanId!
),
humanId!
)
)
) )
.post( .post(
"/", "/",
({ ({
params: { instanceId }, params: { instanceId },
body: { action }, body: { action },
headers: { Human: humanId }, headers: { human: humanId },
}) => }) =>
prisma.instance prisma.instance
.findUniqueOrThrow({ .findUniqueOrThrow({
@@ -134,20 +158,22 @@ export const simpleApi = new Elysia({ prefix: "/simple" })
id: instanceId, id: instanceId,
}, },
}) })
.then((game) => { .then(async (game) => {
const newState = resolveAction( const newState = resolveAction(
game.gameState as GameState, game.gameState as GameState,
humanId!, humanId!,
action action
); );
const knownState = getKnowledge(newState, humanId!); await prisma.instance.update({
void prisma.instance.update({
data: { gameState: newState }, data: { gameState: newState },
where: { where: {
id: instanceId, id: instanceId,
}, },
}); });
return knownState; return getView(
getKnowledge(newState, humanId!),
humanId!
);
}), }),
{ {
body: t.Object({ body: t.Object({