170 lines
6.0 KiB
TypeScript
170 lines
6.0 KiB
TypeScript
import { content } from "@deadhead/content";
|
||
import { describe, expect, it } from "vitest";
|
||
import {
|
||
affordableRefuel,
|
||
createDispatchRecord,
|
||
fuelPriceAt,
|
||
legFuel,
|
||
legFuelCost,
|
||
pilotShare,
|
||
refuel,
|
||
resolveDispatch,
|
||
routeDistance,
|
||
type Contract,
|
||
type PilotState,
|
||
type ResolveInput,
|
||
type ShipState,
|
||
} from "../src/index";
|
||
|
||
const { config } = content;
|
||
const courier = content.shipModel("courier")!;
|
||
const [earth, moon] = [content.location("earth")!, content.location("moon")!];
|
||
const now = Date.parse("2100-01-01T00:30:00Z");
|
||
const MOON_PRICE = fuelPriceAt(moon, config);
|
||
|
||
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 pilot: PilotState = {
|
||
id: "p1",
|
||
isAvatar: false,
|
||
assignedShipId: "s1",
|
||
inFlight: false,
|
||
skills: {},
|
||
restEndsAt: null,
|
||
};
|
||
/** A Courier at Earth with a full tank bought at Earth's price. */
|
||
const ship: ShipState = {
|
||
id: "s1",
|
||
modelId: "courier",
|
||
locationId: "earth",
|
||
inFlight: false,
|
||
tank: { fuelFu: courier.tankCapacityFu, avgPrice: fuelPriceAt(earth, config) },
|
||
};
|
||
const record = createDispatchRecord(
|
||
{ id: "d1", ship, pilot, contract, tutorial: false, now, worldSeed: 7 },
|
||
content,
|
||
);
|
||
/** The tank on landing: what was left after the leg's fuel came out at departure. */
|
||
const landedTank = { ...ship.tank!, fuelFu: ship.tank!.fuelFu - record.fuelBurnedFu };
|
||
|
||
function landing(policy: "auto" | "manual", credits: number): ResolveInput {
|
||
return {
|
||
record,
|
||
pilotIsAvatar: false,
|
||
discoveredIds: new Set(["earth", "moon"]),
|
||
loanBalance: 0,
|
||
pilotDutyHours: 0,
|
||
pilotSkills: {},
|
||
now: record.arrivalAt,
|
||
fuel: { tank: landedTank, capacityFu: courier.tankCapacityFu, refuelPolicy: policy, credits },
|
||
};
|
||
}
|
||
function resolve(input: ResolveInput) {
|
||
const result = resolveDispatch(input, content);
|
||
if (result.status !== "resolved") throw new Error("expected a resolution");
|
||
return result.resolution;
|
||
}
|
||
|
||
describe("fuel tanks (Step 15.4)", () => {
|
||
it("burns less from the Moon: the leg is shortened by 1.3 before the pay curve (0.5.95)", () => {
|
||
const d = routeDistance(earth, moon);
|
||
// The CR's ÷ 1.3, applied before the economic-distance curve (user decision).
|
||
expect(legFuel(courier, moon, d, config)).toBeCloseTo(legFuel(courier, earth, d / 1.3, config));
|
||
expect(legFuel(courier, moon, d, config)).toBeCloseTo(
|
||
legFuel(courier, earth, d, config) / 1.3 ** config.economicDistance.exponent,
|
||
);
|
||
});
|
||
|
||
it("averages the price of what is in the tank", () => {
|
||
const filled = refuel({ fuelFu: 50, avgPrice: 2 }, 200, 50, 1);
|
||
expect(filled.tank).toEqual({ fuelFu: 100, avgPrice: 1.5 });
|
||
expect(filled.boughtFu).toBe(50);
|
||
expect(filled.cost).toBe(50);
|
||
});
|
||
|
||
it("never fills past capacity", () => {
|
||
const filled = refuel({ fuelFu: 90, avgPrice: 2 }, 100, 50, 1);
|
||
expect(filled.boughtFu).toBe(10);
|
||
expect(filled.tank.fuelFu).toBe(100);
|
||
});
|
||
|
||
it("buys exactly what the credits cover, up to a full tank", () => {
|
||
// Half a tank's worth of credits at 10 cr/fu.
|
||
const half = affordableRefuel({ fuelFu: 0, avgPrice: 0 }, 100, 10, 500);
|
||
expect(half.boughtFu).toBe(50);
|
||
expect(half.cost).toBe(500);
|
||
expect(affordableRefuel({ fuelFu: 0, avgPrice: 0 }, 100, 10, 5_000).boughtFu).toBe(100);
|
||
expect(affordableRefuel({ fuelFu: 0, avgPrice: 0 }, 100, 10, 0).boughtFu).toBe(0);
|
||
});
|
||
|
||
it("takes the leg's fuel from the tank and costs it at the tank's average price", () => {
|
||
const burned = legFuel(courier, earth, routeDistance(earth, moon), config);
|
||
expect(record.fuelBurnedFu).toBeCloseTo(burned);
|
||
expect(record.fuelCost).toBe(legFuelCost(burned, ship.tank!));
|
||
// Filled at Earth: 1.6 × the base price, 8 cr/fu since CR-018 (decision 0.5.110).
|
||
expect(record.fuelCost).toBe(Math.round(burned * 8));
|
||
});
|
||
|
||
it("pays a hired pilot from fuel burned × average price (decision 0.5.98)", () => {
|
||
expect(resolve(landing("manual", 0)).pilotPay).toBe(
|
||
pilotShare(contract.payout, record.fuelCost, config),
|
||
);
|
||
});
|
||
|
||
it("tops up to a full tank on landing with Auto, paying the local price", () => {
|
||
const { topUp } = resolve(landing("auto", 10_000));
|
||
expect(topUp).not.toBeNull();
|
||
expect(topUp!.locationId).toBe("moon");
|
||
expect(topUp!.tank.fuelFu).toBeCloseTo(courier.tankCapacityFu);
|
||
expect(topUp!.boughtFu).toBeCloseTo(record.fuelBurnedFu);
|
||
expect(topUp!.price).toBe(MOON_PRICE);
|
||
expect(topUp!.cost).toBe(Math.round(record.fuelBurnedFu * MOON_PRICE));
|
||
});
|
||
|
||
it("buys nothing with Manual", () => {
|
||
expect(resolve(landing("manual", 10_000)).topUp).toBeNull();
|
||
});
|
||
|
||
it("buys only what the credits after this delivery cover", () => {
|
||
const delivery = resolve(landing("manual", 0)).credits;
|
||
// Credits that, with this delivery's pay, cover half the fuel burned.
|
||
const halfCost = (record.fuelBurnedFu / 2) * MOON_PRICE;
|
||
const { topUp } = resolve(landing("auto", halfCost - delivery));
|
||
expect(topUp!.boughtFu).toBeCloseTo(record.fuelBurnedFu / 2);
|
||
expect(topUp!.cost).toBeLessThanOrEqual(Math.floor(halfCost));
|
||
});
|
||
|
||
it("leaves the tutorial loaner's fuel alone", () => {
|
||
const loaner: ShipState = {
|
||
id: "loaner",
|
||
modelId: "company-courier",
|
||
locationId: "earth",
|
||
inFlight: false,
|
||
};
|
||
const avatar = { ...pilot, id: "avatar", isAvatar: true, assignedShipId: null };
|
||
const tutorialRun = createDispatchRecord(
|
||
{ id: "t1", ship: loaner, pilot: avatar, contract, tutorial: true, now, worldSeed: 7 },
|
||
content,
|
||
);
|
||
expect(tutorialRun.fuelBurnedFu).toBe(0);
|
||
expect(tutorialRun.fuelCost).toBe(0);
|
||
expect(resolve({ ...landing("auto", 10_000), record: tutorialRun }).topUp).toBeNull();
|
||
});
|
||
});
|