deadhead-space-freight-tycoon/packages/sim/test/profit-share.test.ts

127 lines
3.8 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 {
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);
}
});
});