diff options
Diffstat (limited to 'docs/api-reference/sim-kernel.md')
| -rw-r--r-- | docs/api-reference/sim-kernel.md | 82 |
1 files changed, 82 insertions, 0 deletions
diff --git a/docs/api-reference/sim-kernel.md b/docs/api-reference/sim-kernel.md new file mode 100644 index 0000000..8fd4d34 --- /dev/null +++ b/docs/api-reference/sim-kernel.md @@ -0,0 +1,82 @@ +# 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` 是 `[<Struct>]` 单例 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。 |
