Files
Soul_Game/scripts/World.cs
T
mmcghen e661a3f739 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>
2026-07-21 15:23:18 -04:00

170 lines
6.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using Godot;
namespace WorldSim;
public enum MaterialKind { Wood, Stone, Clay, Water, Food }
/// <summary>
/// A harvestable resource node on the grid (SPEC §6).
/// Wood regenerates; stone is static; clay sits near water; water is unlimited.
/// </summary>
public class ResourceNode
{
public MaterialKind Kind;
public Vector2I Cell;
public float Amount;
public float MaxAmount;
public float RegenPerDay; // 0 for stone; water ignores Amount entirely
public bool IsDepleted => Kind != MaterialKind.Water && Amount <= 0f;
}
/// <summary>
/// World generation, resource placement, and the shared resource pool (SPEC §9).
/// Grid is 256×256 (SPEC §9 performance target). No scripted town layout —
/// NPCs are dropped in and the community forms where it forms.
/// </summary>
public class World
{
public const int GridSize = 256;
public List<ResourceNode> Nodes { get; } = new();
/// <summary>Units harvested since the last daily stats snapshot.</summary>
public float HarvestedToday;
private readonly Random _rng;
public World(int seed)
{
_rng = new Random(seed);
}
/// <summary>
/// Place resources: water source tiles first, clay near water,
/// wood in clusters (groves), stone in a few deposits.
/// TODO(Week 1): replace uniform scatter with noise-based clustering.
/// </summary>
public void Generate()
{
PlaceMany(MaterialKind.Water, count: 6, amount: float.PositiveInfinity, regen: 0f);
PlaceMany(MaterialKind.Clay, count: 10, amount: 40f, regen: 0.5f); // TODO: constrain near water
PlaceMany(MaterialKind.Wood, count: 60, amount: 30f, regen: 2f);
PlaceMany(MaterialKind.Stone, count: 12, amount: 80f, regen: 0f);
PlaceMany(MaterialKind.Food, count: 40, amount: 20f, regen: 4f); // South: flora most abundant
}
private void PlaceMany(MaterialKind kind, int count, float amount, float regen)
{
for (int i = 0; i < count; i++)
{
Nodes.Add(new ResourceNode
{
Kind = kind,
Cell = new Vector2I(_rng.Next(GridSize), _rng.Next(GridSize)),
Amount = amount,
MaxAmount = amount,
RegenPerDay = regen,
});
}
}
/// <summary>
/// Harvest respecting the harvester's sustainability weight:
/// a South soul stops at ~50% of the node (SPEC §10) so nodes stay healthy.
/// Taking from below the commons line marks the taker's soul — it is
/// taking from everyone, whether anyone sees it or not.
/// Returns the amount actually taken.
/// </summary>
public float Harvest(ResourceNode node, float requested, Npc npc)
{
if (node.Kind == MaterialKind.Water) return requested; // unlimited at source
// 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;
if (living)
{
// 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;
}
/// <summary>
/// Called once per in-game day by GameManager. Season multipliers throttle
/// regeneration — winter is when the granary earns its clay.
/// </summary>
public void RegenerateDaily(float foodMult = 1f, float woodMult = 1f)
{
bool frost = foodMult < 0.1f; // deep winter
foreach (var node in Nodes)
{
// Frost kills standing food — the wild larder rots on the stem.
// Without this, the sustainability floors act as a huge hidden
// savings account and winter never reaches an actual villager.
if (frost && node.Kind == MaterialKind.Food)
{
node.Amount = Math.Max(node.Amount * 0.93f, node.MaxAmount * 0.15f);
continue;
}
if (node.RegenPerDay <= 0f) continue;
float mult = node.Kind switch
{
MaterialKind.Food => foodMult,
MaterialKind.Wood => woodMult,
_ => 1f,
};
node.Amount = Math.Min(node.MaxAmount, node.Amount + node.RegenPerDay * mult);
}
}
/// <summary>
/// Nearest node this particular soul considers harvestable. What counts
/// as "worth taking" is a moral judgment, not a world fact: a nature
/// soul walks past a node at half strength; a selfish soul strips it to
/// the stem. The commons' savings account only exists for those who
/// honor it.
/// </summary>
public ResourceNode? FindNearest(MaterialKind kind, Vector2 position, float minFraction = 0.5f)
{
ResourceNode? best = null;
float bestDist = float.MaxValue;
foreach (var node in Nodes)
{
if (node.Kind != kind || node.IsDepleted) continue;
// 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; }
}
return best;
}
}