summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Headless/Program.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-19 03:20:56 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-19 03:20:56 +0800
commit4993bc37cb00356b83cb4f8a2e4a92fc2fe77922 (patch)
tree5b08f5fde9c420c30a588c113b78c421f828d0ab /src/LivingVillage.Headless/Program.fs
parent2f46d1789088eb165271bd915844376aa5a84768 (diff)
downloadliving-village-4993bc37cb00356b83cb4f8a2e4a92fc2fe77922.tar.gz
feat: M3a 30 NPC 社区 — initialWorldN 网格布局 + Wander 寻伴闲聊 v1(ChatInit 事件队列 + 双方 Chatted 记忆 valence+0.3 + Social+40) + headless 聊天统计; 修复 clamp 泛型比较装箱(30npc 1天 63.5s→15.7s, 输出逐字节一致) + M3a 对拍/闲聊对称测试
Diffstat (limited to 'src/LivingVillage.Headless/Program.fs')
-rw-r--r--src/LivingVillage.Headless/Program.fs83
1 files changed, 54 insertions, 29 deletions
diff --git a/src/LivingVillage.Headless/Program.fs b/src/LivingVillage.Headless/Program.fs
index 6f86f45..28fe121 100644
--- a/src/LivingVillage.Headless/Program.fs
+++ b/src/LivingVillage.Headless/Program.fs
@@ -9,6 +9,7 @@ let secondsPerDay = 86400L
let ticksPerDay = secondsPerDay * ticksPerSecond
let segmentTicks = 900L
+let summaryInterval = 5000L
let zeroInput = { MoveX = 0.0f; MoveY = 0.0f }
@@ -46,18 +47,34 @@ let actionName (k: NpcActionKind) : string =
| Sleep -> "sleep"
| Wander -> "wander"
| Work -> "work"
+ | Chat -> "chat"
-let runSimulation (days: int64) (seed: uint64) : int =
+let npcIndex (id: NpcId) : int =
+ match id with NpcId i -> i
+
+let actionDistribution (npcs: Npc[]) : int * int * int * int * int =
+ let counts =
+ npcs
+ |> Array.fold (fun (e, s, w, k, c) npc ->
+ match npc.Mind.Action with
+ | Eat -> (e + 1, s, w, k, c)
+ | Sleep -> (e, s + 1, w, k, c)
+ | Wander -> (e, s, w + 1, k, c)
+ | Work -> (e, s, w, k + 1, c)
+ | Chat -> (e, s, w, k, c + 1)) (0, 0, 0, 0, 0)
+ counts
+
+let runSimulation (days: int64) (seed: uint64) (npcCount: int) : int =
let totalTicks = days * ticksPerDay
- printfn "living-village headless days=%d seed=%d ticks=%d tickrate=%d" days seed totalTicks ticksPerSecond
- let mutable world = Sim.initialWorld seed
+ printfn "living-village headless days=%d seed=%d npcs=%d ticks=%d tickrate=%d" days seed npcCount totalTicks ticksPerSecond
+ let mutable world = Sim.initialWorldN seed npcCount
let mutable driver = { Rng = Rng.ofSeed seed; Until = segmentTicks; Input = zeroInput }
let maxX = float32 (Sim.mapWidthTiles * Sim.tilePixels - Sim.tilePixels)
let maxY = float32 (Sim.mapHeightTiles * Sim.tilePixels - Sim.tilePixels)
let mutable nonFinite = 0L
let mutable outOfBounds = 0L
let mutable t = 0L
- let mutable lastAction = (List.head world.Npcs).Mind.Action
+ let mutable lastAction = (world.Npcs.[0]).Mind.Action
let mutable lastStart = 0L
let mutable switchCount = 0L
let mutable switchDurSum = 0L
@@ -65,6 +82,9 @@ let runSimulation (days: int64) (seed: uint64) : int =
let mutable sleepDay = 0L
let mutable nightTicks = 0L
let mutable dayTicks = 0L
+ let npcTotal = Array.create npcCount 0L
+ let lastActions = Array.create npcCount (world.Npcs.[0]).Mind.Action
+ let mutable chatsWindowStart = 0L
while t < totalTicks do
let d, input =
if t < driver.Until then driver, driver.Input
@@ -76,7 +96,12 @@ let runSimulation (days: int64) (seed: uint64) : int =
t <- t + 1L
let nightNow = Sim.isNightTick t
if nightNow then nightTicks <- nightTicks + 1L else dayTicks <- dayTicks + 1L
- let head = List.head world.Npcs
+ for npc in world.Npcs do
+ let idx = npcIndex npc.Id
+ if npc.Mind.Action = Chat && lastActions.[idx] <> Chat then
+ npcTotal.[idx] <- npcTotal.[idx] + 1L
+ lastActions.[idx] <- npc.Mind.Action
+ let head = world.Npcs.[0]
if head.Mind.Action = Sleep then
if nightNow then sleepNight <- sleepNight + 1L else sleepDay <- sleepDay + 1L
if head.Mind.Action <> lastAction then
@@ -88,27 +113,18 @@ let runSimulation (days: int64) (seed: uint64) : int =
if Single.IsNaN p.X || Single.IsNaN p.Y || Single.IsInfinity p.X || Single.IsInfinity p.Y then
nonFinite <- nonFinite + 1L
if p.X < 0.0f || p.X > maxX || p.Y < 0.0f || p.Y > maxY then outOfBounds <- outOfBounds + 1L
- if t % 2000L = 0L then
- let day = float t / float ticksPerDay
- let memTotal = GC.GetTotalMemory(true)
- for npc in world.Npcs do
- let n = npc.Mind.Needs
- let p' = npc.Pos
- let memCount = List.length npc.Mind.Memory
- let lastValence =
- match List.tryHead npc.Mind.Memory with
- | Some e -> e.Valence
- | None -> 0.0f
- printfn "npc=%A tick=%d day=%.4f pos=(%.1f,%.1f) hunger=%.2f energy=%.2f social=%.2f money=%.2f action=%s mem_count=%d last_valence=%.2f"
- npc.Id t day p'.X p'.Y n.Hunger n.Energy n.Social n.Money (actionName npc.Mind.Action) memCount lastValence
- if t % 1000L = 0L && t % 2000L <> 0L then
+ if t % summaryInterval = 0L then
+ let eat, sleep, wander, work, chat = actionDistribution world.Npcs
+ let chatsTotal = Array.sum npcTotal
let day = float t / float ticksPerDay
- printfn "tick=%d time=%.4fs day=%.4f pos=(%.1f,%.1f) rng=%016x" t world.Time day p.X p.Y world.Rng.State
+ printfn "summary tick=%d day=%.4f eat=%d sleep=%d wander=%d work=%d chat=%d chats_window=%d"
+ t day eat sleep wander work chat (chatsTotal - chatsWindowStart)
+ chatsWindowStart <- chatsTotal
let segments = switchCount + 1L
let avgActionTicks = float (switchDurSum + (totalTicks - lastStart)) / float segments
let nightPct = if nightTicks > 0L then 100.0 * float sleepNight / float nightTicks else 0.0
let dayPct = if dayTicks > 0L then 100.0 * float sleepDay / float dayTicks else 0.0
- let npc0 = List.head world.Npcs
+ let npc0 = world.Npcs.[0]
let memCount = List.length npc0.Mind.Memory
let lastValence =
match List.tryHead npc0.Mind.Memory with
@@ -116,31 +132,40 @@ let runSimulation (days: int64) (seed: uint64) : int =
| None -> 0.0f
printfn "npc=%A action_switches=%d avg_action_ticks=%.1f sleep_night_ticks=%d sleep_day_ticks=%d sleep_night_pct=%.2f sleep_day_pct=%.2f mem_count=%d last_valence=%.2f"
npc0.Id switchCount avgActionTicks sleepNight sleepDay nightPct dayPct memCount lastValence
+ let chatsTotal = Array.sum npcTotal
+ let minChats = if npcCount > 0 then Array.min npcTotal else 0L
+ let maxChats = if npcCount > 0 then Array.max npcTotal else 0L
+ let avgChats = if npcCount > 0 then float chatsTotal / float npcCount else 0.0
+ printfn "chats_total=%d chats_min=%d chats_max=%d chats_avg=%.2f" chatsTotal minChats maxChats avgChats
printfn "done tick=%d non-finite=%d out-of-bounds=%d" world.Tick nonFinite outOfBounds
if nonFinite > 0L || outOfBounds > 0L then 1 else 0
[<EntryPoint>]
let main argv =
- let rec parse (i: int) (days: int64 option) (seed: uint64 option) : Result<int64 * uint64, string> =
+ let rec parse (i: int) (days: int64 option) (seed: uint64 option) (npc: int option) : Result<int64 * uint64 * int, string> =
if i >= argv.Length then
- match days, seed with
- | Some d, Some s -> Ok(d, s)
+ match days, seed, npc with
+ | Some d, Some s, Some n -> Ok(d, s, n)
| _ -> 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
+ | true, d when d > 0L -> parse (i + 2) (Some d) seed npc
| _ -> 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)
+ | true, s -> parse (i + 2) days (Some s) npc
| _ -> 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)
+ | _ -> Error $"invalid --npc '{argv.[i + 1]}'")
| other -> Error $"unknown argument '{other}'"
- match parse 0 None None with
- | Ok(days, seed) -> runSimulation days seed
+ match parse 0 None None (Some 30) with
+ | Ok(days, seed, npc) -> runSimulation days seed npc
| Error msg ->
eprintfn $"headless: {msg}"
- eprintfn "usage: dotnet run -c Release --project src/LivingVillage.Headless -- --days N --seed S"
+ eprintfn "usage: dotnet run -c Release --project src/LivingVillage.Headless -- --days N --seed S [--npc N]"
2