1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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。
|