Files
probably-decentralized/tests/Decentralized.Domain.Tests/Program.cs
T

1203 lines
90 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 System.Linq;
using Decentralized.Domain;
// 纯逻辑单元测试(不需要游戏)。每个 Test 是一个场景,失败时打印原因,最后返回非零退出码。
// 涉及数值的检查以离线经济模拟的结果为对照。
internal static class Program
{
private static int _failed, _passed;
private static double BlackMarketTestRug() => Tuning.BmRugBase * 3;
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 每晚约 26.9 信用点;减半在第 90 天", () =>
{
var net = new Network();
double perH = net.Mine(1) * Tuning.PscStartPrice;
Check(Near(perH, 26.9, 0.2), $"1 H 每晚约 26.9 信用点(抽成后):{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["RTX70"].DisplayHash == 50 && Hardware.Specs["RX80"].DisplayHash == 40, "RTX70 50 MH/s,RX80 40 MH/s");
Check(Near(Hardware.Specs["M3"].DisplayHash, 3, 1e-9), "RatDigg M3 3 GH/s");
var u = new Unit { Model = "RTX70" };
Check(u.Hash(ModuleBonus.None) == 5, "满健康 5 H");
u.Health = 25;
Check(Near(u.Hash(ModuleBonus.None), 4.0, 1e-9), "健康 25:5 × (0.6 + 0.4 × 0.5) = 4 H");
u.Health = 0;
Check(Near(u.Hash(ModuleBonus.None), 3.0, 1e-9), "健康 0:60%");
Check(Near(new Unit { Model = "RTX70" }.Hash(ModuleBonus.From(36, -44, 0)), 5 * 1.09, 1e-9), "一个超频模块(+36 性能):算力 +9%");
var fake = new Unit { Model = "RTX90", FakeOf = "RTX60" };
Check(fake.Hash(ModuleBonus.None) == 1.0 && fake.Label.Model == "RTX90", "假卡标着 RTX90,算力按 RTX60");
Check(Near(fake.DeathChance(ModuleBonus.None), new Unit { Model = "RTX90" }.DeathChance(ModuleBonus.None) * 2, 1e-12), "假卡暴毙率 ×2");
var r = new Unit { Model = "RTX70", Health = 50 };
Check(r.Refurbish() && r.Health == 80 && !r.Refurbish(), "翻新一次 +30,不能再翻新");
});
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, 20 + (i % 40), false);
var fake = CardLook.Generate(rng, 20 + (i % 80), true);
if (real.HasFakeSign || oldReal.HasFakeSign) realSign++;
if (!fake.HasFakeSign) fakeNoSign++;
fakeSigns += (fake.Print != 0 ? 1 : 0) + (fake.Serial != 0 ? 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 > n * 0.1 && fakeNoSign < n * 0.3, $"假卡里一成多的骗子没动丝印和序列号(只剩供电与风扇这一处实物破绽):{fakeNoSign / (double)n:P0}");
Check(Near(fakeSigns / (double)n, 0.55 * 1 + 0.65 * 1, 0.1), $"假卡平均 1.2 处额外破绽(重印丝印、补贴序列号):{fakeSigns / (double)n:0.00}");
Check(oldWorn > newWorn * 1.8, $"低健康度的真卡明显更旧:{oldWorn / (double)n:P0} 对 {newWorn / (double)n:P0}");
Check(seen.Count >= 60, $"抽样见到 {seen.Count} 种不同组合(上限 243)");
Check(Hardware.Specs["RTX60"].Price == 220 && Hardware.Specs["RTX90"].Price == 5000 && Hardware.Specs["RX900"].Price == 4000, "价格已抬高到约 1.8 倍");
var u = new Unit { Model = "RTX90", FakeOf = "RTX60", Health = 80 };
Check(CardValue.True(u) == CardValue.Of("RTX60", 80) && CardValue.Of("RTX90", 80) > CardValue.True(u) * 8, "真实价值按芯片型号,标称型号的价远高于真值");
var worn = new Unit { Model = "RTX70", Health = 100, Look = new CardLook { Fan = 2, Fingers = 2, Bracket = 1, Sticker = 2 } };
Check(CardValue.True(worn) == (long)Math.Round(800 * (1 - 0.12 - 0.12 - 0.02 - 0.06)), "各标签折扣相加进真实价值:风扇缺角 + 金手指氧化 + 划痕 + 封条缺失");
var clean = new Unit { Model = "RTX70", Health = 80 };
var dusty = new Unit { Model = "RTX70", Health = 80, Look = new CardLook { Fan = 2, Fingers = 2 } };
Check(dusty.DeathChance(ModuleBonus.None) > clean.DeathChance(ModuleBonus.None) * 1.5 && Near(CardLook.Decode("20000").WearFactor, 1.3, 1e-9), "风扇缺角、金手指氧化抬高暴毙概率与损耗");
Check(CardValue.Of("RTX70", 100) == 800 && CardValue.Of("RTX70", 0) == 200 && CardValue.Of("RTX70", 50) == 500, "健康 100 满价、0 剩 25%、50 为 62.5%");
});
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 槽后不再收");
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 × 槽数行可放");
});
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, "没有模块时不变");
});
Test("显卡:累计暴毙概率与模拟器一致(第 50 / 100 / 150 天)", () =>
{
double[] Run(string model, ModuleBonus b, 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(b, 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("RTX70", ModuleBonus.None, 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("RX80", ModuleBonus.None, 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 w = new Wallet { Address = "x" };
w.Add("PSC", 10); // 挖出的 10 个,无买入记录
Check(w.AvgBuy("PSC") == null, "没有买入记录时没有均价");
w.Add("PSC", 5); w.RecordBuy("PSC", 5, 400);
w.Add("PSC", 5); w.RecordBuy("PSC", 5, 600);
Check(Near(w.AvgBuy("PSC").Value, 100, 1e-9), "买 5 个花 400 + 买 5 个花 600:均价 100");
w.Add("PSC", -10); // 卖掉一半(20 个里的 10 个)
Check(Near(w.AvgBuy("PSC").Value, 100, 1e-9) && Near(w.BuyQty["PSC"], 5, 1e-9), "卖出后均价不变,买入记录按比例减少");
w.Add("PSC", -10);
Check(w.AvgBuy("PSC") == null, "清仓后记录清除");
});
Test("市场:减半抬高价格锚(随机幅度,逐日靠拢),减半前一周有预期行情", () =>
{
var m = new Market();
Check(Near(m.SupplyTarget(10), 1.0, 1e-9), "减半前很远:目标因子 1");
double before = m.SupplyTarget(89), start = m.SupplyTarget(83);
Check(start > 1.0 && before > start && before <= 1 + Tuning.HalvingRamp + 1e-9, $"减半前 7 天逐日抬高,最多 +8%:{start:0.000} → {before:0.000}");
double lo = 1e9, hi = 0;
for (int seed = 1; seed <= 200; seed++)
{
var x = new Market(); x.OnHalving(new Rng((ulong)seed));
lo = Math.Min(lo, x.HalvingLog); hi = Math.Max(hi, x.HalvingLog);
}
Check(lo >= Tuning.HalvingPassMin && hi <= Tuning.HalvingPassMax && hi - lo > 0.5, $"传导指数在区间内且确有变化:{lo:0.00}–{hi:0.00}");
m.OnHalving(new Rng(7UL));
double after = m.SupplyTarget(91);
Check(Near(after, Math.Pow(2, m.HalvingLog), 1e-9), $"减半后目标因子 2^{m.HalvingLog:0.00} = {after:0.000}");
var n = new Market { HalvingLog = m.HalvingLog };
for (int i = 0; i < 7; i++) n.UpdateSupply(91 + i);
Check(n.Supply > 1.0 && n.Supply < after, "逐晚靠拢目标,不一次跳到位");
for (int i = 0; i < 40; i++) n.UpdateSupply(100 + i % 5);
Check(Near(n.Supply, after, 0.02), "几十晚后基本到位");
Check(n.Anchor(new MarketEvent[0]) > Tuning.PscStartPrice * 1.05, "锚随之上移");
});
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 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, "老实人的卡是真卡");
for (int i = 0; i < 300; i++)
foreach (var c in UsedDealers.Cards(DealerKind.Scammer, 40, rng))
{
cards++;
if (c.FakeOf == null) continue;
fakes++;
Check(Hardware.Specs[c.FakeOf].Price * 2 <= Hardware.Specs[c.Model].Price, "假卡的芯片至少便宜一半");
}
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("RTX90"), "型号按天数放开");
// 标签:假 RTX 70(芯片 RTX 50)、健康 60、风扇缺角、金手指氧化、丝印印着真实型号
var u = new Unit { Model = "RTX70", FakeOf = "RTX60", Health = 60, Look = new CardLook { Fan = 2, Fingers = 2, Print = 1 } };
var claim = CardLabels.DealerClaim(u, DealerKind.Scammer, new Rng(3));
Check(claim.Model == "RTX70" && claim.Fan == 0 && claim.Fingers == 0 && claim.Print == 0, "骗子的说法:标称型号,外观全是没问题");
long hidden = CardLabels.Value(u, claim, 0), real = CardValue.True(u);
Check(hidden == CardValue.Of("RTX70", 60) && real == (long)Math.Round(CardValue.Of("RTX60", 60) * 0.76), "没指出时按说法算(标称型号满价),真实价值按芯片和外观算");
int ex = 0;
Check(CardLabels.Point(u, claim, CardLabels.RowFan, ref ex) == 1 && CardLabels.Value(u, claim, ex) == (long)Math.Round(CardValue.Of("RTX70", 60) * 0.88), "指出风扇缺角:扣 12%");
Check(CardLabels.Point(u, claim, CardLabels.RowPrint, ref ex) == 2 && (ex & CardLabels.RowSpec) != 0, "指出丝印印着真实型号:拆穿假货,型号标签一并拆穿");
Check(CardLabels.Point(u, claim, CardLabels.RowFingers, ref ex) == 1 && CardLabels.Value(u, claim, ex) == real, "全部指出之后价格回到真实价值");
int ex2 = 0;
var honestUnit = new Unit { Model = "RTX70", Health = 60, Look = new CardLook { Fan = 1 } };
Check(CardLabels.Point(honestUnit, CardLabels.DealerClaim(honestUnit, DealerKind.Honest, new Rng(1)), CardLabels.RowFan, ref ex2) == 0, "老实人说的是实话,指出去没用");
Check(CardLabels.Lies(u, claim, 0) == (CardLabels.RowSpec | CardLabels.RowFan | CardLabels.RowFingers), "骗子隐瞒了型号、叶片、金手指");
Check(CardLabels.Lies(u, claim, ex) == 0, "指出之后不再算隐瞒");
Check(Claim.Decode(claim.Encode()).Encode() == claim.Encode(), "说法能存取");
});
Test("序列号与供电风扇:四档实物特征;序列号按牌子和档次鉴定;指出供电风扇自动改回真实型号", () =>
{
Check(Hardware.FanCount.SequenceEqual(new[] { 1, 2, 3, 3 }) && Hardware.PowerPins.SequenceEqual(new[] { "6", "8", "8", "8+8" }), "低端 6 针单风扇、中端 8 针双风扇、高端 8 针三风扇、旗舰双 8 针三风扇");
Check(Hardware.Specs.Values.Where(x => !x.Asic && !x.Scrap).All(x => Hardware.Specs.Values.Count(y => !y.Asic && !y.Scrap && y.Brand == x.Brand && y.Tier == x.Tier) == 1), "每个牌子每个档次只有一个型号(废片不算)");
var rngF = new Rng(99UL); bool lowFake = false, midScrap = false;
for (int i = 0; i < 400; i++)
{
foreach (var c in UsedDealers.Cards(DealerKind.Scammer, 60, rngF))
{
if (c.FakeOf == null) continue;
if (Hardware.Specs[c.Model].Tier == 0 && Hardware.Specs[c.FakeOf].Scrap) lowFake = true;
if (Hardware.Specs[c.Model].Tier > 0 && Hardware.Specs[c.FakeOf].Scrap) midScrap = true;
}
}
Check(lowFake && !midScrap, "低档次的卡也会被冒充(背后是废片),中高档的假卡不用废片");
var real = new Unit { Model = "RX80", Health = 100, Look = new CardLook { Seed = 7 } };
var t = CardLabels.SerialText(real);
Check(t.StartsWith("MD2-") && CardLabels.Factories("PMD").Contains(int.Parse(t.Substring(4, 2))) && t.Length == 11, $"真卡的序列号:牌子 + 档次码 + 合法工厂码 + 四位流水:{t}");
var forged = new Unit { Model = "RTX90", FakeOf = "RTX60", Health = 100, Look = new CardLook { Seed = 7, Serial = 1 } };
var f = CardLabels.SerialText(forged);
Check(f.StartsWith("PB4-") && (!CardLabels.Factories("Probidia").Contains(int.Parse(f.Substring(4, 2))) || f.Length == 10), $"补贴的序列号:档次码对,工厂码不合法或位数不对:{f}");
var original = new Unit { Model = "RTX90", FakeOf = "RTX60", Health = 100, Look = new CardLook { Seed = 7, Serial = 2 } };
Check(CardLabels.SerialText(original).StartsWith("PB1-"), "贴的是原芯片的序列号:档次码是真实芯片的档次");
var claim = CardLabels.DealerClaim(original, DealerKind.Scammer, new Rng(1));
int ex = 0;
Check(CardLabels.Point(original, claim, CardLabels.RowBuild, ref ex) == 2 && (ex & CardLabels.RowSpec) != 0 && CardLabels.Value(original, claim, ex) == CardValue.True(original), "指出供电与风扇:型号自动改回真实型号,价格回到真实价值");
});
Test("外观随损耗重算:越旧外观越差;种子固定;算力受风扇与金手指影响", () =>
{
var rng = new Rng(5);
int worse = 0, total = 400;
for (int i = 0; i < total; i++)
{
var look = CardLook.Generate(rng, 100, false);
var a = look.Appearance(100); var b = look.Appearance(20);
Check(b.Fan >= a.Fan && b.Fingers >= a.Fingers && b.Bracket >= a.Bracket, "健康度下降时外观只会变差");
if (b.Fan + b.Fingers + b.Bracket > a.Fan + a.Fingers + a.Bracket) worse++;
Check(look.Appearance(60).Encode() == look.Appearance(60).Encode(), "同一健康度外观固定");
}
Check(worse > total * 0.9, $"健康度从 100 掉到 20,九成以上的卡外观变差:{worse}/{total}");
var fake = CardLook.Generate(rng, 80, true);
Check(fake.HasFakeSign && fake.Appearance(10).HasFakeSign, "假卡的破绽不随健康度变化");
var clean = new Unit { Model = "RTX70", Health = 100 };
var bad = new Unit { Model = "RTX70", Health = 100, Look = new CardLook { Fan = 2, Fingers = 2 } };
Check(Near(bad.Hash(ModuleBonus.None) / clean.Hash(ModuleBonus.None), 0.92 * 0.94, 1e-9), "风扇缺角 + 金手指氧化:算力 ×0.92 ×0.94");
});
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), "上架的型号都是登记过的显卡");
});
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), "读回后继续推进,行情与原来完全相同");
});
Test("用币付款:派系对应的币、按币价换算、污点与成本", () =>
{
var w = new Wallet { Address = "t" };
Check(CoinPayment.AssetFor("FACTION_BLACK_MARKET") == Assets.CRT && CoinPayment.AssetFor("FACTION_SECURITY") == null, "派系对应的币");
double q = CoinPayment.Receive(w, Assets.CRT, 200, 0.5, 40);
Check(Near(q, 400 * (1 + Tuning.CoinPayPremium), 1e-9) && Near(w.Get(Assets.CRT), q, 1e-9), "按币价换算数量,含溢价");
Check(Near(w.Taint, 80, 1e-9) && Near(w.Basis, 200, 1e-9), "污点 = 货值 × 热度%,成本 = 货值(溢价不带污点)");
Check(CoinPayment.Receive(w, Assets.CRC, 0, 1, 10) == 0, "零金额不收");
});
Test("混币器:按价值比例带走污点、轮数即天数、手续费与清洗、匿名集、制裁标记", () =>
{
Func<string, double> px = a => a == Assets.PSC ? 10 : 1;
var w = new Wallet { Address = "t" };
w.Add(Assets.CRC, 1000); w.Add(Assets.PSC, 100); // 总价值 2000
w.Taint = 400; w.Basis = 1000;
var m = new Mixer();
Check(m.Deposit(w, Assets.CRC, 1000, 9, 1, px) == null && m.Deposit(w, Assets.CRC, 50, 1, 1, px) == null, "轮数超限、低于最小额不收");
var d = m.Deposit(w, Assets.CRC, 1000, 2, 1, px);
Check(d != null && Near(d.Taint, 200, 1e-9) && Near(w.Taint, 200, 1e-9) && Near(w.Get(Assets.CRC), 0, 1e-9), "存入 1000 / 2000:带走一半污点");
var rng = new Rng(3);
Check(m.Night(w, 2, px, rng).Count == 0, "未到期不提回");
var res = m.Night(w, 3, px, rng);
Check(res.Count == 1 && !res[0].Rugged, "到期提回");
double rate = Mixer.RoundRate(1000);
Check(Near(w.Get(Assets.CRC), 1000 * (1 - Tuning.MixerFee) * (1 - Tuning.MixerFee), 1e-6) && Near(res[0].TaintAfter, 200 * (1 - rate) * (1 - rate), 1e-6), "两轮:手续费与污点按轮数递减");
Check(Mixer.RoundRate(100000) < Mixer.RoundRate(1000) / 3, "存入越大匿名集占比越大,清洗越差");
Check(m.RecentlyMixed(3) && !m.RecentlyMixed(3 + Tuning.AmlWindowDays + 1), "提回后一段时间内出金按混币来源加重");
m.OnEvent(MarketEvent.Crackdown, 5, new Rng(1));
int hit = 0; for (int i = 0; i < 400; i++) { var t = new Mixer(); t.OnEvent(MarketEvent.Crackdown, 1, new Rng((ulong)i + 1)); if (t.Sanctioned(2)) hit++; }
Check(hit > 150 && hit < 250, $"镇压时约一半概率被制裁:{hit}/400");
var w2 = new Wallet { Address = "u" }; w2.Add(Assets.CRC, 500);
var m2 = new Mixer { SanctionedUntil = 10 };
m2.Deposit(w2, Assets.CRC, 500, 1, 1, px);
m2.Night(w2, 2, px, new Rng(5));
Check(w2.Flagged, "制裁期间提出的币被标记");
});
Test("CRT 冻结:冻一半、可动用余额减少、到期解冻;声望越高越不容易被冻", () =>
{
var w = new Wallet { Address = "f" };
w.Add(Assets.CRT, 1000);
double fz = w.Freeze(Assets.CRT, Tuning.CrtFreezeShare, 10);
Check(Near(fz, 500, 1e-9) && Near(w.Avail(Assets.CRT), 500, 1e-9) && Near(w.Get(Assets.CRT), 1000, 1e-9), "冻结一半:余额不变,可动用 500");
Check(Near(w.Freeze(Assets.CRT, 1, 12), 500, 1e-9) && Near(w.Avail(Assets.CRT), 0, 1e-9), "再冻一次只冻可动用部分");
Check(w.Thaw(9).Count == 0 && w.Thaw(12).Count == 1 && Near(w.Avail(Assets.CRT), 1000, 1e-9), "到期(取较晚的日子)全部解冻");
Check(new Mixer().Deposit(w, Assets.CRT, 2000, 1, 1, a => 1) == null, "余额不足不能存入混币器");
int low = 0, high = 0;
for (int i = 0; i < 2000; i++) { if (Stablecoins.CrtFrozenByCrackdown(0, new Rng((ulong)i + 1))) low++; if (Stablecoins.CrtFrozenByCrackdown(50, new Rng((ulong)i + 1))) high++; }
Check(low > 700 && low < 900 && high == 0, $"声望 0 约 40% 被冻:{low}/2000;声望 50 不会被冻:{high}");
});
Test("Web3 黑市", () =>
{
Func<string, double> price = a => a == Assets.Ponero ? 50 : a == Assets.PSC ? 100 : 1;
var bm = new BlackMarket();
Check(!bm.Refresh(10, 5, new Rng(1), id => 100) && bm.Offers.Count == 0, "声望不够不开放");
Check(bm.Refresh(10, 45, new Rng(1), id => 100) && bm.Offers.Count > 0 && bm.Offers.Where(o => o.Kind == GoodKind.Contraband).All(o => o.Level <= 2 && o.Price == 130), "声望 45 开放中档,溢价 30%");
Check(!bm.Refresh(12, 45, new Rng(2), id => 100) && bm.Refresh(17, 45, new Rng(2), id => 100), "每周刷新");
var w = new Wallet { Address = "t" };
w.Add(Assets.PSC, 10); w.Add(Assets.CRT, 1000); w.Taint = 400; w.Basis = 800; // 总价值 2000
var offer = new BmOffer { Item = "x", Qty = 1, Price = 500, Level = 1 };
bm.Offers.Add(offer);
Check(!bm.Buy(offer, w, Assets.CRC, price), "不收 CRC");
Check(bm.Buy(offer, w, Assets.CRT, price) && Near(w.Get(Assets.CRT), 500, 1e-9) && Near(w.Taint, 300, 1e-6) && Near(w.Basis, 600, 1e-6) && !bm.Offers.Contains(offer), "付币:污点与成本按比例留给卖家,卖完下架");
w = new Wallet { Address = "t" };
w.Add(Assets.PSC, 10); w.Taint = 500; w.Basis = 1000; // 总价值 1000,PSC 价 100
double got = bm.ToPonero(w, Assets.PSC, 5, 20, price); // 价值 500
Check(Near(got, 500 * (1 - Tuning.PoneroFee) / 50, 1e-9) && Near(w.Taint, 250, 1e-6) && Near(w.Get(Assets.PSC), 5, 1e-9), "兑入 Ponero:扣手续费,这部分污点留在原地");
Check(bm.ToPonero(w, Assets.PSC, 5.1, 20, price) < 0, "超过每日额度不能兑");
Check(bm.ToPonero(w, Assets.PSC, 5, 21, price) >= 0, "第二天额度恢复");
double back = bm.FromPonero(w, Assets.CRT, 10, 22, price);
Check(Near(back, 10 * 50 * (1 - Tuning.PoneroFee), 1e-9) && Near(w.Get(Assets.CRT), back, 1e-9), "兑出 Ponero");
});
Test("黑市订单", () =>
{
Func<string, double> price = a => a == Assets.CRT ? 1 : 100;
Func<string, long> value = id => 200;
var bm = new BlackMarket();
bm.RefreshOrders(10, 75, new Rng(5), value);
Check(bm.Sellers.Count >= Tuning.BmMerchantCount && bm.Catalog.Count > 0 && bm.Catalog.All(l => l.Qty > 0 && l.Price >= 1) && bm.Catalog.Where(l => l.Kind != GoodKind.Gpu && bm.Sellers[l.Seller].Type != MerchantType.Personal).All(l => l.Price >= 200 * bm.Sellers[l.Seller].Factor * 0.97 - 1 && l.Price <= 200 * bm.Sellers[l.Seller].Factor * 1.03 + 1), "目录:商家与单价(基础价 × 商家系数)");
Check(BlackMarket.Pay(170, PayMode.Escrow) == 170 + 9 && BlackMarket.Pay(170, PayMode.Advance) == 153 && BlackMarket.OrderTotal(100, 5) == 475 && BlackMarket.OrderTotal(100, 10) == 900 && BlackMarket.OrderTotal(100, 4) == 400, "托管多付 5%,提前放款打九折");
var good = new BmSeller { Name = "g", Trades = 100, Rating = 95 };
var bad = new BmSeller { Name = "b", Trades = 100, Rating = 95, Scam = true };
bm.Sellers.Clear(); bm.Sellers.Add(good); bm.Sellers.Add(bad);
var lg = new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 1000 };
var lb = new BmListing { Qty = 99, Seller = 1, Item = "x", Price = 1000 };
var w = new Wallet { Address = "t" };
w.Add(Assets.CRT, 10000); w.Taint = 4000; w.Basis = 5000;
var o = bm.Place(lb, 1, PayMode.Escrow, w, Assets.CRT, 10, price);
Check(o != null && Near(w.Get(Assets.CRT), 10000 - 1050, 1e-9) && w.Taint < 4000 && bm.Place(lg, 1, PayMode.Escrow, w, Assets.CRC, 10, price) == null, "托管下单:扣币、污点留在订单上;不收 CRC");
double tBefore = 4000;
var ev = bm.Night(13, false, w, new Rng(1));
Check(ev.Any(e => e.Kind == "scam_refund") && Near(w.Get(Assets.CRT), 10000, 1e-6) && Near(w.Taint, tBefore, 1e-6) && Near(w.Basis, 5000, 1e-6), "骗子卖家 + 托管:退款,污点与成本一并还回(不能借退款洗钱)");
var o2 = bm.Place(lb, 1, PayMode.Advance, w, Assets.CRT, 20, price);
var ev2 = bm.Night(23, false, w, new Rng(2));
Check(o2.State == OrderState.Lost && Near(w.Get(Assets.CRT), 10000 - 900, 1e-6), "骗子卖家 + 提前放款:钱没了");
var o3 = bm.Place(lg, 1, PayMode.Escrow, w, Assets.CRT, 30, price);
Check(bm.Night(31, false, w, new Rng(3)).Count == 0 && o3.State == OrderState.Pending, "没到期不动");
int ready = 0, lost = 0;
for (int i = 0; i < 400; i++)
{
var t = new BlackMarket(); t.Sellers.Add(good);
var ww = new Wallet(); ww.Add(Assets.CRT, 5000);
var oo = t.Place(new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 100 }, 1, PayMode.Escrow, ww, Assets.CRT, 1, price);
t.Night(oo.DueDay, false, ww, new Rng((ulong)i + 7));
if (oo.State == OrderState.Ready) ready++; else lost++;
}
Check(ready > 340 && lost > 10, $"上门:约 {Tuning.BmInterceptDoor:P0} 被截获(跑路另算):到货 {ready},丢失 {lost}");
var c = new BlackMarket(); c.Sellers.Add(good);
var cw = new Wallet(); cw.Add(Assets.CRT, 5000);
var co = c.Place(new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 100 }, 1, PayMode.Escrow, cw, Assets.CRT, 1, price);
c.Night(co.DueDay, true, cw, new Rng(1));
Check(co.State == OrderState.Pending && co.DueDay == 5, "镇压期间到期顺延一天");
// 蜜罐期间下单会被追查;受控交付签收记罪证、拒签不记
var h = new BlackMarket(); h.Sellers.Add(good); h.HoneypotUntil = 12;
var hw = new Wallet(); hw.Add(Assets.CRT, 5000);
var ho = h.Place(new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 100 }, 1, PayMode.Escrow, hw, Assets.CRT, 10, price);
Check(h.Traps.Count == 1 && h.Traps[0].Day == 10 + Tuning.BmTrapDelay, "蜜罐期间下单:记一笔追查");
Check(h.Night(h.Traps[0].Day, false, hw, new Rng(3)).Any(e => e.Kind == "trap" && Near(e.Amount, ho.Price * Tuning.BmTrapShare, 1e-9)) && h.Traps.Count == 0, "追查到期:产生罪证事件");
int ctrl = 0;
for (int i = 0; i < 600; i++)
{
var t2 = new BlackMarket(); t2.Sellers.Add(good);
var w2 = new Wallet(); w2.Add(Assets.CRT, 5000);
var o2b = t2.Place(new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 100 }, 1, PayMode.Escrow, w2, Assets.CRT, 1, price);
t2.Night(o2b.DueDay, false, w2, new Rng((ulong)i + 99), 0);
if (o2b.Controlled) ctrl++;
}
Check(ctrl > 5 && ctrl < 50, $"被截获里一部分变成受控交付:{ctrl}/600");
var co2 = new BmOrder { Item = "x", Price = 150, State = OrderState.Ready, Controlled = true };
var cm = new BlackMarket();
Check(Near(cm.Sign(co2, true), 150, 1e-9) && co2.State == OrderState.Lost && co2.Why == "seized" && Near(cm.Sign(new BmOrder { Price = 50 }, false), 0, 1e-9), "签收记罪证(金额)并查封,拒签不记");
Check(BlackMarket.HoneypotChance(100) > BlackMarket.HoneypotChance(0) && BlackMarket.HoneypotChance(100000) <= Tuning.BmHoneypotMax, "治安声望越高越容易被接管,有封顶");
// 没收到货的订单不会被追查:骗子退款后蜜罐追查取消
var hb = new BlackMarket(); hb.Sellers.Add(bad); hb.HoneypotUntil = 50;
var hw2 = new Wallet(); hw2.Add(Assets.CRT, 5000);
var hl = new BmListing { Seller = 0, Item = "x", Price = 100, Qty = 9 };
var ho2 = hb.Place(hl, 1, PayMode.Escrow, hw2, Assets.CRT, 10, price);
Check(hb.Traps.Count == 1 && ho2.Id > 0, "蜜罐期间下单:先挂追查");
hb.Night(ho2.DueDay, false, hw2, new Rng(4));
Check(ho2.State == OrderState.Refunded && hb.Traps.Count == 0, "骗子卖家退款后追查取消(没拿到货不记罪证)");
// 跑路:托管资金多则概率高;跑路后关闭
var r = new BlackMarket(); r.Sellers.Add(good);
var rw = new Wallet(); rw.Add(Assets.CRT, 1000000);
for (int i = 0; i < 20; i++) r.Place(new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 10000 }, 1, PayMode.Escrow, rw, Assets.CRT, 1, price);
Check(r.RugChance > BlackMarketTestRug(), "托管资金越多越容易跑路");
r.ClosedUntil = 40;
Check(r.Closed(40) && !r.Closed(41) && r.Place(new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 1 }, 1, PayMode.Escrow, rw, Assets.CRT, 30, price) == null, "关闭期间不能下单");
});
Test("黑市出售", () =>
{
Func<string, double> price = a => a == Assets.CRT ? 1 : 100;
Check(Near(BlackMarket.ExpectedDays(0.6), 1, 1e-9) && Near(BlackMarket.ExpectedDays(0.9), 1, 1e-9) && Near(BlackMarket.ExpectedDays(1.0), 2, 1e-9) && Near(BlackMarket.ExpectedDays(1.8), 14, 1e-6), "期望成交天数:0.9 以下 1 天,1.0 两天,1.8 十四天");
Check(BlackMarket.ExpectedDays(1.2) > BlackMarket.ExpectedDays(1.0) && BlackMarket.ExpectedDays(1.5) > BlackMarket.ExpectedDays(1.2), "倍率越高越慢");
Check(BlackMarket.TaintChance(1.8) > BlackMarket.TaintChance(0.6) && BlackMarket.TaintRatio(1.8) > BlackMarket.TaintRatio(0.6) && Near(BlackMarket.TaintChance(0.6), Tuning.BmSaleTaintChanceMin, 1e-9), "倍率越高,带污点的概率和比例越高");
var bm = new BlackMarket();
var w = new Wallet(); w.Add(Assets.CRT, 1000);
Check(bm.List("c4", null, null, 0, 500, 2.0, w, Assets.CRT, 1, price) == null && bm.List("c4", null, null, 0, 500, 0.5, w, Assets.CRT, 1, price) == null, "倍率超出 0.6–1.8 不能上架");
var s = bm.List("c4", null, null, 0, 500, 1.0, w, Assets.CRT, 1, price);
Check(s != null && s.ListPrice == 500 && Near(w.Get(Assets.CRT), 1000 - 25, 1e-9), "上架:标价 500,上架费 5%");
int sold1 = 0, sold18 = 0;
for (int i = 0; i < 2000; i++) { if (new Rng((ulong)i + 1).Chance(bm.SaleChance(0.8))) sold1++; if (new Rng((ulong)i + 1).Chance(bm.SaleChance(1.8))) sold18++; }
Check(sold1 == 2000 && sold18 > 80 && sold18 < 220, $"每晚成交概率:0.8 倍必成交,1.8 倍约 1/14:{sold18}/2000");
// 成交与到账:夜晚 1 成交(0.8 倍必成交),托管 3 天后到钱包
var b2 = new BlackMarket();
var w2 = new Wallet(); w2.Add(Assets.CRT, 1000);
var s2 = b2.List("c4", null, null, 0, 500, 0.8, w2, Assets.CRT, 1, price);
double crt0 = w2.Get(Assets.CRT);
var e1 = b2.Night(2, false, w2, new Rng(7), 0, price);
Check(s2.State == SaleState.Sold && s2.Proceeds == (int)Math.Round(400 * (1 - Tuning.BmCommission)) && s2.PayDay == 2 + Tuning.BmSaleHold && Near(w2.Get(Assets.CRT), crt0, 1e-9), "成交:扣抽成,货款先托管");
b2.Night(4, false, w2, new Rng(8), 0, price);
Check(s2.State == SaleState.Sold, "没到放款日不动");
b2.Night(5, false, w2, new Rng(9), 0, price);
Check(s2.State == SaleState.Paid && Near(w2.Get(Assets.CRT), crt0 + s2.Proceeds, 1e-9) && Near(w2.Taint, s2.Taint, 1e-9) && (s2.Taint == 0 || Near(s2.Taint, s2.Proceeds * BlackMarket.TaintRatio(0.8), 1e-6)), "放款:货款进钱包,污点按比例");
// 假卡被看穿
int fake = 0;
for (int i = 0; i < 400; i++)
{
var b3 = new BlackMarket(); var w3 = new Wallet(); w3.Add(Assets.CRT, 1000);
var s3 = b3.List("gpu:RTX70", "RTX70", "RTX60", 60, 500, 0.8, w3, Assets.CRT, 1, price);
b3.Night(2, false, w3, new Rng((ulong)i + 3), 0, price);
if (s3.State == SaleState.Delisted && s3.Why == "fake") { fake++; if (b3.Rating >= Tuning.BmRatingStart) throw new Exception("好评没降"); }
}
Check(fake > 150 && fake < 250, $"假卡约一半被看穿退货:{fake}/400");
// 平台跑路:上架的货与未放款货款都没了
var b4 = new BlackMarket(); var w4 = new Wallet(); w4.Add(Assets.CRT, 100000);
for (int i = 0; i < 30; i++) b4.List("c4", null, null, 0, 5000, 1.8, w4, Assets.CRT, 1, price);
bool rugged = false;
for (int d = 2; d < 400 && !rugged; d++) { var ev = b4.Night(d, false, w4, new Rng((ulong)d * 31), 0, price); rugged = ev.Any(e => e.Kind == "sale_rug"); }
Check(rugged && b4.Sales.All(x => x.State != SaleState.Listed && x.State != SaleState.Sold), "跑路:上架与未放款全部归零");
var b6 = new BlackMarket(); var w6 = new Wallet(); w6.Add(Assets.CRT, 100000);
int ok6 = 0; for (int i = 0; i < 15; i++) if (b6.List("c4", null, null, 0, 500, 1.0, w6, Assets.CRT, 1, price) != null) ok6++;
Check(ok6 == Tuning.BmMaxListings && b6.ActiveListings == 10, "同时最多上架 10 件");
var b7 = new BlackMarket(); var w7 = new Wallet(); w7.Add(Assets.CRT, 5000);
var s7 = b7.List("c4", null, null, 0, 500, 1.8, w7, Assets.CRT, 1, price);
double c7 = w7.Get(Assets.CRT);
Check(b7.Reprice(s7, 1.0, w7, Assets.CRT, 2, price) && s7.ListPrice == 500 && s7.Mult == 1.0 && Near(c7 - w7.Get(Assets.CRT), BlackMarket.RepriceFee(500), 1e-9), "改价:按参考价重算标价,收 2% 改价费");
Check(!b7.Reprice(s7, 1.0, w7, Assets.CRT, 2, price) && !b7.Reprice(s7, 2.5, w7, Assets.CRT, 2, price) && Near(b7.SaleChance(s7.Mult), 0.5 * Math.Min(1.1, 90 / 90.0), 1e-9), "同倍率或超范围不改;改价后成交概率按新倍率");
b7.Night(3, false, w7, new Rng(5), 0, price);
Check(!b7.Reprice(s7, 0.6, w7, Assets.CRT, 4, price) || s7.State == SaleState.Listed, "已成交的货不能改价");
// 镇压期间不推进
var b5 = new BlackMarket(); var w5 = new Wallet(); w5.Add(Assets.CRT, 1000);
var s5 = b5.List("c4", null, null, 0, 500, 0.8, w5, Assets.CRT, 1, price);
b5.Night(2, true, w5, new Rng(1), 0, price);
Check(s5.State == SaleState.Listed, "镇压期间平台下线,不成交");
});
Test("黑市全新卡", () =>
{
int fresh = 0, total = 0, bad = 0;
for (int i = 0; i < 300; i++)
{
var bm = new BlackMarket();
bm.Refresh(10, 75, new Rng((ulong)i + 11), id => 100);
foreach (var o in bm.Offers.Where(x => x.Kind == GoodKind.Gpu))
{
total++;
if (o.Health >= 100) { fresh++; if (o.FakeOf != null) bad++; }
}
}
Check(total > 300 && fresh > total * 0.08 && fresh < total * 0.25 && bad == 0, $"显卡里约 {Tuning.BmGpuNew:P0} 是全新卡,且不会是假卡:{fresh}/{total},假 {bad}");
Check(Near(Tuning.PoneroFee, 0.06, 1e-9) && Near(1 - Math.Pow(1 - Tuning.PoneroFee, 2), 0.12, 0.01), "Ponero 一进一出约 12%");
});
Test("黑市商家", () =>
{
double sum = 0; int bulk = 0, n = 300;
var types = new System.Collections.Generic.HashSet<MerchantType>();
for (int i = 0; i < n; i++)
{
var bm = new BlackMarket();
bm.RefreshOrders(20, 75, new Rng((ulong)i + 5), id => 250);
sum += bm.CatalogValue;
if (bm.Sellers.Any(x => x.Type == MerchantType.Bulk)) bulk++;
foreach (var s in bm.Sellers) types.Add(s.Type);
if (bm.Sellers.Where(x => x.Type != MerchantType.Bulk && x.Type != MerchantType.Personal).Select(x => x.Type).Distinct().Count() != bm.Sellers.Count(x => x.Type != MerchantType.Bulk && x.Type != MerchantType.Personal)) throw new Exception("同类型商家重复");
}
double avg = sum / n;
Check(avg > 8000 && avg < 40000, $"每周货架总值(游戏里实测平均约 1.4 万,这里用统一基础价 250):平均 {avg:0}");
Check(bulk > n * 0.15 && bulk < n * 0.35 && types.Count >= 20, $"大宗商家约 {Tuning.BmBulkChance:P0} 的周出现:{bulk}/{n};七种类型都出现过");
// 批量下单:折扣、库存扣减
Func<string, double> price = a => 1;
var m = new BlackMarket(); m.Sellers.Add(new BmSeller { Name = "a", Trades = 100, Type = MerchantType.Modules, Factor = 0.8 });
var l = new BmListing { Seller = 0, Item = "x", Price = 100, Qty = 8 };
m.Catalog.Add(l);
var w = new Wallet(); w.Add(Assets.CRT, 5000);
var o = m.Place(l, 5, PayMode.Advance, w, Assets.CRT, 1, price);
Check(o != null && o.Count == 5 && o.Price == (int)Math.Round(475 * 0.9) && l.Qty == 3 && m.Catalog.Contains(l), "订 5 件:95 折,再按提前放款打折,库存扣 5");
Check(m.Place(l, 4, PayMode.Escrow, w, Assets.CRT, 1, price) == null && m.Place(l, 3, PayMode.Escrow, w, Assets.CRT, 1, price) != null && !m.Catalog.Contains(l), "超过库存不能订;订完下架");
var bs = new BmSeller { Type = MerchantType.Bulk };
Check(bs.Days == Tuning.BmBulkDays && new BmSeller { Type = MerchantType.Modules }.Days == Tuning.BmDoorDays, "大宗商家送货更久");
// 出售量推高污点
var b2 = new BlackMarket();
double v0 = b2.VolumeTaint(10);
b2.Sales.Add(new BmSale { State = SaleState.Sold, Proceeds = 14000, PayDay = 12 });
Check(Near(v0, 0, 1e-9) && Near(b2.VolumeTaint(10), Tuning.BmVolumeTaintMax, 1e-9) && b2.VolumeTaint(30) == 0, "近 7 天成交额推高污点,过期不算");
});
Test("尖端店铺与个人卖家", () =>
{
int elite = 0, n = 400, personalListings = 0; double lo = 9, hi = 0;
for (int i = 0; i < n; i++)
{
var bm = new BlackMarket();
bm.RefreshOrders(20, 75, new Rng((ulong)i + 77), id => id == "hypertech_bay" || id == "turbo_booster_hypertech" ? 10000 : id == "mind_reprogrammer" ? 5000 : 200);
if (bm.Sellers.Any(x => x.Type == MerchantType.Elite)) elite++;
if (bm.Sellers.Count(x => x.Type == MerchantType.Personal) != Tuning.BmPersonalCount) throw new Exception("个人卖家数不对");
foreach (var l in bm.Catalog.Where(x => bm.Sellers[x.Seller].Type == MerchantType.Elite))
if (l.Kind != GoodKind.Gpu && l.Price < Tuning.BmEliteMinValue * Tuning.BmFactorEliteMin * 0.97 - 1) throw new Exception("尖端店铺卖了便宜货:" + l.Item + " " + l.Price);
foreach (var l in bm.Catalog.Where(x => bm.Sellers[x.Seller].Type == MerchantType.Personal && x.Kind == GoodKind.Contraband)) { personalListings++; double f = l.Price / 200.0; lo = Math.Min(lo, f); hi = Math.Max(hi, f); }
foreach (var s in bm.Sellers.Where(x => x.Type == MerchantType.Personal)) if (s.Advance) throw new Exception("个人卖家不该接受提前放款");
}
Check(elite > n * 0.08 && elite < n * 0.25, $"尖端店铺约 {Tuning.BmEliteChance:P0} 的周出现:{elite}/{n}");
Check(personalListings > 100 && lo < 0.65 && hi > 1.05, $"个人卖家每件价格系数分散:{lo:0.00}–{hi:0.00}");
});
Test("链上地址校验位", () =>
{
var rng = new Rng(5);
int caught = 0, typos = 0;
for (int i = 0; i < 2000; i++)
{
string a = ChainAddress.Make(rng.NextUInt());
Check(ChainAddress.Valid(a) && ChainAddress.Valid(a.ToUpperInvariant().Replace("0X", "0x")) && ChainAddress.Valid(" " + a + " "), "生成的地址合法,大小写、空格不影响:" + a);
int pos = 2 + rng.Range(0, ChainAddress.BodyLength + ChainAddress.CheckLength - 1);
char c = a[pos];
char t = "0123456789abcdef"[(Convert.ToInt32(c.ToString(), 16) + 1 + rng.Range(0, 14)) % 16];
string typo = a.Substring(0, pos) + t + a.Substring(pos + 1);
typos++;
if (!ChainAddress.Valid(typo)) caught++;
}
Check(caught > typos * 0.98 && caught < typos, $"输错一位几乎都被校验位拦下,极少数碰巧合法:{caught}/{typos}");
Check(!ChainAddress.Valid("0x12345") && !ChainAddress.Valid("0xzzzzzzzz") && !ChainAddress.WellFormed("12345678ab"), "格式不对的地址不合法");
});
Test("RatDigg 矿机:规格、维修、矿机时代、矿机商", () =>
{
var m1 = Hardware.Specs["M1"]; var m3 = Hardware.Specs["M3"];
Check(m1.Asic && Math.Abs(m1.Hash - 50) < 1e-9 && m3.Hash == 300 && m1.Kj / m1.Hash < Hardware.Specs["RTX90"].Kj / Hardware.Specs["RTX90"].Hash, "矿机每 H 耗电低于最好的显卡");
var u = new Unit { Model = "M2", Health = 100 };
Check(Math.Abs(u.Hash(ModuleBonus.None) - 120) < 1e-9 && Math.Abs(u.Kj(ModuleBonus.None) - 110) < 1e-9, "M2 满健康 120 H、110 kJ");
Check(Math.Abs(u.DeathChance(ModuleBonus.None) - Tuning.DeathBase * 0.5) < 1e-12 && Math.Abs(new Unit { Model = "RTX90", Health = 100 }.DeathChance(ModuleBonus.None) - Tuning.DeathBase * 0.7) < 1e-12, "矿机暴毙系数 0.5(Probidia 0.7)");
Check(Math.Abs(CardValue.Of("M1", 100) - 11000) < 1 && CardValue.Of("M1", 0) < 11000 * 0.3, "矿机价值按健康折算,和显卡同一公式");
// 维修:按顺序收元件,别的不收
int step = 0, prog = 0;
Check(AsicRepair.Insert("M1", ref step, ref prog, "rare_electronic", 1) == 0, "M1 先要普通元件,稀有的不收");
Check(AsicRepair.Insert("M1", ref step, ref prog, "common_electronic", 3) == 3 && step == 0 && prog == 3, "收 3 个普通元件,进度 3/4");
Check(AsicRepair.Insert("M1", ref step, ref prog, "common_electronic", 5) == 1 && step == 1 && prog == 0, "只再收 1 个,进下一步");
Check(AsicRepair.Insert("M1", ref step, ref prog, "rare_electronic", 2) == 1 && AsicRepair.Done("M1", step), "再放 1 个稀有,修好;多的不收");
double cost = new[] { "M1", "M2", "M3" }.Max(m => AsicRepair.Steps(m).Sum(x => x.Count * (x.Item == "common_electronic" ? 33 : x.Item == "rare_electronic" ? 350 : 2500)) / (double)Hardware.Specs[m].Price);
Check(cost > 0.02 && cost < 0.06, $"修理约占机价 4%:最贵的 {cost:P1}");
// 矿机时代:定一天、提前预兆、永久减半
var net = new Network(); var rng = new Rng(3);
double kj0 = net.NpcKj; int warns = 0, starts = 0, warnDay = -1, startDay = -1;
for (int d = 1; d <= 120; d++) { var st = net.EraNight(d, rng); if (st == Network.EraStep.Warn) { warns++; warnDay = d; } if (st == Network.EraStep.Start) { starts++; startDay = d; } }
Check(warns == 1 && starts == 1 && startDay >= Tuning.EraMin && startDay <= Tuning.EraMax && startDay - warnDay >= Tuning.EraWarnMin && startDay - warnDay <= Tuning.EraWarnMax, $"预兆第 {warnDay} 天,开始第 {startDay} 天");
Check(Math.Abs(net.NpcKj - kj0 * Tuning.EraKjMult) < 1e-9 && net.EraNight(200, rng) == Network.EraStep.None, "NPC 耗电效率翻倍,只发生一次");
var late = new Network(); Check(late.EraNight(200, new Rng(1)) == Network.EraStep.None && late.EraDay >= 200 + Tuning.EraWarnMax + 1, "存档很晚才接入:至少留出预兆天数");
// 均衡:效率翻倍后 NPC 算力涨
var a = new Network(); var b = new Network { NpcKj = Tuning.NpcKjPerH * Tuning.EraKjMult };
for (int d = 1; d <= 200; d++) { a.Night(d, 100, 0); b.Night(d, 100, 0); a.Reward = b.Reward = 100; }
Check(b.NpcHash > a.NpcHash * 1.5, $"耗电减半后均衡算力 {a.NpcHash:0} → {b.NpcHash:0}");
// 矿机商:按声望放开型号
int none = 0, m1only = 0, withM3 = 0, weeks = 600;
for (int i = 0; i < weeks; i++)
{
foreach (var rep in new[] { 30, 50, 90 })
{
var bm = new BlackMarket();
bm.RefreshOrders(10, rep, new Rng((ulong)(i * 7 + rep)), _ => 200);
var asic = bm.Catalog.Where(l => bm.Sellers[l.Seller].Type == MerchantType.Asic).ToList();
if (rep == 30 && asic.Count == 0) none++;
if (rep == 50 && asic.Count > 0 && asic.All(l => l.Model == "M1")) m1only++;
if (rep == 90 && asic.Any(l => l.Model == "M3")) withM3++;
foreach (var l in asic) if (l.Model == "M3" && rep < Tuning.AsicRepM3 || l.Model == "M2" && rep < Tuning.AsicRepM2) throw new Exception("声望不够不该出现:" + l.Model + " " + rep);
}
}
Check(none == weeks && m1only > weeks * 0.05 && withM3 > weeks * 0.01, $"声望 30 不出现;50 只有 M1({m1only}/{weeks} 周);90 有 M3({withM3}/{weeks} 周)");
});
Test("永续合约:开平仓、强平、基金与欠款、自动减仓、没收", () =>
{
Func<string, double> pr = a => a == Assets.PSC ? 100 : 1;
Wallet NewAcct(double crc, double taint = 0) { var w = new Wallet { Address = "pb" }; w.Add(Assets.CRC, crc); w.Taint = taint; w.Basis = crc; return w; }
// 滑点:名义价值占池深的比例,买入往上、卖出往下
var bs = new PerpBook();
double pool = Tuning.PoolDepth(1);
Check(Math.Abs(bs.Exec(100, 2000, true, 1) - 100 * (1 + 2000 / (Tuning.PerpDepthMult * pool))) < 1e-9 && bs.Exec(100, 2000, false, 1) < 100 && bs.Exec(100, 1e12, true, 1) <= 150.0001, "滑点:买入上滑、卖出下滑,封顶 50%");
// 滑点吃掉了强平线之间的空间:开仓被拒
var bt = new PerpBook { DepthMult = 0.05 }; var at = NewAcct(5000);
Check(bt.Open(at, "PSC", true, 1000, 5, 100, 1, pr) == PerpResult.Thin && bt.Positions.Count == 0 && at.Get(Assets.CRC) == 5000, "滑点大到一开就被强平的单子被拒,不扣钱");
// 开仓与手续费、保证金、污点
var b = new PerpBook { DepthMult = 1e12 }; var a = NewAcct(2000, 1000);
Check(b.Open(a, "PSC", true, 1000, 6, 100, 1, pr) == PerpResult.Leverage && b.Open(a, "PSC", true, 3000, 3, 100, 1, pr) == PerpResult.Funds, "杠杆超限、余额不足被拒");
Check(b.Open(a, "PSC", true, 1000, 5, 100, 1, pr) == PerpResult.Ok, "开 5× 多头");
var pos = b.Of("PSC");
double fee = 5000 * Tuning.PerpFee;
Check(Math.Abs(pos.Size - 50) < 1e-9 && Math.Abs(a.Get(Assets.CRC) - (1000 - fee)) < 1e-9 && Math.Abs(pos.Taint - 500) < 1e-9, $"名义 5000,手续费 {fee},污点 500 随保证金进仓位");
Check(b.Open(a, "PSC", false, 100, 2, 100, 1, pr) == PerpResult.Opposite, "不能同时多空");
Check(Math.Abs(pos.LiqPrice - (50 * 100 - 1000) / (50 * (1 - Tuning.PerpMaint))) < 1e-9 && pos.LiqPrice > 85 && pos.LiqPrice < 90, $"强平价 {pos.LiqPrice:0.0}(约 −11%)");
// 平仓盈利:干净的新币,污点原额退回
double paid = b.Close(a, "PSC", 110, 2, out double d0);
double expect = 1000 + 50 * 10 - 50 * 110 * Tuning.PerpFee;
Check(Math.Abs(paid - expect) < 1e-6 && d0 == 0 && Math.Abs(a.Taint - 1000) < 1e-6 && b.Positions.Count == 0, $"盈利平仓 {paid:0.00},污点全额退回");
// 平仓亏损:污点按剩下的比例退回
var a2 = NewAcct(2000, 1000); var b2 = new PerpBook { DepthMult = 1e12 };
b2.Open(a2, "PSC", true, 1000, 5, 100, 1, pr);
double paid2 = b2.Close(a2, "PSC", 95, 2, out _);
Check(paid2 > 700 && paid2 < 760 && a2.Taint < 1000 - 500 * (1 - paid2 / 1000) + 1, $"亏损平仓 {paid2:0.0},污点按比例少回");
// 夜间强平:权益不到 10% 名义价值;剩余进基金
var b3 = new PerpBook { DepthMult = 1e12 }; var a3 = NewAcct(5000);
b3.Open(a3, "PSC", true, 1000, 5, 100, 1, pr);
double fund0 = b3.InsuranceFund;
var ev = b3.Night(2, new Dictionary<string, double> { ["PSC"] = 88 }, a3, pr);
Check(ev.Any(e => e.Kind == "liq") && b3.Positions.Count == 0 && b3.Debt == 0, "−12% 被强平,没有欠款");
Check(b3.InsuranceFund > fund0, "剩余权益进保险基金");
var b3b = new PerpBook { DepthMult = 1e12 }; var a3b = NewAcct(5000);
b3b.Open(a3b, "PSC", true, 1000, 5, 100, 1, pr);
Check(!b3b.Night(2, new Dictionary<string, double> { ["PSC"] = 95 }, a3b, pr).Any(e => e.Kind == "liq") && b3b.Positions.Count == 1, "−5% 不强平");
// 穿仓:基金先垫,不够记欠款;有欠款不能开仓;账户 CRC 每晚先还
var b4 = new PerpBook { DepthMult = 1e12, InsuranceFund = 100 }; var a4 = NewAcct(1100);
b4.Open(a4, "SPARK", true, 1000, 3, 10, 1, pr);
double crc4 = a4.Get(Assets.CRC);
b4.Night(2, new Dictionary<string, double> { ["SPARK"] = 5 }, a4, pr);
Check(b4.Positions.Count == 0 && b4.Debt > 0 && b4.InsuranceFund < 1 + Tuning.PerpFundDrift, $"大跌穿仓,基金见底,欠款 {b4.Debt:0}");
Check(b4.Open(a4, "SPARK", true, 10, 1, 5, 2, pr) == PerpResult.Debt, "有欠款不能开仓");
double debt0 = b4.Debt; a4.Add(Assets.CRC, 100);
b4.Night(3, new Dictionary<string, double> { ["SPARK"] = 5 }, a4, pr);
Check(Math.Abs(b4.Debt - (debt0 * (1 + Tuning.PerpDebtInterest) - (crc4 + 100))) < 1e-6 || b4.Debt < debt0 * (1 + Tuning.PerpDebtInterest), "欠款计息,账户 CRC 先还");
// 逾期没收、核销、拉黑
var b5 = new PerpBook { DepthMult = 1e12, Debt = 500, DebtSince = 1 }; var a5 = NewAcct(0);
Check(!b5.Night(5, new Dictionary<string, double> { ["PSC"] = 100 }, a5, pr).Any(e => e.Kind == "seize"), "没到 7 天不没收");
Check(b5.Night(8, new Dictionary<string, double> { ["PSC"] = 100 }, a5, pr).Any(e => e.Kind == "seize"), "逾期触发没收");
double wo = b5.ApplySeizure(200, 8);
Check(wo > 300 && b5.Debt == 0 && b5.Banned(20) && !b5.Banned(8 + Tuning.PerpBanDays + 1), $"没收抵掉 200,核销 {wo:0},拉黑 {Tuning.PerpBanDays} 天");
// 资金费:价格高于 7 日均价时多头付、空头收
var b6 = new PerpBook { DepthMult = 1e12 }; var a6 = NewAcct(5000);
for (int d = 1; d <= 5; d++) b6.Night(d, new Dictionary<string, double> { ["PSC"] = 100 }, a6, pr);
b6.Open(a6, "PSC", true, 1000, 2, 100, 5, pr);
double m0 = b6.Of("PSC").Margin;
b6.Night(6, new Dictionary<string, double> { ["PSC"] = 106 }, a6, pr);
Check(b6.Rate("PSC") > 0 && b6.Of("PSC").Margin < m0, $"溢价时多头付资金费 {b6.Rate("PSC"):P2}");
// 自动减仓:NPC 大行情穿仓抽干基金,盈利的空头只拿到一部分
var b7 = new PerpBook { DepthMult = 1e12, InsuranceFund = 500 }; var a7 = NewAcct(5000);
b7.Open(a7, "SPARK", false, 1000, 3, 10, 1, pr);
b7.Night(2, new Dictionary<string, double> { ["SPARK"] = 10 }, a7, pr);
var ev7 = b7.Night(3, new Dictionary<string, double> { ["SPARK"] = 3 }, a7, pr);
Check(ev7.Any(e => e.Kind == "adl") && b7.Positions.Count == 0, "基金见底,盈利空头被自动减仓");
});
Test("CRA 质押池:计息、解锁期、污点随币、螺旋", () =>
{
Func<string, double> price = a => a == Assets.PSC ? 100 : 1;
var w = new Wallet { Address = "0xa" }; w.Add(Assets.CRA, 1000); w.Add(Assets.PSC, 10); w.Taint = 400; w.Basis = 600;
var st = new Staking();
Check(!st.Stake(w, 2000, price), "余额不足不能质押");
Check(st.Stake(w, 500, price) && Math.Abs(w.Get(Assets.CRA) - 500) < 1e-9, "质押 500,钱包剩 500");
double lotTaint = st.Of("0xa").Taint;
Check(Math.Abs(lotTaint - 400 * 500.0 / 2000) < 1e-6 && Math.Abs(w.Taint - (400 - lotTaint)) < 1e-6, $"污点按价值比例进仓位:{lotTaint}");
var e1 = st.Night(1, Tuning.CraRate, _ => w);
Check(Math.Abs(st.Of("0xa").Amount - 500 * (1 + Tuning.CraRate)) < 1e-9 && e1.Count == 1, "每晚计息并入本金");
Check(!st.RequestUnstake("0xa", 9999, 1) && st.RequestUnstake("0xa", 200, 1), "申请取出 200");
double a0 = st.Of("0xa").Amount;
st.Night(2, Tuning.CraRate, _ => w);
Check(Math.Abs(st.Of("0xa").Amount - a0 * (1 + Tuning.CraRate)) < 1e-9, "排队的部分不计息");
Check(w.Get(Assets.CRA) < 501, "解锁期没到不能取");
for (int d = 3; d <= 1 + Tuning.CraUnlockDays; d++) st.Night(d, Tuning.CraRate, _ => w);
Check(Math.Abs(w.Get(Assets.CRA) - 700) < 1e-6, $"到期自动回钱包 200:{w.Get(Assets.CRA):0.##}");
Check(w.Taint > 400 - lotTaint + 1, "污点按比例回来");
// 螺旋:质押的 CRA 取回后只值当时的价格
var s = new Stablecoins(); s.CraSpiralDay = 5;
for (int d = 5; d <= 7; d++) s.Night(d, new Rng(1), 0);
Check(s.Cra < 0.05 && s.Spark < Tuning.SparkStart * 0.1, $"螺旋后 CRA {s.Cra},SPARK {s.Spark:0.00}");
});
Test("SPARK:随机游走有界,铸销来回只损手续费,事件冲击", () =>
{
double lo = 99, hi = 0;
for (ulong seed = 1; seed <= 20; seed++)
{
var s = new Stablecoins(); var rng = new Rng(seed);
for (int d = 1; d <= 150 && s.CraWarnDay < 0; d++) { s.Night(d, rng, 0); lo = Math.Min(lo, s.Spark); hi = Math.Max(hi, s.Spark); } // 螺旋前
}
Check(lo >= Tuning.SparkStart * Tuning.SparkMinMult - 1e-9 && hi <= Tuning.SparkStart * Tuning.SparkMaxMult + 1e-9 && hi / lo > 1.5, $"螺旋前的范围 {lo:0.0}–{hi:0.0}");
var s2 = new Stablecoins(); var w = new Wallet { Address = "w" }; w.Add(Assets.CRA, 100);
double sp = s2.Mint(w, 100), back = s2.Burn(w, sp);
Check(Math.Abs(back - 100 * Math.Pow(1 - Tuning.SparkFee, 2)) < 1e-6 && s2.Mint(w, 1e6) < 0, $"来回 100 → {back:0.00},余额不足返回 -1");
double before = s2.Spark; s2.OnEventStart(MarketEvent.RevRaidUpper, 1, new Rng(1));
Check(Math.Abs(s2.Spark - before * Tuning.SparkRaidShock) < 1e-9, "袭击上层 +15%");
});
Test("相似地址、手续费、确认数", () =>
{
var rng = new Rng(21);
int sameShort = 0;
for (int i = 0; i < 300; i++)
{
string a = ChainAddress.Make(rng.NextUInt());
string f = ChainAddress.Lookalike(a, rng);
Check(ChainAddress.Valid(f) && !ChainAddress.Same(a, f) && f.Substring(0, 5) == a.Substring(0, 5), $"相似地址合法、不同、前缀相同:{a} / {f}");
if (ChainAddress.Short(a) == ChainAddress.Short(f)) sameShort++;
}
Check(sameShort > 290, $"缩写看起来一样:{sameShort}/300");
Check(TxRules.FeeCredits(FeeTier.Slow, false) < TxRules.FeeCredits(FeeTier.Normal, false) && TxRules.FeeCredits(FeeTier.Normal, false) < TxRules.FeeCredits(FeeTier.Fast, false), "慢 < 普通 < 快");
Check(Math.Abs(TxRules.FeeCredits(FeeTier.Fast, true) - Tuning.TxFeeFast * Tuning.TxCongestedFeeMult) < 1e-9, "拥堵时手续费翻倍");
Check(TxRules.Confirmations(100) == 3 && TxRules.Confirmations(5000) == 6 && TxRules.Confirmations(50000) == 12, "大额要更多确认");
Check(TxRules.DropChance(FeeTier.Slow, true) > 0 && TxRules.DropChance(FeeTier.Slow, false) == 0 && TxRules.DropChance(FeeTier.Fast, true) == 0, "只有拥堵时的慢档会被丢");
double lo = 9, hi = 0;
for (int dd = 0; dd < 7; dd++) { double g = TxRules.GasCycle(dd, 2); lo = Math.Min(lo, g); hi = Math.Max(hi, g); }
Check(Math.Abs(TxRules.GasCycle(3, 2) - TxRules.GasCycle(10, 2)) < 1e-9 && lo < 0.6 && hi > 1.4, $"gas 7 天一个周期,0.5×–1.5×:{lo:0.00}–{hi:0.00}");
var gr = new Rng(4); bool inRange = true;
for (int dd = 0; dd < 200; dd++) { double g = TxRules.Gas(dd, 0, gr); if (g < 0.5 * 0.85 - 1e-9 || g > 1.5 * 1.15 + 1e-9) inRange = false; }
Check(inRange, "加上随机波动后仍在 0.425–1.725");
Check(Math.Abs(TxRules.FeeCredits(FeeTier.Normal, false, 1.5) - Tuning.TxFeeNormal * 1.5) < 1e-9, "手续费乘 gas");
var h = new List<TxRecord>();
for (int i = 0; i < 30; i++) TxRules.Record(h, new TxRecord { Day = i });
Check(h.Count == TxRules.HistoryMax && h[0].Day == 29, "记录最新在前、有上限");
});
Test("被投毒的钱包:门槛以下不转,越多越快,转空", () =>
{
Check(Wallet.DrainChance(Tuning.WalletDrainMin - 1) == 0, "门槛以下不会被转走");
Check(Math.Abs(Wallet.DrainChance(Tuning.WalletDrainMin) - 1 / 7.0) < 1e-9, "刚到门槛:每晚 1/7(平均 7 天)");
Check(Wallet.DrainChance(Tuning.WalletDrainFull) == 1 && Wallet.DrainChance(Tuning.WalletDrainFull * 3) == 1, "到满额当晚就转走");
Check(Wallet.DrainChance(10000) > Wallet.DrainChance(5000), "钱越多越快");
double mid = (Tuning.WalletDrainMin + Tuning.WalletDrainFull) / 2;
Check(Math.Abs(Wallet.DrainDays(mid) - 4) < 1e-9 && Math.Abs(Wallet.DrainChance(mid) - 0.25) < 1e-9, "中间值:期望 4 天,每晚 25%");
var rng = new Rng(9); double sum = 0; int n = 4000;
for (int i = 0; i < n; i++) { int d = 1; while (!rng.Chance(Wallet.DrainChance(mid))) d++; sum += d; }
Check(Math.Abs(sum / n - 4) < 0.25, $"模拟平均 {sum / n:0.00} 天被转走(期望 4)");
var w = new Wallet { Address = "a" };
w.Add(Assets.PSC, 50); w.Add(Assets.CRT, 1000); w.Taint = 300; w.Basis = 900; w.Freeze(Assets.CRT, 0.5, 9);
double v = w.Drain(a => a == Assets.PSC ? 100 : 1);
Check(Math.Abs(v - 6000) < 1e-6 && w.Balances.Count == 0 && w.Frozen.Count == 0 && w.Taint == 0 && w.Basis == 0, $"全部转走(含冻结),污点和成本清零:{v}");
});
Test("被标记的钱包转出可追踪的币,收款方也被标记;Ponero 不会", () =>
{
Func<string, double> price = x => 1;
var f = new Wallet { Address = "f", Flagged = true }; f.Add(Assets.CRC, 100); f.Add(Assets.Ponero, 100);
var p1 = new Wallet { Address = "p1" }; var p2 = new Wallet { Address = "p2" };
Wallet.Transfer(f, p1, Assets.CRC, 10, price);
Wallet.Transfer(f, p2, Assets.Ponero, 10, price);
Check(p1.Flagged && !p2.Flagged, "CRC 带标记,Ponero 不带");
});
Test("多钱包:退款、出售回款、混币提回都回到原来的钱包", () =>
{
Func<string, double> price = a => a == Assets.PSC ? 100 : 1;
var a = new Wallet { Address = "0xa" }; var b = new Wallet { Address = "0xb" };
a.Add(Assets.CRT, 5000); b.Add(Assets.CRT, 5000);
Func<string, Wallet> pick = addr => addr == "0xa" ? a : b;
var mx = new Mixer();
var dep = mx.Deposit(a, Assets.CRT, 1000, 1, 1, price);
Check(dep.Wallet == "0xa", "混币存入记下钱包地址");
mx.Night(pick, 2, price, new Rng(3));
Check(mx.Pending.Count == 0 ? a.Get(Assets.CRT) > 4900 && b.Get(Assets.CRT) == 5000 : true, $"混币提回 A:A {a.Get(Assets.CRT):0},B {b.Get(Assets.CRT):0}");
var bm = new BlackMarket();
bm.RefreshOrders(5, 80, new Rng(11), _ => 300);
var seller = bm.Sellers.FindIndex(x => !x.Scam);
bm.Sellers[seller].Scam = true;
var l = bm.Catalog.First(x => x.Seller == seller);
double bBefore = b.Get(Assets.CRT);
var o = bm.Place(l, 1, PayMode.Escrow, b, Assets.CRT, 5, price);
Check(o.Wallet == "0xb" && b.Get(Assets.CRT) < bBefore, "订单记下付款钱包");
double aBefore = a.Get(Assets.CRT);
for (int d = 6; d < 30 && o.State == OrderState.Pending; d++) bm.Night(d, false, pick, new Rng((ulong)d), 0, price);
Check(o.State == OrderState.Refunded && Math.Abs(b.Get(Assets.CRT) - bBefore) < 1e-6 && a.Get(Assets.CRT) == aBefore, $"骗子托管退款回 B:{o.State}");
});
Test("预测市场:LMSR 定价、最大亏损、买卖", () =>
{
foreach (double p0 in new[] { 0.5, 0.08, 0.9 })
{
var m = PredictMarket.Create(1, PredictKind.Event, "x", "t", 0, 10, 11, 12, p0);
Check(Math.Abs(m.PYes - p0) < 1e-9, $"初始价 = 先验 {p0}:{m.PYes:0.0000}");
Check(Math.Abs(m.MaxLoss - PredictMarket.LossFor(10)) < 1e-6, "最大亏损等于预设");
double cost = 100, sh = m.SharesFor(true, cost);
Check(Math.Abs(m.BuyCost(true, sh) - cost) < 1e-6, $"闭式反解和成本函数一致({p0})");
Check(Math.Abs(m.SellValue(true, 0)) < 1e-9, "卖 0 份收回 0");
// 系统最大亏损:玩家买满一边,赢了系统最多亏 MaxLoss
double paid = m.BuyCost(true, 5000);
Check(5000 - paid <= m.MaxLoss + 1e-6, $"买得再多系统也亏不过最大亏损:{5000 - paid:0.0} ≤ {m.MaxLoss:0.0}");
var m2 = PredictMarket.Create(1, PredictKind.Event, "x", "t", 0, 10, 11, 12, p0);
double paidNo = m2.BuyCost(false, 5000);
Check(5000 - paidNo <= m2.MaxLoss + 1e-6, "反方向同理");
// 买了再卖:只损失手续费之外的价差为 0
var m3 = PredictMarket.Create(1, PredictKind.Event, "x", "t", 0, 10, 11, 12, p0);
double s3 = m3.SharesFor(true, 50);
m3.QYes += s3;
Check(Math.Abs(m3.SellValue(true, s3) - 50) < 1e-6, "原路卖回收回成本");
}
});
Test("预测市场:买入、卖出、手续费、污点、持仓限制", () =>
{
Func<string, double> pr = a => 1;
var acct = new Wallet { Address = "poinbase" };
acct.Add(Assets.CRC, 1000); acct.Taint = 400; acct.Basis = 1000;
var bk = new PredictBook();
var m = PredictMarket.Create(++bk.Seq, PredictKind.Purity, "gutter", "t", 8500, 5, 6, 6, 0.5);
bk.Markets.Add(m);
Check(bk.Buy(acct, m, true, 0, 5, pr, false, out _) == PredictResult.Amount, "金额为 0 被拒");
Check(bk.Buy(acct, m, true, 2000, 5, pr, false, out _) == PredictResult.Funds, "钱不够被拒");
Check(bk.Buy(acct, m, true, 100, 6, pr, false, out _) == PredictResult.Closed, "窗口开始后封盘");
var res = bk.Buy(acct, m, true, 102, 5, pr, false, out double sh);
Check(res == PredictResult.Ok && sh > 0, "买入成功");
var pos = bk.PosOf(m.Id);
Check(Math.Abs(pos.Cost - 100) < 1e-6 && Math.Abs(acct.Get(Assets.CRC) - 898) < 1e-6, $"成本 100(手续费 2):{pos.Cost:0.00}");
Check(Math.Abs(pos.Taint - 40.8) < 1e-6 && Math.Abs(acct.Taint - 359.2) < 1e-6, $"污点按花掉的价值比例带进持仓:{pos.Taint:0.00}");
Check(bk.Buy(acct, m, false, 10, 5, pr, false, out _) == PredictResult.Opposite, "不能同盘反方向");
// 卖出:价差为 0,只付 2% 手续费
double before = acct.Get(Assets.CRC);
Check(bk.SellAll(acct, m, 5, out double got) == PredictResult.Ok && Math.Abs(got - 98) < 1e-6, $"原价卖回只扣手续费:{got:0.00}");
Check(Math.Abs(acct.Get(Assets.CRC) - before - got) < 1e-6 && bk.PosOf(m.Id) == null, "回到账户");
Check(acct.Taint < 400 && acct.Taint > 359.2, "卖回时污点按退回的比例回来");
// 持仓上限
var big = new Wallet { Address = "x" }; big.Add(Assets.CRC, 1e7);
Check(bk.Buy(big, m, true, 1e6, 5, pr, false, out _) == PredictResult.Cap, "超过份额上限被拒");
Check(bk.SellAll(acct, m, 5, out _) == PredictResult.NoPosition, "没持仓不能卖");
});
Test("预测市场:结算、作废、内幕被查、观测", () =>
{
Func<string, double> pr = a => 1;
var rng = new Rng(5);
var acct = new Wallet { Address = "poinbase" };
acct.Add(Assets.CRC, 100000);
var bk = new PredictBook();
// 观测:第一晚没有 Prev,不算 Started
bk.Night(4, new[] { "oxymoreWave" }, m => null, acct, rng);
Check(!bk.Started.ContainsKey(4), "第一晚没有对照,不记新出现的事件");
var win = PredictMarket.Create(++bk.Seq, PredictKind.Event, "powerShortage", "停电", 0, 4, 5, 6, 0.2);
var lose = PredictMarket.Create(++bk.Seq, PredictKind.Event, "blackout", "x", 0, 4, 5, 6, 0.2);
var voided = PredictMarket.Create(++bk.Seq, PredictKind.Faction, "FACTION_SECURITY", "镇压", 0, 4, 5, 8, 0.2);
bk.Markets.AddRange(new[] { win, lose, voided });
bk.Buy(acct, win, true, 51, 4, pr, false, out double sw);
bk.Buy(acct, lose, true, 51, 4, pr, false, out _);
bk.Buy(acct, voided, true, 51, 4, pr, false, out _);
double cash = acct.Get(Assets.CRC);
bk.Night(5, new[] { "oxymoreWave", "powerShortage" }, bk.Happened, acct, rng);
Check(win.Outcome < 0 && lose.Outcome < 0, "窗口没结束不结算");
var evs = bk.Night(6, new[] { "oxymoreWave", "powerShortage" }, bk.Happened, acct, rng);
Check(win.Outcome == -1 || bk.Markets.All(x => x.Id != win.Id), "到期的盘口移出列表");
Check(evs.Any(e => e.Kind == "win" && e.Title == "停电" && Math.Abs(e.Amount - sw) < 1e-6), "押中兑付份数");
Check(evs.Any(e => e.Kind == "lose" && e.Title == "x"), "押错没收");
Check(Math.Abs(acct.Get(Assets.CRC) - cash - sw) < 1e-6, "账户只多了赢的那份");
Check(bk.Markets.Count == 1 && bk.Markets[0].Id == voided.Id, "后面的窗口还没到");
// 缺观测:8 号之前 7、8 的记录缺,到期作废退本金
double c2 = acct.Get(Assets.CRC);
var ev2 = bk.Night(9, new string[0], bk.Happened, acct, rng);
Check(ev2.Any(e => e.Kind == "void") && Math.Abs(acct.Get(Assets.CRC) - c2 - 50) < 1e-6, $"缺数据作废并退本金:{acct.Get(Assets.CRC) - c2:0.00}");
// 内幕被查:持仓达到上限的 40% 就有概率;200 次里大约一半
int caught = 0, wins = 0;
for (int i = 0; i < 400; i++)
{
var b2 = new PredictBook(); var a2 = new Wallet { Address = "p" }; a2.Add(Assets.CRC, 1e6);
var mk = PredictMarket.Create(++b2.Seq, PredictKind.Event, "e", "t", 0, 4, 5, 5, 0.1); b2.Markets.Add(mk);
double target = Tuning.PredictInsiderShare * mk.ShareCap * 1.05, spend = 0;
spend = (mk.BuyCost(true, target)) * (1 + Tuning.PredictFee);
b2.Buy(a2, mk, true, spend, 4, pr, true, out _);
var e = b2.Night(5, new string[0], mm => true, a2, new Rng((ulong)(i + 1)));
if (e[0].Kind == "caught") { caught++; Check(e[0].Fraud > 0 && e[0].Fraud <= Tuning.PredictFraudCap, "罪证有上限"); }
else wins++;
}
Check(caught > 140 && caught < 260, $"内幕大单约一半被查:{caught}/400");
// 小额内幕不会被查
var b3 = new PredictBook(); var a3 = new Wallet { Address = "p" }; a3.Add(Assets.CRC, 1e6);
var m3 = PredictMarket.Create(++b3.Seq, PredictKind.Event, "e", "t", 0, 4, 5, 5, 0.1); b3.Markets.Add(m3);
b3.Buy(a3, m3, true, m3.BuyCost(true, 0.2 * m3.ShareCap) * 1.02, 4, pr, true, out _);
Check(b3.Night(5, new string[0], mm => true, a3, new Rng(1))[0].Kind == "win", "小额内幕安全");
// 被查:只退本金
var b4 = new PredictBook(); var a4 = new Wallet { Address = "p" }; a4.Add(Assets.CRC, 1e6);
var m4 = PredictMarket.Create(++b4.Seq, PredictKind.Event, "e", "t", 0, 4, 5, 5, 0.1); b4.Markets.Add(m4);
double s4 = m4.BuyCost(true, 0.5 * m4.ShareCap) * 1.02;
b4.Buy(a4, m4, true, s4, 4, pr, true, out _);
double c4 = a4.Get(Assets.CRC);
for (ulong sd = 1; sd < 50; sd++)
{
var bb = new PredictBook(); bb.Markets.Add(m4); bb.Positions.AddRange(b4.Positions.Select(x => new PredictPos { Market = x.Market, Yes = x.Yes, Shares = x.Shares, Cost = x.Cost, Insider = x.Insider }));
var aa = new Wallet { Address = "p" };
m4.Outcome = -1;
var ee = bb.Night(5, new string[0], mm => true, aa, new Rng(sd));
if (ee[0].Kind == "caught") { Check(Math.Abs(aa.Get(Assets.CRC) - (b4.Positions[0].Cost)) < 1e-6, "被查只退本金"); break; }
}
});
Test("预测市场:先验概率", () =>
{
Check(Math.Abs(PredictMath.PurityAtLeast(8500, 8000, 8999) - 0.5) < 1e-9, "纯度 8500 以上一半");
Check(PredictMath.PurityAtLeast(7000, 8000, 8999) == 1 && PredictMath.PurityAtLeast(9500, 8000, 8999) == 0, "越界取 0 和 1");
double p3 = PredictMath.EventProb(0.4, 15, 150, 3);
Check(Math.Abs(p3 - (1 - Math.Pow(1 - 0.04, 3))) < 1e-12, "事件概率:每天的概率连乘");
Check(PredictMath.FactionProb(1) > 0 && PredictMath.FactionProb(3) > PredictMath.FactionProb(1), "天数越长概率越大");
Check(Math.Abs(PredictMath.Phi(0) - 0.5) < 1e-6 && PredictMath.Phi(1.96) > 0.974 && PredictMath.Phi(1.96) < 0.976, "正态分布函数");
// 行情先验和 Market 的真实过程对得上
foreach (var (mult, nights) in new[] { (1.0, 1), (1.1, 2), (0.9, 3), (1.2, 4) })
{
int hit = 0, n = 3000;
for (int i = 0; i < n; i++)
{
var mk = Market.CreateDefault(20);
var rng = new Rng((ulong)(i * 7 + 3));
double start = mk.Price, anchor = mk.Anchor(new List<MarketEvent>());
double thr = start * mult;
for (int k = 0; k < nights; k++) { mk.Night(new List<MarketEvent>(), rng, 20 + k, a => 1); mk.Open(new List<MarketEvent>(), rng, 21 + k); }
if (mk.Price >= thr) hit++;
}
double sim = hit / (double)n, calc = PredictMath.PriceAtLeast(Market.CreateDefault(20).Price, Market.CreateDefault(20).Anchor(new List<MarketEvent>()), Tuning.PscVol, Tuning.Revert, nights, Market.CreateDefault(20).Price * mult);
Check(Math.Abs(sim - calc) < 0.04, $"行情先验 ×{mult} {nights} 晚:模拟 {sim:0.000} vs 公式 {calc:0.000}");
}
});
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;
}