fix deterministic authoritative RNG fallbacks
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
@@ -18,6 +19,7 @@ import type {
|
||||
WorldSeedPayload,
|
||||
WorldSnapshot,
|
||||
} from './types.js';
|
||||
import { simpleSerialize } from '../war/utils.js';
|
||||
|
||||
export interface ScenarioBootstrapOptions {
|
||||
includeNeutralNation?: boolean;
|
||||
@@ -144,16 +146,38 @@ const resolveKillturnFromDeathYear = (
|
||||
currentYear: number | null,
|
||||
currentMonth: number,
|
||||
deathYear: number,
|
||||
deathMonth: number,
|
||||
fallback: number
|
||||
): number => {
|
||||
if (currentYear === null || !Number.isFinite(deathYear) || deathYear <= 0) {
|
||||
return fallback;
|
||||
}
|
||||
const deathMonth = Math.floor(Math.random() * 12) + 1;
|
||||
const diff = (deathYear - currentYear) * 12 + (deathMonth - currentMonth);
|
||||
return Math.max(diff, 0);
|
||||
};
|
||||
|
||||
export const resolveScenarioGeneralDeathMonth = (input: {
|
||||
scenarioTitle: string;
|
||||
startYear: number | null;
|
||||
contextLabel: string;
|
||||
generalId: number;
|
||||
generalName: string;
|
||||
deathYear: number;
|
||||
}): number =>
|
||||
new RandUtil(
|
||||
new LiteHashDRBG(
|
||||
simpleSerialize(
|
||||
input.scenarioTitle,
|
||||
input.startYear ?? 0,
|
||||
input.contextLabel,
|
||||
input.generalId,
|
||||
input.generalName,
|
||||
input.deathYear,
|
||||
'deathMonth'
|
||||
)
|
||||
)
|
||||
).nextRangeInt(1, 12);
|
||||
|
||||
const resolveAge = (startYear: number | null, birthYear: number): number => {
|
||||
if (startYear === null || birthYear <= 0) {
|
||||
return 20;
|
||||
@@ -309,6 +333,14 @@ const buildGeneralSeeds = (
|
||||
const cityId = resolveCityId(row.city, cityByName, warnings, row.name);
|
||||
const birthYear = resolveBirthYear(row.birthYear, scenario.startYear);
|
||||
const deathYear = resolveDeathYear(row.deathYear, birthYear, scenario.startYear);
|
||||
const deathMonth = resolveScenarioGeneralDeathMonth({
|
||||
scenarioTitle: scenario.title,
|
||||
startYear: scenario.startYear,
|
||||
contextLabel,
|
||||
generalId: id,
|
||||
generalName: row.name,
|
||||
deathYear,
|
||||
});
|
||||
const officerLevel = resolveOfficerLevel(row.officerLevel, nationId);
|
||||
const age = resolveAge(scenario.startYear, birthYear);
|
||||
const stats = {
|
||||
@@ -319,6 +351,7 @@ const buildGeneralSeeds = (
|
||||
|
||||
const seedMeta: Record<string, unknown> = {
|
||||
source: contextLabel,
|
||||
deathMonth,
|
||||
specage: buildSpecialityAge(retirementYear, age, 12),
|
||||
specage2: buildSpecialityAge(retirementYear, age, 6),
|
||||
};
|
||||
@@ -379,7 +412,14 @@ const buildGeneralSeeds = (
|
||||
seeds.push(seed);
|
||||
|
||||
const generalMeta: GeneralMeta = {
|
||||
killturn: resolveKillturnFromDeathYear(scenario.startYear, 1, deathYear, DEFAULT_GENERAL_KILLTURN),
|
||||
killturn: resolveKillturnFromDeathYear(
|
||||
scenario.startYear,
|
||||
1,
|
||||
deathYear,
|
||||
deathMonth,
|
||||
DEFAULT_GENERAL_KILLTURN
|
||||
),
|
||||
deathMonth,
|
||||
npcType,
|
||||
crewTypeId: defaultCrewTypeId,
|
||||
specage: buildSpecialityAge(retirementYear, age, 12),
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ScenarioDefinition } from '../src/scenario/types.js';
|
||||
import type { MapDefinition, UnitSetDefinition } from '../src/world/types.js';
|
||||
import { buildScenarioBootstrap } from '../src/world/bootstrap.js';
|
||||
|
||||
describe('scenario bootstrap', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => {
|
||||
throw new Error('scenario bootstrap must not use Math.random');
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('builds snapshot and seed from scenario/map inputs', () => {
|
||||
const scenario: ScenarioDefinition = {
|
||||
title: 'Test Scenario',
|
||||
@@ -120,7 +130,14 @@ describe('scenario bootstrap', () => {
|
||||
expect(result.snapshot.generals[0]?.role.specialDomestic).toBe('Special');
|
||||
expect(result.snapshot.generals[0]?.role.specialWar).toBeNull();
|
||||
expect(result.snapshot.generals[0]?.meta).toMatchObject({ specage: 25, specage2: 30 });
|
||||
expect(result.seed.generals[0]?.meta).toMatchObject({ specage: 25, specage2: 30 });
|
||||
expect(result.seed.generals[0]?.meta).toMatchObject({
|
||||
deathMonth: expect.any(Number),
|
||||
specage: 25,
|
||||
specage2: 30,
|
||||
});
|
||||
expect(buildScenarioBootstrap({ scenario, map, unitSet }).snapshot.generals[0]?.meta).toEqual(
|
||||
result.snapshot.generals[0]?.meta
|
||||
);
|
||||
expect(result.seed.generals[0]?.npcType).toBe(2);
|
||||
expect(result.snapshot.scenarioMeta?.title).toBe('Test Scenario');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user