151 lines
5.3 KiB
TypeScript
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);
|
|
});
|
|
});
|