Run 15: known-nodes exploration (addendum stage 1), per-town debug, O(n^2) fix

Exploration addendum stage 1: per-NPC known-nodes. NPCs gather only from
resources they've personally sensed (radius 22) or been told about;
founders seed with home ground, children inherit parents' maps, node
knowledge diffuses between neighbors like skills. Starving NPCs venture
outward and sense as they go - non-random needs-driven exploration, the
seed of first contact. Repopulation preserved (seeding + survival food
fallback): all towns fed, population stable 86-109 over 240 days.

Debug screen rebuilt: the ~68-row per-villager roster (overflowing at
four-town scale) replaced with a per-town summary - pop, fed%, knowledge,
debris, state breakdown, buildings, culture-colored, plus world
births/deaths. Scales to any population.

Performance: added phase timers and measured instead of guessing - npc.Tick
was 94% of cost, O(n^2) via TickResting calling RestingOccupancy (full
population scan) per building per resting NPC. Fixed with a once-per-tick
occupancy cache on Building. npc phase ~4x faster and now linear; full
240-day four-town run in 180s, faster than the pre-exploration baseline.
Profiling scaffolding removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:17:24 -04:00
parent 6cbe4bcf83
commit 10e772d087
7 changed files with 378 additions and 66 deletions
+26 -1
View File
@@ -123,6 +123,10 @@ public partial class GameManager : Node2D
LifeExpectancyDays = 150 + rng.Next(0, 80),
};
npc.Know.Capacity = 1.0f + (float)rng.NextDouble();
// Founders know their home ground — the resources around the
// town seed. They begin with a working local map, not blind.
foreach (var node in World.NodesWithin(center, 130f))
npc.KnownNodes.Add(node);
Npcs.Add(npc);
}
}
@@ -308,6 +312,10 @@ public partial class GameManager : Node2D
LifeExpectancyDays = 150 + rng.Next(0, 80),
};
child.Know.Capacity = 1.0f + (float)rng.NextDouble();
// A child inherits its parents' map — the family's known grounds,
// passed down like everything else a generation hands forward.
foreach (var node in a.KnownNodes) child.KnownNodes.Add(node);
foreach (var node in b.KnownNodes) child.KnownNodes.Add(node);
newborns.Add(child);
BirthsToday++;
pop[a.HomeTown] = (int)townPop + 1; // count the newborn toward capacity
@@ -331,6 +339,23 @@ public partial class GameManager : Node2D
{
Grid.Rebuild(Npcs);
// Recompute shelter occupancy once per tick (was O(n²): each resting
// NPC re-scanned everyone, per building). Now: one pass, cached — each
// sleeper counts toward the nearest shelter they're actually inside.
foreach (var b in Buildings) b.Occupancy = 0;
foreach (var npc in Npcs)
{
if (npc.State != NpcState.Resting) continue;
Building? at = null; float best = 6.25f; // within 2.5 units
foreach (var b in Buildings)
{
if (b.RestCapacity <= 0) continue;
float d = npc.Position.DistanceSquaredTo(b.Site);
if (d < best) { best = d; at = b; }
}
if (at != null) at.Occupancy++;
}
TotalTicks++;
foreach (var npc in Npcs)
@@ -349,7 +374,7 @@ public partial class GameManager : Node2D
TickReproduction();
StatsLogger.LogDay(this);
if (Day % 10 == 0)
GD.Print($"[WorldSim] Day {Day} complete.");
GD.Print($"[WorldSim] Day {Day} complete (pop {Npcs.Count}).");
if (StopAfterDays > 0 && Day >= StopAfterDays)
{