import { describe, expect, it } from 'vitest'; import { haversineKm } from '@/lib/geo'; import { PLANE_TYPES, STARTER_MODEL } from './catalog'; import { BASE_REWARD, buildSeed, CONTRACT_COUNT_MAX, CONTRACT_COUNT_MIN, HUB_ICAO, mulberry32, REWARD_PER_KM, rewardForDistanceKm, STARTING_CASH, STARTING_REPUTATION, type SeedAirport, } from './seed'; const NOW = 1_750_000_000_000; // Small fixed fixture — includes the hub (EDDF Frankfurt) so the happy path is covered. const AIRPORTS: SeedAirport[] = [ { icao: 'EDDF', name: 'Frankfurt Main', lat: 50.0333, lon: 8.5706 }, { icao: 'EHAM', name: 'Amsterdam Schiphol', lat: 52.31, lon: 4.76 }, { icao: 'EGLL', name: 'London Heathrow', lat: 51.47, lon: -0.454 }, { icao: 'LEBC', name: 'Madrid Barajas', lat: 40.47, lon: -3.56 }, { icao: 'LIRF', name: 'Rome Fiumicino', lat: 41.8, lon: 12.25 }, ]; const seedWith = (rngSeed: number, airports: SeedAirport[] = AIRPORTS) => buildSeed({ airports, rng: mulberry32(rngSeed), nowMs: NOW }); describe('buildSeed', () => { it('grounds the starter plane full of fuel at the hub airport', () => { const seed = seedWith(42); const type = PLANE_TYPES[STARTER_MODEL]; expect(seed.plane).toEqual({ id: 'plane-starter', model: STARTER_MODEL, status: 'available', fuel: type.maxFuel, maxFuel: type.maxFuel, locationIcao: HUB_ICAO, contractId: null, lat: 50.0333, lon: 8.5706, }); }); it('falls back to the first airport when the hub ICAO is missing', () => { const seed = buildSeed({ airports: AIRPORTS.slice(1), rng: mulberry32(42), nowMs: NOW }); expect(seed.plane.locationIcao).toBe(AIRPORTS[1].icao); }); it('seeds 8–12 open contracts between distinct fixture airports', () => { const seed = seedWith(42); expect(seed.contracts.length).toBeGreaterThanOrEqual(CONTRACT_COUNT_MIN); expect(seed.contracts.length).toBeLessThanOrEqual(CONTRACT_COUNT_MAX); const icaoSet = new Set(AIRPORTS.map((a) => a.icao)); for (const c of seed.contracts) { expect(icaoSet.has(c.originIcao)).toBe(true); expect(icaoSet.has(c.destIcao)).toBe(true); expect(c.originIcao).not.toBe(c.destIcao); expect(c.status).toBe('open'); expect(c.planeId).toBeNull(); expect(c.createdSimTime).toBe(NOW); } expect(new Set(seed.contracts.map((c) => c.id)).size).toBe(seed.contracts.length); }); it('scales reward by haversine distance: round(500 + 2.5 * km)', () => { const seed = seedWith(42); for (const c of seed.contracts) { const o = AIRPORTS.find((a) => a.icao === c.originIcao)!; const d = AIRPORTS.find((a) => a.icao === c.destIcao)!; expect(c.reward).toBe(Math.round(BASE_REWARD + REWARD_PER_KM * haversineKm(o.lat, o.lon, d.lat, d.lon))); } }); it('sets deadlines inside the 6–72h game-time window after now', () => { const seed = seedWith(42); for (const c of seed.contracts) { expect(c.deadlineSimTime).toBeGreaterThan(NOW + 6 * 3_600_000 - 1); expect(c.deadlineSimTime).toBeLessThanOrEqual(NOW + 72 * 3_600_000); } }); it('seeds the starting account and sim state', () => { const seed = seedWith(42); expect(seed.account).toEqual({ cash: STARTING_CASH, reputation: STARTING_REPUTATION }); expect(seed.simState).toEqual({ clockMs: NOW, timeScale: 1, paused: false }); }); it('is fully deterministic for a fixed RNG and inputs', () => { expect(seedWith(42)).toEqual(seedWith(42)); }); it('produces different contracts for a different RNG seed', () => { expect(seedWith(1).contracts).not.toEqual(seedWith(2).contracts); }); it('rejects an airport list too small to form a route', () => { expect(() => buildSeed({ airports: AIRPORTS.slice(0, 1), rng: mulberry32(42), nowMs: NOW })).toThrow(); }); it('rewardForDistanceKm applies the documented formula', () => { expect(rewardForDistanceKm(0)).toBe(BASE_REWARD); expect(rewardForDistanceKm(1000)).toBe(Math.round(BASE_REWARD + REWARD_PER_KM * 1000)); }); });