Run 10: organic recovery achieved - minerals are not a commons
Recovery blocker was material, not motivational. Two fixes: (1) harvest floors and commons debris apply only to living, regenerating resources - stone does not regrow, so "leave half" merely stranded the world supply and kept the homeless homeless forever; minerals now quarry to depletion for every soul equally. (2) Matter conservation: idle variety-gathering touches only renewables, and packs may only shed what the land replaces - lossy make-room had milled 900+ stone to dust during demand flapping. Verified over 260 headless days: year-1 winter still wrecks the naive town, it rebuilds in 20 days, famine survivors found the first granaries from hunger memory, and year-2 winter passes with zero ruins on stored reserves. The town has a history now: naive youth, catastrophe, learning, resilient maturity. Remaining tuning in FINDINGS.md: granary sprawl, post-famine nourishment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+19
-3
@@ -78,6 +78,11 @@ public class Npc
|
||||
/// who they are becoming. Indexed by (int)NpcState.</summary>
|
||||
public int[] StateTicksToday = new int[5];
|
||||
|
||||
// --- Diagnostics (read-only views for the stats logger) ---------------
|
||||
public MaterialKind CurrentDemand(GameManager gm) => PickGatherTarget(gm);
|
||||
public int RoughNights => _unshelteredRests;
|
||||
public bool HungerMemory => _hungerMemory;
|
||||
|
||||
public NpcState DominantStateToday()
|
||||
{
|
||||
int best = 0;
|
||||
@@ -116,6 +121,17 @@ public class Npc
|
||||
private static readonly MaterialKind[] GatherKinds =
|
||||
{ MaterialKind.Wood, MaterialKind.Stone, MaterialKind.Clay };
|
||||
|
||||
// Idle variety only touches what regrows. Minerals are mined on purpose
|
||||
// (construction, a roof of your own) — never as a hobby. Run 10 taught
|
||||
// us that idle mining + lossy packs can mill a world's stone to dust.
|
||||
private static readonly MaterialKind[] VarietyKinds =
|
||||
{ MaterialKind.Wood, MaterialKind.Food };
|
||||
|
||||
// What a pack may shed to make room: only matter the land replaces.
|
||||
// Set-down stone is stone destroyed — and the world doesn't make more.
|
||||
private static readonly MaterialKind[] DroppableKinds =
|
||||
{ MaterialKind.Wood, MaterialKind.Food, MaterialKind.Clay };
|
||||
|
||||
/// <summary>
|
||||
/// One simulation tick = one in-game minute (SPEC §9).
|
||||
/// Order per SPEC §10: vitals check → day/night bias → state behavior → fatigue.
|
||||
@@ -253,9 +269,9 @@ public class Npc
|
||||
return MaterialKind.Wood;
|
||||
}
|
||||
|
||||
MaterialKind best = GatherKinds[(int)(Mathf.Abs(PersonalityModifier) * 100f) % GatherKinds.Length];
|
||||
MaterialKind best = VarietyKinds[(int)(Mathf.Abs(PersonalityModifier) * 100f) % VarietyKinds.Length];
|
||||
float least = float.MaxValue;
|
||||
foreach (var kind in GatherKinds)
|
||||
foreach (var kind in VarietyKinds)
|
||||
{
|
||||
Inventory.TryGetValue(kind, out float have);
|
||||
if (have < least) { least = have; best = kind; }
|
||||
@@ -292,7 +308,7 @@ public class Npc
|
||||
Inventory.TryGetValue(demanded, out float haveDemanded);
|
||||
if (haveDemanded >= EffectiveCapacity * 0.25f) return;
|
||||
|
||||
foreach (var kind in GatherKinds)
|
||||
foreach (var kind in DroppableKinds)
|
||||
{
|
||||
if (kind == demanded) continue;
|
||||
if (TotalCarried() <= EffectiveCapacity * 0.5f) break;
|
||||
|
||||
@@ -25,7 +25,7 @@ public static class StatsLogger
|
||||
|
||||
private const string NpcHeader =
|
||||
"day,npc,name,soul,town,dominant,gather_ticks,build_ticks,trade_ticks,rest_ticks,social_ticks," +
|
||||
"nourish,health,debris";
|
||||
"nourish,health,debris,demand,rough_nights,hunger_mem";
|
||||
|
||||
private const string GiftHeader =
|
||||
"day,giver,giver_soul,receiver,receiver_soul,material,amount";
|
||||
@@ -167,7 +167,10 @@ public static class StatsLogger
|
||||
.Append(t[(int)NpcState.Socializing]).Append(',')
|
||||
.Append(npc.Nourishment.ToString("0.000", ci)).Append(',')
|
||||
.Append(npc.Health.ToString("0.0", ci)).Append(',')
|
||||
.Append(npc.Imprint.Total.ToString("0.0000", ci)).Append('\n');
|
||||
.Append(npc.Imprint.Total.ToString("0.0000", ci)).Append(',')
|
||||
.Append(npc.CurrentDemand(gm)).Append(',')
|
||||
.Append(npc.RoughNights).Append(',')
|
||||
.Append(npc.HungerMemory ? 1 : 0).Append('\n');
|
||||
System.Array.Clear(npc.StateTicksToday, 0, npc.StateTicksToday.Length);
|
||||
}
|
||||
File.AppendAllText(_npcPath, npcRows.ToString());
|
||||
|
||||
+23
-10
@@ -82,21 +82,29 @@ public class World
|
||||
{
|
||||
if (node.Kind == MaterialKind.Water) return requested; // unlimited at source
|
||||
|
||||
float floorFraction = npc.Soul.Sustainability * 0.5f; // how much of the node they leave standing
|
||||
// Reciprocity is with the living land: floors and the commons line
|
||||
// apply only to what regrows. Minerals quarry to depletion, for
|
||||
// every soul equally, with no mark on anyone.
|
||||
bool living = node.RegenPerDay > 0f;
|
||||
|
||||
float floorFraction = living ? npc.Soul.Sustainability * 0.5f : 0f;
|
||||
float takeable = Math.Max(0f, node.Amount - node.MaxAmount * floorFraction);
|
||||
float taken = Math.Min(requested, takeable);
|
||||
node.Amount -= taken;
|
||||
HarvestedToday += taken;
|
||||
|
||||
// The moral line sits below any respectful harvest floor (nature souls
|
||||
// stop at 45%) — ordinary sustainable taking never grazes it, while
|
||||
// strip-harvesting (selfish floor ~10%) lives deep inside it.
|
||||
float commons = node.MaxAmount * 0.4f;
|
||||
if (node.Amount < commons)
|
||||
if (living)
|
||||
{
|
||||
float takenBelow = Math.Min(taken, commons - node.Amount);
|
||||
npc.Imprint.OthersRegard =
|
||||
Math.Clamp(npc.Imprint.OthersRegard + takenBelow * 0.004f, 0f, 1f);
|
||||
// The moral line sits below any respectful harvest floor (nature
|
||||
// souls stop at 45%) — ordinary sustainable taking never grazes
|
||||
// it, while strip-harvesting (selfish ~10%) lives deep inside it.
|
||||
float commons = node.MaxAmount * 0.4f;
|
||||
if (node.Amount < commons)
|
||||
{
|
||||
float takenBelow = Math.Min(taken, commons - node.Amount);
|
||||
npc.Imprint.OthersRegard =
|
||||
Math.Clamp(npc.Imprint.OthersRegard + takenBelow * 0.004f, 0f, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
return taken;
|
||||
@@ -146,7 +154,12 @@ public class World
|
||||
foreach (var node in Nodes)
|
||||
{
|
||||
if (node.Kind != kind || node.IsDepleted) continue;
|
||||
if (node.Kind != MaterialKind.Water && node.Amount <= node.MaxAmount * minFraction)
|
||||
// The harvest floor is an ethic of regrowth — it applies only to
|
||||
// living, regenerating things. Leaving half a berry bush lets it
|
||||
// live; leaving half a stone deposit protects nobody, and once
|
||||
// stranded it homelessly, we learned that the hard way (run 9).
|
||||
if (node.Kind != MaterialKind.Water && node.RegenPerDay > 0f &&
|
||||
node.Amount <= node.MaxAmount * minFraction)
|
||||
continue;
|
||||
float d = position.DistanceSquaredTo(node.Cell);
|
||||
if (d < bestDist) { best = node; bestDist = d; }
|
||||
|
||||
Reference in New Issue
Block a user