refactor: rename RNG method from nextFloat to nextFloat1 for consistency across the codebase

This commit is contained in:
2026-01-24 13:58:12 +00:00
parent 8735d350fa
commit f199427ff1
17 changed files with 46 additions and 67 deletions
@@ -22,7 +22,7 @@ import {
resolveGeneralAction, resolveGeneralAction,
} from '@sammo-ts/logic'; } from '@sammo-ts/logic';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic'; import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
import { asRecord, LiteHashDRBG } from '@sammo-ts/common'; import { asRecord, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
import type { ConstraintContext, StateView } from '@sammo-ts/logic'; import type { ConstraintContext, StateView } from '@sammo-ts/logic';
@@ -76,31 +76,6 @@ const serializeSeed = (...values: Array<string | number>): string =>
.map((value) => (typeof value === 'string' ? `str(${value.length},${value})` : `int(${Math.floor(value)})`)) .map((value) => (typeof value === 'string' ? `str(${value.length},${value})` : `int(${Math.floor(value)})`))
.join('|'); .join('|');
class DeterministicRandom {
constructor(private readonly rng: LiteHashDRBG) {}
nextFloat(): number {
return this.rng.nextFloat1();
}
nextBool(probability: number): boolean {
if (probability >= 1) {
return true;
}
if (probability <= 0) {
return false;
}
return this.nextFloat() < probability;
}
nextInt(minInclusive: number, maxExclusive: number): number {
const span = maxExclusive - minInclusive;
if (span <= 1) {
return minInclusive;
}
return minInclusive + this.rng.nextInt(span - 1);
}
}
type WorldView = { type WorldView = {
getGeneralById(id: number): TurnGeneral | null; getGeneralById(id: number): TurnGeneral | null;
@@ -539,7 +514,7 @@ export const createReservedTurnHandler = async (options: {
context.world.currentMonth, context.world.currentMonth,
currentGeneral.id currentGeneral.id
); );
return new DeterministicRandom(new LiteHashDRBG(rngSeed)); return new RandUtil(new LiteHashDRBG(rngSeed));
}; };
const actionArgsRecord = extractArgsRecord(actionArgs); const actionArgsRecord = extractArgsRecord(actionArgs);
+1 -1
View File
@@ -1,5 +1,5 @@
export interface RandomGenerator { export interface RandomGenerator {
nextFloat(): number; nextFloat1(): number;
nextBool(probability: number): boolean; nextBool(probability: number): boolean;
nextInt(minInclusive: number, maxExclusive: number): number; nextInt(minInclusive: number, maxExclusive: number): number;
} }
+9 -5
View File
@@ -13,13 +13,17 @@ export class RandUtil {
return this.nextFloat1() * range + min; return this.nextFloat1() * range + min;
} }
public nextRangeInt(min: number, max: number): number { public nextRangeInt(minInclusive: number, maxInclusive: number): number {
const range = max - min; const range = maxInclusive - minInclusive;
return this.rng.nextInt(range) + min; return this.rng.nextInt(range) + minInclusive;
} }
public nextInt(max?: number): number { public nextInt(minInclusive: number, maxExclusive: number): number {
return this.rng.nextInt(max); const span = maxExclusive - minInclusive;
if (span <= 1) {
return minInclusive;
}
return minInclusive + this.rng.nextInt(span - 1);
} }
public nextBit(): boolean { public nextBit(): boolean {
@@ -4,7 +4,7 @@ import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js'; import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
export interface ActionRandomSource { export interface ActionRandomSource {
nextFloat(): number; nextFloat1(): number;
nextBool(probability: number): boolean; nextBool(probability: number): boolean;
nextInt(minInclusive: number, maxExclusive: number): number; nextInt(minInclusive: number, maxExclusive: number): number;
} }
@@ -132,7 +132,7 @@ const pickByWeight = (rng: GeneralActionResolveContext['rng']): { flags: number;
const text = base.texts[0] ?? ''; const text = base.texts[0] ?? '';
return { flags: base.flags, text }; return { flags: base.flags, text };
} }
let cursor = rng.nextFloat() * total; let cursor = rng.nextFloat1() * total;
for (const entry of SIGHTSEEING_MESSAGES) { for (const entry of SIGHTSEEING_MESSAGES) {
const weight = Math.max(entry.weight, 0); const weight = Math.max(entry.weight, 0);
cursor -= weight; cursor -= weight;
@@ -47,7 +47,7 @@ const pickByWeight = <T extends string>(rng: DrillContext['rng'], weights: Recor
if (total <= 0) { if (total <= 0) {
return first[0]; return first[0];
} }
let cursor = rng.nextFloat() * total; let cursor = rng.nextFloat1() * total;
for (const [key, weight] of entries) { for (const [key, weight] of entries) {
if (weight <= 0) { if (weight <= 0) {
continue; continue;
@@ -91,7 +91,7 @@ const pickUsingWeightPair = <T>(rng: RandomGenerator, items: Array<[T, number]>)
if (total <= 0) { if (total <= 0) {
return items[0]?.[0] ?? null; return items[0]?.[0] ?? null;
} }
let cursor = rng.nextFloat() * total; let cursor = rng.nextFloat1() * total;
for (const [item, weight] of items) { for (const [item, weight] of items) {
if (weight <= 0) { if (weight <= 0) {
continue; continue;
@@ -53,7 +53,7 @@ export class ActionResolver<
// For now, assume simplified or no bonus unless strictly required to port helper. // For now, assume simplified or no bonus unless strictly required to port helper.
// Assuming 1.0 for now if helper not available, or implement simple bonus. // Assuming 1.0 for now if helper not available, or implement simple bonus.
// Legacy: $score *= $rng->nextRange(0.8, 1.2); // Legacy: $score *= $rng->nextRange(0.8, 1.2);
score *= context.rng.nextFloat() * 0.4 + 0.8; score *= context.rng.nextFloat1() * 0.4 + 0.8;
// 3. Success/Fail Ratio // 3. Success/Fail Ratio
let successRatio = 0.1; let successRatio = 0.1;
@@ -63,7 +63,7 @@ export class ActionResolver<
failRatio = this.pipeline.onCalcDomestic(context, '조달', 'fail', failRatio); failRatio = this.pipeline.onCalcDomestic(context, '조달', 'fail', failRatio);
// 4. Determine Outcome // 4. Determine Outcome
const roll = context.rng.nextFloat(); const roll = context.rng.nextFloat1();
let outcome: 'fail' | 'success' | 'normal' = 'normal'; let outcome: 'fail' | 'success' | 'normal' = 'normal';
if (roll < failRatio) { if (roll < failRatio) {
@@ -95,7 +95,7 @@ export class ActionResolver<
// Stat Exp // Stat Exp
// Legacy: choose weighted among L/S/I // Legacy: choose weighted among L/S/I
const statChoice = const statChoice =
context.rng.nextFloat() * (general.stats.leadership + general.stats.strength + general.stats.intelligence); context.rng.nextFloat1() * (general.stats.leadership + general.stats.strength + general.stats.intelligence);
let statKey: 'leadership_exp' | 'strength_exp' | 'intel_exp' = 'leadership_exp'; let statKey: 'leadership_exp' | 'strength_exp' | 'intel_exp' = 'leadership_exp';
if (statChoice < general.stats.leadership) { if (statChoice < general.stats.leadership) {
@@ -68,14 +68,14 @@ const getMetaNumber = (meta: Record<string, unknown>, key: string): number | nul
return typeof raw === 'number' ? raw : null; return typeof raw === 'number' ? raw : null;
}; };
const randomRange = (rng: RandomGenerator, min: number, max: number): number => min + (max - min) * rng.nextFloat(); const randomRange = (rng: RandomGenerator, min: number, max: number): number => min + (max - min) * rng.nextFloat1();
const pickByWeight = (rng: RandomGenerator, weights: Record<DomesticCriticalPick, number>): DomesticCriticalPick => { const pickByWeight = (rng: RandomGenerator, weights: Record<DomesticCriticalPick, number>): DomesticCriticalPick => {
const total = weights.fail + weights.normal + weights.success; const total = weights.fail + weights.normal + weights.success;
if (total <= 0) { if (total <= 0) {
return 'normal'; return 'normal';
} }
let cursor = rng.nextFloat() * total; let cursor = rng.nextFloat1() * total;
for (const key of ['fail', 'normal', 'success'] as const) { for (const key of ['fail', 'normal', 'success'] as const) {
cursor -= weights[key]; cursor -= weights[key];
if (cursor <= 0) { if (cursor <= 0) {
@@ -114,7 +114,7 @@ const pickByWeight = <T extends string>(rng: RandomGenerator, weights: Record<T,
if (total <= 0) { if (total <= 0) {
return first[0]; return first[0];
} }
let cursor = rng.nextFloat() * total; let cursor = rng.nextFloat1() * total;
for (const [key, weight] of entries) { for (const [key, weight] of entries) {
if (weight <= 0) { if (weight <= 0) {
continue; continue;
@@ -47,7 +47,7 @@ const addMetaNumber = (meta: Record<string, unknown>, key: string, delta: number
return { ...meta, [key]: current + delta }; return { ...meta, [key]: current + delta };
}; };
const randomRange = (rng: RandomGenerator, min: number, max: number): number => min + (max - min) * rng.nextFloat(); const randomRange = (rng: RandomGenerator, min: number, max: number): number => min + (max - min) * rng.nextFloat1();
const remainCityTrust = (): Constraint => ({ const remainCityTrust = (): Constraint => ({
name: 'remainCityTrust', name: 'remainCityTrust',
+18 -18
View File
@@ -68,16 +68,16 @@ export const resolveTournamentBattle = (input: TournamentBattleInput): Tournamen
let selected = 2; let selected = 2;
for (let phase = 1; phase <= maxTurns; phase += 1) { for (let phase = 1; phase <= maxTurns; phase += 1) {
const baseDamageAttacker = round(defenderStat * (rng.nextInt(21) + 90) / 130); const baseDamageAttacker = round(defenderStat * (rng.nextInt(0, 22) + 90) / 130);
const baseDamageDefender = round(attackerStat * (rng.nextInt(21) + 90) / 130); const baseDamageDefender = round(attackerStat * (rng.nextInt(0, 22) + 90) / 130);
let damageAttacker = baseDamageAttacker; let damageAttacker = baseDamageAttacker;
let damageDefender = baseDamageDefender; let damageDefender = baseDamageDefender;
if (attackerStat >= rng.nextInt(100)) { if (attackerStat >= rng.nextInt(0, 101)) {
damageDefender += round(attackerStat * (rng.nextInt(41) + 10) / 130); damageDefender += round(attackerStat * (rng.nextInt(0, 42) + 10) / 130);
} }
if (defenderStat >= rng.nextInt(100)) { if (defenderStat >= rng.nextInt(0, 101)) {
damageAttacker += round(defenderStat * (rng.nextInt(41) + 10) / 130); damageAttacker += round(defenderStat * (rng.nextInt(0, 42) + 10) / 130);
} }
let criticalAttacker = false; let criticalAttacker = false;
@@ -85,13 +85,13 @@ export const resolveTournamentBattle = (input: TournamentBattleInput): Tournamen
let factorAttacker = 1; let factorAttacker = 1;
let factorDefender = 1; let factorDefender = 1;
if (energyBaseAttacker / 5 > energyAttacker && damageAttacker > damageDefender && attackerStat >= rng.nextInt(300)) { if (energyBaseAttacker / 5 > energyAttacker && damageAttacker > damageDefender && attackerStat >= rng.nextInt(0, 301)) {
factorDefender = round((rng.nextInt(301) + 200) / 100); factorDefender = round((rng.nextInt(0, 302) + 200) / 100);
criticalAttacker = true; criticalAttacker = true;
log.push(`<S>●</> <Y>${attacker.name}</>의 분노의 일격!`); log.push(`<S>●</> <Y>${attacker.name}</>의 분노의 일격!`);
} }
if (energyBaseDefender / 5 > energyDefender && damageDefender > damageAttacker && defenderStat >= rng.nextInt(300)) { if (energyBaseDefender / 5 > energyDefender && damageDefender > damageAttacker && defenderStat >= rng.nextInt(0, 301)) {
factorAttacker = round((rng.nextInt(301) + 200) / 100); factorAttacker = round((rng.nextInt(0, 302) + 200) / 100);
criticalDefender = true; criticalDefender = true;
log.push(`<S>●</> <Y>${defender.name}</>의 분노의 일격!`); log.push(`<S>●</> <Y>${defender.name}</>의 분노의 일격!`);
} }
@@ -100,18 +100,18 @@ export const resolveTournamentBattle = (input: TournamentBattleInput): Tournamen
damageDefender = round(damageDefender * factorDefender); damageDefender = round(damageDefender * factorDefender);
if (phase === 1) { if (phase === 1) {
if (attackerStat * 0.9 > defenderStat && attackerStat >= rng.nextInt(400)) { if (attackerStat * 0.9 > defenderStat && attackerStat >= rng.nextInt(0, 401)) {
damageDefender += round(attackerStat * (rng.nextInt(31) + 70) / 100); damageDefender += round(attackerStat * (rng.nextInt(0, 32) + 70) / 100);
} }
if (defenderStat * 0.9 > attackerStat && defenderStat >= rng.nextInt(400)) { if (defenderStat * 0.9 > attackerStat && defenderStat >= rng.nextInt(0, 401)) {
damageAttacker += round(defenderStat * (rng.nextInt(31) + 70) / 100); damageAttacker += round(defenderStat * (rng.nextInt(0, 32) + 70) / 100);
} }
} else { } else {
if (!criticalAttacker && attackerStat >= rng.nextInt(1000)) { if (!criticalAttacker && attackerStat >= rng.nextInt(0, 1001)) {
damageDefender += round(attackerStat * (rng.nextInt(31) + 20) / 100); damageDefender += round(attackerStat * (rng.nextInt(0, 32) + 20) / 100);
} }
if (!criticalDefender && defenderStat >= rng.nextInt(1000)) { if (!criticalDefender && defenderStat >= rng.nextInt(0, 1001)) {
damageAttacker += round(defenderStat * (rng.nextInt(31) + 20) / 100); damageAttacker += round(defenderStat * (rng.nextInt(0, 32) + 20) / 100);
} }
} }
@@ -166,7 +166,7 @@ const aftermathConfig: WarAftermathConfig = {
}; };
const rng = { const rng = {
nextFloat: () => 0.1, nextFloat1: () => 0.1,
nextBool: (probability: number) => probability >= 0.1, nextBool: (probability: number) => probability >= 0.1,
nextInt: (minInclusive: number, _maxExclusive: number) => minInclusive, nextInt: (minInclusive: number, _maxExclusive: number) => minInclusive,
}; };
@@ -175,7 +175,7 @@ describe('General Commands New Scenario', () => {
nextBool: () => true, nextBool: () => true,
nextRange: (_min: number, max: number) => max, nextRange: (_min: number, max: number) => max,
nextRangeInt: (_min: number, max: number) => max, nextRangeInt: (_min: number, max: number) => max,
nextFloat: () => 0.9, // Ensure success nextFloat1: () => 0.9, // Ensure success
}, },
}, },
}, },
@@ -121,7 +121,7 @@ class ReservedTurnRunner extends TestGameRunner {
// Simple RNG mock // Simple RNG mock
const rng: RandomGenerator = { const rng: RandomGenerator = {
nextFloat: () => 0.5, nextFloat1: () => 0.5,
nextBool: () => true, nextBool: () => true,
nextInt: (min: number, _max: number) => min, nextInt: (min: number, _max: number) => min,
}; };
+1 -1
View File
@@ -229,7 +229,7 @@ describe('trait modules', () => {
listGenerals: () => [general, patient], listGenerals: () => [general, patient],
}; };
const rng: RandomGenerator = { const rng: RandomGenerator = {
nextFloat: () => 0, nextFloat1: () => 0,
nextBool: () => true, nextBool: () => true,
nextInt: (minInclusive: number, _maxExclusive: number) => minInclusive, nextInt: (minInclusive: number, _maxExclusive: number) => minInclusive,
}; };
+1 -1
View File
@@ -211,7 +211,7 @@ export class TestGameRunner {
nextBool: () => Math.random() < 0.5, nextBool: () => Math.random() < 0.5,
nextRange: (min: number, max: number) => Math.random() * (max - min) + min, nextRange: (min: number, max: number) => Math.random() * (max - min) + min,
nextRangeInt: (min: number, max: number) => Math.floor(Math.random() * (max - min)) + min, nextRangeInt: (min: number, max: number) => Math.floor(Math.random() * (max - min)) + min,
nextFloat: () => Math.random(), nextFloat1: () => Math.random(),
} as any, } as any,
year: this.currentDate.getFullYear(), year: this.currentDate.getFullYear(),