summaryrefslogtreecommitdiff
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
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 对拍/闲聊对称测试
-rw-r--r--src/LivingVillage.Desktop/Game.fs19
-rw-r--r--src/LivingVillage.Headless/Program.fs83
-rw-r--r--src/LivingVillage.Kernel.Tests/DeterminismTests.fs60
-rw-r--r--src/LivingVillage.Kernel/Sim.fs213
4 files changed, 291 insertions, 84 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index e2b520f..2e28760 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -70,6 +70,8 @@ module NpcView =
| Sleep -> "sleep"
| Wander -> "wander"
| Work -> "work"
+ | Chat -> "chat"
+ | Work -> "work"
type LivingVillageGame() as this =
inherit Game()
@@ -82,7 +84,7 @@ type LivingVillageGame() as this =
let mutable atlas = Unchecked.defaultof<Texture2D>
let mutable pixel = Unchecked.defaultof<Texture2D>
let mutable map = MapGen.generate Sim.mapWidthTiles Sim.mapHeightTiles
- let mutable world = Sim.initialWorld 42UL
+ let mutable world = Sim.initialWorldN 42UL Sim.npcCount
let mutable camera = Vector2.Zero
let mutable fpsFrames = 0
let mutable fpsSeconds = 0.0
@@ -139,10 +141,10 @@ type LivingVillageGame() as this =
fpsFrames <- 0
fpsSeconds <- 0.0
let npcAction =
- match world.Npcs with
- | npc :: _ -> NpcView.actionName npc.Mind.Action
- | [] -> "none"
- this.Window.Title <- $"Living Village M1{titleSuffix} | fps {fps:F1} | tick {world.Tick} | npc {npcAction}"
+ if world.Npcs.Length > 0 then
+ NpcView.actionName world.Npcs.[0].Mind.Action
+ else "none"
+ this.Window.Title <- $"Living Village M3a{titleSuffix} | fps {fps:F1} | tick {world.Tick} | npc {npcAction}"
printfn $"fps={fps:F1} tick={world.Tick} pos=({world.Avatar.Pos.X:F0},{world.Avatar.Pos.Y:F0})"
override this.Draw(gameTime: GameTime) =
@@ -163,9 +165,12 @@ type LivingVillageGame() as this =
spriteBatch.Draw(atlas, dst, src, Color.White)
let avatarDst =
Rectangle(int world.Avatar.Pos.X - camX, int world.Avatar.Pos.Y - camY, Sim.tilePixels, Sim.tilePixels)
- spriteBatch.Draw(pixel, avatarDst, Color.IndianRed)
+ spriteBatch.Draw(pixel, avatarDst, Color.Red)
+ let npcShades =
+ [| Color.Navy; Color.MediumBlue; Color.Blue; Color.RoyalBlue; Color.DodgerBlue |]
for npc in world.Npcs do
let npcDst =
Rectangle(int npc.Pos.X - camX, int npc.Pos.Y - camY, Sim.tilePixels, Sim.tilePixels)
- spriteBatch.Draw(pixel, npcDst, Color.Blue)
+ let (NpcId idx) = npc.Id
+ spriteBatch.Draw(pixel, npcDst, npcShades.[idx % npcShades.Length])
spriteBatch.End()
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
diff --git a/src/LivingVillage.Kernel.Tests/DeterminismTests.fs b/src/LivingVillage.Kernel.Tests/DeterminismTests.fs
index 0e1766e..972ce19 100644
--- a/src/LivingVillage.Kernel.Tests/DeterminismTests.fs
+++ b/src/LivingVillage.Kernel.Tests/DeterminismTests.fs
@@ -153,12 +153,12 @@ type DeterminismTests () =
[<TestMethod>]
member _.NpcNeedsDecayOverTimeAndActionsDifferAcrossSeeds () =
let w0 = Sim.initialWorld 42UL
- let npc0 = List.exactlyOne w0.Npcs
+ let npc0 = w0.Npcs |> Array.exactlyOne
let zero = { Input = { MoveX = 0.0f; MoveY = 0.0f } }
let mutable w = w0
for _ in 1 .. 500 do
w <- Sim.step zero w
- let npc1 = List.exactlyOne w.Npcs
+ let npc1 = w.Npcs |> Array.exactlyOne
let n0 = npc0.Mind.Needs
let n1 = npc1.Mind.Needs
Assert.IsTrue(n1.Hunger < n0.Hunger, "hunger must decay")
@@ -241,3 +241,59 @@ type DeterminismTests () =
| None -> ()
Assert.IsTrue(sawEmpty, "memory must start empty")
Assert.IsTrue(sawEvent, "expected a valence memory event within 6000 ticks")
+
+ [<TestMethod>]
+ member _.Npc30SameSeedTracesAreTickByTickEqualAndEventsDrain () =
+ let zero = { Input = { MoveX = 0.0f; MoveY = 0.0f } }
+ let mutable a = Sim.initialWorldN 42UL 30
+ let mutable b = Sim.initialWorldN 42UL 30
+ for t in 1L .. 30000L do
+ a <- Sim.step zero a
+ b <- Sim.step zero b
+ if a <> b then Assert.Fail($"30-npc worlds diverged at tick {t}")
+ if not a.Events.IsEmpty then Assert.Fail($"Events queue not drained at tick {t}")
+ Assert.IsTrue(30000L = a.Tick)
+
+ [<TestMethod>]
+ member _.Npc30ChatsRecordSymmetricPositiveMemories () =
+ let zero = { Input = { MoveX = 0.0f; MoveY = 0.0f } }
+ let mutable w = Sim.initialWorldN 42UL 30
+ let mutable prev0 = w.Npcs.[0].Mind.Action
+ let mutable symmetryChecked = false
+ let mutable chatObserved = 0
+ let total = 600000L
+ for _ in 1L .. total do
+ w <- Sim.step zero w
+ for npc in w.Npcs do
+ if npc.Mind.Action = Chat then chatObserved <- chatObserved + 1
+ let npc0 = w.Npcs.[0]
+ if not symmetryChecked && npc0.Mind.Action = Chat && prev0 <> Chat then
+ match List.tryHead npc0.Mind.Memory with
+ | Some entry ->
+ match entry.Kind with
+ | Chatted partner ->
+ if partner = npc0.Id then Assert.Fail("chat partner must differ from self")
+ let partnerNpc = w.Npcs |> Array.find (fun n -> n.Id = partner)
+ let partnerAlsoChatted =
+ partnerNpc.Mind.Memory
+ |> List.exists (fun e -> match e.Kind with Chatted p -> p = npc0.Id | _ -> false)
+ if not partnerAlsoChatted then
+ Assert.Fail("chat memory must be recorded symmetrically for both partners")
+ symmetryChecked <- true
+ | _ -> Assert.Fail($"chat start must record a Chatted memory, got {entry.Kind}")
+ | None -> Assert.Fail("chat start must record a Chatted memory")
+ prev0 <- npc0.Mind.Action
+ Assert.IsTrue(symmetryChecked, "expected npc0 to start at least one chat within the run")
+ Assert.IsTrue(chatObserved > 0, "expected some NPC to be chatting during the run")
+ let mutable chattedMemories = 0
+ for npc in w.Npcs do
+ Assert.IsTrue(npc.Mind.Memory.Length <= Sim.memoryCapacity, "memory exceeds ring capacity")
+ for e in npc.Mind.Memory do
+ match e.Kind with
+ | Chatted partner ->
+ chattedMemories <- chattedMemories + 1
+ if partner = npc.Id then Assert.Fail("chat partner must differ from self")
+ if e.Valence <> Sim.chatValence then
+ Assert.Fail($"chat valence must be {Sim.chatValence}, got {e.Valence}")
+ | _ -> ()
+ Assert.IsTrue(chattedMemories > 0, "expected Chatted memories after the run")
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 = [] }