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 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(); 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(); 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 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(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(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 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 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 price = a => a == Assets.CRT ? 1 : 100; Func 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).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, 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 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 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 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(); 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).Select(x => x.Type).Distinct().Count() != bm.Sellers.Count(x => x.Type != MerchantType.Bulk)) 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 == 7, $"大宗商家约 {Tuning.BmBulkChance:P0} 的周出现:{bulk}/{n};七种类型都出现过"); // 批量下单:折扣、库存扣减 Func price = a => 1; var m = new BlackMarket(); m.Sellers.Add(new BmSeller { Name = "a", Trades = 100, Type = MerchantType.Mods, 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.Mods }.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 天成交额推高污点,过期不算"); }); 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; }