deadhead-space-freight-tycoon/apps/server/test/resolution.test.ts

151 lines
5.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { withPlayerLock } from "../src/game/lock";
import { resolveArrivedDispatches } from "../src/game/resolve";
import { makeOwner, setCredits, useTestDatabase } from "./db";
import { getState, insertDispatch, signUp, testContract, useTestApp } from "./game";
const database = useTestDatabase();
const testApp = useTestApp(database);
/** An owner (one Courier at Earth, crewed by the avatar) with 1,000 credits. */
async function owner() {
const { app } = testApp();
const guest = await signUp(app);
const shipId = await makeOwner(database().db, guest.playerId, guest.avatarId);
await setCredits(database().db, guest.playerId, 1000);
return { guest, shipId };
}
describe("lazy resolution on read", () => {
it("applies a past arrival's credits, XP, ship location and discovery", async () => {
const { app, clock } = testApp();
const { guest, shipId } = await owner();
// A delivery to Mars, which the player has not discovered yet.
const contract = testContract({ destinationId: "mars", payout: 5000 });
const record = await insertDispatch(database().db, {
playerId: guest.playerId,
shipId,
pilotId: guest.avatarId,
departAt: clock.now,
contract,
});
clock.now = record.arrivalAt + 60_000;
const state = await getState(app, guest);
// The avatar flew it, so no pilot pay is deducted.
expect(state.company.credits).toBe(1000 + 5000);
expect(state.pilots[0]!.xp).toBeGreaterThan(0);
expect(state.pilots[0]!.status).toBe("idle");
expect(state.ships[0]).toMatchObject({ status: "idle", locationId: "mars", dispatchId: null });
expect(state.locations.find((l) => l.id === "mars")!.discovered).toBe(true);
expect(state.activeDispatches).toHaveLength(0);
expect(state.summaries).toEqual([
expect.objectContaining({
dispatchId: record.id,
credits: 5000,
discoveredLocationId: "mars",
}),
]);
});
it("leaves dispatches that have not arrived yet in flight", async () => {
const { app, clock } = testApp();
const { guest, shipId } = await owner();
const record = await insertDispatch(database().db, {
playerId: guest.playerId,
shipId,
pilotId: guest.avatarId,
departAt: clock.now,
});
clock.now = record.arrivalAt - 1;
const state = await getState(app, guest);
expect(state.company.credits).toBe(1000);
expect(state.ships[0]).toMatchObject({
status: "in-flight",
locationId: null,
dispatchId: record.id,
});
expect(state.activeDispatches.map((d) => d.id)).toEqual([record.id]);
});
it("does not apply an arrival twice on a second fetch", async () => {
const { app, clock } = testApp();
const { guest, shipId } = await owner();
const record = await insertDispatch(database().db, {
playerId: guest.playerId,
shipId,
pilotId: guest.avatarId,
departAt: clock.now,
});
clock.now = record.arrivalAt;
const first = await getState(app, guest);
const second = await getState(app, guest);
expect(second.company.credits).toBe(first.company.credits);
expect(second.pilots[0]!.xp).toBe(first.pilots[0]!.xp);
expect(first.company.credits).toBe(1000 + 2000);
});
it("applies an arrival exactly once under two concurrent fetches", async () => {
const { app, clock } = testApp();
const { guest, shipId } = await owner();
const record = await insertDispatch(database().db, {
playerId: guest.playerId,
shipId,
pilotId: guest.avatarId,
departAt: clock.now,
});
clock.now = record.arrivalAt;
const [a, b] = await Promise.all([getState(app, guest), getState(app, guest)]);
expect(a.company.credits).toBe(3000);
expect(b.company.credits).toBe(3000);
});
it("resolves exactly once when two resolvers race outside HTTP", async () => {
const { clock } = testApp();
const { guest, shipId } = await owner();
const { db } = database();
const record = await insertDispatch(db, {
playerId: guest.playerId,
shipId,
pilotId: guest.avatarId,
departAt: clock.now,
});
const run = () =>
withPlayerLock(db, guest.playerId, (tx) =>
resolveArrivedDispatches(tx, guest.playerId, record.arrivalAt),
);
const counts = await Promise.all([run(), run(), run()]);
expect(counts.reduce((a, b) => a + b, 0)).toBe(1);
});
it("resolves several arrivals in arrival order", async () => {
const { app, clock } = testApp();
const { guest, shipId } = await owner();
const { db } = database();
// Fly to the Moon, then (already resolved in the DB as of arrival) on to Earth.
const first = await insertDispatch(db, {
playerId: guest.playerId,
shipId,
pilotId: guest.avatarId,
departAt: clock.now,
});
clock.now = first.arrivalAt;
await getState(app, guest);
const second = await insertDispatch(db, {
playerId: guest.playerId,
shipId,
pilotId: guest.avatarId,
departAt: first.arrivalAt,
contract: testContract({ originId: "moon", destinationId: "earth", payout: 700 }),
});
clock.now = second.arrivalAt + 1;
const state = await getState(app, guest);
expect(state.company.credits).toBe(1000 + 2000 + 700);
expect(state.ships[0]!.locationId).toBe("earth");
expect(state.summaries).toHaveLength(2);
});
});