116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { content } from "@deadhead/content";
|
|
import { contractBoardResponseSchema, errorResponseSchema } from "@deadhead/schema";
|
|
import { boardPeriodMs, createDispatchRecord, type Contract } from "@deadhead/sim";
|
|
import { eq } from "drizzle-orm";
|
|
import { describe, expect, it } from "vitest";
|
|
import type { Db } from "../src/db/client";
|
|
import { acceptedContracts, contractBoards, dispatches, players } from "../src/db/schema";
|
|
import { makeOwner, useTestDatabase } from "./db";
|
|
import { call, signUp, useTestApp, type Guest } from "./game";
|
|
|
|
const database = useTestDatabase();
|
|
const testApp = useTestApp(database);
|
|
|
|
async function board(guest: Guest) {
|
|
const response = await call(testApp().app, guest, "GET", "/api/tutorial/board");
|
|
expect(response.statusCode).toBe(200);
|
|
return contractBoardResponseSchema.parse(response.json());
|
|
}
|
|
|
|
/** Records a contract as accepted, with the tutorial dispatch it would create. */
|
|
async function accept(db: Db, guest: Guest, contract: Contract, now: number) {
|
|
const id = randomUUID();
|
|
const record = createDispatchRecord(
|
|
{
|
|
id,
|
|
ship: { id: "loaner", modelId: "courier", locationId: contract.originId, inFlight: false },
|
|
pilot: {
|
|
id: guest.avatarId,
|
|
isAvatar: true,
|
|
assignedShipId: null,
|
|
inFlight: false,
|
|
skills: {},
|
|
restEndsAt: null,
|
|
},
|
|
contract,
|
|
tutorial: true,
|
|
now,
|
|
worldSeed: 1,
|
|
},
|
|
content,
|
|
);
|
|
await db.insert(dispatches).values({
|
|
id,
|
|
playerId: guest.playerId,
|
|
pilotId: guest.avatarId,
|
|
tutorial: true,
|
|
record,
|
|
arrivalAt: record.arrivalAt,
|
|
});
|
|
await db.insert(acceptedContracts).values({
|
|
playerId: guest.playerId,
|
|
contractId: contract.id,
|
|
periodIndex: 0,
|
|
dispatchId: id,
|
|
acceptedAt: now,
|
|
});
|
|
}
|
|
|
|
describe("tutorial board endpoint", () => {
|
|
it("gives a tutorial player at Earth a full board of Earth→Moon contracts", async () => {
|
|
const { app, clock } = testApp();
|
|
const guest = await signUp(app);
|
|
const { contracts, periodEndsAt } = await board(guest);
|
|
|
|
expect(contracts).toHaveLength(content.config.tutorialBoardSize);
|
|
for (const c of contracts) {
|
|
expect([c.originId, c.destinationId]).toEqual(["earth", "moon"]);
|
|
expect(c.cargoTypeId).toBe("standard");
|
|
}
|
|
expect(periodEndsAt).toBeGreaterThan(clock.now);
|
|
expect(periodEndsAt - clock.now).toBeLessThanOrEqual(boardPeriodMs(content.config));
|
|
});
|
|
|
|
it("offers Moon→Earth when the loaner is at the Moon", async () => {
|
|
const { app } = testApp();
|
|
const guest = await signUp(app);
|
|
await database()
|
|
.db.update(players)
|
|
.set({ loanerLocationId: "moon" })
|
|
.where(eq(players.id, guest.playerId));
|
|
for (const c of (await board(guest)).contracts) {
|
|
expect([c.originId, c.destinationId]).toEqual(["moon", "earth"]);
|
|
}
|
|
});
|
|
|
|
it("stores the board: repeated requests in a period return the same contracts", async () => {
|
|
const { clock } = testApp();
|
|
const guest = await signUp(testApp().app);
|
|
const first = await board(guest);
|
|
clock.advance(60_000);
|
|
expect((await board(guest)).contracts).toEqual(first.contracts);
|
|
expect(await database().db.select().from(contractBoards)).toHaveLength(1);
|
|
});
|
|
|
|
it("hides contracts the player has already accepted", async () => {
|
|
const { clock } = testApp();
|
|
const guest = await signUp(testApp().app);
|
|
const before = await board(guest);
|
|
await accept(database().db, guest, before.contracts[0]!, clock.now);
|
|
|
|
const after = await board(guest);
|
|
expect(after.contracts).toHaveLength(before.contracts.length - 1);
|
|
expect(after.contracts.map((c) => c.id)).not.toContain(before.contracts[0]!.id);
|
|
});
|
|
|
|
it("refuses players who are past the tutorial", async () => {
|
|
const { app } = testApp();
|
|
const guest = await signUp(app);
|
|
await makeOwner(database().db, guest.playerId, guest.avatarId);
|
|
const response = await call(app, guest, "GET", "/api/tutorial/board");
|
|
expect(response.statusCode).toBe(409);
|
|
expect(errorResponseSchema.parse(response.json()).error.code).toBe("not-in-tutorial");
|
|
});
|
|
});
|