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

173 lines
6.6 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 { describe, expect, it } from "vitest";
import { buildAwaySummary, flightNet, type AwayProblem, type LogbookEntry } from "../src/index";
const FROM = 1_000_000;
const HOUR = 3_600_000;
let n = 0;
const flight = (pilotId: string, payout: number, over: Partial<LogbookEntry> = {}): LogbookEntry =>
({
id: `f${n++}`,
at: FROM + 60_000 + n,
pilotId,
shipId: `ship-${pilotId}`,
kind: "flight",
dispatchId: `d${n}`,
deadhead: false,
tutorial: false,
independent: true,
originId: "earth",
destinationId: "moon",
goodId: "consumer-goods",
massTonnes: 6,
shipper: "Pacific Orbital Logistics",
payout,
pilotPay: Math.round(payout / 5),
fuelCost: 100,
loanRepayment: 0,
xp: 10,
scriptedId: null,
...over,
}) as LogbookEntry;
const other = (entry: Partial<LogbookEntry> & { kind: LogbookEntry["kind"] }): LogbookEntry =>
({ id: `e${n++}`, at: FROM + 30_000 + n, pilotId: "p0", shipId: null, ...entry }) as LogbookEntry;
const pilots = (count: number) =>
Array.from({ length: count }, (_, i) => ({ id: `p${i}`, name: `Pilot ${i}`, now: "At Earth" }));
const summarise = (
entries: LogbookEntry[],
extra: { pilots?: number; problems?: AwayProblem[] } = {},
) =>
buildAwaySummary({
from: FROM,
to: FROM + 10 * HOUR,
entries,
pilots: pilots(extra.pilots ?? 3),
problems: extra.problems ?? [],
});
describe("the away summary builder (Step 25.2, decision 0.5.140)", () => {
it("shows 5 pilot lines by net profit and sums the other 45 into one line", () => {
// Pilot i earns (i + 1) × 1,000 per job, two jobs each.
const entries = pilots(50).flatMap((p, i) => [
flight(p.id, (i + 1) * 1_000),
flight(p.id, (i + 1) * 1_000),
]);
const summary = summarise(entries, { pilots: 50 });
expect(summary.pilots.map((l) => l.pilotId)).toEqual(["p49", "p48", "p47", "p46", "p45"]);
expect(summary.pilots[0]).toMatchObject({ deliveries: 2, now: "At Earth", leveledUp: false });
const net = (id: string) =>
entries
.filter((e) => e.pilotId === id && e.kind === "flight")
.reduce((sum, e) => sum + flightNet(e as never), 0);
expect(summary.pilots[0]!.net).toBe(net("p49"));
const rest = pilots(45).map((p) => net(p.id));
expect(summary.morePilots).toEqual({ count: 45, net: rest.reduce((a, b) => a + b, 0) });
expect(summary.idlePilots).toBe(0);
expect(summary.direct).toBeNull();
});
it("puts highlights in priority order, at most 3, with the rest counted", () => {
const problem: AwayProblem = { kind: "low-fuel", shipId: "s1", locationId: "io-sulfur-works" };
const all = [
flight("p0", 8_420, { scriptedId: "unmarked-crates" }),
flight("p1", 2_000),
other({ kind: "goal-ready", goalId: "first-hire" }),
other({ kind: "level-up", level: 4, skillPoints: 1 }),
other({ kind: "discovery", locationId: "mars" }),
];
const summary = summarise(all, { problems: [problem] });
expect(summary.highlights.map((h) => h.kind)).toEqual(["discovery", "level-up", "goal-ready"]);
expect(summary.moreHighlights).toBe(3);
// Four flights: not a short absence, so every highlight counts.
const fewer = summarise([...all.slice(0, 2), flight("p1", 10), flight("p1", 20)], {
problems: [problem],
});
expect(fewer.highlights).toEqual([
{ kind: "scripted", scriptedId: "unmarked-crates" },
{
kind: "best-job",
goodId: "consumer-goods",
destinationId: "moon",
payout: 8_420,
pilotId: "p0",
},
{ kind: "problem", problem },
]);
expect(fewer.moreHighlights).toBe(0);
});
it("adds up the totals from the entries", () => {
const entries = [
flight("p0", 3_000, { loanRepayment: 300 }),
flight("p1", 5_000),
flight("p1", 0, { deadhead: true, goodId: null, pilotPay: 0, fuelCost: 80 }),
other({ kind: "discovery", locationId: "mars" }),
other({ kind: "refuel", locationId: "earth", boughtFu: 10, cost: 90, source: "pilot" }),
];
const summary = summarise(entries);
const flights = entries.filter((e) => e.kind === "flight");
expect(summary.totals).toEqual({
net: flights.reduce((sum, e) => sum + flightNet(e as never), 0),
deliveries: 2,
discoveries: 1,
loanRepaid: 300,
});
// Fuel bought counts when it's used, not when it's bought (decision 0.5.142).
expect(summary.totals.net).toBe(3_000 - 600 - 100 - 300 + (5_000 - 1_000 - 100) - 80);
// Pilot 2 flew nothing.
expect(summary.idlePilots).toBe(1);
expect(summary.pilots.map((l) => [l.pilotId, l.deliveries])).toEqual([
["p1", 1],
["p0", 1],
]);
});
it("lists a short absence's events directly: 3 or fewer", () => {
const two = [flight("p0", 410), other({ kind: "level-up", level: 2, skillPoints: 1 })];
expect(summarise(two).direct!.map((e) => e.kind)).toEqual(["level-up", "flight"]);
const four = [flight("p0", 1), flight("p0", 2), flight("p1", 3), flight("p1", 4)];
expect(summarise(four).direct).toBeNull();
// Refuels and rests aren't events.
const quiet = [
flight("p0", 1),
other({ kind: "rest", until: FROM + HOUR }),
other({ kind: "refuel", locationId: "earth", boughtFu: 1, cost: 5, source: "bought" }),
];
expect(summarise(quiet).direct!.map((e) => e.kind)).toEqual(["flight"]);
// What the list shows isn't a highlight too, and isn't counted as "more".
const listed = summarise(
[flight("p0", 500), other({ kind: "discovery", locationId: "mars" })],
{ problems: [{ kind: "below-floor", credits: 10, floor: 500 }] },
);
expect(listed.highlights.map((h) => h.kind)).toEqual(["problem"]);
expect(listed.moreHighlights).toBe(0);
});
it("covers only the absence, and says how long it was", () => {
const before = flight("p0", 9_999, { at: FROM });
const during = flight("p0", 1_000);
const summary = summarise([before, during]);
expect(summary.totals.deliveries).toBe(1);
expect(summary.durationMs).toBe(10 * HOUR);
});
});
describe("level-ups in the highlights", () => {
it("shows one per pilot, at the highest level reached", () => {
const summary = summarise([
other({ kind: "level-up", pilotId: "p0", level: 2, skillPoints: 2 }),
other({ kind: "level-up", pilotId: "p0", level: 3, skillPoints: 3 }),
other({ kind: "level-up", pilotId: "p1", level: 1, skillPoints: 1 }),
flight("p2", 0, { goodId: null, deadhead: true }),
]);
expect(summary.highlights).toEqual([
{ kind: "level-up", pilotId: "p0", level: 3, skillPoints: 3 },
{ kind: "level-up", pilotId: "p1", level: 1, skillPoints: 1 },
]);
});
});