deadhead-space-freight-tycoon/packages/sim/test/service.test.ts

168 lines
6.0 KiB
TypeScript

import { content } from "@deadhead/content";
import { describe, expect, it } from "vitest";
import {
autoRepairBooks,
boardPeriodMs,
canReachHq,
chooseAction,
defaultOrders,
fuelPriceAt,
independentOptions,
MS_PER_MINUTE,
nearestReachableHq,
repairRecommended,
type Contract,
type IndependentInput,
} from "../src/index";
const { config } = content;
const clipper = content.shipModel("clipper")!;
const PERIOD = boardPeriodMs(config);
const PERIOD_START = 1000 * PERIOD;
const AFTER_LOOK =
PERIOD_START + (config.independentPilots.firstLookRealMinutes + 1) * MS_PER_MINUTE;
const EARTH_PRICE = fuelPriceAt(content.location("earth")!, config);
const DISCOVERED = new Set(["earth", "moon", "mars", "ceres-station", "jupiter-cloud-platform"]);
const job = (id: string, destinationId: string): Contract => ({
id,
originId: "earth",
destinationId,
cargoTypeId: "standard",
goodId: "consumer-goods",
shipper: "Pacific Orbital Logistics",
priority: false,
deliveryWindowGameHours: null,
tier: 0,
massTonnes: 10,
payout: 30_000,
requiredSkill: null,
requiredLongHaulLevel: 0,
offeredAt: PERIOD_START,
expiresAt: PERIOD_START + PERIOD,
firstContact: false,
});
function input(board: Contract[], over: Partial<IndependentInput> = {}): IndependentInput {
return {
ship: {
id: "s",
modelId: "clipper",
locationId: "earth",
inFlight: false,
tank: { fuelFu: clipper.tankCapacityFu, avgPrice: EARTH_PRICE },
},
pilot: {
id: "p",
isAvatar: false,
assignedShipId: "s",
inFlight: false,
skills: { "long-haul": 3 },
restEndsAt: null,
},
orders: defaultOrders("earth", content),
board,
now: AFTER_LOOK,
credits: 1_000_000,
spendingFloor: config.independentPilots.defaultSpendingFloor,
discoveredIds: DISCOVERED,
emptyPeriods: 0,
...over,
};
}
// Mars is one leg from Earth for a Clipper (CR-015); Ceres isn't.
const mars = { hqId: "mars" };
describe("service routing (Step 27.2, decisions 0.5.153, 0.5.154)", () => {
it("can reach Mars but not Ceres from Earth in a Clipper", () => {
expect(canReachHq(input([]).ship, "mars", DISCOVERED, content)).toBe(true);
expect(canReachHq(input([]).ship, "ceres-station", DISCOVERED, content)).toBe(false);
});
it("with a booking at Mars, considers only jobs that end at Mars", () => {
const board = [job("a", "moon"), job("b", "ceres-station"), job("c", "mars")];
const options = independentOptions(input(board, { booking: mars }), content);
expect(options.map((o) => o.contract.destinationId)).toEqual(["mars"]);
expect(chooseAction(input(board, { booking: mars }), content)).toMatchObject({
kind: "take",
contractId: "c",
});
});
it("deadheads straight to Mars when no job goes there", () => {
const board = [job("a", "moon")];
expect(chooseAction(input(board, { booking: mars }), content)).toMatchObject({
kind: "deadhead-service",
destinationId: "mars",
});
});
it("waits at the booked HQ for the work to start", () => {
const at = input([job("a", "moon")], { booking: mars });
const there = { ...at, ship: { ...at.ship, locationId: "mars" } };
expect(chooseAction(there, content)).toEqual({ kind: "wait" });
});
it("works as usual when the booked HQ is out of reach", () => {
// A Courier can't reach Jupiter from Earth.
const courier = content.shipModel("courier")!;
const base = input([job("a", "moon")], { booking: { hqId: "jupiter-cloud-platform" } });
const small = {
...base,
ship: {
...base.ship,
modelId: "courier",
tank: { fuelFu: courier.tankCapacityFu, avgPrice: 5 },
},
};
expect(canReachHq(small.ship, "jupiter-cloud-platform", DISCOVERED, content)).toBe(false);
expect(chooseAction(small, content)).toMatchObject({ kind: "take", contractId: "a" });
});
it("finds the nearest reachable HQ", () => {
const ship = input([]).ship;
expect(nearestReachableHq(ship, ["mars", "moon"], DISCOVERED, content)).toBe("moon");
expect(nearestReachableHq(ship, ["earth", "moon"], DISCOVERED, content)).toBe("earth");
const courier = { ...ship, modelId: "courier", tank: { fuelFu: 40, avgPrice: 5 } };
expect(nearestReachableHq(courier, ["jupiter-cloud-platform"], DISCOVERED, content)).toBeNull();
});
});
describe("auto-repair (Step 27.2, decision 0.5.157)", () => {
const on = { on: true, thresholdPct: 40 };
const drop = { hullBefore: 45, hullAfter: 35, autoRepair: on, booking: null };
it("books a repair when an Independent crew's hull drops below the threshold", () => {
expect(autoRepairBooks({ ...drop, pilotMode: "independent" })).toBe(true);
});
it("doesn't with a Manual crew, with auto-repair off, or without crossing the threshold", () => {
expect(autoRepairBooks({ ...drop, pilotMode: "manual" })).toBe(false);
expect(
autoRepairBooks({ ...drop, pilotMode: "independent", autoRepair: { ...on, on: false } }),
).toBe(false);
expect(autoRepairBooks({ ...drop, pilotMode: "independent", hullBefore: 38 })).toBe(false);
expect(autoRepairBooks({ ...drop, pilotMode: "independent", hullAfter: 41 })).toBe(false);
});
it("adds the repair to an upgrade booking, but not a second repair", () => {
expect(autoRepairBooks({ ...drop, pilotMode: "independent", booking: { repair: false } })).toBe(
true,
);
expect(autoRepairBooks({ ...drop, pilotMode: "independent", booking: { repair: true } })).toBe(
false,
);
});
it("recommends a repair for a Manual crew below the threshold", () => {
const base = { hullPct: 35, autoRepair: on, booking: null };
expect(repairRecommended({ ...base, pilotMode: "manual" })).toBe(true);
expect(repairRecommended({ ...base, pilotMode: "independent" })).toBe(false);
expect(repairRecommended({ ...base, pilotMode: "manual", hullPct: 50 })).toBe(false);
expect(repairRecommended({ ...base, pilotMode: "manual", booking: { repair: true } })).toBe(
false,
);
});
});