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

170 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
});
});