8aab1a763b
Run 5 findings: justice arrived (selfish health 31-64 vs nature 100, all 108 refusals theirs) but two pathologies emerged: the whole village maxed others-regard debris because the commons penalty line (50%) sat above the nature harvest floor (45%) and debris had no clearing path; and warmth- weighted asking + near-total gift transfers produced a 2,501-gift winter food-swapping loop. Fixes: commons line lowered to 40% (respectful harvesting never grazes it; strip-harvesting lives deep inside it); gifts capped at 3 with a kept reserve of 5; asking gated on genuine hunger; and giving now heals the giver slightly (-0.003 others-regard vs +0.02 per refusal) — harm stays an order of magnitude easier than healing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
4.6 KiB
C#
112 lines
4.6 KiB
C#
namespace WorldSim;
|
|
|
|
/// <summary>
|
|
/// Trade evaluation and execution (SPEC §9). South MVP: generous, voluntary,
|
|
/// no currency — a gift economy where the ratio favors the receiver.
|
|
///
|
|
/// Soul consequences: a generous completed exchange eases both parties'
|
|
/// imprints (external perception — the world pressed kindly on them).
|
|
/// Later soul types change the ratio logic only; the mechanism is shared.
|
|
/// </summary>
|
|
public static class TradeSystem
|
|
{
|
|
/// <summary>Trades since the last daily stats snapshot.</summary>
|
|
public static int TradesToday;
|
|
|
|
/// <summary>Refusals since the last daily stats snapshot.</summary>
|
|
public static int RefusalsToday;
|
|
|
|
/// <summary>
|
|
/// A hungry villager asks another for food, face to face.
|
|
///
|
|
/// Willingness comes from the soul (or from a warm history with this
|
|
/// particular person). Refusing the hungry while holding food is a
|
|
/// deliberate darkening of another — it marks the refuser's soul
|
|
/// (others-regard debris, per DESIGN.md) and leaves a cold echo in the
|
|
/// asker's memory. The world keeps score the only way it knows how.
|
|
/// </summary>
|
|
public static void RequestGift(Npc giver, Npc asker)
|
|
{
|
|
giver.Inventory.TryGetValue(MaterialKind.Food, out float giverFood);
|
|
bool hasFood = giverFood > 6f; // above their own reserve — enough to share
|
|
bool willing = giver.Soul.Generosity >= 0.3f || giver.GetWarmth(asker.Name) > 0.5f;
|
|
|
|
if (hasFood && willing)
|
|
{
|
|
Execute(giver, asker, MaterialKind.Food);
|
|
return;
|
|
}
|
|
|
|
if (hasFood && !willing)
|
|
{
|
|
// Refused with a full pack — the act that costs.
|
|
RefusalsToday++;
|
|
asker.RecordEcho(giver.Name, -0.25f);
|
|
giver.Imprint.OthersRegard = Godot.Mathf.Clamp(giver.Imprint.OthersRegard + 0.02f, 0f, 1f);
|
|
StatsLogger.LogRefusal(giver, asker);
|
|
}
|
|
// Empty-handed is not refusal: no debris, no echo. You cannot darken
|
|
// your soul with what you do not have.
|
|
}
|
|
|
|
/// <summary>
|
|
/// Need-based: the trigger is a person in need, not an inventory gap.
|
|
/// Food flows to the hungry first; raw material surplus vs. genuine
|
|
/// lack remains as a fallback.
|
|
/// </summary>
|
|
public static bool CanTrade(Npc giver, Npc receiver, out MaterialKind material)
|
|
{
|
|
material = MaterialKind.Food;
|
|
|
|
// The ungenerous do not give. Not from an empty pack — from a full one.
|
|
if (giver.Soul.Generosity < 0.3f) return false;
|
|
|
|
giver.Inventory.TryGetValue(MaterialKind.Food, out float giverFood);
|
|
receiver.Inventory.TryGetValue(MaterialKind.Food, out float receiverFood);
|
|
if (receiver.Nourishment < 0.6f && receiverFood < 2f && giverFood > 3f)
|
|
return true;
|
|
|
|
foreach (var kv in giver.Inventory)
|
|
{
|
|
bool receiverLacks = !receiver.Inventory.TryGetValue(kv.Key, out float has) || has < 2f;
|
|
if (kv.Value > 5f && receiverLacks) { material = kv.Key; return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static void Execute(Npc giver, Npc receiver, MaterialKind material)
|
|
{
|
|
TradesToday++;
|
|
|
|
// Generosity decides how much of the surplus is given — but a gift
|
|
// is a portion, not a pantry: the giver keeps a real reserve (5)
|
|
// and hands over at most 3, so giving doesn't create the next asker.
|
|
float surplus = Godot.Mathf.Max(0f, giver.Inventory[material] - 5f);
|
|
float given = Godot.Mathf.Min(3f, surplus * giver.Soul.Generosity);
|
|
if (given <= 0f) return;
|
|
|
|
giver.Inventory[material] -= given;
|
|
receiver.Inventory.TryGetValue(material, out float has);
|
|
receiver.Inventory[material] = has + given;
|
|
|
|
StatsLogger.LogGift(giver, receiver, material, given);
|
|
|
|
// Warmth lands on both sides — more on the one who was fed.
|
|
receiver.RecordEcho(giver.Name, +0.15f);
|
|
giver.RecordEcho(receiver.Name, +0.05f);
|
|
|
|
// Acts of regard mend regard-blindness — slowly. Harm stays easier
|
|
// than healing by an order of magnitude (refusal +0.02, gift -0.003).
|
|
giver.Imprint.OthersRegard = Godot.Mathf.Max(0f, giver.Imprint.OthersRegard - 0.003f);
|
|
|
|
// The kindness lands on both atmospheres — gently.
|
|
giver.Imprint.ExternalPerception -= 0.002f;
|
|
receiver.Imprint.ExternalPerception -= 0.002f;
|
|
giver.Imprint.Clamp();
|
|
receiver.Imprint.Clamp();
|
|
|
|
// TODO(Week 3): soul-imprint-weighted PARTNER selection (trusted vs
|
|
// tainted history) — the constrained-emergence reference pattern (SPEC §7).
|
|
}
|
|
}
|