# Sim(F# 模拟核心) 源码:`src/LivingVillage.Kernel/Sim.fs`(`module Sim`,命名空间 `LivingVillage.Kernel`)。 ## 职责 纯确定性模拟核心:维护 `World` 状态、按固定 `tick` 推进(`Sim.step`)、做对话/交易/谣言/需求决策。 不负责渲染、输入采集、存档文本(后者在 `WorldSave.fs`)、职业档案(后者在 `Occupation.fs`)。 ## 对外 API 概览 - 时间与量纲:`ticksPerSecond = 60L`、`secondsPerDay = 86400L`、`ticksPerDay = 5_184_000L`、 `dtSeconds = 1/60`、`dtSecondsF`。 - 世界尺寸:`mapWidthTiles` / `mapHeightTiles`(默认 64×48)、`configureBounds width height`、 `tilePixels = 32`、`avatarSpeed = 160.0f`、`npcSpeed = 80.0f`、`arriveEpsilon = 2.0f`。 - 构造:`initialWorld seed`、`initialWorldN seed count`(NPC 网格 6 列、间距 64px)。 - 主推进:`step (ts: TimeStep) (world: World) : World`;`TimeStep = { Input: Input }`, `Input = { MoveX; MoveY }`(分量建议在 `[-1, 1]`)。 - 对话:`openDialogueMenu world`、`responseFor intent personality`、 `chooseDialogueWith respond target intent world`、`chooseDialogue target intent world` (后者 = `chooseDialogueWith responseFor`,历史入口)。 - 聊天/谣言:`chat { Narrator; Receiver } world`、`rumorStrengthAt now rumor`、 `trimRumors now rumors`、`rumorWorkingSetStats rumors`、`rumorPath world target`、 `rumorTraceText world`、`findChatPartner self pos npcs`、`chatableForChat npc`。 - 交易/定价:`quotePrice request world`、`trade request world`。 - 观察/年鉴:`observeVisible bounds world`、`annalText world`、`appendAnnal entry world`、 `needsPanel world`、`relationMatrix world`、`relationCounts matrix`。 - 常量:`dialogueOptions = [SmallTalk; AskHelp; OfferTrade; Joke; Apologize; Provoke]`、 `chatRangePx = 96.0f`、`chatTicks = 300L`、`rumorCapacity = 16384`、`rumorRetentionDays = 3L`、 `rumorFreshnessTicks = 3 * ticksPerDay`、`rumorMinimumStrength = 0.125f`、 `relationHalfLifeTicks = ticksPerDay`、`playerId = NpcId -1`、`maxAnnalEntries = 128`。 ## 单位与量纲 - `Tick: int64`(模拟 tick,从 0 起单调 +1);`Time: float`(= `tick * dtSeconds` 秒)。 - `Vec2`:像素坐标,左上原点 `(0,0)`,右下 `(mapWidthTiles*32 - 32, mapHeightTiles*32 - 32)`。 - `Needs`:四维均为 `float32`,语义取值域经 `needsClamp` 限制在 `[0, 100]`(Money 亦为 0..100 的口径)。 - `Personality`:五维 `float32`,由 `personalityOfSeed` 生成,域 `[0, 1]`。 - 价格 `unitPrice: float32`,货币单位与 `Needs.Money` 同尺度;`quotePrice` 结果被 clamp 到 `[0.01, 1000]`。 - 谣言 `Strength: float32`(初始 1.0)、`Depth: int`(0 起)、`DayIndex = tick / ticksPerDay`。 ## 不变量 1. **数组快照**:`World.Npcs` 在每个动作中要么原样引用、要么 `Array.copy` 后写新数组; `stepNpc` 读取 `oldNpcs` 同时写 `newNpcs`,保证同一 tick 内每个 NPC 看到一致快照。 2. **确定性**:`step` 每 tick 只推进一步 `Rng.nextUInt64`(`rngOut` 当前被 `ignore`, 仅推进状态);所有派生(性格、谣言、动画)都是 `seed`/`tick` 的纯函数。 3. **谣言行序**:工作集 `World.Rumors` 约定**最新在前**、`Tick` 非递增、头节点 `Id` 最大; `nextRumorId` 依赖该头不变量做 O(1) 取号;裁剪只丢尾部旧条目。 4. **派生镜像**:`RumorCount` / `RumorOldestDay` 是列表的派生量,不参与存档序列化,读档由列表重建。 5. **年鉴上限**:`Annals` 经 `appendAnnal` 裁剪到 `maxAnnalEntries = 128`。 6. **记忆双配额**:`recordMemory` 容量 64,且交互类记忆(`Chatted`/`Dialogue`)优先保留。 7. **边界 clamp**:Avatar 位置在 `step` 中 clamp 到地图内;`Needs` 经 `needsClamp`。 ## 失败分支 - **对话**:`chooseDialogueWith` 返回 `DialogueResult`—— `DialogueRejected(DialogueTargetNotFound, world)`(目标不在 `Npcs`)、 `DialogueRejected(DialogueTargetOutOfRange, world)`(距离平方 > `chatRangeSq`)、 `DialogueRejected(DialogueTargetUnavailable, world)`(目标在 `Sleep`/`Chat`); 成功为 `DialogueSucceeded(outcome, world)`。 - **聊天**:`chat` 返回 `ChatResult`——`ChatRejected(NarratorNotFound | ReceiverNotFound, world)`、 `ChatRejected(ChatSameParticipant, world)`、`ChatRejected(ParticipantNotChatable, world)`; 重复谣言(同 narrator/receiver/parent 且新鲜可用)返回 `ChatSucceeded(None, world)`(只写记忆,不新增谣言)。 - **交易**:`trade` 返回 `TradeResult`——`TradeRejected(BuyerNotFound | SellerNotFound | SameParticipant | InvalidQuantity | OutOfStock | InsufficientFunds, world)`;`quotePrice` 对非法参与者/同人返回 `None`。 - **异常**:`configureBounds` 本身不校验;调用方不得在推进世界期间改边界(边界每 tick 读取)。 非法数值不会抛出,而是被 clamp。`WorldSave` 的解析错误在 `WorldSave` 侧处理。 ## 算法与性能取舍 - NPC 决策:`scoreAction` 线性加权(需求缺口 × 性格因子),`decideAction` 取 `maxBy`; 夜间 `Sleep` 乘 `nightSleepMultiplier = 3.0`。 - 移动:`moveToward` 向量归一化 + `arriveEpsilon` 到达容差;无寻路,直线目标点。 - 谣言扫描去装箱:`NpcId`/`RumorId` 是 `[]` 单例 DU,直接用 `=` 会比较时装箱; 热路径改用 `npcIdValue`/`rumorIdValue` 解构比较(语义等价、去掉逐条分配)。 - 谣言裁剪:`appendRumor` 用派生镜像把「每次追加全表扫描」降为 O(1) 判定, 仅在超容量/超保留窗口时 `trimRumors`(`takeWhile + truncate`)。 - 实测(`docs/evidence/p70-kernel-baseline.txt`,seed=42 / npc=4 / warmup=120 / measure=6000 ×3): `ticks_per_second` mean ≈ 466,679(min 278,557 / max 646,089);`allocated_bytes` ≈ 3,032,184; GC `gen0/gen1/gen2 = 0/0/0`;`final_digest = 953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`、`performance_determinism = PASS`。 - 复跑:`bash scripts/bench-p70.sh`;护栏:Kernel.Tests `P70PerformanceBaselineTests` 钉住 digest。