summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel/Sim.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-19 00:18:43 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-19 00:18:43 +0800
commit59458d149d7cecf73e708faa6d9e6db2bcd89dfd (patch)
treeaf342c6a01766be326857f48ebe5c65529e50882 /src/LivingVillage.Kernel/Sim.fs
parentb237a11908442a227361fae24dbdd0dbb287fac9 (diff)
downloadliving-village-59458d149d7cecf73e708faa6d9e6db2bcd89dfd.tar.gz
feat: M1 模拟器内核 — SplitMix64 RNG + 确定性 step + 对拍测试 + headless 冒烟工具 + Desktop 自动演示
[变更性质] 本 commit 为功能开发,交付设计文档 §2 模拟器内核(里程碑 M1),非缺陷修复或结构重构。 [新增功能] - Kernel:Rng.fs 提供 SplitMix64 纯函数 RNG(ofSeed / nextUInt64 / nextFloat32),World 携带种子状态。 - Kernel:Sim.fs 重写为 struct record(Vec2/Avatar/Input/TimeStep/World),step : World → TimeStep → World 纯函数,dt 固定 1/60 由内核常量给定,Time = Tick * dt 不做浮点累加;initialWorld 由无参改为带 seed。 - Tests:MSTest 确定性对拍——同 seed 轨迹逐 tick 相等、异 seed 发散、step 纯度、长跑有限/有界/时间-刻度关系、SplitMix64 已知向量、nextFloat32 值域。 - Headless:--days N --seed S 冒烟工具,每 1000 tick 输出摘要行,统计 NaN/越界计数并决定退出码。 - Desktop:LV_AUTOPLAY=1 环境变量启用正弦驱动自动演示,窗口标题与启动日志标注模式。 [实现方案] - 全部状态用 [<Struct>] record,模拟路径零堆分配,支撑 51.84 亿刻长跑。 - step 不消费 World 内 RNG,种子经由外部确定性驱动脚本生效(每 900 刻换段,8 方向查表 + 三档幅值,避开三角函数保证跨平台位精确)。 - Headless 内存口径取 GC.GetTotalMemory(true)(全收集后的稳态驻留),避免 GetTotalMemory(false) 随 Gen0 段扩张漂移误报泄漏。 [影响范围] - Kernel 保持零 MonoGame 引用,模拟与渲染彻底分离;sln 新增 Headless 与 Tests 两个项目。 - Release 构建 0 警告 0 错误,7/7 测试通过;1000 天长跑 5,184,000,000 刻退出码 0,无 NaN 无越界,内存 87,824 → 93,000 字节(1.06x < 2x)。 - 同 seed 两次运行 stdout 字节一致,seed 42/43 轨迹不同。
Diffstat (limited to 'src/LivingVillage.Kernel/Sim.fs')
-rw-r--r--src/LivingVillage.Kernel/Sim.fs40
1 files changed, 31 insertions, 9 deletions
diff --git a/src/LivingVillage.Kernel/Sim.fs b/src/LivingVillage.Kernel/Sim.fs
index d1b2e57..0e3683b 100644
--- a/src/LivingVillage.Kernel/Sim.fs
+++ b/src/LivingVillage.Kernel/Sim.fs
@@ -2,10 +2,12 @@ namespace LivingVillage.Kernel
module Sim =
+ [<Struct>]
type Vec2 =
{ X: float32
Y: float32 }
+ [<Struct>]
type Avatar =
{ Pos: Vec2 }
@@ -14,33 +16,53 @@ module Sim =
MoveY: float32 }
type TimeStep =
- { Dt: float32
- Input: Input }
+ { Input: Input }
+ [<Struct>]
+ type NoHost =
+ { Reserved: uint64 }
+
+ [<Struct>]
type World =
{ Tick: int64
- Avatar: Avatar }
+ Time: float
+ Rng: RngState
+ Avatar: Avatar
+ NoHost: NoHost }
+
+ let ticksPerSecond = 60L
+ let secondsPerDay = 86400L
+ let ticksPerDay = secondsPerDay * ticksPerSecond
+ let dtSeconds = 1.0 / float ticksPerSecond
+ let dtSecondsF = 1.0f / float32 ticksPerSecond
let mapWidthTiles = 64
let mapHeightTiles = 48
let tilePixels = 32
let avatarSpeed = 160.0f
- let initialWorld () : World =
+ let initialWorld (seed: uint64) : World =
let centerX = float32 (mapWidthTiles * tilePixels / 2 - tilePixels / 2)
let centerY = float32 (mapHeightTiles * tilePixels / 2 - tilePixels / 2)
{ Tick = 0L
- Avatar = { Pos = { X = centerX; Y = centerY } } }
+ Time = 0.0
+ Rng = Rng.ofSeed seed
+ Avatar = { Pos = { X = centerX; Y = centerY } }
+ NoHost = { Reserved = 0UL } }
let clamp v lo hi = if v < lo then lo elif v > hi then hi else v
let step (ts: TimeStep) (world: World) : World =
+ let tick = world.Tick + 1L
let maxX = float32 (mapWidthTiles * tilePixels - tilePixels)
let maxY = float32 (mapHeightTiles * tilePixels - tilePixels)
- let dx = ts.Input.MoveX * avatarSpeed * ts.Dt
- let dy = ts.Input.MoveY * avatarSpeed * ts.Dt
- { Tick = world.Tick + 1L
+ let dx = ts.Input.MoveX * avatarSpeed * dtSecondsF
+ let dy = ts.Input.MoveY * avatarSpeed * dtSecondsF
+ { Tick = tick
+ Time = float tick * dtSeconds
+ Rng = world.Rng
Avatar =
{ Pos =
{ X = clamp (world.Avatar.Pos.X + dx) 0.0f maxX
- Y = clamp (world.Avatar.Pos.Y + dy) 0.0f maxY } } }
+ Y = clamp (world.Avatar.Pos.Y + dy) 0.0f maxY } }
+ NoHost = world.NoHost }