Run 14: reproduction from bonds, and towns anchored to the compass
Children from bonds (Phase B): death no longer auto-spawns a replacement; bonded, fed, adult pairs conceive, logistic-damped toward a per-town carrying capacity. Population is now dynamic - births vs deaths, a band not a fixed count. Towns anchored to their cardinal seeds (build-centroid 70% weighted to the fixed compass point) so settlements stay planted instead of drifting to map-center - North now founds at ~(388,139) by its (384,130) seed vs West's prior 100+ cell drift. Tuning took three passes (logged in FINDINGS): explosion, then die-out, then a generational-gap bug where founders aged out of fertility before their children matured. Fix: wide fertility window + generous rate. Result: births 108 ~= deaths 104, healthy population band, and the four-town thesis sharpened into demographics - North/South thrive soul-clean, West shrinks with debris, East collapses to a starving remnant. Selfishness now literally fails to sustain a population. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+33
-2
@@ -300,8 +300,39 @@ is hungriest (teaches no one, shares nothing). 99 bonds, 80 deaths — a
|
||||
living, pairing, generational population in four distinct societies, all
|
||||
from behavior weights. Nothing is scripted; the cultures are emergent.
|
||||
|
||||
**Next:** reproduction on top of bonds (Phase B); crisis-recovery
|
||||
experiment (North's resilience); save/load; then the player pivot.
|
||||
## Run 14 — Reproduction, and towns anchored to the compass
|
||||
|
||||
**Built:** children from bonds (Phase B). Death no longer auto-spawns a
|
||||
replacement — people die and are gone; bonded, fed, adult pairs conceive
|
||||
(logistic-damped toward a per-town carrying capacity, so population is a
|
||||
band, not an explosion or a collapse). Population is now genuinely
|
||||
dynamic: births vs deaths. Also anchored towns to their cardinal seeds —
|
||||
the build-centroid is now 70% weighted to the fixed compass point, so
|
||||
settlements stay planted (North founds at ~(388,139) by its (384,130)
|
||||
seed) instead of drifting to map-center where foraging paths overlap
|
||||
(West had wandered 100+ cells before).
|
||||
|
||||
**Tuning story (three failed calibrations, logged as the lesson):**
|
||||
first pass exploded (80→211 by day 80 — young founders, nobody dying,
|
||||
every couple breeding); over-damped to fix it and the world died out
|
||||
(80→6, East extinct); the real bug was a *generational gap* — founders
|
||||
aged out of fertility before their children matured and bonded, so
|
||||
births stopped even as room opened. Fix: wide fertility window (to 85%
|
||||
of life) + generous rate (0.12/couple/day × room). Population dynamics
|
||||
are a control loop; the birth rate must track the death rate or the
|
||||
system runs away in whichever direction it's biased.
|
||||
|
||||
**Result — reproduction sharpens the four-town thesis:** births (108) ≈
|
||||
deaths (104), population settled in a healthy band (~68–109). And the
|
||||
cultures now diverge in *demographics*, not just soul-state: North (26)
|
||||
and South (22) thrive and stay soul-clean; West shrinks (16) with debris
|
||||
creeping in; **East collapses to 4, starving (nourishment 0.00), highest
|
||||
debris — selfishness now literally fails to sustain a population.** A
|
||||
town that won't share or cooperate can't feed its people or raise a next
|
||||
generation. The most damning verdict the sim has produced.
|
||||
|
||||
**Next:** crisis-recovery experiment (North's resilience); save/load;
|
||||
then the player pivot.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+91
-42
@@ -19,7 +19,7 @@ public partial class GameManager : Node2D
|
||||
// centroid, buildings, and granary. NPCs affiliate with a home town
|
||||
// but roam freely — the space between towns is where cultures meet.
|
||||
|
||||
[Export] public int TownPopulationEach = 20; // per settlement (~80 total)
|
||||
[Export] public int TownPopulationEach = 16; // per settlement (~64 total, room to grow)
|
||||
|
||||
public record TownDef(string Name, Vector2 Center, SoulType Soul);
|
||||
|
||||
@@ -151,6 +151,15 @@ public partial class GameManager : Node2D
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>A town's fixed cardinal seed — its permanent home on the
|
||||
/// map, unaffected by where its people happen to wander.</summary>
|
||||
public Vector2 TownAnchor(string town)
|
||||
{
|
||||
foreach (var t in _towns)
|
||||
if (t.Name == town) return t.Center;
|
||||
return new Vector2(World.GridSize / 2f, World.GridSize / 2f);
|
||||
}
|
||||
|
||||
public Vector2 TownCentroid(string town)
|
||||
{
|
||||
Vector2 sum = Vector2.Zero;
|
||||
@@ -161,13 +170,13 @@ public partial class GameManager : Node2D
|
||||
sum += npc.Position;
|
||||
count++;
|
||||
}
|
||||
if (count == 0)
|
||||
{
|
||||
foreach (var t in _towns)
|
||||
if (t.Name == town) return t.Center;
|
||||
return new Vector2(World.GridSize / 2f, World.GridSize / 2f);
|
||||
}
|
||||
return sum / count;
|
||||
if (count == 0) return TownAnchor(town);
|
||||
|
||||
// Anchor-weighted: the settlement's build-center stays near its
|
||||
// cardinal seed even as villagers roam outward to distant resources.
|
||||
// Towns keep their place on the compass instead of drifting to the
|
||||
// middle of the map where everyone's foraging paths overlap.
|
||||
return (sum / count).Lerp(TownAnchor(town), 0.7f);
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
@@ -210,63 +219,102 @@ public partial class GameManager : Node2D
|
||||
}
|
||||
}
|
||||
|
||||
// --- Mortality: generations end, and what wasn't passed on ends too --
|
||||
// --- Mortality & birth: generations end, and new ones are born --------
|
||||
// Death no longer auto-spawns a replacement. People die and are simply
|
||||
// gone; children come from bonded pairs (TickReproduction). Population
|
||||
// is now births vs deaths — a town can grow, shrink, or die out.
|
||||
|
||||
public int DeathsToday { get; set; }
|
||||
public int BirthsToday { get; set; }
|
||||
private int _bornCount;
|
||||
|
||||
private void TickMortality()
|
||||
{
|
||||
var dying = new List<Npc>();
|
||||
foreach (var npc in Npcs)
|
||||
{
|
||||
npc.AgeDays += 1f;
|
||||
if (npc.AgeDays >= npc.LifeExpectancyDays)
|
||||
dying.Add(npc);
|
||||
}
|
||||
|
||||
var rng = new System.Random(Seed + (int)TotalTicks);
|
||||
|
||||
foreach (var dead in Npcs.ToArray())
|
||||
{
|
||||
if (!dying.Contains(dead)) continue;
|
||||
dead.AgeDays += 1f;
|
||||
if (dead.AgeDays < dead.LifeExpectancyDays) continue;
|
||||
|
||||
Npcs.Remove(dead);
|
||||
DeathsToday++;
|
||||
|
||||
// Everything unshared dies with them: skills, memories, echoes.
|
||||
// What survives is what they taught, built, and stored.
|
||||
// What survives is what they taught, built, stored — and bore.
|
||||
foreach (var other in Npcs)
|
||||
other.ForgetPerson(dead);
|
||||
|
||||
// A youth comes of age in the same town, same culture, knowing
|
||||
// almost nothing — but raised among the living, not in a void.
|
||||
// They spawn beside the town's wisest elder, so the young begin
|
||||
// life within reach of what the community still remembers. The
|
||||
// town's knowledge is only what its living carry — but the young
|
||||
// start where that knowledge is.
|
||||
_bornCount++;
|
||||
Npc? elder = null;
|
||||
float bestKnow = -1f;
|
||||
foreach (var other in Npcs)
|
||||
if (other.HomeTown == dead.HomeTown && other.Know.Total > bestKnow)
|
||||
{ bestKnow = other.Know.Total; elder = other; }
|
||||
Vector2 cradle = elder?.Position ?? TownCentroid(dead.HomeTown);
|
||||
GD.Print($"[Passing] day {Day}: {dead.Name} dies at {dead.AgeDays:0} days " +
|
||||
$"(knowledge {dead.Know.Total:0.00} lost).");
|
||||
}
|
||||
}
|
||||
|
||||
var youth = new Npc
|
||||
/// <summary>
|
||||
/// Children come from bonds (Phase B). A bonded pair — both grown adults,
|
||||
/// both fed, with a roof in town — may conceive once per season. The
|
||||
/// child spawns beside its parents, inherits their town and culture, and
|
||||
/// begins knowing almost nothing but growing up among those who do. This
|
||||
/// is how a town persists: not by respawn-on-death, but by families.
|
||||
/// </summary>
|
||||
/// <summary>Rough carrying capacity per town — beds plus a margin the
|
||||
/// land can feed. Births taper toward zero as a town fills it.</summary>
|
||||
private const int TownCarryingCapacity = 28;
|
||||
|
||||
private void TickReproduction()
|
||||
{
|
||||
var rng = new System.Random(Seed * 7 + (int)TotalTicks);
|
||||
var newborns = new List<Npc>();
|
||||
|
||||
// Town populations, to damp births near carrying capacity.
|
||||
var pop = new Dictionary<string, int>();
|
||||
foreach (var npc in Npcs)
|
||||
pop[npc.HomeTown] = pop.GetValueOrDefault(npc.HomeTown) + 1;
|
||||
|
||||
foreach (var a in Npcs)
|
||||
{
|
||||
var b = a.Partner;
|
||||
if (b == null) continue;
|
||||
if (a.GetHashCode() >= b.GetHashCode()) continue; // count each pair once
|
||||
if (!a.IsAdult || !b.IsAdult) continue;
|
||||
if (a.Nourishment < 0.5f || b.Nourishment < 0.5f) continue; // both fed
|
||||
// Fertile from adulthood through most of life — a wide window so
|
||||
// generations overlap and a town isn't one bad cohort from ruin.
|
||||
if (a.AgeDays > a.LifeExpectancyDays * 0.85f) continue;
|
||||
|
||||
// Logistic damping: a full town barely breeds; an emptied one
|
||||
// (after a plague) breeds back toward capacity. This is what
|
||||
// keeps population in a band instead of exploding or dying out.
|
||||
float townPop = pop.GetValueOrDefault(a.HomeTown);
|
||||
float roomFactor = Mathf.Clamp(1f - townPop / TownCarryingCapacity, 0f, 1f);
|
||||
if (roomFactor <= 0f) continue;
|
||||
|
||||
// High enough to outrun the founding cohort's die-off and keep
|
||||
// generations overlapping; logistic damping caps the ceiling so
|
||||
// it can't explode. Deliberately generous — a shrinking town is
|
||||
// a worse failure than a full one.
|
||||
double dailyChance = 0.12 * roomFactor;
|
||||
if (rng.NextDouble() > dailyChance) continue;
|
||||
|
||||
_bornCount++;
|
||||
var child = new Npc
|
||||
{
|
||||
Name = $"{dead.HomeTown}_{(char)('A' + _bornCount % 26)}{_bornCount + 25}",
|
||||
HomeTown = dead.HomeTown,
|
||||
Soul = dead.Soul, // raised in the culture that raised them
|
||||
Name = $"{a.HomeTown}_{(char)('a' + _bornCount % 26)}{_bornCount + 25}",
|
||||
HomeTown = a.HomeTown,
|
||||
Soul = a.Soul, // raised in the culture that bore them
|
||||
PersonalityModifier = (float)(rng.NextDouble() * 0.4 - 0.2),
|
||||
Position = cradle + new Vector2(rng.Next(-4, 5), rng.Next(-4, 5)),
|
||||
Position = a.Position + new Vector2(rng.Next(-3, 4), rng.Next(-3, 4)),
|
||||
AgeDays = 0f,
|
||||
LifeExpectancyDays = 150 + rng.Next(0, 80),
|
||||
};
|
||||
youth.Know.Capacity = 1.0f + (float)rng.NextDouble();
|
||||
Npcs.Add(youth);
|
||||
|
||||
GD.Print($"[Passing] day {Day}: {dead.Name} dies at {dead.AgeDays:0} days " +
|
||||
$"(knowledge {dead.Know.Total:0.00} lost); {youth.Name} comes of age.");
|
||||
child.Know.Capacity = 1.0f + (float)rng.NextDouble();
|
||||
newborns.Add(child);
|
||||
BirthsToday++;
|
||||
pop[a.HomeTown] = (int)townPop + 1; // count the newborn toward capacity
|
||||
GD.Print($"[Birth] day {Day}: {a.Name} & {b.Name} ({a.HomeTown}) → {child.Name}.");
|
||||
}
|
||||
|
||||
Npcs.AddRange(newborns);
|
||||
}
|
||||
|
||||
/// <summary>Where the community's feet actually are — new communal
|
||||
@@ -298,6 +346,7 @@ public partial class GameManager : Node2D
|
||||
foreach (var npc in Npcs)
|
||||
npc.FadeEchoes(0.005f);
|
||||
TickMortality();
|
||||
TickReproduction();
|
||||
StatsLogger.LogDay(this);
|
||||
if (Day % 10 == 0)
|
||||
GD.Print($"[WorldSim] Day {Day} complete.");
|
||||
|
||||
@@ -21,7 +21,7 @@ public static class StatsLogger
|
||||
"avg_fatigue,avg_nourish,avg_health,carried," +
|
||||
"gather_ticks,build_ticks,trade_ticks,rest_ticks,social_ticks," +
|
||||
"shelters_done,ruins,avg_cond,upkeep_stock,burn_per_day,modules,granary_food," +
|
||||
"avg_debris,refusals,incomplete_sites,avg_knowledge,deaths";
|
||||
"avg_debris,refusals,incomplete_sites,avg_knowledge,deaths,births,population";
|
||||
|
||||
private const string NpcHeader =
|
||||
"day,npc,name,soul,town,dominant,gather_ticks,build_ticks,trade_ticks,rest_ticks,social_ticks," +
|
||||
@@ -153,7 +153,9 @@ public static class StatsLogger
|
||||
float knowSum = 0f;
|
||||
foreach (var npc in gm.Npcs) knowSum += npc.Know.Total;
|
||||
row.Append((knowSum / count).ToString("0.000", ci)).Append(',');
|
||||
row.Append(gm.DeathsToday);
|
||||
row.Append(gm.DeathsToday).Append(',');
|
||||
row.Append(gm.BirthsToday).Append(',');
|
||||
row.Append(gm.Npcs.Count);
|
||||
|
||||
File.AppendAllText(_path, row + "\n");
|
||||
|
||||
@@ -194,5 +196,6 @@ public static class StatsLogger
|
||||
TradeSystem.TradesToday = 0;
|
||||
TradeSystem.RefusalsToday = 0;
|
||||
gm.DeathsToday = 0;
|
||||
gm.BirthsToday = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user