deadhead-space-freight-tycoon/packages/sim/test/pay-rate.test.ts

81 lines
2.7 KiB
TypeScript

import { content } from "@deadhead/content";
import { describe, expect, it } from "vitest";
import { contractPayout, generateContractBoard, routeDistance, type Contract } from "../src/index";
const { config } = content;
const v = config.contractPayVariance;
const all = new Set(content.locations.map((l) => l.id));
/** The contract's payout without its market rate, from the same inputs. */
function basePayout(c: Contract): number {
const good = content.good(c.goodId)!;
return contractPayout(
{
cargoType: content.cargoType(c.cargoTypeId)!,
payMultiplier: good.payMultiplier,
tier: c.tier,
massTonnes: c.massTonnes,
distanceMillionKm: routeDistance(
content.location(c.originId)!,
content.location(c.destinationId)!,
),
firstContact: c.firstContact,
priority: c.priority,
},
config,
);
}
const contracts = Array.from({ length: 1000 }, (_, p) =>
generateContractBoard(
{ location: content.location("earth")!, discoveredIds: all, periodIndex: p, worldSeed: 99 },
content,
),
).flat();
describe("market rate (CR-008)", () => {
it("scales a payout by its rate", () => {
// Plain distance keeps the base payout whole, so the rate's effect is exact.
const linear = {
...config,
economicDistance: { ...config.economicDistance, exponent: 1 },
};
const input = {
cargoType: content.cargoType("standard")!,
payMultiplier: 1,
tier: 0,
massTonnes: 10,
distanceMillionKm: 100,
firstContact: false,
priority: false,
};
expect(contractPayout({ ...input, marketRate: 1.1 }, linear)).toBe(
Math.round(contractPayout(input, linear) * 1.1),
);
});
it("keeps every board contract's rate within ±variance, averaging 1", () => {
// Ignore tiny payouts, where rounding to whole credits blurs the rate.
const rates = contracts
.filter((c) => basePayout(c) >= 200)
.map((c) => c.payout / basePayout(c));
expect(rates.length).toBeGreaterThan(1000);
for (const rate of rates) {
expect(rate).toBeGreaterThanOrEqual(1 - v - 0.005);
expect(rate).toBeLessThanOrEqual(1 + v + 0.005);
}
const mean = rates.reduce((a, b) => a + b, 0) / rates.length;
expect(Math.abs(mean - 1)).toBeLessThanOrEqual(0.01);
});
it("no longer pays the identical amount for the same good, mass and route", () => {
const seen = new Map<string, Set<number>>();
for (const c of contracts) {
const key = `${c.goodId}|${c.massTonnes}|${c.destinationId}|${c.priority}|${c.firstContact}`;
seen.set(key, (seen.get(key) ?? new Set()).add(c.payout));
}
const repeated = [...seen.values()].filter((payouts) => payouts.size > 1);
expect(repeated.length).toBeGreaterThan(50);
});
});