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:
@@ -29,10 +29,10 @@ public partial class GameManager : Node2D
|
||||
public IReadOnlyList<TownDef> Towns => _towns;
|
||||
private readonly List<TownDef> _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
|
||||
|
||||
+84
-23
@@ -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;
|
||||
|
||||
+10
-9
@@ -28,7 +28,7 @@ public class ResourceNode
|
||||
/// </summary>
|
||||
public class World
|
||||
{
|
||||
public const int GridSize = 768;
|
||||
public const int GridSize = 1152;
|
||||
|
||||
public List<ResourceNode> Nodes { get; } = new();
|
||||
|
||||
@@ -63,14 +63,15 @@ public class World
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user