BattleRandomState.kt
package io.github.lishangbu.avalon.game.battle.engine.core.model
/**
* battle session 级确定性随机状态。
*
* 设计目标:
*
* - 让 battle 内部所有随机数都从同一条序列中消费
* - 让当前随机游标能够跟随 session state 一起导出、恢复
* - 避免依赖 JVM 全局随机源,确保回放时序列严格一致
*
* 当前采用 SplitMix64 作为底层状态推进算法:
*
* - 状态体积小,便于持久化
* - 实现简单,跨平台行为稳定
* - 每次生成只依赖上一状态,适合导出/恢复
*/
data class BattleRandomState(
/** 初始种子,便于审计与问题定位。 */
val seed: Long,
/** 当前游标状态。 */
val state: Long = seed,
/** 已消费的随机值数量。 */
val generatedValueCount: Long = 0,
) {
/**
* 生成 `[0, bound)` 范围内的均匀整数,并推进随机状态。
*/
fun nextInt(bound: Int): BattleRandomIntResult {
require(bound > 0) { "bound must be greater than 0." }
val unsignedBound = bound.toLong()
val threshold = java.lang.Long.remainderUnsigned(-unsignedBound, unsignedBound)
var cursor = this
while (true) {
val nextLongResult = cursor.nextLong()
if (java.lang.Long.compareUnsigned(nextLongResult.value, threshold) >= 0) {
return BattleRandomIntResult(
value =
java.lang.Long
.remainderUnsigned(nextLongResult.value, unsignedBound)
.toInt(),
nextState = nextLongResult.nextState,
)
}
cursor = nextLongResult.nextState
}
}
private fun nextLong(): BattleRandomLongResult {
val nextStateValue = state + GAMMA
var mixed = nextStateValue
mixed = (mixed xor (mixed ushr 30)) * MIX_CONST_1
mixed = (mixed xor (mixed ushr 27)) * MIX_CONST_2
val value = mixed xor (mixed ushr 31)
return BattleRandomLongResult(
value = value,
nextState =
copy(
state = nextStateValue,
generatedValueCount = generatedValueCount + 1,
),
)
}
companion object {
/**
* 依据 battle 标识稳定派生一个初始种子。
*
* 这样即使只拿到建局参数重新创建 session,只要 battleId / formatId 相同,
* 默认初始随机序列也保持一致;而一旦 session state 被导出保存,
* 后续则以持久化下来的 [state] 为准继续推进。
*/
fun seeded(
battleId: String,
formatId: String,
): BattleRandomState = BattleRandomState(seed = stableSeed("$battleId#$formatId"))
private fun stableSeed(value: String): Long {
var hash = FNV_OFFSET_BASIS
value.forEach { ch ->
hash = (hash xor ch.code.toLong()) * FNV_PRIME
}
return if (hash == 0L) FALLBACK_SEED else hash
}
private const val GAMMA: Long = -7046029254386353131L
private const val MIX_CONST_1: Long = -4658895280553007687L
private const val MIX_CONST_2: Long = -7723592293110705685L
private const val FNV_OFFSET_BASIS: Long = -3750763034362895579L
private const val FNV_PRIME: Long = 1099511628211L
private const val FALLBACK_SEED: Long = -7046029254386353131L
}
}
data class BattleRandomIntResult(
val value: Int,
val nextState: BattleRandomState,
)
private data class BattleRandomLongResult(
val value: Long,
val nextState: BattleRandomState,
)