/// /** * Prints a balance run for both first-purchase policies (plan Step 12.1), then * the fuel-to-payout check (CR-018 Step 18.1): * pnpm --filter @deadhead/sim balance [worldSeed] [hours] */ import { MS_PER_HOUR } from "../src/index"; import { content } from "@deadhead/content"; import { formatFuelShares, fuelShares } from "./fuel-share"; import { simulate, type PurchasePolicy } from "./simulate"; const worldSeed = Number(process.argv[2] ?? 12345); const hours = Number(process.argv[3] ?? 8); for (const policy of ["lighter-first", "freighter-first"] as PurchasePolicy[]) { const result = simulate({ worldSeed, policy, durationMs: hours * MS_PER_HOUR }); console.log(`\n=== ${policy} (seed ${worldSeed}) ===`); console.log("milestones (real minutes):", result.milestones); console.log( `tutorial runs ${result.tutorialRuns}, contracts ${result.contractsDelivered}, ` + `veteran contracts in first 2 h ${result.veteranContractsFirstSession}, purchases ${result.purchases.join(", ")}`, ); console.log("violations:", result.violations.length); for (const v of result.violations.slice(0, 10)) console.log(` ${v.minute} min ${v.rule} ${v.detail}`); console.log("minute credits loan ships pilot levels"); for (const s of result.samples.filter((_, i) => i % 3 === 0)) { console.log( `${String(s.minute).padStart(6)} ${String(s.credits).padStart(7)} ${String(s.loanBalance).padStart(6)} ${s.ships} ${s.pilotLevels.join(" ")}`, ); } } console.log("\n=== fuel share of median payout, per ship and route (CR-018) ==="); console.log(formatFuelShares(fuelShares(content))); // The away scenario (CR-021 Step 21.8): 3 hours of play, then 8 hours either way. const window = (mode: "active" | "away") => simulate({ worldSeed, policy: "lighter-first", durationMs: 12 * MS_PER_HOUR, scenario: { atMinute: 180, minutes: 8 * 60, mode }, }); const active = window("active"); const away = window("away"); console.log("\n=== away scenario: 8 h with Independent pilots vs active play (CR-021) ==="); console.log( `active ${active.scenarioEarnings} cr, away ${away.scenarioEarnings} cr, ` + `ratio ${((away.scenarioEarnings! / active.scenarioEarnings!) * 100).toFixed(0)}% (target 40–80%), ` + `violations ${away.violations.length}`, ); console.log( `away service (CR-027): ${away.service.bookings} auto-repair bookings, ${away.service.started} done, ` + `most trips in a row below 25% hull ${away.service.maxLowHullTrips} (≤ 1), ` + `repairs ${away.events.windowRepairSpend} cr`, ); // A week away (CR-024 Step 24.7): the fleet goes quiet 24 hours into the absence. const absence = (hours: number) => simulate({ worldSeed, policy: "lighter-first", durationMs: (3 + hours) * MS_PER_HOUR, scenario: { atMinute: 180, minutes: hours * 60, mode: "away" }, }); const day = absence(24); const week = absence(7 * 24); console.log("\n=== a week away vs a day away, fleet asleep after 24 h (CR-024) ==="); console.log( `day ${day.scenarioEarnings} cr, week ${week.scenarioEarnings} cr, ` + `in flight at sleep ${week.payoutInFlightAtSleep} cr: ` + (week.scenarioEarnings! <= day.scenarioEarnings! + week.payoutInFlightAtSleep! ? "OK" : "TOO MUCH"), ); // Events (CR-026 Step 26.9): a long-haul fleet over 48 h, events left to their defaults. const longHaul = simulate({ worldSeed, policy: "lighter-first", durationMs: 54 * MS_PER_HOUR, scenario: { atMinute: 300, minutes: 48 * 60, mode: "active" }, events: "defaults", eventsFromMinute: 300, longHaulMinGameHours: 12, firstEventDone: true, }); const ev = longHaul.events; const pct = (v: number) => `${(v * 100).toFixed(1)}%`; console.log("\n=== events on a long-haul fleet, 48 h (CR-026) ==="); console.log( `trips ${ev.windowTrips}, with an event ${pct(ev.windowWithEvent / ev.windowTrips)} (8–15%), ` + `good ${pct(ev.good / Math.max(1, ev.withEvent))} (28–38%), ` + `defaults ${pct(1 + ev.windowDefaultValue / longHaul.scenarioEarnings!)} (≥ 93%), ` + `answered well ${pct(1 + ev.windowBestValue / longHaul.scenarioEarnings!)} (≥ 100%), ` + `repairs ${pct(ev.windowRepairSpend / longHaul.scenarioEarnings!)} of income (5–10%)`, );