From 0c0c7088f8d1e1ba4c990592f9824349f9a7dcce Mon Sep 17 00:00:00 2001 From: Mike McGhen Date: Tue, 21 Jul 2026 22:11:59 -0400 Subject: [PATCH] Known-structures memory + bigger map: fix the East re-founding loop Root cause of the homeless East: FoundOwnShelter searched all buildings by proximity (within 30 units), so a lone selfish builder who wandered off to gather wood for the walls came back, missed their own foundation, and founded a NEW one - 5,900+ abandoned foundations per run, none finished. Fix (per the exploration architecture): structures are known like resource nodes. NPCs gain a KnownStructures set; a builder registers the home they lay and returns to it via known-structures memory however far they roamed, instead of re-founding. Children inherit their family's known buildings. Result: 5,953 -> 31 founding events, East now houses itself with COMPLETED homes and is the best-fed town (0.84), still lowest-knowledge/highest-debris as selfishness should be. Also expanded the world 768 -> 1152 with towns pushed to wider cardinal points and resources scaled to area, giving the four cultures more room and less accidental center overlap. Full 240-day four-town run in 189s. Co-Authored-By: Claude Fable 5 --- scripts/GameManager.cs | 12 +++-- scripts/NPC.cs | 107 ++++++++++++++++++++++++++++++++--------- scripts/World.cs | 19 ++++---- 3 files changed, 102 insertions(+), 36 deletions(-) diff --git a/scripts/GameManager.cs b/scripts/GameManager.cs index 57df8fe..69789a0 100644 --- a/scripts/GameManager.cs +++ b/scripts/GameManager.cs @@ -29,10 +29,10 @@ public partial class GameManager : Node2D public IReadOnlyList Towns => _towns; private readonly List _towns = new() { - new("North", new Vector2(384f, 130f), SoulType.Generational), - new("South", new Vector2(384f, 638f), SoulType.Nature), - new("West", new Vector2(130f, 384f), SoulType.Materialist), - new("East", new Vector2(638f, 384f), SoulType.Selfish), + new("North", new Vector2(576f, 190f), SoulType.Generational), + new("South", new Vector2(576f, 962f), SoulType.Nature), + new("West", new Vector2(190f, 576f), SoulType.Materialist), + new("East", new Vector2(962f, 576f), SoulType.Selfish), }; private static SoulProfile ProfileFor(SoulType t) => t switch @@ -324,6 +324,10 @@ public partial class GameManager : Node2D // 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); + // A child also inherits the family's known buildings — the homes + // and stores of the people who raised them. + foreach (var s in a.KnownStructures) child.KnownStructures.Add(s); + foreach (var s in b.KnownStructures) child.KnownStructures.Add(s); newborns.Add(child); BirthsToday++; pop[a.HomeTown] = (int)townPop + 1; // count the newborn toward capacity diff --git a/scripts/NPC.cs b/scripts/NPC.cs index 5338d27..0bb5b1c 100644 --- a/scripts/NPC.cs +++ b/scripts/NPC.cs @@ -49,6 +49,22 @@ public class Npc // NPC-to-place memory, its own structure per the addendum's open Q. public readonly HashSet KnownNodes = new(); + // Known structures: same principle as known nodes — you can only use or + // return to a building you've discovered or raised yourself. This is what + // lets a lone builder finish their OWN home (they remember the foundation + // they laid) instead of re-founding a new one every time they wander off + // for wood, and it keeps towns insular: you don't beeline to a granary + // across the world you've never seen. + public readonly HashSet KnownStructures = new(); + + /// Discover buildings within sensing range this tick. + public void SenseStructures(GameManager gm) + { + foreach (var b in gm.Buildings) + if (Position.DistanceSquaredTo(b.Site) <= SensingRadius * SensingRadius) + KnownStructures.Add(b); + } + public const float SensingRadius = 22f; /// Discover any nodes within sensing range this tick. @@ -268,6 +284,7 @@ public class Npc else if (--_senseTimer <= 0) { SenseNodes(world); + SenseStructures(gm); _senseTimer = 30; } @@ -370,12 +387,17 @@ public class Npc if (foodCarried < reserveTarget && (Nourishment < 0.8f || Soul.Accumulation > 0.5f)) return MaterialKind.Food; - // Homeless too long → gather stone for a roof, whatever your soul. - // Even the selfish build private shelter; they just won't build it - // for anyone else. (Communal souls also queue this below, guarded by - // whether the town already has a shelter under construction.) + // Homeless too long → build a roof, whatever your soul. Even the + // selfish build private shelter (just never for anyone else). If they + // already have one under construction, gather what its NEXT stage + // needs — a lone builder must supply their own build end to end, or + // it stalls at the walls (this is why East homes never finished). if (!IsCommunal && _unshelteredRests >= 3) - return MaterialKind.Stone; + { + var mine = MyUnfinishedShelter(gm); + if (mine?.NeededMaterial is MaterialKind need) return need; + return MaterialKind.Stone; // none yet — gather the foundation + } if (IsCommunal) { @@ -520,8 +542,9 @@ public class Npc // a selfish soul goes to raise their own roof. if (!IsCommunal) { - bool hasStone = Inventory.TryGetValue(MaterialKind.Stone, out float s) && s > 0f; - if (_unshelteredRests >= 3 && hasStone) { Enter(NpcState.Building); return; } + // Homeless and carrying what their roof needs next? Go build. + if (_unshelteredRests >= 3 && CarriesShelterMaterial(gm)) + { Enter(NpcState.Building); return; } Enter(NpcState.Socializing); return; } @@ -612,8 +635,7 @@ public class Npc Inventory.TryGetValue(PickGatherTarget(gm), out float demandedHave); if (IsCommunal && demandedHave >= EffectiveCapacity * 0.5f) Enter(NpcState.Building); - else if (!IsCommunal && _unshelteredRests >= 3 && - Inventory.TryGetValue(MaterialKind.Stone, out float st) && st >= 10f) + else if (!IsCommunal && _unshelteredRests >= 3 && CarriesShelterMaterial(gm)) Enter(NpcState.Building); } } @@ -648,6 +670,34 @@ public class Npc } } + /// True if this homeless builder is carrying enough of the + /// material their private roof needs next (or enough stone to found one). + private bool CarriesShelterMaterial(GameManager gm) + { + var mine = MyUnfinishedShelter(gm); + var need = mine?.NeededMaterial ?? MaterialKind.Stone; + return Inventory.TryGetValue(need, out float have) && have >= 8f; + } + + /// This soul's own in-progress private shelter, if any — the + /// nearest unfinished shelter they KNOW about in their town. Because it + /// searches known structures (not all buildings by proximity), a builder + /// reliably returns to the foundation they laid instead of forgetting it + /// the moment they wander off and founding another. + private Building? MyUnfinishedShelter(GameManager gm) + { + Building? mine = null; + float best = float.MaxValue; + foreach (var b in KnownStructures) + { + if (b.Town != HomeTown || b.IsComplete || b.IsRuined || + b.Kind != BuildingKind.Shelter) continue; + float d = Position.DistanceSquaredTo(b.Site); + if (d < best) { best = d; mine = b; } + } + return mine; + } + /// /// A self-interested soul raising a roof for itself — private property, /// not communal service. Contributes to their own in-progress shelter, @@ -657,15 +707,10 @@ public class Npc /// private void FoundOwnShelter(GameManager gm) { - // Contribute to an unfinished shelter near me (likely my own). - Building? mine = null; - float best = 900f; // within 30 units - foreach (var b in gm.Buildings) - { - if (b.Town != HomeTown || b.IsComplete || b.Kind != BuildingKind.Shelter) continue; - float d = Position.DistanceSquaredTo(b.Site); - if (d < best) { best = d; mine = b; } - } + // My own unfinished home — found via known-structures memory, so I + // return to the foundation I laid however far I wandered for wood, + // rather than re-founding a new one each trip. + Building? mine = MyUnfinishedShelter(gm); if (mine == null) { @@ -680,16 +725,30 @@ public class Npc mine = Building.NewShelter(spot); mine.Town = HomeTown; gm.Buildings.Add(mine); - _unshelteredRests = 0; - Godot.GD.Print($"[Homestead] day {gm.Day}: {Name} ({HomeTown}) raises a private roof."); + KnownStructures.Add(mine); // you never forget the home you laid + // Don't clear the homeless drive yet — a foundation isn't a home. + // It resets only once the roof is on (below), so the builder keeps + // supplying walls and roof instead of stalling at the foundation. + Godot.GD.Print($"[Homestead] day {gm.Day}: {Name} ({HomeTown}) breaks ground on a private home."); } - Activity = "building my own shelter"; + Activity = "building my own home"; MoveToward(mine.Site); if (Position.DistanceTo(mine.Site) < ArriveDist) { - bool delivered = mine.Deliver(this); - if (!delivered || mine.IsComplete) Enter(NpcState.Socializing); + mine.Deliver(this); + if (mine.IsComplete) + { + _unshelteredRests = 0; // a home at last — drive satisfied + Enter(NpcState.Socializing); + } + else + { + // Foundation down but walls/roof still needed — go fetch the + // next material and come back. A lone builder finishes their + // own house or it never gets finished. + Enter(NpcState.Gathering); + } } } @@ -773,6 +832,7 @@ public class Npc site = Building.NewShelter(home + jitter); site.Town = HomeTown; gm.Buildings.Add(site); + KnownStructures.Add(site); _unshelteredRests = 0; GD.Print($"[Found] day {gm.Day}: {Name} founds shelter at {site.Site} " + $"(centroid dist {(site.Site - gm.TownCentroid(HomeTown)).Length():0})"); @@ -787,6 +847,7 @@ public class Npc site = Building.NewGranary(Position + jitter); site.Town = HomeTown; gm.Buildings.Add(site); + KnownStructures.Add(site); } // Nothing needs your stone: take it back to the quarry rather // than carry a house on your back forever. Matter is conserved; diff --git a/scripts/World.cs b/scripts/World.cs index 38acaeb..2a07b79 100644 --- a/scripts/World.cs +++ b/scripts/World.cs @@ -28,7 +28,7 @@ public class ResourceNode /// public class World { - public const int GridSize = 768; + public const int GridSize = 1152; public List Nodes { get; } = new(); @@ -63,14 +63,15 @@ public class World /// public void Generate() { - // Resource counts scale ~9× with the 3× larger world so density holds. - PlaceMany(MaterialKind.Water, count: 40, amount: float.PositiveInfinity, regen: 0f); - PlaceMany(MaterialKind.Clay, count: 80, amount: 40f, regen: 0.5f); - PlaceMany(MaterialKind.Wood, count: 480, amount: 30f, regen: 2f); - PlaceMany(MaterialKind.Stone, count: 140, amount: 80f, regen: 0f); // more deposits — housing was stone-capped - PlaceMany(MaterialKind.Food, count: 360, amount: 20f, regen: 4f); - PlaceMany(MaterialKind.Ore, count: 40, amount: 60f, regen: 0f); // metal source (post-MVP tier) - PlaceMany(MaterialKind.Herb, count: 120, amount: 15f, regen: 3f); // medicine / trade good + // Resource counts scale with area (1152² ≈ 2.25× the 768² world) so + // density holds as the towns spread to wider cardinal points. + PlaceMany(MaterialKind.Water, count: 90, amount: float.PositiveInfinity, regen: 0f); + PlaceMany(MaterialKind.Clay, count: 180, amount: 40f, regen: 0.5f); + PlaceMany(MaterialKind.Wood, count: 1080, amount: 30f, regen: 2f); + PlaceMany(MaterialKind.Stone, count: 315, amount: 80f, regen: 0f); + PlaceMany(MaterialKind.Food, count: 810, amount: 20f, regen: 4f); + PlaceMany(MaterialKind.Ore, count: 90, amount: 60f, regen: 0f); + PlaceMany(MaterialKind.Herb, count: 270, amount: 15f, regen: 3f); } private void PlaceMany(MaterialKind kind, int count, float amount, float regen)