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,
} 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';
@@ -76,31 +76,6 @@ const serializeSeed = (...values: Array<string | number>): string =>
.map((value) => (typeof value === 'string' ? `str(${value.length},${value})` : `int(${Math.floor(value)})`))
.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 = {
getGeneralById(id: number): TurnGeneral | null;
@@ -539,7 +514,7 @@ export const createReservedTurnHandler = async (options: {
context.world.currentMonth,
currentGeneral.id
);
return new DeterministicRandom(new LiteHashDRBG(rngSeed));
return new RandUtil(new LiteHashDRBG(rngSeed));
};
const actionArgsRecord = extractArgsRecord(actionArgs);
+1 -1
View File
@@ -1,5 +1,5 @@
export interface RandomGenerator {
nextFloat(): number;
nextFloat1(): number;
nextBool(probability: number): boolean;
nextInt(minInclusive: number, maxExclusive: number): number;
}
+9 -5
View File
@@ -13,13 +13,17 @@ export class RandUtil {
return this.nextFloat1() * range + min;
}
public nextRangeInt(min: number, max: number): number {
const range = max - min;
return this.rng.nextInt(range) + min;
public nextRangeInt(minInclusive: number, maxInclusive: number): number {
const range = maxInclusive - minInclusive;
return this.rng.nextInt(range) + minInclusive;
}
public nextInt(max?: number): number {
return this.rng.nextInt(max);
public nextInt(minInclusive: number, maxExclusive: number): number {
const span = maxExclusive - minInclusive;
if (span <= 1) {
return minInclusive;
}
return minInclusive + this.rng.nextInt(span - 1);
}
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';
export interface ActionRandomSource {
nextFloat(): number;
nextFloat1(): number;
nextBool(probability: number): boolean;
nextInt(minInclusive: number, maxExclusive: number): number;
}
@@ -132,7 +132,7 @@ const pickByWeight = (rng: GeneralActionResolveContext['rng']): { flags: number;
const text = base.texts[0] ?? '';
return { flags: base.flags, text };
}
let cursor = rng.nextFloat() * total;
let cursor = rng.nextFloat1() * total;
for (const entry of SIGHTSEEING_MESSAGES) {
const weight = Math.max(entry.weight, 0);
cursor -= weight;
@@ -47,7 +47,7 @@ const pickByWeight = <T extends string>(rng: DrillContext['rng'], weights: Recor
if (total <= 0) {
return first[0];
}
let cursor = rng.nextFloat() * total;
let cursor = rng.nextFloat1() * total;
for (const [key, weight] of entries) {
if (weight <= 0) {
continue;
@@ -91,7 +91,7 @@ const pickUsingWeightPair = <T>(rng: RandomGenerator, items: Array<[T, number]>)
if (total <= 0) {
return items[0]?.[0] ?? null;
}
let cursor = rng.nextFloat() * total;
let cursor = rng.nextFloat1() * total;
for (const [item, weight] of items) {
if (weight <= 0) {
continue;
@@ -53,7 +53,7 @@ export class ActionResolver<
// 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.
// 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
let successRatio = 0.1;
@@ -63,7 +63,7 @@ export class ActionResolver<
failRatio = this.pipeline.onCalcDomestic(context, '조달', 'fail', failRatio);
// 4. Determine Outcome
const roll = context.rng.nextFloat();
const roll = context.rng.nextFloat1();
let outcome: 'fail' | 'success' | 'normal' = 'normal';
if (roll < failRatio) {
@@ -95,7 +95,7 @@ export class ActionResolver<
// Stat Exp
// Legacy: choose weighted among L/S/I
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';
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;
};
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 total = weights.fail + weights.normal + weights.success;
if (total <= 0) {
return 'normal';
}
let cursor = rng.nextFloat() * total;
let cursor = rng.nextFloat1() * total;
for (const key of ['fail', 'normal', 'success'] as const) {
cursor -= weights[key];
if (cursor <= 0) {
@@ -114,7 +114,7 @@ const pickByWeight = <T extends string>(rng: RandomGenerator, weights: Record<T,
if (total <= 0) {
return first[0];
}
let cursor = rng.nextFloat() * total;
let cursor = rng.nextFloat1() * total;
for (const [key, weight] of entries) {
if (weight <= 0) {
continue;
@@ -47,7 +47,7 @@ const addMetaNumber = (meta: Record<string, unknown>, key: string, delta: number
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 => ({
name: 'remainCityTrust',
+18 -18
View File
@@ -68,16 +68,16 @@ export const resolveTournamentBattle = (input: TournamentBattleInput): Tournamen
let selected = 2;
for (let phase = 1; phase <= maxTurns; phase += 1) {
const baseDamageAttacker = round(defenderStat * (rng.nextInt(21) + 90) / 130);
const baseDamageDefender = round(attackerStat * (rng.nextInt(21) + 90) / 130);
const baseDamageAttacker = round(defenderStat * (rng.nextInt(0, 22) + 90) / 130);
const baseDamageDefender = round(attackerStat * (rng.nextInt(0, 22) + 90) / 130);
let damageAttacker = baseDamageAttacker;
let damageDefender = baseDamageDefender;
if (attackerStat >= rng.nextInt(100)) {
damageDefender += round(attackerStat * (rng.nextInt(41) + 10) / 130);
if (attackerStat >= rng.nextInt(0, 101)) {
damageDefender += round(attackerStat * (rng.nextInt(0, 42) + 10) / 130);
}
if (defenderStat >= rng.nextInt(100)) {
damageAttacker += round(defenderStat * (rng.nextInt(41) + 10) / 130);
if (defenderStat >= rng.nextInt(0, 101)) {
damageAttacker += round(defenderStat * (rng.nextInt(0, 42) + 10) / 130);
}
let criticalAttacker = false;
@@ -85,13 +85,13 @@ export const resolveTournamentBattle = (input: TournamentBattleInput): Tournamen
let factorAttacker = 1;
let factorDefender = 1;
if (energyBaseAttacker / 5 > energyAttacker && damageAttacker > damageDefender && attackerStat >= rng.nextInt(300)) {
factorDefender = round((rng.nextInt(301) + 200) / 100);
if (energyBaseAttacker / 5 > energyAttacker && damageAttacker > damageDefender && attackerStat >= rng.nextInt(0, 301)) {
factorDefender = round((rng.nextInt(0, 302) + 200) / 100);
criticalAttacker = true;
log.push(`<S>●</> <Y>${attacker.name}</>의 분노의 일격!`);
}
if (energyBaseDefender / 5 > energyDefender && damageDefender > damageAttacker && defenderStat >= rng.nextInt(300)) {
factorAttacker = round((rng.nextInt(301) + 200) / 100);
if (energyBaseDefender / 5 > energyDefender && damageDefender > damageAttacker && defenderStat >= rng.nextInt(0, 301)) {
factorAttacker = round((rng.nextInt(0, 302) + 200) / 100);
criticalDefender = true;
log.push(`<S>●</> <Y>${defender.name}</>의 분노의 일격!`);
}
@@ -100,18 +100,18 @@ export const resolveTournamentBattle = (input: TournamentBattleInput): Tournamen
damageDefender = round(damageDefender * factorDefender);
if (phase === 1) {
if (attackerStat * 0.9 > defenderStat && attackerStat >= rng.nextInt(400)) {
damageDefender += round(attackerStat * (rng.nextInt(31) + 70) / 100);
if (attackerStat * 0.9 > defenderStat && attackerStat >= rng.nextInt(0, 401)) {
damageDefender += round(attackerStat * (rng.nextInt(0, 32) + 70) / 100);
}
if (defenderStat * 0.9 > attackerStat && defenderStat >= rng.nextInt(400)) {
damageAttacker += round(defenderStat * (rng.nextInt(31) + 70) / 100);
if (defenderStat * 0.9 > attackerStat && defenderStat >= rng.nextInt(0, 401)) {
damageAttacker += round(defenderStat * (rng.nextInt(0, 32) + 70) / 100);
}
} else {
if (!criticalAttacker && attackerStat >= rng.nextInt(1000)) {
damageDefender += round(attackerStat * (rng.nextInt(31) + 20) / 100);
if (!criticalAttacker && attackerStat >= rng.nextInt(0, 1001)) {
damageDefender += round(attackerStat * (rng.nextInt(0, 32) + 20) / 100);
}
if (!criticalDefender && defenderStat >= rng.nextInt(1000)) {
damageAttacker += round(defenderStat * (rng.nextInt(31) + 20) / 100);
if (!criticalDefender && defenderStat >= rng.nextInt(0, 1001)) {
damageAttacker += round(defenderStat * (rng.nextInt(0, 32) + 20) / 100);
}
}
@@ -166,7 +166,7 @@ const aftermathConfig: WarAftermathConfig = {
};
const rng = {
nextFloat: () => 0.1,
nextFloat1: () => 0.1,
nextBool: (probability: number) => probability >= 0.1,
nextInt: (minInclusive: number, _maxExclusive: number) => minInclusive,
};
@@ -175,7 +175,7 @@ describe('General Commands New Scenario', () => {
nextBool: () => true,
nextRange: (_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
const rng: RandomGenerator = {
nextFloat: () => 0.5,
nextFloat1: () => 0.5,
nextBool: () => true,
nextInt: (min: number, _max: number) => min,
};
+1 -1
View File
@@ -229,7 +229,7 @@ describe('trait modules', () => {
listGenerals: () => [general, patient],
};
const rng: RandomGenerator = {
nextFloat: () => 0,
nextFloat1: () => 0,
nextBool: () => true,
nextInt: (minInclusive: number, _maxExclusive: number) => minInclusive,
};
+1 -1
View File
@@ -211,7 +211,7 @@ export class TestGameRunner {
nextBool: () => Math.random() < 0.5,
nextRange: (min: number, max: number) => 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,
year: this.currentDate.getFullYear(),