Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 5 additions & 18 deletions apps/web/src/pages/CasinoMachine.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
import { useEffect, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api, keys, renderNode } from "@gl3/client";
import { CasinoMachineResponseSchema, type CasinoMachine, type CasinoGame } from "@gl3/shared";
import { renderNode, useCasinoMachine, useCasinoMachineAction, type CasinoMachineVerb } from "@gl3/client";
import type { CasinoMachine, CasinoGame } from "@gl3/shared";
import { ErrorText, Money, Panel } from "../components/ui.js";
import { PageRenderer } from "../plugins/PageRenderer.js";
import { ReelCabinet } from "./ReelCabinet.js";
import styles from "./pages.module.css";

const QUERY_KEY = ["casino-machine"];
export function useCasinoMachine() {
return useQuery({ queryKey: QUERY_KEY, queryFn: async () => CasinoMachineResponseSchema.parse(await api("/api/casino/machine")) });
}
function useMachineAction() {
const queries = useQueryClient();
return useMutation({
mutationFn: async ({ verb, body }: { verb: string; body: Record<string, unknown> }) =>
CasinoMachineResponseSchema.parse(await api(`/api/casino/machine/${verb}`, { method: "POST", body: JSON.stringify(body) })),
onSuccess: response => { queries.setQueryData(QUERY_KEY, response); },
onError: () => { void queries.invalidateQueries({ queryKey: QUERY_KEY }); },
onSettled: () => { void queries.invalidateQueries({ queryKey: keys.me() }); },
});
}
export { useCasinoMachine };
const useMachineAction = useCasinoMachineAction;

export function MachineEntry({ game, minBet, cash, jailed }: { game: CasinoGame; minBet: string; cash: string; jailed: boolean }): JSX.Element {
const [credits, setCredits] = useState("");
Expand Down Expand Up @@ -51,7 +38,7 @@ export function MachineScreen({ machine, jailed }: { machine: CasinoMachine; jai
return () => window.clearTimeout(timer);
}, [machine.revision, machine.animation, animating]);
const busy = action.isPending || animating;
const send = (verb: string, fields: Record<string, unknown> = {}) => {
const send = (verb: CasinoMachineVerb, fields: Record<string, unknown> = {}) => {
setConfirmCashout(false);
action.mutate({ verb, body: { id: machine.id, revision: machine.revision, ...fields } });
};
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/pages/ReelCabinet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
import type { CasinoMachine, ReelDisplay } from "@gl3/shared";
import { renderNode } from "@gl3/client";
import { renderNode, type CasinoMachineVerb } from "@gl3/client";
import { Money } from "../components/ui.js";
import { PageRenderer } from "../plugins/PageRenderer.js";
import css from "./ReelCabinet.module.css";
Expand All @@ -26,7 +26,7 @@ function WinAmount({ amount, animate }: { amount: string; animate: boolean }): J
export function ReelCabinet({ machine, display, busy, animating, jailed, reducedMotion, wager, onWager, send, cashout }: {
machine: CasinoMachine; display: ReelDisplay; busy: boolean; animating: boolean;
jailed: boolean; reducedMotion: boolean; wager: string; onWager: (value: string) => void;
send: (verb: string, fields?: Record<string, unknown>) => void; cashout: ReactNode;
send: (verb: CasinoMachineVerb, fields?: Record<string, unknown>) => void; cashout: ReactNode;
}): JSX.Element {
const seenRevision = useRef(machine.revision);
const [celebration, setCelebration] = useState<number | null>(null);
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "@gl3/client",
"version": "0.1.7",
"version": "0.1.8",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
"files": ["dist", "src"],
"publishConfig": { "registry": "https://npm.gl3.dev", "access": "public" },
"scripts": { "build": "tsc --build", "prepack": "tsc --build" },
"dependencies": { "@gl3/shared": "^1.0.17", "zod": "^3.23.8" },
"dependencies": { "@gl3/shared": "^1.0.18", "zod": "^3.23.8" },
"peerDependencies": { "react": "^18.3.0 || ^19.0.0", "@tanstack/react-query": "^5.0.0" },
"devDependencies": { "@tanstack/react-query": "^5.62.2", "@testing-library/react": "^16.3.2", "@types/react": "^18.3.12", "jsdom": "^29.1.1", "react": "^18.3.1", "react-dom": "^18.3.1" }
}
4 changes: 4 additions & 0 deletions packages/client/src/api/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export const keys = {
// two always move together (sitting changes both, and a settle changes the
// seat counts the lobby lists), and the table poll is a 2.5s query anyway.
casinoTable: () => ["casino", "table"] as const,
// The caller's open machine session — `GET /api/casino/machine`. Nested
// under `casino()` for the same reason as the table: the lobby's own
// `invalidates: ["casino"]` and a pull-to-refresh then cover it too.
casinoMachine: () => ["casino", "machine"] as const,

// Admin sections (grant-gated).
adminSections: () => ["admin", "sections"] as const,
Expand Down
43 changes: 43 additions & 0 deletions packages/client/src/queries/casino.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
CasinoLeaveResponseSchema,
CasinoLobbyResponseSchema,
CasinoMachineResponseSchema,
CasinoSitResponseSchema,
CasinoStepResponseSchema,
CasinoTableResponseSchema,
type CasinoLeaveResponse,
type CasinoLobbyResponse,
type CasinoMachineResponse,
type CasinoSitResponse,
type CasinoStepResponse,
type CasinoTableResponse,
Expand Down Expand Up @@ -190,3 +192,44 @@ export function useTableAct() {
onSettled: () => { void queryClient.invalidateQueries({ queryKey: keys.me() }); },
});
}

// ---------------------------------------------------------------------------
// Machines
// ---------------------------------------------------------------------------

/**
* The caller's open machine session, wherever it is — `{ machine: null }`
* when they hold none. A machine outranks both the seat and the lobby: its
* credits are the player's cash on loan, so a page with one open shows it
* before anything else.
*/
export function useCasinoMachine() {
return useQuery<CasinoMachineResponse>({
queryKey: keys.casinoMachine(),
queryFn: async () => CasinoMachineResponseSchema.parse(await api("/api/casino/machine")),
});
}

/** The body every machine verb takes past `open`: the session and the revision it was read at. */
export type CasinoMachineVerb = "open" | "spin" | "act" | "cashout";

/**
* One mutation for all four machine verbs. Every reply is the whole machine,
* written straight into the machine query so the reels the player is looking
* at are the ones the server settled; an error invalidates instead, because a
* `stale_machine` 409 means the cached revision is the one that is wrong.
* Cash moves on `open` (credits in) and `cashout` (credits back), so `me` is
* refreshed whichever verb ran.
*/
export function useCasinoMachineAction() {
const queryClient = useQueryClient();
return useMutation<CasinoMachineResponse, Error, { verb: CasinoMachineVerb; body: Record<string, unknown> }>({
mutationFn: async ({ verb, body }) =>
CasinoMachineResponseSchema.parse(
await api(`/api/casino/machine/${verb}`, { method: "POST", body: JSON.stringify(body) }),
),
onSuccess: (response) => { queryClient.setQueryData(keys.casinoMachine(), response); },
onError: () => { void queryClient.invalidateQueries({ queryKey: keys.casinoMachine() }); },
onSettled: () => { void queryClient.invalidateQueries({ queryKey: keys.me() }); },
});
}
2 changes: 1 addition & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gl3/shared",
"version": "1.0.17",
"version": "1.0.18",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
Loading