From 4f8aca922e6cf47dce95aab79541065d487d5272 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sun, 20 Sep 2026 12:18:38 +0800 Subject: feat(m4b): 增加可追踪谣言传播与回放 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [变更性质] - 本提交新增 M4b 谣言传播与确定性验收能力,不是单纯缺陷修复。 [新增功能] - 支持带来源、父节点、深度、强度和路径的谣言传播、去重、衰减与过期处理。 - Headless 支持稳定谣言 dump、replay,以及与 batch 模式的互斥校验。 [实现方案] - 通过不可变 World 更新和失败原子返回保持模拟确定性。 - 新增多跳传播、重复转述、过期父节点、非法参与者和稳定 trace 测试。 [影响范围] - 影响 Kernel、Headless 和 Kernel 测试;不包含 M5/M6 或无关配置与文档改动。 --- src/LivingVillage.Headless/Program.fs | 43 +++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 12 deletions(-) (limited to 'src/LivingVillage.Headless') diff --git a/src/LivingVillage.Headless/Program.fs b/src/LivingVillage.Headless/Program.fs index bcf2e41..6ba0f5f 100644 --- a/src/LivingVillage.Headless/Program.fs +++ b/src/LivingVillage.Headless/Program.fs @@ -164,6 +164,16 @@ let dumpRelations (world: World) : unit = printfn "rel_counts %s" (String.concat " " cells) printfn "rel_total=%d rel_max=%d rel_min=%d" (Array.sum counts) (Array.max counts) (Array.min counts) +let dumpRumors (world: World) : unit = + printfn "%s" (Sim.rumorTraceText world) + +let replayRumors (days: int64) (seed: uint64) (npcCount: int) (world: World) : bool = + let expected = Sim.rumorTraceText world + let replayed = runSimulation false days seed npcCount |> fun stats -> Sim.rumorTraceText stats.World + let same = expected = replayed + printfn "rumor_replay=%s tick=%d count=%d" (if same then "PASS" else "FAIL") world.Tick world.Rumors.Length + same + let gini (xs: int64[]) : float = let n = float xs.Length let s = float (Array.sum xs) @@ -266,45 +276,54 @@ let main argv = (seed: uint64 option) (npc: int option) (dump: bool) + (rumorTrace: bool) + (rumorReplay: bool) (batch: (int * int64) option) - : Result = + : Result = if i >= argv.Length then match batch with - | Some(k, d) -> Ok(defaultArg days 1L, defaultArg seed 42UL, defaultArg npc 30, dump, Some(k, d)) + | Some(k, d) -> Ok(defaultArg days 1L, defaultArg seed 42UL, defaultArg npc 30, dump, rumorTrace, rumorReplay, Some(k, d)) | None -> match days, seed, npc with - | Some d, Some s, Some n -> Ok(d, s, n, dump, None) + | Some d, Some s, Some n -> Ok(d, s, n, dump, rumorTrace, rumorReplay, None) | _ -> Error "missing --days/--seed" else match argv.[i] with | "--days" when i + 1 < argv.Length -> (match Int64.TryParse argv.[i + 1] with - | true, d when d > 0L -> parse (i + 2) (Some d) seed npc dump batch + | true, d when d > 0L -> parse (i + 2) (Some d) seed npc dump rumorTrace rumorReplay batch | _ -> Error $"invalid --days '{argv.[i + 1]}'") | "--seed" when i + 1 < argv.Length -> (match UInt64.TryParse argv.[i + 1] with - | true, s -> parse (i + 2) days (Some s) npc dump batch + | true, s -> parse (i + 2) days (Some s) npc dump rumorTrace rumorReplay batch | _ -> Error $"invalid --seed '{argv.[i + 1]}'") | "--npc" when i + 1 < argv.Length -> (match Int32.TryParse argv.[i + 1] with - | true, n when n > 0 && n <= 1000 -> parse (i + 2) days seed (Some n) dump batch + | true, n when n > 0 && n <= 1000 -> parse (i + 2) days seed (Some n) dump rumorTrace rumorReplay batch | _ -> Error $"invalid --npc '{argv.[i + 1]}'") - | "--dump-relations" -> parse (i + 1) days seed npc true batch + | "--dump-relations" -> parse (i + 1) days seed npc true rumorTrace rumorReplay batch + | "--dump-rumors" -> parse (i + 1) days seed npc dump true rumorReplay batch + | "--replay-rumors" -> parse (i + 1) days seed npc dump rumorTrace true batch | "--batch" when i + 2 < argv.Length -> (match Int32.TryParse argv.[i + 1], Int64.TryParse argv.[i + 2] with - | (true, k), (true, d) when k > 0 && d > 0L -> parse (i + 3) days seed npc dump (Some(k, d)) + | (true, k), (true, d) when k > 0 && d > 0L -> parse (i + 3) days seed npc dump rumorTrace rumorReplay (Some(k, d)) | _ -> Error $"invalid --batch '{argv.[i + 1]} {argv.[i + 2]}'") | other -> Error $"unknown argument '{other}'" - match parse 0 None None (Some 30) false None with - | Ok(days, seed, npc, dump, batch) -> + match parse 0 None None (Some 30) false false false None with + | Ok(days, seed, npc, dump, rumorTrace, rumorReplay, batch) -> match batch with + | Some(k, d) when rumorTrace || rumorReplay -> + eprintfn "headless: rumor trace/replay cannot be combined with --batch" + 2 | Some(k, d) -> runBatch k d | None -> let stats = runSimulation true days seed npc if dump then dumpRelations stats.World - if stats.NonFinite > 0L || stats.OutOfBounds > 0L then 1 else 0 + if rumorTrace then dumpRumors stats.World + let replayOk = if rumorReplay then replayRumors days seed npc stats.World else true + if stats.NonFinite > 0L || stats.OutOfBounds > 0L || not replayOk then 1 else 0 | Error msg -> eprintfn $"headless: {msg}" - eprintfn "usage: dotnet run -c Release --project src/LivingVillage.Headless -- --days N --seed S [--npc N] [--dump-relations] | --batch K D" + eprintfn "usage: dotnet run -c Release --project src/LivingVillage.Headless -- --days N --seed S [--npc N] [--dump-relations] [--dump-rumors] [--replay-rumors] | --batch K D" 2 -- cgit v1.2.3