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;
|
public IReadOnlyList<TownDef> Towns => _towns;
|
||||||
private readonly List<TownDef> _towns = new()
|
private readonly List<TownDef> _towns = new()
|
||||||
{
|
{
|
||||||
new("North", new Vector2(384f, 130f), SoulType.Generational),
|
new("North", new Vector2(576f, 190f), SoulType.Generational),
|
||||||
new("South", new Vector2(384f, 638f), SoulType.Nature),
|
new("South", new Vector2(576f, 962f), SoulType.Nature),
|
||||||
new("West", new Vector2(130f, 384f), SoulType.Materialist),
|
new("West", new Vector2(190f, 576f), SoulType.Materialist),
|
||||||
new("East", new Vector2(638f, 384f), SoulType.Selfish),
|
new("East", new Vector2(962f, 576f), SoulType.Selfish),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static SoulProfile ProfileFor(SoulType t) => t switch
|
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.
|
// passed down like everything else a generation hands forward.
|
||||||
foreach (var node in a.KnownNodes) child.KnownNodes.Add(node);
|
foreach (var node in a.KnownNodes) child.KnownNodes.Add(node);
|
||||||
foreach (var node in b.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);
|
newborns.Add(child);
|
||||||
BirthsToday++;
|
BirthsToday++;
|
||||||
pop[a.HomeTown] = (int)townPop + 1; // count the newborn toward capacity
|
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.
|
// NPC-to-place memory, its own structure per the addendum's open Q.
|
||||||
public readonly HashSet<ResourceNode> KnownNodes = new();
|
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;
|
public const float SensingRadius = 22f;
|
||||||
|
|
||||||
/// <summary>Discover any nodes within sensing range this tick.</summary>
|
/// <summary>Discover any nodes within sensing range this tick.</summary>
|
||||||
@@ -268,6 +284,7 @@ public class Npc
|
|||||||
else if (--_senseTimer <= 0)
|
else if (--_senseTimer <= 0)
|
||||||
{
|
{
|
||||||
SenseNodes(world);
|
SenseNodes(world);
|
||||||
|
SenseStructures(gm);
|
||||||
_senseTimer = 30;
|
_senseTimer = 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,12 +387,17 @@ public class Npc
|
|||||||
if (foodCarried < reserveTarget && (Nourishment < 0.8f || Soul.Accumulation > 0.5f))
|
if (foodCarried < reserveTarget && (Nourishment < 0.8f || Soul.Accumulation > 0.5f))
|
||||||
return MaterialKind.Food;
|
return MaterialKind.Food;
|
||||||
|
|
||||||
// Homeless too long → gather stone for a roof, whatever your soul.
|
// Homeless too long → build a roof, whatever your soul. Even the
|
||||||
// Even the selfish build private shelter; they just won't build it
|
// selfish build private shelter (just never for anyone else). If they
|
||||||
// for anyone else. (Communal souls also queue this below, guarded by
|
// already have one under construction, gather what its NEXT stage
|
||||||
// whether the town already has a shelter under construction.)
|
// 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)
|
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)
|
if (IsCommunal)
|
||||||
{
|
{
|
||||||
@@ -520,8 +542,9 @@ public class Npc
|
|||||||
// a selfish soul goes to raise their own roof.
|
// a selfish soul goes to raise their own roof.
|
||||||
if (!IsCommunal)
|
if (!IsCommunal)
|
||||||
{
|
{
|
||||||
bool hasStone = Inventory.TryGetValue(MaterialKind.Stone, out float s) && s > 0f;
|
// Homeless and carrying what their roof needs next? Go build.
|
||||||
if (_unshelteredRests >= 3 && hasStone) { Enter(NpcState.Building); return; }
|
if (_unshelteredRests >= 3 && CarriesShelterMaterial(gm))
|
||||||
|
{ Enter(NpcState.Building); return; }
|
||||||
Enter(NpcState.Socializing);
|
Enter(NpcState.Socializing);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -612,8 +635,7 @@ public class Npc
|
|||||||
Inventory.TryGetValue(PickGatherTarget(gm), out float demandedHave);
|
Inventory.TryGetValue(PickGatherTarget(gm), out float demandedHave);
|
||||||
if (IsCommunal && demandedHave >= EffectiveCapacity * 0.5f)
|
if (IsCommunal && demandedHave >= EffectiveCapacity * 0.5f)
|
||||||
Enter(NpcState.Building);
|
Enter(NpcState.Building);
|
||||||
else if (!IsCommunal && _unshelteredRests >= 3 &&
|
else if (!IsCommunal && _unshelteredRests >= 3 && CarriesShelterMaterial(gm))
|
||||||
Inventory.TryGetValue(MaterialKind.Stone, out float st) && st >= 10f)
|
|
||||||
Enter(NpcState.Building);
|
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>
|
/// <summary>
|
||||||
/// A self-interested soul raising a roof for itself — private property,
|
/// A self-interested soul raising a roof for itself — private property,
|
||||||
/// not communal service. Contributes to their own in-progress shelter,
|
/// not communal service. Contributes to their own in-progress shelter,
|
||||||
@@ -657,15 +707,10 @@ public class Npc
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void FoundOwnShelter(GameManager gm)
|
private void FoundOwnShelter(GameManager gm)
|
||||||
{
|
{
|
||||||
// Contribute to an unfinished shelter near me (likely my own).
|
// My own unfinished home — found via known-structures memory, so I
|
||||||
Building? mine = null;
|
// return to the foundation I laid however far I wandered for wood,
|
||||||
float best = 900f; // within 30 units
|
// rather than re-founding a new one each trip.
|
||||||
foreach (var b in gm.Buildings)
|
Building? mine = MyUnfinishedShelter(gm);
|
||||||
{
|
|
||||||
if (b.Town != HomeTown || b.IsComplete || b.Kind != BuildingKind.Shelter) continue;
|
|
||||||
float d = Position.DistanceSquaredTo(b.Site);
|
|
||||||
if (d < best) { best = d; mine = b; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mine == null)
|
if (mine == null)
|
||||||
{
|
{
|
||||||
@@ -680,16 +725,30 @@ public class Npc
|
|||||||
mine = Building.NewShelter(spot);
|
mine = Building.NewShelter(spot);
|
||||||
mine.Town = HomeTown;
|
mine.Town = HomeTown;
|
||||||
gm.Buildings.Add(mine);
|
gm.Buildings.Add(mine);
|
||||||
_unshelteredRests = 0;
|
KnownStructures.Add(mine); // you never forget the home you laid
|
||||||
Godot.GD.Print($"[Homestead] day {gm.Day}: {Name} ({HomeTown}) raises a private roof.");
|
// 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);
|
MoveToward(mine.Site);
|
||||||
if (Position.DistanceTo(mine.Site) < ArriveDist)
|
if (Position.DistanceTo(mine.Site) < ArriveDist)
|
||||||
{
|
{
|
||||||
bool delivered = mine.Deliver(this);
|
mine.Deliver(this);
|
||||||
if (!delivered || mine.IsComplete) Enter(NpcState.Socializing);
|
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 = Building.NewShelter(home + jitter);
|
||||||
site.Town = HomeTown;
|
site.Town = HomeTown;
|
||||||
gm.Buildings.Add(site);
|
gm.Buildings.Add(site);
|
||||||
|
KnownStructures.Add(site);
|
||||||
_unshelteredRests = 0;
|
_unshelteredRests = 0;
|
||||||
GD.Print($"[Found] day {gm.Day}: {Name} founds shelter at {site.Site} " +
|
GD.Print($"[Found] day {gm.Day}: {Name} founds shelter at {site.Site} " +
|
||||||
$"(centroid dist {(site.Site - gm.TownCentroid(HomeTown)).Length():0})");
|
$"(centroid dist {(site.Site - gm.TownCentroid(HomeTown)).Length():0})");
|
||||||
@@ -787,6 +847,7 @@ public class Npc
|
|||||||
site = Building.NewGranary(Position + jitter);
|
site = Building.NewGranary(Position + jitter);
|
||||||
site.Town = HomeTown;
|
site.Town = HomeTown;
|
||||||
gm.Buildings.Add(site);
|
gm.Buildings.Add(site);
|
||||||
|
KnownStructures.Add(site);
|
||||||
}
|
}
|
||||||
// Nothing needs your stone: take it back to the quarry rather
|
// Nothing needs your stone: take it back to the quarry rather
|
||||||
// than carry a house on your back forever. Matter is conserved;
|
// than carry a house on your back forever. Matter is conserved;
|
||||||
|
|||||||
+10
-9
@@ -28,7 +28,7 @@ public class ResourceNode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class World
|
public class World
|
||||||
{
|
{
|
||||||
public const int GridSize = 768;
|
public const int GridSize = 1152;
|
||||||
|
|
||||||
public List<ResourceNode> Nodes { get; } = new();
|
public List<ResourceNode> Nodes { get; } = new();
|
||||||
|
|
||||||
@@ -63,14 +63,15 @@ public class World
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Generate()
|
public void Generate()
|
||||||
{
|
{
|
||||||
// Resource counts scale ~9× with the 3× larger world so density holds.
|
// Resource counts scale with area (1152² ≈ 2.25× the 768² world) so
|
||||||
PlaceMany(MaterialKind.Water, count: 40, amount: float.PositiveInfinity, regen: 0f);
|
// density holds as the towns spread to wider cardinal points.
|
||||||
PlaceMany(MaterialKind.Clay, count: 80, amount: 40f, regen: 0.5f);
|
PlaceMany(MaterialKind.Water, count: 90, amount: float.PositiveInfinity, regen: 0f);
|
||||||
PlaceMany(MaterialKind.Wood, count: 480, amount: 30f, regen: 2f);
|
PlaceMany(MaterialKind.Clay, count: 180, amount: 40f, regen: 0.5f);
|
||||||
PlaceMany(MaterialKind.Stone, count: 140, amount: 80f, regen: 0f); // more deposits — housing was stone-capped
|
PlaceMany(MaterialKind.Wood, count: 1080, amount: 30f, regen: 2f);
|
||||||
PlaceMany(MaterialKind.Food, count: 360, amount: 20f, regen: 4f);
|
PlaceMany(MaterialKind.Stone, count: 315, amount: 80f, regen: 0f);
|
||||||
PlaceMany(MaterialKind.Ore, count: 40, amount: 60f, regen: 0f); // metal source (post-MVP tier)
|
PlaceMany(MaterialKind.Food, count: 810, amount: 20f, regen: 4f);
|
||||||
PlaceMany(MaterialKind.Herb, count: 120, amount: 15f, regen: 3f); // medicine / trade good
|
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)
|
private void PlaceMany(MaterialKind kind, int count, float amount, float regen)
|
||||||
|
|||||||
Reference in New Issue
Block a user