Files
probably-decentralized/tests/Decentralized.Domain.Tests/Program.cs
T
ZHU MINCHAOandClaude Opus 5.5 ce3bed1467 Domain core: seeded RNG, asset registry, wallets, AMM pairs, mining
Pure logic layer with no game references: xoshiro128** RNG with savable
state, asset registry (PSC, CRC, CRT, CRA, SPARK, Ponero), wallets whose
taint and cost basis move with transfers, constant-product pools with
nightly reversion, and mining (Probidia/PMD GPUs, RatDigg ASICs, network
hash response, halving, wear and sudden death). 35 checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 08:52:32 +08:00

165 lines
8.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.Linq;
using Decentralized.Domain;
// 纯逻辑单元测试(不需要游戏)。每个 Test 是一个场景,失败时打印原因,最后返回非零退出码。
// 涉及数值的检查以离线经济模拟的结果为对照。
internal static class Program
{
private static int _failed, _passed;
private static void Check(bool ok, string what)
{
if (ok) _passed++;
else { _failed++; Console.WriteLine(" ✗ " + what); }
}
private static void Test(string name, Action body)
{
Console.WriteLine("• " + name);
try { body(); }
catch (Exception ex) { _failed++; Console.WriteLine(" ✗ 异常: " + ex); }
}
private static bool Near(double a, double b, double tol) => Math.Abs(a - b) <= tol;
private static int Main()
{
Test("随机数:同种子同序列;状态可复制后继续", () =>
{
var a = new Rng(42);
var b = new Rng(42);
Check(Enumerable.Range(0, 100).All(_ => a.NextUInt() == b.NextUInt()), "同种子序列相同");
var c = new Rng { S0 = a.S0, S1 = a.S1, S2 = a.S2, S3 = a.S3 };
Check(Enumerable.Range(0, 100).All(_ => a.NextDouble() == c.NextDouble()), "复制状态后序列相同(存档读档)");
var r = new Rng(7);
double mean = Enumerable.Range(0, 20000).Select(_ => r.NextDouble()).Average();
Check(Near(mean, 0.5, 0.01), $"均匀分布均值约 0.5:{mean:0.000}");
double g = Enumerable.Range(0, 20000).Select(_ => r.Gauss()).Select(x => x * x).Average();
Check(Near(g, 1.0, 0.05), $"正态分布方差约 1:{g:0.000}");
});
Test("资产登记:Ponero 不可追踪、不上合规交易所;稳定币挂钩", () =>
{
Check(!Assets.Get(Assets.Ponero).Traceable && !Assets.Get(Assets.Ponero).Compliant, "Ponero");
Check(Assets.Get(Assets.CRC).Stable && Assets.Get(Assets.CRC).Freezable, "CRC 可冻结");
Check(Assets.Get(Assets.CRA).Stable && !Assets.Get(Assets.CRA).Freezable, "CRA 不可冻结");
Assets.Register(new AssetSpec { Id = "NPC_TEST", Issued = true });
Check(Assets.Get("NPC_TEST")?.Issued == true, "新增币种只是登记一条");
});
Test("交易对:恒定乘积、手续费、500 信用点约 2% 价格冲击", () =>
{
var pool = Pool.Create(Venue.Dex, Assets.PSC, Assets.CRC, 100, 50000, Tuning.DexFee);
Check(Near(pool.Price, 100, 1e-9), "初始价格 100");
double k = pool.BaseReserve * pool.QuoteReserve;
Check(Near(pool.ImpactOfBuy(500), 0.02, 0.001), $"500 信用点买入的冲击约 2%:{pool.ImpactOfBuy(500):P2}");
double got = pool.BuyBase(500);
Check(got > 4.9 && got < 4.97, $"500 信用点买到约 4.93 PSC:{got:0.000}");
Check(pool.BaseReserve * pool.QuoteReserve >= k, "手续费留在池里,k 不减少");
double back = pool.SellBase(got);
Check(back < 500 && back > 496, $"马上卖回只损失手续费:{back:0.00}");
});
Test("交易对:夜间向目标价回归 15%(对数空间)并重设深度", () =>
{
var pool = Pool.Create(Venue.Dex, Assets.PSC, Assets.CRC, 80, 50000, Tuning.DexFee);
pool.Revert(100, Tuning.Revert, 60000);
double expect = Math.Exp(Math.Log(80) + 0.15 * (Math.Log(100) - Math.Log(80)));
Check(Near(pool.Price, expect, 1e-9), $"回归后价格 {pool.Price:0.00},期望 {expect:0.00}");
Check(Near(pool.QuoteReserve, 60000, 1e-9), "深度重设");
});
Test("地址:污点和成本按价值比例随币走;Ponero 换入时污点停止传播", () =>
{
Func<string, double> px = a => a == Assets.PSC ? 100 : 1;
var a = new Wallet { Address = "A", Taint = 300, Basis = 1000 };
a.Add(Assets.CRT, 1000);
var b = new Wallet { Address = "B" };
Wallet.Transfer(a, b, Assets.CRT, 250, px);
Check(Near(b.Taint, 75, 1e-9) && Near(a.Taint, 225, 1e-9), $"转出 1/4,污点带走 1/4:{b.Taint}");
Check(Near(b.Basis, 250, 1e-9), "成本带走 1/4");
var c = new Wallet { Address = "C" };
b.Add(Assets.Ponero, 10);
Wallet.Transfer(b, c, Assets.Ponero, 10, px);
Check(c.Taint == 0, "Ponero 转入的地址不带污点");
bool threw = false;
try { a.Add(Assets.CRT, -10000); } catch (InvalidOperationException) { threw = true; }
Check(threw, "余额不足时拒绝");
});
Test("挖矿:开局 1 H 每晚约 13.4 信用点;减半在第 90 天", () =>
{
var net = new Network();
double perH = net.Mine(1) * Tuning.PscStartPrice;
Check(Near(perH, 13.4, 0.2), $"1 H 每晚约 13.4 信用点(抽成后):{perH:0.00}");
for (int d = 1; d <= 89; d++) net.Night(d, 100, 0);
Check(net.Reward == Tuning.BlockReward, "第 89 天还没减半");
net.Night(90, 100, 0);
Check(net.Reward == Tuning.BlockReward / 2, "第 90 天减半");
});
Test("挖矿:NPC 算力在有利可图时扩张,币价低时关机", () =>
{
var up = new Network();
up.Night(1, 100, 0);
Check(up.NpcHash > Tuning.NpcHash0, $"币价 100:扩张到 {up.NpcHash:0.0}");
var down = new Network();
down.Night(1, 30, 0);
Check(down.NpcHash < Tuning.NpcHash0 && down.NpcHash >= Tuning.NpcHash0 * 0.95, $"币价 30:关机,最多 −5%:{down.NpcHash:0.0}");
});
Test("显卡:型号、显示单位、健康度与算力衰减", () =>
{
Check(Hardware.Specs["RZX70"].DisplayHash == 50 && Hardware.Specs["RZ6800"].DisplayHash == 40, "RZX70 50 MH/s,RZ6800 40 MH/s");
Check(Near(Hardware.Specs["M3"].DisplayHash, 3, 1e-9), "RatDigg M3 3 GH/s");
var u = new Unit { Model = "RZX70" };
Check(u.Hash(false) == 5, "满健康 5 H");
u.Health = 25;
Check(Near(u.Hash(false), 4.0, 1e-9), "健康 25:5 × (0.6 + 0.4 × 0.5) = 4 H");
u.Health = 0;
Check(Near(u.Hash(false), 3.0, 1e-9), "健康 0:60%");
Check(Near(new Unit { Model = "RZX70" }.Hash(true), 6.25, 1e-9), "超频 +25%");
var fake = new Unit { Model = "RZX90", FakeOf = "RZX60" };
Check(fake.Hash(false) == 1.5 && fake.Label.Model == "RZX90", "假卡标着 RZX90,算力按 RZX60");
Check(Near(fake.DeathChance(false), new Unit { Model = "RZX90" }.DeathChance(false) * 2, 1e-12), "假卡暴毙率 ×2");
var r = new Unit { Model = "RZX70", Health = 50 };
Check(r.Refurbish() && r.Health == 80 && !r.Refurbish(), "翻新一次 +30,不能再翻新");
});
Test("显卡:累计暴毙概率与模拟器一致(第 50 / 100 / 150 天)", () =>
{
double[] Run(string model, bool oc, int n)
{
var rng = new Rng(2026);
var deadBy = new int[3];
for (int i = 0; i < n; i++)
{
var u = new Unit { Model = model };
for (int d = 1; d <= 150; d++)
{
u.Night(oc, rng);
if (u.Dead)
{
if (d <= 50) deadBy[0]++;
if (d <= 100) deadBy[1]++;
deadBy[2]++;
break;
}
}
}
return deadBy.Select(x => (double)x / n).ToArray();
}
var p = Run("RZX70", false, 20000);
Check(Near(p[0], 0.11, 0.02) && Near(p[1], 0.53, 0.02) && Near(p[2], 0.84, 0.02),
$"Probidia 11% / 53% / 84%:{p[0]:P0} / {p[1]:P0} / {p[2]:P0}");
var q = Run("RZ6800", false, 20000);
Check(Near(q[0], 0.20, 0.02) && Near(q[1], 0.76, 0.02) && Near(q[2], 0.95, 0.02),
$"PMD 20% / 76% / 95%:{q[0]:P0} / {q[1]:P0} / {q[2]:P0}");
});
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
return _failed == 0 ? 0 : 1;
}
}