diff --git a/app/game-engine/src/turn/reservedTurnHandler.ts b/app/game-engine/src/turn/reservedTurnHandler.ts index ca232db..cdb5a53 100644 --- a/app/game-engine/src/turn/reservedTurnHandler.ts +++ b/app/game-engine/src/turn/reservedTurnHandler.ts @@ -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 => .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); diff --git a/packages/common/src/rng.ts b/packages/common/src/rng.ts index 44871de..cbbf936 100644 --- a/packages/common/src/rng.ts +++ b/packages/common/src/rng.ts @@ -1,5 +1,5 @@ export interface RandomGenerator { - nextFloat(): number; + nextFloat1(): number; nextBool(probability: number): boolean; nextInt(minInclusive: number, maxExclusive: number): number; } diff --git a/packages/common/src/util/RandUtil.ts b/packages/common/src/util/RandUtil.ts index 65cc0db..a308b07 100644 --- a/packages/common/src/util/RandUtil.ts +++ b/packages/common/src/util/RandUtil.ts @@ -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 { diff --git a/packages/logic/src/actions/turn/actionContext.ts b/packages/logic/src/actions/turn/actionContext.ts index 193fe82..473a30a 100644 --- a/packages/logic/src/actions/turn/actionContext.ts +++ b/packages/logic/src/actions/turn/actionContext.ts @@ -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; } diff --git a/packages/logic/src/actions/turn/general/che_견문.ts b/packages/logic/src/actions/turn/general/che_견문.ts index d670e0d..8923c3d 100644 --- a/packages/logic/src/actions/turn/general/che_견문.ts +++ b/packages/logic/src/actions/turn/general/che_견문.ts @@ -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; diff --git a/packages/logic/src/actions/turn/general/che_단련.ts b/packages/logic/src/actions/turn/general/che_단련.ts index cc2393b..a055b1a 100644 --- a/packages/logic/src/actions/turn/general/che_단련.ts +++ b/packages/logic/src/actions/turn/general/che_단련.ts @@ -47,7 +47,7 @@ const pickByWeight = (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; diff --git a/packages/logic/src/actions/turn/general/che_랜덤임관.ts b/packages/logic/src/actions/turn/general/che_랜덤임관.ts index e9a3fbc..59ae627 100644 --- a/packages/logic/src/actions/turn/general/che_랜덤임관.ts +++ b/packages/logic/src/actions/turn/general/che_랜덤임관.ts @@ -91,7 +91,7 @@ const pickUsingWeightPair = (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; diff --git a/packages/logic/src/actions/turn/general/che_물자조달.ts b/packages/logic/src/actions/turn/general/che_물자조달.ts index b20d949..80f95dd 100644 --- a/packages/logic/src/actions/turn/general/che_물자조달.ts +++ b/packages/logic/src/actions/turn/general/che_물자조달.ts @@ -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) { diff --git a/packages/logic/src/actions/turn/general/che_상업투자.ts b/packages/logic/src/actions/turn/general/che_상업투자.ts index b12e873..45e1caa 100644 --- a/packages/logic/src/actions/turn/general/che_상업투자.ts +++ b/packages/logic/src/actions/turn/general/che_상업투자.ts @@ -68,14 +68,14 @@ const getMetaNumber = (meta: Record, 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 => { 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) { diff --git a/packages/logic/src/actions/turn/general/che_인재탐색.ts b/packages/logic/src/actions/turn/general/che_인재탐색.ts index 7e4b8e6..22cd06e 100644 --- a/packages/logic/src/actions/turn/general/che_인재탐색.ts +++ b/packages/logic/src/actions/turn/general/che_인재탐색.ts @@ -114,7 +114,7 @@ const pickByWeight = (rng: RandomGenerator, weights: Record, 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', diff --git a/packages/logic/src/tournament/battle.ts b/packages/logic/src/tournament/battle.ts index 94abd91..c8b0818 100644 --- a/packages/logic/src/tournament/battle.ts +++ b/packages/logic/src/tournament/battle.ts @@ -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(`${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(`${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); } } diff --git a/packages/logic/test/dispatchWarAction.test.ts b/packages/logic/test/dispatchWarAction.test.ts index 3d296b0..3e02f61 100644 --- a/packages/logic/test/dispatchWarAction.test.ts +++ b/packages/logic/test/dispatchWarAction.test.ts @@ -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, }; diff --git a/packages/logic/test/scenarios/general_commands_new.test.ts b/packages/logic/test/scenarios/general_commands_new.test.ts index 31d679d..445925e 100644 --- a/packages/logic/test/scenarios/general_commands_new.test.ts +++ b/packages/logic/test/scenarios/general_commands_new.test.ts @@ -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 }, }, }, diff --git a/packages/logic/test/scenarios/reservedTurnExecution.test.ts b/packages/logic/test/scenarios/reservedTurnExecution.test.ts index 62a8f91..ae89955 100644 --- a/packages/logic/test/scenarios/reservedTurnExecution.test.ts +++ b/packages/logic/test/scenarios/reservedTurnExecution.test.ts @@ -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, }; diff --git a/packages/logic/test/specialActions.test.ts b/packages/logic/test/specialActions.test.ts index 217322c..27e523f 100644 --- a/packages/logic/test/specialActions.test.ts +++ b/packages/logic/test/specialActions.test.ts @@ -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, }; diff --git a/packages/logic/test/testEnv.ts b/packages/logic/test/testEnv.ts index 59be9a0..628ea7f 100644 --- a/packages/logic/test/testEnv.ts +++ b/packages/logic/test/testEnv.ts @@ -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(),