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 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 22:11:59 -04:00
parent 9e8ee1d748
commit 0c0c7088f8
3 changed files with 102 additions and 36 deletions
+84 -23
View File
@@ -49,6 +49,22 @@ public class Npc
// NPC-to-place memory, its own structure per the addendum's open Q.
public readonly HashSet<ResourceNode> 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<Building> KnownStructures = new();
/// <summary>Discover buildings within sensing range this tick.</summary>
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;
/// <summary>Discover any nodes within sensing range this tick.</summary>
@@ -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
}
}
/// <summary>True if this homeless builder is carrying enough of the
/// material their private roof needs next (or enough stone to found one).</summary>
private bool CarriesShelterMaterial(GameManager gm)
{
var mine = MyUnfinishedShelter(gm);
var need = mine?.NeededMaterial ?? MaterialKind.Stone;
return Inventory.TryGetValue(need, out float have) && have >= 8f;
}
/// <summary>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.</summary>
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;
}
/// <summary>
/// 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
/// </summary>
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;