2026-11-03 08:52:32 +08:00
|
|
|
|
using System;
|
2026-11-03 08:55:31 +08:00
|
|
|
|
using System.Collections.Generic;
|
2026-11-03 08:52:32 +08:00
|
|
|
|
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, "余额不足时拒绝");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 11:22:27 +08:00
|
|
|
|
Test("挖矿:开局 1 H 每晚约 24.2 信用点;减半在第 90 天", () =>
|
2026-11-03 08:52:32 +08:00
|
|
|
|
{
|
|
|
|
|
|
var net = new Network();
|
|
|
|
|
|
double perH = net.Mine(1) * Tuning.PscStartPrice;
|
2026-11-03 11:22:27 +08:00
|
|
|
|
Check(Near(perH, 24.2, 0.2), $"1 H 每晚约 24.2 信用点(抽成后):{perH:0.00}");
|
2026-11-03 08:52:32 +08: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" };
|
2026-11-03 09:16:46 +08:00
|
|
|
|
Check(u.Hash(ModuleBonus.None) == 5, "满健康 5 H");
|
2026-11-03 08:52:32 +08:00
|
|
|
|
u.Health = 25;
|
2026-11-03 09:16:46 +08:00
|
|
|
|
Check(Near(u.Hash(ModuleBonus.None), 4.0, 1e-9), "健康 25:5 × (0.6 + 0.4 × 0.5) = 4 H");
|
2026-11-03 08:52:32 +08:00
|
|
|
|
u.Health = 0;
|
2026-11-03 09:16:46 +08:00
|
|
|
|
Check(Near(u.Hash(ModuleBonus.None), 3.0, 1e-9), "健康 0:60%");
|
|
|
|
|
|
Check(Near(new Unit { Model = "RZX70" }.Hash(ModuleBonus.From(36, -44, 0)), 5 * 1.09, 1e-9), "一个超频模块(+36 性能):算力 +9%");
|
2026-11-03 08:52:32 +08:00
|
|
|
|
var fake = new Unit { Model = "RZX90", FakeOf = "RZX60" };
|
2026-11-03 09:16:46 +08:00
|
|
|
|
Check(fake.Hash(ModuleBonus.None) == 1.5 && fake.Label.Model == "RZX90", "假卡标着 RZX90,算力按 RZX60");
|
|
|
|
|
|
Check(Near(fake.DeathChance(ModuleBonus.None), new Unit { Model = "RZX90" }.DeathChance(ModuleBonus.None) * 2, 1e-12), "假卡暴毙率 ×2");
|
2026-11-03 08:52:32 +08:00
|
|
|
|
var r = new Unit { Model = "RZX70", Health = 50 };
|
|
|
|
|
|
Check(r.Refurbish() && r.Health == 80 && !r.Refurbish(), "翻新一次 +30,不能再翻新");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 11:20:33 +08:00
|
|
|
|
Test("显卡外观:5 个维度 243 种;真卡没有破绽类取值,假卡至少一处;老化随健康度;价值函数统一", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var rng = new Rng(99);
|
|
|
|
|
|
int n = 6000;
|
|
|
|
|
|
int fakeNoSign = 0, realSign = 0, newWorn = 0, oldWorn = 0, fakeSigns = 0;
|
|
|
|
|
|
var seen = new System.Collections.Generic.HashSet<string>();
|
|
|
|
|
|
for (int i = 0; i < n; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var real = CardLook.Generate(rng, 100, false);
|
|
|
|
|
|
var oldReal = CardLook.Generate(rng, 30, false);
|
|
|
|
|
|
var fake = CardLook.Generate(rng, 60, true);
|
|
|
|
|
|
if (real.HasFakeSign || oldReal.HasFakeSign) realSign++;
|
|
|
|
|
|
if (!fake.HasFakeSign) fakeNoSign++;
|
|
|
|
|
|
fakeSigns += (fake.Bracket == 2 ? 1 : 0) + (fake.Print != 0 ? 1 : 0) + (fake.Sticker == 1 ? 1 : 0);
|
|
|
|
|
|
if (real.Fan > 0 || real.Fingers > 0) newWorn++;
|
|
|
|
|
|
if (oldReal.Fan > 0 || oldReal.Fingers > 0) oldWorn++;
|
|
|
|
|
|
seen.Add($"{fake.Fan}{fake.Fingers}{fake.Bracket}{fake.Print}{fake.Sticker}");
|
|
|
|
|
|
seen.Add($"{oldReal.Fan}{oldReal.Fingers}{oldReal.Bracket}{oldReal.Print}{oldReal.Sticker}");
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(realSign == 0, "真卡从不出现破绽类取值(接口数不对、丝印不对、手写序列号)");
|
|
|
|
|
|
Check(fakeNoSign == 0, "假卡至少有一处破绽");
|
|
|
|
|
|
Check(Near(fakeSigns / (double)n, 1.65, 0.12), $"假卡平均 1.65 处破绽:{fakeSigns / (double)n:0.00}");
|
|
|
|
|
|
Check(oldWorn > newWorn * 1.8, $"低健康度的真卡明显更旧:{oldWorn / (double)n:P0} 对 {newWorn / (double)n:P0}");
|
|
|
|
|
|
Check(seen.Count >= 120, $"抽样见到 {seen.Count} 种不同组合(上限 243)");
|
|
|
|
|
|
Check(Hardware.Specs["RZX50"].Price == 220 && Hardware.Specs["RZX90"].Price == 5000 && Hardware.Specs["RZ7900"].Price == 4000, "价格已抬高到约 1.8 倍");
|
|
|
|
|
|
var u = new Unit { Model = "RZX90", FakeOf = "RZX60", Health = 80 };
|
|
|
|
|
|
Check(CardValue.True(u) == CardValue.Of("RZX60", 80) && CardValue.Quoted(u, 80) == CardValue.Of("RZX90", 80), "真实价值按芯片型号,标称价按标称型号");
|
|
|
|
|
|
Check(CardValue.Quoted(u, 80) > CardValue.True(u) * 8, $"假 RZX 90 的标称价 {CardValue.Quoted(u, 80)} 远高于真值 {CardValue.True(u)}");
|
|
|
|
|
|
Check(CardValue.Of("RZX70", 100) == 800 && CardValue.Of("RZX70", 0) == 200 && CardValue.Of("RZX70", 50) == 500, "健康 100 满价、0 剩 25%、50 为 62.5%");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 10:18:19 +08:00
|
|
|
|
Test("矿架槽位:默认 2 个,投入电子元件逐个升级到最多 5 个;只收下一级需要的;形状随槽数变化", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
int slots = RigSlots.Base, prog = 0;
|
|
|
|
|
|
Check(slots == 2 && RigSlots.Next(2) == ("common_electronic", 4), "默认 2 槽,升到第 3 槽要 4 个普通电子元件");
|
|
|
|
|
|
Check(RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 0 && slots == 2 && prog == 0, "第 3 槽不收稀有元件");
|
|
|
|
|
|
Check(RigSlots.Insert(ref slots, ref prog, "common_electronic", 3) == 3 && slots == 2 && prog == 3, "先投 3 个:进度 3/4");
|
|
|
|
|
|
Check(RigSlots.Insert(ref slots, ref prog, "common_electronic", 3) == 1 && slots == 3 && prog == 0, "再投 3 个只收 1 个,解锁第 3 槽,多的 2 个退回");
|
|
|
|
|
|
Check(RigSlots.Next(3) == ("rare_electronic", 1) && RigSlots.Insert(ref slots, ref prog, "rare_electronic", 5) == 1 && slots == 4, "第 4 槽:1 个稀有元件");
|
|
|
|
|
|
Check(RigSlots.Next(4) == ("rare_electronic", 2) && RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 1 && slots == 4 && prog == 1, "第 5 槽:2 个稀有元件,先投 1 个进度 1/2");
|
|
|
|
|
|
Check(RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 1 && slots == 5, "投满解锁第 5 槽");
|
|
|
|
|
|
Check(RigSlots.Next(5) == null && RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 0, "满 5 槽后不再收");
|
2026-11-03 11:26:11 +08:00
|
|
|
|
Check(RigSlots.Shape(2) == "000000" + "000000" + new string('1', 18) && RigSlots.Shape(5) == new string('0', 30) && RigSlots.Shape(3).Length == 30, "形状:宽 3、共 10 行,前 2 × 槽数行可放");
|
2026-11-03 10:18:19 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 09:16:46 +08:00
|
|
|
|
Test("模块:折算系数 4,算力 +20%、省电 25%、损耗 −30% 封顶;性能加成提高损耗与暴毙", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var perf = ModuleBonus.From(16, 0, 0);
|
|
|
|
|
|
Check(Near(perf.Hash, 1.04, 1e-9) && Near(perf.Wear, 1.04, 1e-9) && Near(perf.Death, 1.04, 1e-9), "一个性能模块(+16):算力 +4%,损耗和暴毙 +4%");
|
|
|
|
|
|
var cores = ModuleBonus.From(200 * 3, -100 * 3, 100 * 3);
|
|
|
|
|
|
Check(Near(cores.Hash, 1.20, 1e-9), $"塞满神经核心,算力也只 +20%:{cores.Hash:0.00}");
|
|
|
|
|
|
Check(Near(cores.Kj, 1.75, 1e-9), $"神经核心的负效率照常增加耗电:×{cores.Kj:0.00}");
|
|
|
|
|
|
Check(Near(cores.Wear, 0.70 * 1.20, 1e-9), "品质封顶 −30%,再乘性能带来的 +20%");
|
|
|
|
|
|
var eco = ModuleBonus.From(-16, 32 * 5, 0);
|
|
|
|
|
|
Check(Near(eco.Kj, 0.75, 1e-9) && Near(eco.Hash, 0.96, 1e-9), "省电封顶 25%;节能模块的负性能照常降低算力");
|
|
|
|
|
|
var none = ModuleBonus.From(0, 0, 0);
|
|
|
|
|
|
Check(none.Hash == 1 && none.Kj == 1 && none.Wear == 1 && none.Death == 1, "没有模块时不变");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 08:52:32 +08:00
|
|
|
|
Test("显卡:累计暴毙概率与模拟器一致(第 50 / 100 / 150 天)", () =>
|
|
|
|
|
|
{
|
2026-11-03 09:16:46 +08:00
|
|
|
|
double[] Run(string model, ModuleBonus b, int n)
|
2026-11-03 08:52:32 +08:00
|
|
|
|
{
|
|
|
|
|
|
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++)
|
|
|
|
|
|
{
|
2026-11-03 09:16:46 +08:00
|
|
|
|
u.Night(b, rng);
|
2026-11-03 08:52:32 +08:00
|
|
|
|
if (u.Dead)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (d <= 50) deadBy[0]++;
|
|
|
|
|
|
if (d <= 100) deadBy[1]++;
|
|
|
|
|
|
deadBy[2]++;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return deadBy.Select(x => (double)x / n).ToArray();
|
|
|
|
|
|
}
|
2026-11-03 09:16:46 +08:00
|
|
|
|
var p = Run("RZX70", ModuleBonus.None, 20000);
|
2026-11-03 08:52:32 +08:00
|
|
|
|
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}");
|
2026-11-03 09:16:46 +08:00
|
|
|
|
var q = Run("RZ6800", ModuleBonus.None, 20000);
|
2026-11-03 08:52:32 +08:00
|
|
|
|
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}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 08:55:31 +08:00
|
|
|
|
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 脱锚时按市价兑出");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 11:40:10 +08:00
|
|
|
|
Test("二手贩子:老实人没有假卡;骗子按标称价九折报价;假卡芯片至少便宜一半;买下后只值真实价值", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var rng = new Rng(7);
|
|
|
|
|
|
int fakes = 0, cards = 0;
|
|
|
|
|
|
for (int i = 0; i < 300; i++)
|
|
|
|
|
|
foreach (var c in UsedDealers.Cards(DealerKind.Honest, 40, rng))
|
|
|
|
|
|
Check(c.FakeOf == null && c.MarkupPct == 0, "老实人的卡是真卡且按真实价值");
|
|
|
|
|
|
for (int i = 0; i < 300; i++)
|
|
|
|
|
|
foreach (var c in UsedDealers.Cards(DealerKind.Scammer, 40, rng))
|
|
|
|
|
|
{
|
|
|
|
|
|
cards++;
|
|
|
|
|
|
if (c.FakeOf == null) continue;
|
|
|
|
|
|
fakes++;
|
|
|
|
|
|
var u = new Unit { Model = c.Model, FakeOf = c.FakeOf, Health = c.Health };
|
|
|
|
|
|
Check(Hardware.Specs[c.FakeOf].Price * 2 <= Hardware.Specs[c.Model].Price, "假卡的芯片至少便宜一半");
|
|
|
|
|
|
long shown = (long)Math.Round(CardValue.True(u) * (1 + c.MarkupPct / 100.0));
|
|
|
|
|
|
Check(Math.Abs(shown - CardValue.Of(c.Model, c.Health) * UsedDealers.ScamDiscount) <= CardValue.True(u) * 0.01 + 1, "柜台价约等于标称价九折");
|
|
|
|
|
|
Check(c.MarkupPct > 0, "假卡的柜台价高于真实价值");
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(fakes > cards * 0.2 && fakes < cards * 0.5, $"骗子的假卡占两成多(入门卡便宜到没法冒充):{fakes}/{cards}");
|
|
|
|
|
|
Check(UsedDealers.Pool(5).All(m => Dealer.Basic.Contains(m)) && UsedDealers.Pool(30).Contains("RZX90"), "型号按天数放开");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 09:27:28 +08:00
|
|
|
|
Test("杰克逊博士:没有矿架必上一个;基础卡每次两张;进阶第 10 天起、旗舰第 30 天起;特供货第 20 天起必有旗舰", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var rng = new Rng(11);
|
|
|
|
|
|
var first = Dealer.Stock(1, false, false, rng);
|
|
|
|
|
|
Check(first.Count(k => k == Dealer.Rig) == 1 && first.Count(k => Dealer.Basic.Contains(k)) == 2 && first.Count == 3, $"第 1 天:矿架 + 两张基础卡:{string.Join(",", first)}");
|
|
|
|
|
|
int adv = 0, flag = 0, rigs = 0, n = 4000;
|
|
|
|
|
|
for (int i = 0; i < n; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var s9 = Dealer.Stock(9, true, false, rng);
|
|
|
|
|
|
Check(!s9.Any(k => Dealer.Advanced.Contains(k) || Dealer.Flagship.Contains(k)), "第 9 天没有进阶与旗舰");
|
|
|
|
|
|
var s40 = Dealer.Stock(40, true, false, rng);
|
|
|
|
|
|
if (s40.Any(k => Dealer.Advanced.Contains(k))) adv++;
|
|
|
|
|
|
if (s40.Any(k => Dealer.Flagship.Contains(k))) flag++;
|
|
|
|
|
|
if (s40.Contains(Dealer.Rig)) rigs++;
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(Near(adv / (double)n, 0.60, 0.03) && Near(flag / (double)n, 0.25, 0.03) && Near(rigs / (double)n, 0.30, 0.03),
|
|
|
|
|
|
$"第 40 天:进阶 {adv / (double)n:P0}(60%),旗舰 {flag / (double)n:P0}(25%),已有矿架时再上 {rigs / (double)n:P0}(30%)");
|
|
|
|
|
|
Check(Dealer.Stock(20, true, true, rng).Any(k => Dealer.Flagship.Contains(k)) && !Dealer.Stock(19, true, true, new Rng(1)).Any(k => Dealer.Flagship.Contains(k)),
|
|
|
|
|
|
"特供货:第 20 天起必有旗舰,第 19 天没有");
|
|
|
|
|
|
Check(Dealer.Basic.Concat(Dealer.Advanced).Concat(Dealer.Flagship).All(m => Hardware.Specs.ContainsKey(m) && !Hardware.Specs[m].Asic), "上架的型号都是登记过的显卡");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 09:11:46 +08:00
|
|
|
|
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), "读回后继续推进,行情与原来完全相同");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 08:52:32 +08:00
|
|
|
|
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
|
|
|
|
|
|
return _failed == 0 ? 0 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-03 09:11:46 +08:00
|
|
|
|
|
|
|
|
|
|
internal sealed class SaveBundle
|
|
|
|
|
|
{
|
|
|
|
|
|
public Market Market;
|
|
|
|
|
|
public Stablecoins Stable;
|
|
|
|
|
|
public Network Network;
|
|
|
|
|
|
public Wallet Hot;
|
|
|
|
|
|
public Exchange Exchange;
|
|
|
|
|
|
public Rng Rng;
|
|
|
|
|
|
}
|