First playable slice, tested in game on the test save: - Network upgrade "Mining Access" (300) delivers a trading terminal, a mining rig and an RZX 50. - The rig is a machine with a battery slot and four card slots; each night it draws its total power from the battery and its cards mine PSCoin. Cards show hashrate, power draw and condition, wear every night and can burn out. All state lives on item tags or in the mod save data. - Trading terminal with wallet, trade (four pairs, deposit credits as CRC, withdraw CRC through the compliance desk) and report pages. - Nightly log lines; exchange opening and halving news on the paper. - English and Chinese text; sprites generated by art/sprites.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
319 lines
17 KiB
C#
319 lines
17 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
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}");
|
||
});
|
||
|
||
Test("市场:价格锚受停电、镇压影响;夜间向锚回归并限制在 0.3–3 倍", () =>
|
||
{
|
||
var m = Market.CreateDefault(1);
|
||
var none = new MarketEvent[0];
|
||
Check(Near(m.Anchor(none), 100, 1e-9) && Near(m.Anchor(new[] { MarketEvent.Blackout }), 110, 1e-9)
|
||
&& Near(m.Anchor(new[] { MarketEvent.Crackdown }), 92, 1e-9), "锚:基准 100,停电 ×1.1,镇压 ×0.92");
|
||
Check(Market.Volatility(new[] { MarketEvent.GangWar }) == Tuning.PscVol * 2, "火并时波动 ×2");
|
||
m.Main.SetPrice(60, Tuning.PoolDepth(1));
|
||
var rng = new Rng(5);
|
||
double sum = 0;
|
||
for (int i = 0; i < 400; i++)
|
||
{
|
||
var mm = Market.CreateDefault(1);
|
||
mm.Main.SetPrice(60, Tuning.PoolDepth(1));
|
||
mm.Night(none, new Rng((ulong)i), 1, a => 1);
|
||
sum += Math.Log(mm.Price);
|
||
}
|
||
double expect = Math.Log(60) + 0.15 * (Math.Log(100) - Math.Log(60));
|
||
Check(Near(sum / 400, expect, 0.01), $"从 60 起一晚平均回到 {Math.Exp(expect):0.0} 附近:{Math.Exp(sum / 400):0.0}");
|
||
var hi = Market.CreateDefault(1);
|
||
hi.Main.SetPrice(1000, Tuning.PoolDepth(1));
|
||
hi.Night(none, rng, 1, a => 1);
|
||
Check(hi.Price <= 300 + 1e-9, $"上限 3 倍:{hi.Price:0.0}");
|
||
});
|
||
|
||
Test("市场:其他交易对每晚被套利拉向参考价,三角价差收敛", () =>
|
||
{
|
||
var m = Market.CreateDefault(1);
|
||
var crt = m.Find(Venue.Dex, Assets.PSC, Assets.CRT);
|
||
crt.SetPrice(130, crt.QuoteReserve);
|
||
for (int d = 1; d <= 5; d++) m.Night(new MarketEvent[0], new Rng((ulong)d), 1, a => 1);
|
||
double gap = Math.Abs(crt.Price / m.Price - 1);
|
||
Check(gap < 0.03, $"五晚后 PSC/CRT 与参考价相差 {gap:P1}");
|
||
var crtCrc = m.Find(Venue.Dex, Assets.CRT, Assets.CRC);
|
||
m.Night(new MarketEvent[0], new Rng(9), 1, a => a == Assets.CRT ? 0.9 : 1);
|
||
Check(crtCrc.Price < 0.98, $"CRT 脱锚时 CRT/CRC 向 0.9 靠拢:{crtCrc.Price:0.000}");
|
||
});
|
||
|
||
Test("稳定币:CRC 脱锚与减记,CRT 脱锚与冻结概率", () =>
|
||
{
|
||
int depegs = 0, haircuts = 0;
|
||
for (int i = 0; i < 2000; i++)
|
||
{
|
||
var s = new Stablecoins();
|
||
var rng = new Rng((ulong)i);
|
||
s.OnEventStart(MarketEvent.RevRaidUpper, 10, rng);
|
||
if (s.Crc < 1)
|
||
{
|
||
depegs++;
|
||
Check(s.Crc >= 0.88 && s.Crc <= 0.93 && s.CrcDepegEnd >= 12 && s.CrcDepegEnd <= 13, "CRC 脱锚深度与持续");
|
||
double f = 1;
|
||
for (int d = 10; d <= s.CrcDepegEnd; d++) f *= s.Night(d, rng, 0);
|
||
if (f < 1) haircuts++;
|
||
Check(s.Crc == 1, "到期恢复");
|
||
}
|
||
}
|
||
Check(Near(depegs / 2000.0, 0.30, 0.03), $"革命军袭击上层时 30% 脱锚:{depegs / 2000.0:P0}");
|
||
Check(Near(haircuts / (double)depegs, 0.15, 0.04), $"其中 15% 减记:{haircuts / (double)depegs:P0}");
|
||
Check(Near(Stablecoins.CrtFreezeChance(0), 0.40, 1e-9) && Stablecoins.CrtFreezeChance(50) == 0 && Near(Stablecoins.CrtFreezeChance(25), 0.2, 1e-9),
|
||
"CRT 冻结概率:声望 0 为 40%,25 为 20%,50 为 0");
|
||
});
|
||
|
||
Test("稳定币:CRA 补贴池告急即为预兆;螺旋三天 0.85 / 0.30 / 0.02", () =>
|
||
{
|
||
var s = new Stablecoins { CraSubsidy = Tuning.CraSubsidy0 * 0.19 };
|
||
Check(s.CraWarning(1) && s.CraRate == Tuning.CraRateLow, "补贴池低于 20%:预兆出现、利率降到 0.2%");
|
||
var t = new Stablecoins { CraSubsidy = 1 };
|
||
var rng = new Rng(3);
|
||
t.Night(1, rng, 100000);
|
||
Check(t.CraWarnDay == 1 && t.CraSpiralDay >= 5 && t.CraSpiralDay <= 8, $"补贴池吃空触发螺旋,提前 4–7 天:第 {t.CraSpiralDay} 天开始");
|
||
var path = new List<double>();
|
||
for (int d = 2; d <= t.CraSpiralDay + 3; d++) { t.Night(d, rng, 0); if (d >= t.CraSpiralDay) path.Add(t.Cra); }
|
||
Check(path.Take(3).SequenceEqual(new[] { 0.85, 0.30, 0.02 }), $"螺旋路径:{string.Join(" / ", path)}");
|
||
Check(!t.CraWarning(t.CraSpiralDay + 1), "螺旋开始后不再算预兆");
|
||
});
|
||
|
||
Test("合规出金:干净小额不查;污点高时重度命中记罪证;轻度命中只没收污点部分;只对收益计税", () =>
|
||
{
|
||
Func<string, double> px = a => 1;
|
||
Check(Exchange.HitChance(500, 0, 0, 200, false) == 0, "干净、低于 5 倍房租:不查");
|
||
Check(Near(Exchange.HitChance(2000, 0, 0, 200, false), 0.2, 1e-9), "单笔为大额阈值 2 倍:20%");
|
||
Check(Near(Exchange.HitChance(1000, 500, 0, 200, false), 0.75, 1e-9), "污点 50%:75%");
|
||
Check(Exchange.HitChance(100000, 100000, 1e6, 200, true) == Tuning.AmlMaxChance, "上限 90%");
|
||
int heavy = 0;
|
||
double crime = 0;
|
||
for (int i = 0; i < 1000; i++)
|
||
{
|
||
var w = new Wallet { Taint = 500, Basis = 1000 };
|
||
w.Add(Assets.CRC, 1000);
|
||
var r = new Exchange().Cashout(w, 1000, px, 1, 200, false, new Rng((ulong)i));
|
||
if (r.Outcome == AmlOutcome.Heavy) { heavy++; crime = r.Crime; Check(Near(r.Received, 998 - 500, 1e-6), "重度:没收污点部分"); }
|
||
else Check(Near(r.Received, 998, 1e-6) && r.Tax == 0, "未命中:全额到账;本金不计税");
|
||
}
|
||
Check(Near(heavy / 1000.0, 0.75, 0.04) && crime == 500, $"污点 50% 时约 75% 重度命中,罪证 = 污点额:{heavy / 1000.0:P0}");
|
||
var light = new Wallet { Taint = 150 };
|
||
light.Add(Assets.CRC, 1000);
|
||
CashoutResult lr = null;
|
||
for (ulong i = 0; lr == null || lr.Outcome == AmlOutcome.Clear; i++)
|
||
{
|
||
var w = new Wallet { Taint = 150 };
|
||
w.Add(Assets.CRC, 1000);
|
||
lr = new Exchange().Cashout(w, 1000, px, 1, 200, false, new Rng(i));
|
||
}
|
||
Check(lr.Outcome == AmlOutcome.Light && lr.Crime == 0 && Near(lr.Confiscated, 150, 1e-6), "污点 15%:轻度命中,没收 150、不记罪证");
|
||
Check(Near(lr.Tax, (998 - 150) * Tuning.TaxRate, 1e-6), "成本为 0 时到账部分全额计税");
|
||
var ex = new Exchange();
|
||
var big = new Wallet();
|
||
big.Add(Assets.CRC, 4000);
|
||
ex.Cashout(big, 2000, px, 1, 200, false, new Rng(1));
|
||
Check(Near(Exchange.HitChance(2000, 0, ex.History.Sum(h => h.Value), 200, false), 0.2 + 0.2 * 1000 / 3000.0, 1e-9),
|
||
"7 天内累计超过 15 倍房租也会提高命中率");
|
||
var dep = new Wallet { Basis = 1000 };
|
||
dep.Add(Assets.CRC, 1000);
|
||
var dr = new Exchange().Cashout(dep, 1000, a => 0.9, 1, 200, false, new Rng(1));
|
||
Check(Near(dr.Received, 900 * (1 - Tuning.ExchangeFee), 1e-6), "CRC 脱锚时按市价兑出");
|
||
});
|
||
|
||
Test("存档:市场、稳定币、全网、钱包、交易所、随机数序列化后读回,之后的结果一致", () =>
|
||
{
|
||
var m = Market.CreateDefault(3);
|
||
var st = new Stablecoins();
|
||
var net = new Network();
|
||
var w = new Wallet { Address = "hot", Taint = 12, Basis = 34 };
|
||
w.Add(Assets.PSC, 5);
|
||
var ex = new Exchange();
|
||
ex.History.Add(new KeyValuePair<int, double>(2, 100));
|
||
var rng = new Rng(77);
|
||
for (int d = 1; d <= 5; d++) { m.Night(new MarketEvent[0], rng, d, a => 1); st.Night(d, rng, 0); net.Night(d, m.Price, 10); }
|
||
var bundle = new SaveBundle { Market = m, Stable = st, Network = net, Hot = w, Exchange = ex, Rng = rng };
|
||
string json = Newtonsoft.Json.JsonConvert.SerializeObject(bundle);
|
||
var back = Newtonsoft.Json.JsonConvert.DeserializeObject<SaveBundle>(json);
|
||
Check(Near(back.Market.Price, m.Price, 1e-9) && back.Market.Pools.Count == m.Pools.Count && back.Market.Pools[1].Id == m.Pools[1].Id, "交易对与价格");
|
||
Check(back.Hot.Get(Assets.PSC) == 5 && back.Hot.Taint == 12 && back.Hot.Basis == 34, "钱包余额、污点、成本");
|
||
Check(back.Exchange.History.Count == 1 && back.Exchange.History[0].Value == 100, "出金记录");
|
||
Check(Near(back.Network.NpcHash, net.NpcHash, 1e-9) && Near(back.Stable.CraSubsidy, st.CraSubsidy, 1e-9), "全网算力、CRA 补贴池");
|
||
for (int d = 6; d <= 10; d++)
|
||
{
|
||
m.Night(new MarketEvent[0], rng, d, a => 1);
|
||
back.Market.Night(new MarketEvent[0], back.Rng, d, a => 1);
|
||
}
|
||
Check(Near(back.Market.Price, m.Price, 1e-9), "读回后继续推进,行情与原来完全相同");
|
||
});
|
||
|
||
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
|
||
return _failed == 0 ? 0 : 1;
|
||
}
|
||
}
|
||
|
||
internal sealed class SaveBundle
|
||
{
|
||
public Market Market;
|
||
public Stablecoins Stable;
|
||
public Network Network;
|
||
public Wallet Hot;
|
||
public Exchange Exchange;
|
||
public Rng Rng;
|
||
}
|