deadhead-space-freight-tycoon/packages/sim/test/duty-dispatch.test.ts

147 lines
4.3 KiB
TypeScript

import { content } from "@deadhead/content";
import { describe, expect, it } from "vitest";
import {
MS_PER_HOUR,
createDispatchRecord,
createRepositionRecord,
dutyLimit,
resolveDispatch,
restEndsAt,
tripGameHours,
validateDispatch,
validateReposition,
type Contract,
type DispatchProposal,
type DispatchRecord,
type PilotState,
type ResolveInput,
type ShipState,
} from "../src/index";
/** Fuel enough for any leg in these tests (decision 0.5.94), bought at 10 cr/fu. */
const PLENTY = { fuelFu: 10_000, avgPrice: 10 };
const now = Date.parse("2100-01-01T00:30:00Z");
const contract: Contract = {
id: "c1",
originId: "earth",
destinationId: "moon",
cargoTypeId: "standard",
goodId: "consumer-goods",
shipper: "Pacific Orbital Logistics",
priority: false,
deliveryWindowGameHours: null,
tier: 0,
massTonnes: 10,
payout: 2000,
requiredSkill: null,
requiredLongHaulLevel: 0,
offeredAt: Date.parse("2100-01-01T00:00:00Z"),
expiresAt: Date.parse("2100-01-01T01:00:00Z"),
firstContact: false,
};
const ship: ShipState = {
id: "s1",
modelId: "courier",
locationId: "earth",
inFlight: false,
tank: PLENTY,
};
const pilot: PilotState = {
id: "p1",
isAvatar: false,
assignedShipId: "s1",
inFlight: false,
skills: {},
restEndsAt: null,
};
const proposal: DispatchProposal = { ship, pilot, contract, tutorial: false, now };
const limit = dutyLimit({}, content.config);
const resolveAt = (record: DispatchRecord, dutyBefore: number, at = record.arrivalAt) => {
const input: ResolveInput = {
record,
pilotIsAvatar: false,
discoveredIds: new Set(["earth", "moon"]),
loanBalance: 0,
pilotDutyHours: dutyBefore,
pilotSkills: {},
now: at,
};
const result = resolveDispatch(input, content);
if (result.status !== "resolved") throw new Error("not resolved");
return result.resolution;
};
describe("duty in validation (Step 10D.3)", () => {
it("refuses a resting pilot with pilot-resting, before other pilot checks", () => {
const resting = { ...pilot, restEndsAt: now + 60_000, inFlight: true, assignedShipId: null };
expect(validateDispatch({ ...proposal, pilot: resting }, content)).toEqual({
ok: false,
reason: "pilot-resting",
});
expect(
validateReposition(
{
ship,
pilot: { ...pilot, restEndsAt: now + 1 },
destinationId: "moon",
discoveredIds: new Set(["earth", "moon"]),
now,
},
content,
),
).toEqual({ ok: false, reason: "pilot-resting" });
});
it("lets a pilot whose rest has ended fly", () => {
expect(
validateDispatch({ ...proposal, pilot: { ...pilot, restEndsAt: now } }, content),
).toEqual({
ok: true,
});
});
});
describe("duty in resolution (Step 10D.3)", () => {
const record = createDispatchRecord({ ...proposal, id: "d1", worldSeed: 7 }, content);
const hours = tripGameHours(record.departureAt, record.arrivalAt, content.config);
it("adds the trip's hours to duty when under the limit", () => {
expect(resolveAt(record, 1)).toMatchObject({
pilotDutyHours: 1 + hours,
pilotRestEndsAt: null,
});
});
it("starts rest at the arrival time when duty reaches the limit, even resolved hours later", () => {
const late = record.arrivalAt + 5 * MS_PER_HOUR;
expect(resolveAt(record, limit - hours / 2, late)).toMatchObject({
pilotDutyHours: 0,
pilotRestEndsAt: restEndsAt(record.arrivalAt, content.config),
});
expect(restEndsAt(record.arrivalAt, content.config)).toBe(
record.arrivalAt +
(content.config.restGameHours * MS_PER_HOUR) / content.config.timeCompression,
);
});
it("counts deadheads but not tutorial runs", () => {
const deadhead = createRepositionRecord(
{ id: "r1", ship, pilot, destinationId: "moon", now, worldSeed: 7 },
content,
);
const deadheadHours = tripGameHours(deadhead.departureAt, deadhead.arrivalAt, content.config);
expect(resolveAt(deadhead, 2).pilotDutyHours).toBeCloseTo(2 + deadheadHours);
const tutorial = createDispatchRecord(
{ ...proposal, tutorial: true, pilot: { ...pilot, isAvatar: true }, id: "t1", worldSeed: 7 },
content,
);
expect(resolveAt(tutorial, limit - 0.1)).toMatchObject({
pilotDutyHours: limit - 0.1,
pilotRestEndsAt: null,
});
});
});