Run 12: knowledge system, mortality, and the North's generational soul

SPEC §5 built: three skill domains (forage/woodcraft/masonry) that pay
off and grow by practice; proximity diffusion soul-calibrated by
absorption x teaching-drive; and mortality - the piece that makes
knowledge generational. Elders die (~150-230 days), everything unshared
dies with them, a youth comes of age near knowing nothing. North soul
(absorption 1.6, teaching 0.9) vs South (1.0, 0.3); East teaches no one.

Two findings: (1) headless soaks were contaminated by orphaned child
processes writing later runs' CSVs - fixed with StopAfterDays clean
self-terminate; (2) passive diffusion barely helped - the North's real
culture is the young seeking elders on purpose, so added deliberate
teacher-seeking and youths now cradle beside the wisest elder. North
now leads knowledge at every checkpoint, but a stable town approaches
the ceiling regardless, so teaching is a recovery advantage not an
equilibrium one. The crisis-recovery experiment is the real test, noted
in FINDINGS as next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 16:27:28 -04:00
parent 3ffde3d9e9
commit 3e4e44c623
15 changed files with 7059 additions and 7171 deletions
+80 -3
View File
@@ -33,6 +33,13 @@ public class Npc
public float Health = 100f; // forced rest below 30
public float Nourishment = 1f; // 1 = fed; drains daily, eats from pack
// --- Mortality: knowledge is generational only if generations end ----
public float AgeDays;
public float LifeExpectancyDays = 180f;
// --- Knowledge (SPEC §5) ----------------------------------------------
public Knowledge Know = new();
// --- State -----------------------------------------------------------
public NpcState State = NpcState.Gathering;
@@ -78,6 +85,33 @@ public class Npc
/// who they are becoming. Indexed by (int)NpcState.</summary>
public int[] StateTicksToday = new int[5];
/// <summary>
/// Learn from another by proximity (SPEC §5). Rate scales with your own
/// absorption and their drive to teach. Returns true if anything passed.
/// </summary>
private bool LearnFrom(Npc other)
{
bool learned = false;
float rate = 0.0016f * Soul.Absorption * (1f + other.Soul.TeachingDrive * 2.5f);
for (int d = 0; d < Knowledge.DomainCount; d++)
{
float gap = other.Know.Skill[d] - Know.Skill[d];
if (gap > 0.03f)
{
Know.Gain((KnowledgeDomain)d, gap * rate);
learned = true;
}
}
return learned;
}
/// <summary>Clear live references to someone who has died.</summary>
public void ForgetPerson(Npc dead)
{
if (_targetNpc == dead) _targetNpc = null;
if (_giftSource == dead) _giftSource = null;
}
// --- Diagnostics (read-only views for the stats logger) ---------------
public MaterialKind CurrentDemand(GameManager gm) => PickGatherTarget(gm);
public int RoughNights => _unshelteredRests;
@@ -426,8 +460,12 @@ public class Npc
else
{
Activity = $"harvesting {_targetNode.Kind.ToString().ToLower()}";
float taken = world.Harvest(_targetNode, requested: 2f, this);
// Skill pays: a practiced hand takes more per reach — and the
// work itself is the teacher.
var domain = Knowledge.DomainFor(_targetNode.Kind);
float taken = world.Harvest(_targetNode, requested: 2f * (1f + Know[domain]), this);
if (taken <= 0f) { _targetNode = null; return; } // node at its floor — re-seek
Know.Gain(domain, 0.0006f);
Inventory.TryGetValue(_targetNode.Kind, out float have);
Inventory[_targetNode.Kind] = have + taken;
@@ -645,9 +683,42 @@ public class Npc
private void TickSocializing(GameManager gm)
{
// Drift toward the nearest neighbor — community forms where feet do.
// The young seek a teacher. A soul that still has much to learn and
// the drive to learn it looks for the wisest elder in town rather
// than drifting to whoever's closest. This is the North's culture in
// motion: the young sitting at the feet of the old, on purpose.
Npc? nearest = null;
float bestDist = float.MaxValue;
if (Soul.Absorption > 1.2f && Know.Total < Know.Capacity * 0.6f)
{
Npc? teacher = null;
float bestWorth = 0.15f; // must actually know more than us
foreach (var other in gm.Npcs)
{
if (other == this || other.HomeTown != HomeTown) continue;
float worth = (other.Know.Total - Know.Total) * (1f + other.Soul.TeachingDrive);
if (worth > bestWorth) { bestWorth = worth; teacher = other; }
}
if (teacher != null)
{
if (Position.DistanceTo(teacher.Position) > 3f)
{
Activity = $"seeking out {teacher.Name} to learn";
MoveToward(teacher.Position);
}
else
{
LearnFrom(teacher);
Activity = $"learning from {teacher.Name}";
}
if (!gm.IsNight && _stateTimer > 15f) Enter(NpcState.Gathering);
return;
}
}
// Otherwise: drift toward the nearest neighbor — community forms
// where feet do.
foreach (var other in gm.Npcs)
{
if (other == this) continue;
@@ -659,9 +730,15 @@ public class Npc
Activity = $"walking over to {nearest.Name}";
MoveToward(nearest.Position);
}
else if (nearest != null)
{
Activity = LearnFrom(nearest)
? $"learning from {nearest.Name}"
: $"chatting with {nearest.Name}";
}
else
{
Activity = nearest != null ? $"chatting with {nearest.Name}" : "idling";
Activity = "idling";
}
// TODO(Week 3): bonds + Generosity-weighted spontaneous help.