deadhead-space-freight-tycoon/apps/server/test/duty-rest.test.ts

159 lines
5.6 KiB
TypeScript

import { content } from "@deadhead/content";
import {
dispatchResponseSchema,
errorResponseSchema,
pilotResponseSchema,
type PlayerState,
} from "@deadhead/schema";
import { restEndsAt } from "@deadhead/sim";
import { describe, expect, it } from "vitest";
import { setCredits, useTestDatabase } from "./db";
import {
call,
getState,
insertDispatch,
putBoard,
signUp,
signUpOwner,
testContract,
useTestApp,
type Guest,
} from "./game";
const database = useTestDatabase();
const testApp = useTestApp(database);
const errorCode = (response: { json(): unknown }) =>
errorResponseSchema.parse(response.json()).error.code;
const avatar = (state: PlayerState) => state.pilots.find((p) => p.isAvatar)!;
const FAR_FUTURE = Date.parse("2200-01-01T00:00:00Z");
/** Flies the owner's Courier Earth↔Moon until a trip ends in mandatory rest. */
async function chainUntilRest(owner: Guest & { shipId: string }) {
const { clock } = testApp();
let at = "earth";
for (let trip = 0; trip < 20; trip++) {
const to = at === "earth" ? "moon" : "earth";
const record = await insertDispatch(database().db, {
playerId: owner.playerId,
shipId: owner.shipId,
pilotId: owner.avatarId,
departAt: clock.now,
contract: testContract({ originId: at, destinationId: to, expiresAt: FAR_FUTURE }),
});
clock.now = record.arrivalAt;
const state = await getState(testApp().app, owner);
at = to;
if (avatar(state).resting) return { state, record, at };
expect(avatar(state).dutyHours).toBeGreaterThan(0);
}
throw new Error("never needed rest");
}
describe("pilot duty and rest on the server (Step 10D.5)", () => {
it("rests a pilot after chained runs, refuses them until rest ends, then dispatches", async () => {
const { app, clock } = testApp();
const owner = await signUpOwner(app, database().db);
await setCredits(database().db, owner.playerId, 50_000);
const { state, record, at } = await chainUntilRest(owner);
const rested = avatar(state);
expect(rested).toMatchObject({ dutyHours: 0, resting: true });
// Rest starts at the arrival time (decision 0.5.45).
expect(rested.restEndsAt).toBe(restEndsAt(record.arrivalAt, content.config));
expect(state.summaries.at(-1)!.restEndsAt).toBe(rested.restEndsAt);
const job = testContract({
originId: at,
destinationId: at === "earth" ? "moon" : "earth",
expiresAt: FAR_FUTURE,
});
await putBoard(database().db, owner.playerId, [job], clock.now, at);
const dispatch = () =>
call(app, owner, "POST", "/api/dispatches", {
kind: "contract",
contractId: job.id,
shipId: owner.shipId,
pilotId: owner.avatarId,
});
const refused = await dispatch();
expect(refused.statusCode).toBe(409);
expect(errorCode(refused)).toBe("pilot-resting");
clock.now = rested.restEndsAt!;
const accepted = await dispatch();
expect(accepted.statusCode).toBe(200);
dispatchResponseSchema.parse(accepted.json());
});
it("rests an idle pilot by hand, and refuses resting one who is flying or already resting", async () => {
const { app, clock } = testApp();
const owner = await signUpOwner(app, database().db);
const response = await call(app, owner, "POST", `/api/pilots/${owner.avatarId}/rest`, {});
expect(response.statusCode).toBe(200);
expect(pilotResponseSchema.parse(response.json()).pilot).toMatchObject({
dutyHours: 0,
resting: true,
restEndsAt: restEndsAt(clock.now, content.config),
});
const again = await call(app, owner, "POST", `/api/pilots/${owner.avatarId}/rest`, {});
expect(again.statusCode).toBe(409);
expect(errorCode(again)).toBe("pilot-resting");
const other = await signUpOwner(app, database().db);
await insertDispatch(database().db, {
playerId: other.playerId,
shipId: other.shipId,
pilotId: other.avatarId,
departAt: clock.now,
});
const flying = await call(app, other, "POST", `/api/pilots/${other.avatarId}/rest`, {});
expect(flying.statusCode).toBe(409);
expect(errorCode(flying)).toBe("pilot-not-idle");
});
it("lets a resting pilot leave the ship at a bay and a fresh pilot fly it at once", async () => {
const { app, clock } = testApp();
const owner = await signUpOwner(app, database().db);
await setCredits(database().db, owner.playerId, 50_000);
await call(app, owner, "POST", `/api/pilots/${owner.avatarId}/rest`, {});
const unassign = await call(app, owner, "POST", `/api/pilots/${owner.avatarId}/unassign`, {});
expect(unassign.statusCode).toBe(200);
const hired = pilotResponseSchema.parse(
(await call(app, owner, "POST", "/api/pilots", { locationId: "earth" })).json(),
).pilot;
const assign = await call(app, owner, "POST", `/api/pilots/${hired.id}/assign`, {
shipId: owner.shipId,
});
expect(assign.statusCode).toBe(200);
const job = testContract({ expiresAt: FAR_FUTURE });
await putBoard(database().db, owner.playerId, [job], clock.now);
const response = await call(app, owner, "POST", "/api/dispatches", {
kind: "contract",
contractId: job.id,
shipId: owner.shipId,
pilotId: hired.id,
});
expect(response.statusCode).toBe(200);
});
it("does not rest the avatar during the tutorial", async () => {
const guest = await signUp(testApp().app);
const response = await call(
testApp().app,
guest,
"POST",
`/api/pilots/${guest.avatarId}/rest`,
{},
);
expect(response.statusCode).toBe(409);
expect(errorCode(response)).toBe("tutorial-active");
});
});