Clamp generated package expiry to travel time at max catalog speed + buffer so long jobs are finishable (ISS-119)
CI / build + unit + e2e (pull_request) Successful in 9m43s
CI / build + unit + e2e (pull_request) Successful in 9m43s
This commit is contained in:
+18
-3
@@ -1,6 +1,7 @@
|
||||
import { haversineKm } from '@/lib/geo';
|
||||
import { travelDurationMs } from '@/lib/sim/movement';
|
||||
import { WORLD_TIME_SCALE } from '@/lib/worldClock';
|
||||
import { PLANE_TYPES, repRate, STARTER_MODEL } from './catalog';
|
||||
import { FASTEST_CATALOG_SPEED_KMH, PLANE_TYPES, repRate, STARTER_MODEL } from './catalog';
|
||||
import { pickSpecial, SPECIALS } from './specials';
|
||||
|
||||
// First-run seed. Everything here is a pure function of (airports, rng, nowMs)
|
||||
@@ -123,6 +124,10 @@ const LEG_CAP_MAX_ATTEMPTS = 25;
|
||||
const DEADLINE_MIN_HOURS = 6;
|
||||
const DEADLINE_MAX_HOURS = 72;
|
||||
|
||||
// v0.54.0 (ISS-119): flat slack added to travel time in the expiry floor — a deadline must leave at
|
||||
// least this much after the fastest plane lands, so dispatch + landing are never instant.
|
||||
export const EXPIRY_BUFFER_MS = 3_600_000; // 1 h
|
||||
|
||||
const CARGO_TYPES = ['Passengers', 'Mail', 'Perishables', 'Electronics', 'Auto parts'] as const;
|
||||
|
||||
// ponytail: balance-tunable unit weights (kg per passenger/pallet). Chosen so every cargo
|
||||
@@ -157,6 +162,9 @@ export function rewardFor(km: number, weightKg: number): number {
|
||||
// v0.38.0 (ISS-072): reputation optionally scales the reward (×(1 + repRate)) at the same spot
|
||||
// as the special multiplier — a pure input, it consumes NO RNG draws and defaults to the
|
||||
// starting value so seed-time behavior is byte-identical.
|
||||
// v0.54.0 (ISS-119): the drawn deadline window is clamped UP by an expiry floor (travel time at
|
||||
// FASTEST_CATALOG_SPEED_KMH + EXPIRY_BUFFER_MS) after special scaling, so every package is
|
||||
// finishable by the fastest catalog plane; short legs are byte-identical to before.
|
||||
export function generatePackage(input: {
|
||||
id: string;
|
||||
airports: SeedAirport[];
|
||||
@@ -230,8 +238,15 @@ export function generatePackage(input: {
|
||||
// forced value (including explicit null) skips it, same pattern as the other overrides.
|
||||
const special = input.special !== undefined ? input.special : pickSpecial(rng);
|
||||
const mods = special !== null ? SPECIALS[special] : undefined;
|
||||
// Forced deadlines skip the special scaling — consistent with overrides skipping draws.
|
||||
const deadlineMs = input.deadlineOffsetMs ?? drawnWindowMs * (mods?.deadlineScale ?? 1);
|
||||
// v0.54.0 (ISS-119): expiry floor — every package must be finishable by the fastest catalog plane:
|
||||
// deadline ≥ now + travel time at FASTEST_CATALOG_SPEED_KMH + EXPIRY_BUFFER_MS. The drawn window is a
|
||||
// single uniform value, so clamping it up AFTER special scaling covers both requirements: short legs
|
||||
// (drawn × scale already above the floor) are byte-identical to before, and when the raw draw falls
|
||||
// below the floor the deadline lands exactly on it — which stays ≤ the scaled ceiling for any pair of
|
||||
// airports on Earth (max leg ~20000 km → floor ≈ 22.8 h < the 36 h priority ceiling), so no separate
|
||||
// ceiling raise is needed. Forced offsets skip the floor, same as they skip the special scaling.
|
||||
const expiryFloorMs = travelDurationMs(km, FASTEST_CATALOG_SPEED_KMH) + EXPIRY_BUFFER_MS;
|
||||
const deadlineMs = input.deadlineOffsetMs ?? Math.max(drawnWindowMs * (mods?.deadlineScale ?? 1), expiryFloorMs);
|
||||
|
||||
// v0.38.0 (ISS-072): reputation bonus composed with the special multiplier, single round at the end.
|
||||
const repBonus = 1 + repRate(input.reputation ?? STARTING_REPUTATION, JOB_BONUS_PER_REP, MAX_JOB_BONUS);
|
||||
|
||||
Reference in New Issue
Block a user