127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
import { content } from "@deadhead/content";
|
||
import { describe, expect, it } from "vitest";
|
||
import {
|
||
createDispatchRecord,
|
||
createRepositionRecord,
|
||
createRng,
|
||
pilotShare,
|
||
resolveDispatch,
|
||
type Contract,
|
||
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: 400,
|
||
requiredSkill: null,
|
||
requiredLongHaulLevel: 0,
|
||
offeredAt: now,
|
||
expiresAt: now + 3_600_000,
|
||
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 base = createDispatchRecord(
|
||
{ id: "d1", ship, pilot, contract, tutorial: false, now, worldSeed: 7 },
|
||
content,
|
||
);
|
||
/** The trip with a chosen payout and fuel cost. */
|
||
const trip = (payout: number, fuelCost: number, extra: Partial<DispatchRecord> = {}) => ({
|
||
...base,
|
||
contract: { ...contract, payout },
|
||
fuelCost,
|
||
...extra,
|
||
});
|
||
const resolve = (record: DispatchRecord, more: Partial<ResolveInput> = {}) => {
|
||
const result = resolveDispatch(
|
||
{
|
||
record,
|
||
pilotIsAvatar: false,
|
||
discoveredIds: new Set(["earth", "moon"]),
|
||
loanBalance: 0,
|
||
pilotDutyHours: 0,
|
||
pilotSkills: {},
|
||
now: record.arrivalAt,
|
||
...more,
|
||
},
|
||
content,
|
||
);
|
||
if (result.status !== "resolved") throw new Error("not resolved");
|
||
return result.resolution;
|
||
};
|
||
|
||
describe("profit-share pilot pay (Step 10J.2)", () => {
|
||
it("pays a hired pilot a quarter of payout minus fuel, rounded like other credits", () => {
|
||
// 0.25 × (400 − 150) = 62.5 → 63 (Math.round, as payouts and fuel).
|
||
expect(pilotShare(400, 150, content.config)).toBe(63);
|
||
expect(resolve(trip(400, 150))).toMatchObject({ pilotPay: 63, credits: 400 - 63 });
|
||
});
|
||
|
||
it("pays nothing when fuel costs as much as the payout or more", () => {
|
||
expect(resolve(trip(400, 400)).pilotPay).toBe(0);
|
||
expect(resolve(trip(400, 900))).toMatchObject({ pilotPay: 0, credits: 400 });
|
||
});
|
||
|
||
it("pays the avatar, deadheads and tutorial runs nothing", () => {
|
||
expect(resolve(trip(400, 150), { pilotIsAvatar: true }).pilotPay).toBe(0);
|
||
expect(resolve(trip(400, 150, { tutorial: true })).pilotPay).toBe(0);
|
||
const deadhead = createRepositionRecord(
|
||
{ id: "r1", ship, pilot, destinationId: "moon", now, worldSeed: 7 },
|
||
content,
|
||
);
|
||
expect(resolve(deadhead)).toMatchObject({ pilotPay: 0, credits: 0 });
|
||
});
|
||
|
||
it("takes the loan repayment from what is left after pilot pay", () => {
|
||
const resolution = resolve(trip(400, 150), { loanBalance: 5000 });
|
||
const afterPay = 400 - 63;
|
||
expect(resolution).toMatchObject({
|
||
pilotPay: 63,
|
||
loanRepayment: Math.floor(afterPay * content.config.loanRepaymentShare),
|
||
});
|
||
});
|
||
|
||
it("always splits the payout exactly into credits, pilot pay and repayment", () => {
|
||
const rng = createRng(10);
|
||
for (let i = 0; i < 500; i++) {
|
||
const payout = rng.int(0, 50_000);
|
||
const fuel = rng.int(0, 20_000);
|
||
const r = resolve(trip(payout, fuel), {
|
||
loanBalance: rng.int(0, 6000),
|
||
pilotIsAvatar: rng.next() < 0.3,
|
||
});
|
||
expect(r.credits + r.pilotPay + r.loanRepayment).toBe(payout);
|
||
expect(r.pilotPay).toBeGreaterThanOrEqual(0);
|
||
}
|
||
});
|
||
});
|