diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-19 03:20:56 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-19 03:20:56 +0800 |
| commit | 4993bc37cb00356b83cb4f8a2e4a92fc2fe77922 (patch) | |
| tree | 5b08f5fde9c420c30a588c113b78c421f828d0ab /src/LivingVillage.Kernel/Sim.fs | |
| parent | 2f46d1789088eb165271bd915844376aa5a84768 (diff) | |
| download | living-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.Kernel/Sim.fs')
| -rw-r--r-- | src/LivingVillage.Kernel/Sim.fs | 213 |
1 files changed, 167 insertions, 46 deletions
diff --git a/src/LivingVillage.Kernel/Sim.fs b/src/LivingVillage.Kernel/Sim.fs index 6ba48e2..62ce1e1 100644 --- a/src/LivingVillage.Kernel/Sim.fs +++ b/src/LivingVillage.Kernel/Sim.fs @@ -46,18 +46,27 @@ module Sim = | Sleep | Wander | Work + | Chat type MemoryKind = | Meal | Rest | Pay | Hungry + | Chatted of NpcId type MemoryEvent = { Tick: int64 Kind: MemoryKind Valence: float32 } + type InteractionKind = + | ChatInit of NpcId * NpcId + + type InteractionEvent = + { Tick: int64 + Kind: InteractionKind } + [<Struct>] type Mind = { Needs: Needs @@ -82,7 +91,8 @@ module Sim = Rng: RngState Avatar: Avatar NoHost: NoHost - Npcs: Npc list } + Npcs: Npc[] + Events: InteractionEvent list } let ticksPerSecond = 60L let secondsPerDay = 86400L @@ -102,8 +112,14 @@ module Sim = let minActionTicks = 600L let urgentThreshold = 20.0f let nightSleepMultiplier = 3.0f + let npcCount = 30 + let chatRangePx = 96.0f + let chatRangeSq = chatRangePx * chatRangePx + let chatTicks = 300L + let chatSocialRestore = 40.0f + let chatValence = 0.3f - let clamp v lo hi = if v < lo then lo elif v > hi then hi else v + let clamp (v: float32) (lo: float32) (hi: float32) : float32 = if v < lo then lo elif v > hi then hi else v let hungerDecayPerTick = 0.0012f let energyDecayPerTick = 0.0010f @@ -125,6 +141,7 @@ module Sim = | Sleep -> homePoint | Wander -> plazaPoint | Work -> worksitePoint + | Chat -> plazaPoint let isNightTick (tick: int64) : bool = let hourTicks = ticksPerDay / 24L @@ -137,6 +154,7 @@ module Sim = | Rest -> 0.4f | Pay -> 0.2f | Hungry -> -0.4f + | Chatted _ -> chatValence let recordMemory (tick: int64) (kind: MemoryKind) (mem: MemoryEvent list) : MemoryEvent list = { Tick = tick; Kind = kind; Valence = valenceOfKind kind } :: mem @@ -148,6 +166,7 @@ module Sim = | Sleep -> Some Rest | Work -> Some Pay | Wander -> None + | Chat -> None let personalityOfSeed (seed: uint64) : Personality = let a, r1 = Rng.nextFloat32 (Rng.ofSeed seed) @@ -174,6 +193,7 @@ module Sim = | Sleep -> (100.0f - needs.Energy) * (0.25f + 2.0f * (1.0f - p.Drive)) | Wander -> (100.0f - needs.Social) * (0.25f + 2.0f * p.Extraversion) | Work -> (100.0f - needs.Money) * (0.25f + 2.0f * p.Greed) + | Chat -> 0.0f if kind = Sleep && night then raw * nightSleepMultiplier else raw let decideAction (night: bool) (needs: Needs) (p: Personality) : NpcActionKind = @@ -192,16 +212,17 @@ module Sim = | Sleep -> { n with Energy = n.Energy + 60.0f } | Wander -> { n with Social = n.Social + 15.0f; Hunger = n.Hunger - 2.0f } | Work -> { n with Money = n.Money + 20.0f; Energy = n.Energy - 10.0f } + | Chat -> { n with Social = n.Social + chatSocialRestore } |> needsClamp - let private moveToward (pos: Vec2) (target: Vec2) (maxStep: float32) : Vec2 * bool = + let private moveToward (pos: Vec2) (target: Vec2) (maxStep: float32) : struct (Vec2 * bool) = let dx = target.X - pos.X let dy = target.Y - pos.Y let len = sqrt (dx * dx + dy * dy) - if len <= maxStep + arriveEpsilon then target, true + if len <= maxStep + arriveEpsilon then struct (target, true) else let inv = maxStep / len - { X = pos.X + dx * inv; Y = pos.Y + dy * inv }, false + struct ({ X = pos.X + dx * inv; Y = pos.Y + dy * inv }, false) let initialWorld (seed: uint64) : World = let centerX = float32 (mapWidthTiles * tilePixels / 2 - tilePixels / 2) @@ -226,9 +247,53 @@ module Sim = Rng = Rng.ofSeed seed Avatar = { Pos = { X = centerX; Y = centerY } } NoHost = { Reserved = 0UL } - Npcs = [ npc ] } + Npcs = [| npc |] + Events = [] } - let private stepNpc (tick: int64) (npc: Npc) : Npc = + let initialWorldN (seed: uint64) (count: int) : World = + let base_ = initialWorld seed + let cols = 6 + let spacing = 64.0f + let needs = { Hunger = 100.0f; Energy = 100.0f; Social = 100.0f; Money = 50.0f } + let npcs = + Array.init count (fun i -> + let col = i % cols + let row = i / cols + let pos = + { X = plazaPoint.X + (float32 col - 2.5f) * spacing + Y = plazaPoint.Y + (float32 row - 2.0f) * spacing } + let personality = personalityOfSeed (seed + uint64 i * 0x9E3779B97F4A7C15UL) + let startAction = decideAction (isNightTick 0L) needs personality + { Id = NpcId i + Pos = pos + Mind = + { Needs = needs + Personality = personality + Action = startAction + Target = actionTarget startAction + ActionAge = 0L + EffectDone = false + HungerFlagged = false + Memory = [] } }) + { base_ with Npcs = npcs } + + let chatableForChat (n: Npc) : bool = + n.Mind.Action <> Sleep && n.Mind.Action <> Chat + + let findChatPartner (self: NpcId) (pos: Vec2) (npcs: Npc[]) : NpcId option = + let mutable best = None + let mutable bestD2 = chatRangeSq + for o in npcs do + if o.Id <> self && chatableForChat o then + let dx = o.Pos.X - pos.X + let dy = o.Pos.Y - pos.Y + let d2 = dx * dx + dy * dy + if d2 < bestD2 then + bestD2 <- d2 + best <- Some o.Id + best + + let private stepNpc (tick: int64) (npcs: Npc[]) (pending: ResizeArray<InteractionEvent>) (npc: Npc) : Npc = let decayed = needsClamp { Hunger = npc.Mind.Needs.Hunger - hungerDecayPerTick @@ -238,8 +303,7 @@ module Sim = let night = isNightTick tick let age = npc.Mind.ActionAge + 1L let urgent = needsUrgent decayed - let target = actionTarget npc.Mind.Action - let nextPos, arrived = moveToward npc.Pos target (npcSpeed * dtSecondsF) + let maxStep = npcSpeed * dtSecondsF let mind0 = { npc.Mind with Needs = decayed @@ -250,46 +314,92 @@ module Sim = if decayed.Hunger < urgentThreshold && not npc.Mind.HungerFlagged then { mind0 with Memory = recordMemory tick Hungry mind0.Memory; HungerFlagged = true } else mind0 - if arrived then - let needs2, mem2 = - if mind1.EffectDone then mind1.Needs, mind1.Memory - else - let mem = - match actionMemoryKind mind1.Action with - | Some k -> recordMemory tick k mind1.Memory - | None -> mind1.Memory - applyActionEffect mind1.Action mind1.Needs, mem - let mind2 = { mind1 with Needs = needs2; Memory = mem2; EffectDone = true } - if age >= minActionTicks || urgent then - let action = decideAction night needs2 mind2.Personality - if action = mind2.Action then - { npc with Pos = nextPos; Mind = { mind2 with ActionAge = 0L; EffectDone = false } } - else - { npc with - Pos = nextPos - Mind = - { mind2 with - Action = action - Target = actionTarget action - ActionAge = 0L - EffectDone = false } } + let switchTo (resetOnSame: bool) (mind: Mind) (needsNow: Needs) (pos: Vec2) : Npc = + let action = decideAction night needsNow mind.Personality + if action = Wander then + match findChatPartner npc.Id pos npcs with + | Some partner -> + pending.Add { Tick = tick; Kind = ChatInit(npc.Id, partner) } + { npc with Pos = pos; Mind = mind } + | None -> + if action = mind.Action then + if resetOnSame then + { npc with Pos = pos; Mind = { mind with ActionAge = 0L; EffectDone = false } } + else { npc with Pos = pos; Mind = mind } + else + { npc with + Pos = pos + Mind = { mind with Action = action; Target = actionTarget action; ActionAge = 0L; EffectDone = false } } + elif action = mind.Action then + if resetOnSame then + { npc with Pos = pos; Mind = { mind with ActionAge = 0L; EffectDone = false } } + else { npc with Pos = pos; Mind = mind } + else + { npc with + Pos = pos + Mind = { mind with Action = action; Target = actionTarget action; ActionAge = 0L; EffectDone = false } } + if mind1.Action = Chat then + let struct (nextPos, _) = moveToward npc.Pos mind1.Target maxStep + if age >= chatTicks then + let needs2 = applyActionEffect Chat mind1.Needs + switchTo true { mind1 with Needs = needs2 } needs2 nextPos else - { npc with Pos = nextPos; Mind = mind2 } - elif urgent && age < minActionTicks then - let action = decideAction night decayed mind1.Personality - if action = mind1.Action then { npc with Pos = nextPos; Mind = mind1 } + else + let target = actionTarget npc.Mind.Action + let struct (nextPos, arrived) = moveToward npc.Pos target maxStep + if arrived then + let needs2, mem2 = + if mind1.EffectDone then mind1.Needs, mind1.Memory + else + let mem = + match actionMemoryKind mind1.Action with + | Some k -> recordMemory tick k mind1.Memory + | None -> mind1.Memory + applyActionEffect mind1.Action mind1.Needs, mem + let mind2 = { mind1 with Needs = needs2; Memory = mem2; EffectDone = true } + if age >= minActionTicks || urgent then + switchTo true mind2 needs2 nextPos + else + { npc with Pos = nextPos; Mind = mind2 } + elif urgent && age < minActionTicks then + switchTo false mind1 decayed nextPos else - { npc with - Pos = nextPos - Mind = - { mind1 with - Action = action - Target = actionTarget action - ActionAge = 0L - EffectDone = false } } + { npc with Pos = nextPos; Mind = mind1 } + + let private applyChatInit (tick: int64) (a: NpcId) (b: NpcId) (npcs: Npc[]) : Npc[] = + if a = b then npcs else - { npc with Pos = nextPos; Mind = mind1 } + let mutable ia = -1 + let mutable ib = -1 + for i in 0 .. npcs.Length - 1 do + let id = npcs.[i].Id + if id = a then ia <- i elif id = b then ib <- i + if ia < 0 || ib < 0 then npcs + else + let na = npcs.[ia] + let nb = npcs.[ib] + if chatableForChat na && chatableForChat nb && ia <> ib then + npcs.[ia] <- + { na with + Mind = + { na.Mind with + Action = Chat + Target = nb.Pos + ActionAge = 0L + EffectDone = false + Memory = recordMemory tick (Chatted b) na.Mind.Memory } } + npcs.[ib] <- + { nb with + Mind = + { nb.Mind with + Action = Chat + Target = na.Pos + ActionAge = 0L + EffectDone = false + Memory = recordMemory tick (Chatted a) nb.Mind.Memory } } + npcs + else npcs let step (ts: TimeStep) (world: World) : World = let tick = world.Tick + 1L @@ -299,6 +409,16 @@ module Sim = let dy = ts.Input.MoveY * avatarSpeed * dtSecondsF let rngOut, rngNext = Rng.nextUInt64 world.Rng ignore rngOut + let pending = ResizeArray<InteractionEvent> () + let oldNpcs = world.Npcs + let newNpcs = Array.copy oldNpcs + for i in 0 .. newNpcs.Length - 1 do + newNpcs.[i] <- stepNpc tick oldNpcs pending newNpcs.[i] + let queue = if pending.Count = 0 then world.Events else world.Events @ List.ofSeq pending + if queue.Length > 0 then + for ev in queue do + match ev.Kind with + | ChatInit (a, b) -> applyChatInit ev.Tick a b newNpcs |> ignore { Tick = tick Time = float tick * dtSeconds Rng = rngNext @@ -307,4 +427,5 @@ module Sim = { X = clamp (world.Avatar.Pos.X + dx) 0.0f maxX Y = clamp (world.Avatar.Pos.Y + dy) 0.0f maxY } } NoHost = world.NoHost - Npcs = world.Npcs |> List.map (stepNpc tick) } + Npcs = newNpcs + Events = [] } |
