2026-11-03 08:52:32 +08:00
|
|
|
|
using System;
|
2026-11-03 08:55:31 +08:00
|
|
|
|
using System.Collections.Generic;
|
2026-11-03 08:52:32 +08:00
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using Decentralized.Domain;
|
|
|
|
|
|
|
|
|
|
|
|
// 纯逻辑单元测试(不需要游戏)。每个 Test 是一个场景,失败时打印原因,最后返回非零退出码。
|
|
|
|
|
|
// 涉及数值的检查以离线经济模拟的结果为对照。
|
|
|
|
|
|
internal static class Program
|
|
|
|
|
|
{
|
|
|
|
|
|
private static int _failed, _passed;
|
|
|
|
|
|
|
2026-11-03 19:48:26 +08:00
|
|
|
|
private static double BlackMarketTestRug() => Tuning.BmRugBase * 3;
|
|
|
|
|
|
|
2026-11-03 08:52:32 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-11-04 23:23:59 +08:00
|
|
|
|
/// <summary>生成 TuningTable.cs:占位符取值走查表而不是运行时反射(游戏的模组扫描会把反射算作未声明的权限)。</summary>
|
|
|
|
|
|
private static string GenerateTuningTable()
|
2026-11-03 08:52:32 +08:00
|
|
|
|
{
|
2026-11-04 23:23:59 +08:00
|
|
|
|
var fields = typeof(Tuning).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)
|
|
|
|
|
|
.OrderBy(f => f.Name, StringComparer.Ordinal).ToList();
|
|
|
|
|
|
var sb = new System.Text.StringBuilder();
|
|
|
|
|
|
sb.Append("// 由测试项目生成:dotnet run --project tests/Decentralized.Domain.Tests -c Release -- --gen-tuning-table\n");
|
|
|
|
|
|
sb.Append("// Tuning 增删数值后要重新生成(单元测试会核对)。\n");
|
|
|
|
|
|
sb.Append("using System;\nusing System.Collections.Generic;\n\nnamespace Decentralized.Domain\n{\n");
|
|
|
|
|
|
sb.Append(" internal static class TuningTable\n {\n");
|
|
|
|
|
|
sb.Append(" public static readonly Dictionary<string, double> Scalars = new Dictionary<string, double>\n {\n");
|
|
|
|
|
|
foreach (var f in fields.Where(f => !f.FieldType.IsArray)) sb.Append($" [\"{f.Name}\"] = Tuning.{f.Name},\n");
|
|
|
|
|
|
sb.Append(" };\n\n public static readonly Dictionary<string, double[]> Arrays = new Dictionary<string, double[]>\n {\n");
|
|
|
|
|
|
foreach (var f in fields.Where(f => f.FieldType.IsArray)) sb.Append($" [\"{f.Name}\"] = Array.ConvertAll(Tuning.{f.Name}, v => (double)v),\n");
|
|
|
|
|
|
sb.Append(" };\n }\n}\n");
|
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string FindRoot() => System.IO.Path.GetFullPath(System.IO.Path.Combine(AppContext.BaseDirectory, "../../../../.."));
|
|
|
|
|
|
|
|
|
|
|
|
private static int Main(string[] args)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (args.Contains("--gen-tuning-table"))
|
|
|
|
|
|
{
|
|
|
|
|
|
System.IO.File.WriteAllText(System.IO.Path.Combine(FindRoot(), "src/Decentralized.Domain/TuningTable.cs"), GenerateTuningTable().Replace("\r", ""));
|
|
|
|
|
|
Console.WriteLine("TuningTable.cs 已生成");
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
Test("占位符查表(TuningTable)和 Tuning 一致", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var path = System.IO.Path.Combine(FindRoot(), "src/Decentralized.Domain/TuningTable.cs");
|
|
|
|
|
|
Check(System.IO.File.ReadAllText(path).Replace("\r", "") == GenerateTuningTable().Replace("\r", ""), "TuningTable.cs 已过期,重新生成:dotnet run --project tests/Decentralized.Domain.Tests -c Release -- --gen-tuning-table");
|
|
|
|
|
|
var src = System.IO.File.ReadAllText(System.IO.Path.Combine(FindRoot(), "src/Decentralized.Domain/TuningText.cs"));
|
|
|
|
|
|
Check(!src.Contains("System.Reflection") && !src.Contains("GetField"), "TuningText 不用反射");
|
|
|
|
|
|
});
|
2026-11-03 08:52:32 +08:00
|
|
|
|
Test("随机数:同种子同序列;状态可复制后继续", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var a = new Rng(42);
|
|
|
|
|
|
var b = new Rng(42);
|
|
|
|
|
|
Check(Enumerable.Range(0, 100).All(_ => a.NextUInt() == b.NextUInt()), "同种子序列相同");
|
|
|
|
|
|
var c = new Rng { S0 = a.S0, S1 = a.S1, S2 = a.S2, S3 = a.S3 };
|
|
|
|
|
|
Check(Enumerable.Range(0, 100).All(_ => a.NextDouble() == c.NextDouble()), "复制状态后序列相同(存档读档)");
|
|
|
|
|
|
var r = new Rng(7);
|
|
|
|
|
|
double mean = Enumerable.Range(0, 20000).Select(_ => r.NextDouble()).Average();
|
|
|
|
|
|
Check(Near(mean, 0.5, 0.01), $"均匀分布均值约 0.5:{mean:0.000}");
|
|
|
|
|
|
double g = Enumerable.Range(0, 20000).Select(_ => r.Gauss()).Select(x => x * x).Average();
|
|
|
|
|
|
Check(Near(g, 1.0, 0.05), $"正态分布方差约 1:{g:0.000}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Test("资产登记:Ponero 不可追踪、不上合规交易所;稳定币挂钩", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Check(!Assets.Get(Assets.Ponero).Traceable && !Assets.Get(Assets.Ponero).Compliant, "Ponero");
|
|
|
|
|
|
Check(Assets.Get(Assets.CRC).Stable && Assets.Get(Assets.CRC).Freezable, "CRC 可冻结");
|
|
|
|
|
|
Check(Assets.Get(Assets.CRA).Stable && !Assets.Get(Assets.CRA).Freezable, "CRA 不可冻结");
|
|
|
|
|
|
Assets.Register(new AssetSpec { Id = "NPC_TEST", Issued = true });
|
|
|
|
|
|
Check(Assets.Get("NPC_TEST")?.Issued == true, "新增币种只是登记一条");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Test("交易对:恒定乘积、手续费、500 信用点约 2% 价格冲击", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var pool = Pool.Create(Venue.Dex, Assets.PSC, Assets.CRC, 100, 50000, Tuning.DexFee);
|
|
|
|
|
|
Check(Near(pool.Price, 100, 1e-9), "初始价格 100");
|
|
|
|
|
|
double k = pool.BaseReserve * pool.QuoteReserve;
|
|
|
|
|
|
Check(Near(pool.ImpactOfBuy(500), 0.02, 0.001), $"500 信用点买入的冲击约 2%:{pool.ImpactOfBuy(500):P2}");
|
|
|
|
|
|
double got = pool.BuyBase(500);
|
|
|
|
|
|
Check(got > 4.9 && got < 4.97, $"500 信用点买到约 4.93 PSC:{got:0.000}");
|
|
|
|
|
|
Check(pool.BaseReserve * pool.QuoteReserve >= k, "手续费留在池里,k 不减少");
|
|
|
|
|
|
double back = pool.SellBase(got);
|
|
|
|
|
|
Check(back < 500 && back > 496, $"马上卖回只损失手续费:{back:0.00}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Test("交易对:夜间向目标价回归 15%(对数空间)并重设深度", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var pool = Pool.Create(Venue.Dex, Assets.PSC, Assets.CRC, 80, 50000, Tuning.DexFee);
|
|
|
|
|
|
pool.Revert(100, Tuning.Revert, 60000);
|
|
|
|
|
|
double expect = Math.Exp(Math.Log(80) + 0.15 * (Math.Log(100) - Math.Log(80)));
|
|
|
|
|
|
Check(Near(pool.Price, expect, 1e-9), $"回归后价格 {pool.Price:0.00},期望 {expect:0.00}");
|
|
|
|
|
|
Check(Near(pool.QuoteReserve, 60000, 1e-9), "深度重设");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Test("地址:污点和成本按价值比例随币走;Ponero 换入时污点停止传播", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Func<string, double> px = a => a == Assets.PSC ? 100 : 1;
|
|
|
|
|
|
var a = new Wallet { Address = "A", Taint = 300, Basis = 1000 };
|
|
|
|
|
|
a.Add(Assets.CRT, 1000);
|
|
|
|
|
|
var b = new Wallet { Address = "B" };
|
|
|
|
|
|
Wallet.Transfer(a, b, Assets.CRT, 250, px);
|
|
|
|
|
|
Check(Near(b.Taint, 75, 1e-9) && Near(a.Taint, 225, 1e-9), $"转出 1/4,污点带走 1/4:{b.Taint}");
|
|
|
|
|
|
Check(Near(b.Basis, 250, 1e-9), "成本带走 1/4");
|
|
|
|
|
|
var c = new Wallet { Address = "C" };
|
|
|
|
|
|
b.Add(Assets.Ponero, 10);
|
|
|
|
|
|
Wallet.Transfer(b, c, Assets.Ponero, 10, px);
|
|
|
|
|
|
Check(c.Taint == 0, "Ponero 转入的地址不带污点");
|
|
|
|
|
|
bool threw = false;
|
|
|
|
|
|
try { a.Add(Assets.CRT, -10000); } catch (InvalidOperationException) { threw = true; }
|
|
|
|
|
|
Check(threw, "余额不足时拒绝");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 18:20:30 +08:00
|
|
|
|
Test("挖矿:开局 1 H 每晚约 26.9 信用点;减半在第 90 天", () =>
|
2026-11-03 08:52:32 +08:00
|
|
|
|
{
|
|
|
|
|
|
var net = new Network();
|
|
|
|
|
|
double perH = net.Mine(1) * Tuning.PscStartPrice;
|
2026-11-03 18:20:30 +08:00
|
|
|
|
Check(Near(perH, 26.9, 0.2), $"1 H 每晚约 26.9 信用点(抽成后):{perH:0.00}");
|
2026-11-03 08:52:32 +08:00
|
|
|
|
for (int d = 1; d <= 89; d++) net.Night(d, 100, 0);
|
|
|
|
|
|
Check(net.Reward == Tuning.BlockReward, "第 89 天还没减半");
|
|
|
|
|
|
net.Night(90, 100, 0);
|
|
|
|
|
|
Check(net.Reward == Tuning.BlockReward / 2, "第 90 天减半");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Test("挖矿:NPC 算力在有利可图时扩张,币价低时关机", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var up = new Network();
|
|
|
|
|
|
up.Night(1, 100, 0);
|
|
|
|
|
|
Check(up.NpcHash > Tuning.NpcHash0, $"币价 100:扩张到 {up.NpcHash:0.0}");
|
|
|
|
|
|
var down = new Network();
|
|
|
|
|
|
down.Night(1, 30, 0);
|
|
|
|
|
|
Check(down.NpcHash < Tuning.NpcHash0 && down.NpcHash >= Tuning.NpcHash0 * 0.95, $"币价 30:关机,最多 −5%:{down.NpcHash:0.0}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Test("显卡:型号、显示单位、健康度与算力衰减", () =>
|
|
|
|
|
|
{
|
2026-11-03 18:34:57 +08:00
|
|
|
|
Check(Hardware.Specs["RTX70"].DisplayHash == 50 && Hardware.Specs["RX80"].DisplayHash == 40, "RTX70 50 MH/s,RX80 40 MH/s");
|
2026-11-03 08:52:32 +08:00
|
|
|
|
Check(Near(Hardware.Specs["M3"].DisplayHash, 3, 1e-9), "RatDigg M3 3 GH/s");
|
2026-11-03 18:34:57 +08:00
|
|
|
|
var u = new Unit { Model = "RTX70" };
|
2026-11-03 09:16:46 +08:00
|
|
|
|
Check(u.Hash(ModuleBonus.None) == 5, "满健康 5 H");
|
2026-11-03 08:52:32 +08:00
|
|
|
|
u.Health = 25;
|
2026-11-03 09:16:46 +08:00
|
|
|
|
Check(Near(u.Hash(ModuleBonus.None), 4.0, 1e-9), "健康 25:5 × (0.6 + 0.4 × 0.5) = 4 H");
|
2026-11-03 08:52:32 +08:00
|
|
|
|
u.Health = 0;
|
2026-11-03 09:16:46 +08:00
|
|
|
|
Check(Near(u.Hash(ModuleBonus.None), 3.0, 1e-9), "健康 0:60%");
|
2026-11-03 18:34:57 +08:00
|
|
|
|
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" };
|
2026-11-04 10:13:16 +08:00
|
|
|
|
Check(fake.Hash(ModuleBonus.None) == Hardware.Specs["RTX60"].Hash && fake.Label.Model == "RTX90", "假卡标着 RTX90,算力按 RTX60");
|
2026-11-03 18:34:57 +08:00
|
|
|
|
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 };
|
2026-11-03 08:52:32 +08:00
|
|
|
|
Check(r.Refurbish() && r.Health == 80 && !r.Refurbish(), "翻新一次 +30,不能再翻新");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 11:20:33 +08:00
|
|
|
|
Test("显卡外观:5 个维度 243 种;真卡没有破绽类取值,假卡至少一处;老化随健康度;价值函数统一", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var rng = new Rng(99);
|
|
|
|
|
|
int n = 6000;
|
|
|
|
|
|
int fakeNoSign = 0, realSign = 0, newWorn = 0, oldWorn = 0, fakeSigns = 0;
|
|
|
|
|
|
var seen = new System.Collections.Generic.HashSet<string>();
|
|
|
|
|
|
for (int i = 0; i < n; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var real = CardLook.Generate(rng, 100, false);
|
2026-11-03 13:37:48 +08:00
|
|
|
|
var oldReal = CardLook.Generate(rng, 20 + (i % 40), false);
|
|
|
|
|
|
var fake = CardLook.Generate(rng, 20 + (i % 80), true);
|
2026-11-03 11:20:33 +08:00
|
|
|
|
if (real.HasFakeSign || oldReal.HasFakeSign) realSign++;
|
|
|
|
|
|
if (!fake.HasFakeSign) fakeNoSign++;
|
2026-11-03 14:07:40 +08:00
|
|
|
|
fakeSigns += (fake.Print != 0 ? 1 : 0) + (fake.Serial != 0 ? 1 : 0);
|
2026-11-03 11:20:33 +08:00
|
|
|
|
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}");
|
|
|
|
|
|
}
|
2026-11-03 14:07:40 +08:00
|
|
|
|
Check(realSign == 0, "真卡从不出现破绽类取值(丝印不对、序列号不合规)");
|
2026-11-03 11:20:33 +08:00
|
|
|
|
Check(oldWorn > newWorn * 1.8, $"低健康度的真卡明显更旧:{oldWorn / (double)n:P0} 对 {newWorn / (double)n:P0}");
|
2026-11-03 14:07:40 +08:00
|
|
|
|
Check(seen.Count >= 60, $"抽样见到 {seen.Count} 种不同组合(上限 243)");
|
2026-11-03 18:34:57 +08:00
|
|
|
|
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 } };
|
2026-11-03 13:50:24 +08:00
|
|
|
|
Check(CardValue.True(worn) == (long)Math.Round(800 * (1 - 0.12 - 0.12 - 0.02 - 0.06)), "各标签折扣相加进真实价值:风扇缺角 + 金手指氧化 + 划痕 + 封条缺失");
|
2026-11-03 18:34:57 +08:00
|
|
|
|
var clean = new Unit { Model = "RTX70", Health = 80 };
|
|
|
|
|
|
var dusty = new Unit { Model = "RTX70", Health = 80, Look = new CardLook { Fan = 2, Fingers = 2 } };
|
2026-11-03 13:04:45 +08:00
|
|
|
|
Check(dusty.DeathChance(ModuleBonus.None) > clean.DeathChance(ModuleBonus.None) * 1.5 && Near(CardLook.Decode("20000").WearFactor, 1.3, 1e-9), "风扇缺角、金手指氧化抬高暴毙概率与损耗");
|
2026-11-03 18:34:57 +08:00
|
|
|
|
Check(CardValue.Of("RTX70", 100) == 800 && CardValue.Of("RTX70", 0) == 200 && CardValue.Of("RTX70", 50) == 500, "健康 100 满价、0 剩 25%、50 为 62.5%");
|
2026-11-03 11:20:33 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 10:18:19 +08:00
|
|
|
|
Test("矿架槽位:默认 2 个,投入电子元件逐个升级到最多 5 个;只收下一级需要的;形状随槽数变化", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
int slots = RigSlots.Base, prog = 0;
|
|
|
|
|
|
Check(slots == 2 && RigSlots.Next(2) == ("common_electronic", 4), "默认 2 槽,升到第 3 槽要 4 个普通电子元件");
|
|
|
|
|
|
Check(RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 0 && slots == 2 && prog == 0, "第 3 槽不收稀有元件");
|
|
|
|
|
|
Check(RigSlots.Insert(ref slots, ref prog, "common_electronic", 3) == 3 && slots == 2 && prog == 3, "先投 3 个:进度 3/4");
|
|
|
|
|
|
Check(RigSlots.Insert(ref slots, ref prog, "common_electronic", 3) == 1 && slots == 3 && prog == 0, "再投 3 个只收 1 个,解锁第 3 槽,多的 2 个退回");
|
|
|
|
|
|
Check(RigSlots.Next(3) == ("rare_electronic", 1) && RigSlots.Insert(ref slots, ref prog, "rare_electronic", 5) == 1 && slots == 4, "第 4 槽:1 个稀有元件");
|
|
|
|
|
|
Check(RigSlots.Next(4) == ("rare_electronic", 2) && RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 1 && slots == 4 && prog == 1, "第 5 槽:2 个稀有元件,先投 1 个进度 1/2");
|
|
|
|
|
|
Check(RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 1 && slots == 5, "投满解锁第 5 槽");
|
|
|
|
|
|
Check(RigSlots.Next(5) == null && RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 0, "满 5 槽后不再收");
|
2026-11-03 11:26:11 +08:00
|
|
|
|
Check(RigSlots.Shape(2) == "000000" + "000000" + new string('1', 18) && RigSlots.Shape(5) == new string('0', 30) && RigSlots.Shape(3).Length == 30, "形状:宽 3、共 10 行,前 2 × 槽数行可放");
|
2026-11-03 10:18:19 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 09:16:46 +08:00
|
|
|
|
Test("模块:折算系数 4,算力 +20%、省电 25%、损耗 −30% 封顶;性能加成提高损耗与暴毙", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var perf = ModuleBonus.From(16, 0, 0);
|
|
|
|
|
|
Check(Near(perf.Hash, 1.04, 1e-9) && Near(perf.Wear, 1.04, 1e-9) && Near(perf.Death, 1.04, 1e-9), "一个性能模块(+16):算力 +4%,损耗和暴毙 +4%");
|
|
|
|
|
|
var cores = ModuleBonus.From(200 * 3, -100 * 3, 100 * 3);
|
|
|
|
|
|
Check(Near(cores.Hash, 1.20, 1e-9), $"塞满神经核心,算力也只 +20%:{cores.Hash:0.00}");
|
|
|
|
|
|
Check(Near(cores.Kj, 1.75, 1e-9), $"神经核心的负效率照常增加耗电:×{cores.Kj:0.00}");
|
|
|
|
|
|
Check(Near(cores.Wear, 0.70 * 1.20, 1e-9), "品质封顶 −30%,再乘性能带来的 +20%");
|
|
|
|
|
|
var eco = ModuleBonus.From(-16, 32 * 5, 0);
|
|
|
|
|
|
Check(Near(eco.Kj, 0.75, 1e-9) && Near(eco.Hash, 0.96, 1e-9), "省电封顶 25%;节能模块的负性能照常降低算力");
|
|
|
|
|
|
var none = ModuleBonus.From(0, 0, 0);
|
|
|
|
|
|
Check(none.Hash == 1 && none.Kj == 1 && none.Wear == 1 && none.Death == 1, "没有模块时不变");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 08:52:32 +08:00
|
|
|
|
Test("显卡:累计暴毙概率与模拟器一致(第 50 / 100 / 150 天)", () =>
|
|
|
|
|
|
{
|
2026-11-03 09:16:46 +08:00
|
|
|
|
double[] Run(string model, ModuleBonus b, int n)
|
2026-11-03 08:52:32 +08:00
|
|
|
|
{
|
|
|
|
|
|
var rng = new Rng(2026);
|
|
|
|
|
|
var deadBy = new int[3];
|
|
|
|
|
|
for (int i = 0; i < n; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var u = new Unit { Model = model };
|
|
|
|
|
|
for (int d = 1; d <= 150; d++)
|
|
|
|
|
|
{
|
2026-11-03 09:16:46 +08:00
|
|
|
|
u.Night(b, rng);
|
2026-11-03 08:52:32 +08:00
|
|
|
|
if (u.Dead)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (d <= 50) deadBy[0]++;
|
|
|
|
|
|
if (d <= 100) deadBy[1]++;
|
|
|
|
|
|
deadBy[2]++;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return deadBy.Select(x => (double)x / n).ToArray();
|
|
|
|
|
|
}
|
2026-11-03 18:34:57 +08:00
|
|
|
|
var p = Run("RTX70", ModuleBonus.None, 20000);
|
2026-11-03 08:52:32 +08:00
|
|
|
|
Check(Near(p[0], 0.11, 0.02) && Near(p[1], 0.53, 0.02) && Near(p[2], 0.84, 0.02),
|
|
|
|
|
|
$"Probidia 11% / 53% / 84%:{p[0]:P0} / {p[1]:P0} / {p[2]:P0}");
|
2026-11-03 18:34:57 +08:00
|
|
|
|
var q = Run("RX80", ModuleBonus.None, 20000);
|
2026-11-03 08:52:32 +08:00
|
|
|
|
Check(Near(q[0], 0.20, 0.02) && Near(q[1], 0.76, 0.02) && Near(q[2], 0.95, 0.02),
|
|
|
|
|
|
$"PMD 20% / 76% / 95%:{q[0]:P0} / {q[1]:P0} / {q[2]:P0}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 08:55:31 +08:00
|
|
|
|
Test("市场:价格锚受停电、镇压影响;夜间向锚回归并限制在 0.3–3 倍", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = Market.CreateDefault(1);
|
|
|
|
|
|
var none = new MarketEvent[0];
|
|
|
|
|
|
Check(Near(m.Anchor(none), 100, 1e-9) && Near(m.Anchor(new[] { MarketEvent.Blackout }), 110, 1e-9)
|
|
|
|
|
|
&& Near(m.Anchor(new[] { MarketEvent.Crackdown }), 92, 1e-9), "锚:基准 100,停电 ×1.1,镇压 ×0.92");
|
|
|
|
|
|
Check(Market.Volatility(new[] { MarketEvent.GangWar }) == Tuning.PscVol * 2, "火并时波动 ×2");
|
|
|
|
|
|
m.Main.SetPrice(60, Tuning.PoolDepth(1));
|
|
|
|
|
|
var rng = new Rng(5);
|
|
|
|
|
|
double sum = 0;
|
|
|
|
|
|
for (int i = 0; i < 400; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var mm = Market.CreateDefault(1);
|
|
|
|
|
|
mm.Main.SetPrice(60, Tuning.PoolDepth(1));
|
|
|
|
|
|
mm.Night(none, new Rng((ulong)i), 1, a => 1);
|
|
|
|
|
|
sum += Math.Log(mm.Price);
|
|
|
|
|
|
}
|
|
|
|
|
|
double expect = Math.Log(60) + 0.15 * (Math.Log(100) - Math.Log(60));
|
|
|
|
|
|
Check(Near(sum / 400, expect, 0.01), $"从 60 起一晚平均回到 {Math.Exp(expect):0.0} 附近:{Math.Exp(sum / 400):0.0}");
|
|
|
|
|
|
var hi = Market.CreateDefault(1);
|
|
|
|
|
|
hi.Main.SetPrice(1000, Tuning.PoolDepth(1));
|
|
|
|
|
|
hi.Night(none, rng, 1, a => 1);
|
|
|
|
|
|
Check(hi.Price <= 300 + 1e-9, $"上限 3 倍:{hi.Price:0.0}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 15:35:48 +08:00
|
|
|
|
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, "清仓后记录清除");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 18:20:30 +08:00
|
|
|
|
Test("市场:减半抬高价格锚(随机幅度,逐日靠拢),减半前一周有预期行情", () =>
|
2026-11-03 15:13:57 +08:00
|
|
|
|
{
|
|
|
|
|
|
var m = new Market();
|
2026-11-03 18:20:30 +08:00
|
|
|
|
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, "锚随之上移");
|
2026-11-03 15:13:57 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 08:55:31 +08:00
|
|
|
|
Test("市场:其他交易对每晚被套利拉向参考价,三角价差收敛", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = Market.CreateDefault(1);
|
|
|
|
|
|
var crt = m.Find(Venue.Dex, Assets.PSC, Assets.CRT);
|
|
|
|
|
|
crt.SetPrice(130, crt.QuoteReserve);
|
|
|
|
|
|
for (int d = 1; d <= 5; d++) m.Night(new MarketEvent[0], new Rng((ulong)d), 1, a => 1);
|
|
|
|
|
|
double gap = Math.Abs(crt.Price / m.Price - 1);
|
|
|
|
|
|
Check(gap < 0.03, $"五晚后 PSC/CRT 与参考价相差 {gap:P1}");
|
|
|
|
|
|
var crtCrc = m.Find(Venue.Dex, Assets.CRT, Assets.CRC);
|
|
|
|
|
|
m.Night(new MarketEvent[0], new Rng(9), 1, a => a == Assets.CRT ? 0.9 : 1);
|
|
|
|
|
|
Check(crtCrc.Price < 0.98, $"CRT 脱锚时 CRT/CRC 向 0.9 靠拢:{crtCrc.Price:0.000}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Test("稳定币:CRC 脱锚与减记,CRT 脱锚与冻结概率", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
int depegs = 0, haircuts = 0;
|
|
|
|
|
|
for (int i = 0; i < 2000; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var s = new Stablecoins();
|
|
|
|
|
|
var rng = new Rng((ulong)i);
|
|
|
|
|
|
s.OnEventStart(MarketEvent.RevRaidUpper, 10, rng);
|
|
|
|
|
|
if (s.Crc < 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
depegs++;
|
|
|
|
|
|
Check(s.Crc >= 0.88 && s.Crc <= 0.93 && s.CrcDepegEnd >= 12 && s.CrcDepegEnd <= 13, "CRC 脱锚深度与持续");
|
|
|
|
|
|
double f = 1;
|
|
|
|
|
|
for (int d = 10; d <= s.CrcDepegEnd; d++) f *= s.Night(d, rng, 0);
|
|
|
|
|
|
if (f < 1) haircuts++;
|
|
|
|
|
|
Check(s.Crc == 1, "到期恢复");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(Near(depegs / 2000.0, 0.30, 0.03), $"革命军袭击上层时 30% 脱锚:{depegs / 2000.0:P0}");
|
|
|
|
|
|
Check(Near(haircuts / (double)depegs, 0.15, 0.04), $"其中 15% 减记:{haircuts / (double)depegs:P0}");
|
|
|
|
|
|
Check(Near(Stablecoins.CrtFreezeChance(0), 0.40, 1e-9) && Stablecoins.CrtFreezeChance(50) == 0 && Near(Stablecoins.CrtFreezeChance(25), 0.2, 1e-9),
|
|
|
|
|
|
"CRT 冻结概率:声望 0 为 40%,25 为 20%,50 为 0");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Test("稳定币:CRA 补贴池告急即为预兆;螺旋三天 0.85 / 0.30 / 0.02", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var s = new Stablecoins { CraSubsidy = Tuning.CraSubsidy0 * 0.19 };
|
|
|
|
|
|
Check(s.CraWarning(1) && s.CraRate == Tuning.CraRateLow, "补贴池低于 20%:预兆出现、利率降到 0.2%");
|
|
|
|
|
|
var t = new Stablecoins { CraSubsidy = 1 };
|
|
|
|
|
|
var rng = new Rng(3);
|
|
|
|
|
|
t.Night(1, rng, 100000);
|
|
|
|
|
|
Check(t.CraWarnDay == 1 && t.CraSpiralDay >= 5 && t.CraSpiralDay <= 8, $"补贴池吃空触发螺旋,提前 4–7 天:第 {t.CraSpiralDay} 天开始");
|
|
|
|
|
|
var path = new List<double>();
|
|
|
|
|
|
for (int d = 2; d <= t.CraSpiralDay + 3; d++) { t.Night(d, rng, 0); if (d >= t.CraSpiralDay) path.Add(t.Cra); }
|
|
|
|
|
|
Check(path.Take(3).SequenceEqual(new[] { 0.85, 0.30, 0.02 }), $"螺旋路径:{string.Join(" / ", path)}");
|
|
|
|
|
|
Check(!t.CraWarning(t.CraSpiralDay + 1), "螺旋开始后不再算预兆");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Test("合规出金:干净小额不查;污点高时重度命中记罪证;轻度命中只没收污点部分;只对收益计税", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Func<string, double> px = a => 1;
|
|
|
|
|
|
Check(Exchange.HitChance(500, 0, 0, 200, false) == 0, "干净、低于 5 倍房租:不查");
|
|
|
|
|
|
Check(Near(Exchange.HitChance(2000, 0, 0, 200, false), 0.2, 1e-9), "单笔为大额阈值 2 倍:20%");
|
|
|
|
|
|
Check(Near(Exchange.HitChance(1000, 500, 0, 200, false), 0.75, 1e-9), "污点 50%:75%");
|
|
|
|
|
|
Check(Exchange.HitChance(100000, 100000, 1e6, 200, true) == Tuning.AmlMaxChance, "上限 90%");
|
|
|
|
|
|
int heavy = 0;
|
|
|
|
|
|
double crime = 0;
|
|
|
|
|
|
for (int i = 0; i < 1000; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var w = new Wallet { Taint = 500, Basis = 1000 };
|
|
|
|
|
|
w.Add(Assets.CRC, 1000);
|
|
|
|
|
|
var r = new Exchange().Cashout(w, 1000, px, 1, 200, false, new Rng((ulong)i));
|
|
|
|
|
|
if (r.Outcome == AmlOutcome.Heavy) { heavy++; crime = r.Crime; Check(Near(r.Received, 998 - 500, 1e-6), "重度:没收污点部分"); }
|
|
|
|
|
|
else Check(Near(r.Received, 998, 1e-6) && r.Tax == 0, "未命中:全额到账;本金不计税");
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(Near(heavy / 1000.0, 0.75, 0.04) && crime == 500, $"污点 50% 时约 75% 重度命中,罪证 = 污点额:{heavy / 1000.0:P0}");
|
|
|
|
|
|
var light = new Wallet { Taint = 150 };
|
|
|
|
|
|
light.Add(Assets.CRC, 1000);
|
|
|
|
|
|
CashoutResult lr = null;
|
|
|
|
|
|
for (ulong i = 0; lr == null || lr.Outcome == AmlOutcome.Clear; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var w = new Wallet { Taint = 150 };
|
|
|
|
|
|
w.Add(Assets.CRC, 1000);
|
|
|
|
|
|
lr = new Exchange().Cashout(w, 1000, px, 1, 200, false, new Rng(i));
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(lr.Outcome == AmlOutcome.Light && lr.Crime == 0 && Near(lr.Confiscated, 150, 1e-6), "污点 15%:轻度命中,没收 150、不记罪证");
|
|
|
|
|
|
Check(Near(lr.Tax, (998 - 150) * Tuning.TaxRate, 1e-6), "成本为 0 时到账部分全额计税");
|
|
|
|
|
|
var ex = new Exchange();
|
|
|
|
|
|
var big = new Wallet();
|
|
|
|
|
|
big.Add(Assets.CRC, 4000);
|
|
|
|
|
|
ex.Cashout(big, 2000, px, 1, 200, false, new Rng(1));
|
|
|
|
|
|
Check(Near(Exchange.HitChance(2000, 0, ex.History.Sum(h => h.Value), 200, false), 0.2 + 0.2 * 1000 / 3000.0, 1e-9),
|
|
|
|
|
|
"7 天内累计超过 15 倍房租也会提高命中率");
|
|
|
|
|
|
var dep = new Wallet { Basis = 1000 };
|
|
|
|
|
|
dep.Add(Assets.CRC, 1000);
|
|
|
|
|
|
var dr = new Exchange().Cashout(dep, 1000, a => 0.9, 1, 200, false, new Rng(1));
|
|
|
|
|
|
Check(Near(dr.Received, 900 * (1 - Tuning.ExchangeFee), 1e-6), "CRC 脱锚时按市价兑出");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 13:37:48 +08:00
|
|
|
|
Test("二手贩子:老实人不报价;中间商加价;骗子夹带假卡;指出问题按维度扣价、拆穿假货按真实芯片算", () =>
|
2026-11-03 11:40:10 +08:00
|
|
|
|
{
|
|
|
|
|
|
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))
|
2026-11-03 13:50:24 +08:00
|
|
|
|
Check(c.FakeOf == null, "老实人的卡是真卡");
|
2026-11-03 11:40:10 +08:00
|
|
|
|
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}");
|
2026-11-03 18:34:57 +08:00
|
|
|
|
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 } };
|
2026-11-03 13:50:24 +08:00
|
|
|
|
var claim = CardLabels.DealerClaim(u, DealerKind.Scammer, new Rng(3));
|
2026-11-03 18:34:57 +08:00
|
|
|
|
Check(claim.Model == "RTX70" && claim.Fan == 0 && claim.Fingers == 0 && claim.Print == 0, "骗子的说法:标称型号,外观全是没问题");
|
2026-11-03 13:50:24 +08:00
|
|
|
|
long hidden = CardLabels.Value(u, claim, 0), real = CardValue.True(u);
|
2026-11-03 18:34:57 +08:00
|
|
|
|
Check(hidden == CardValue.Of("RTX70", 60) && real == (long)Math.Round(CardValue.Of("RTX60", 60) * 0.76), "没指出时按说法算(标称型号满价),真实价值按芯片和外观算");
|
2026-11-03 13:50:24 +08:00
|
|
|
|
int ex = 0;
|
2026-11-03 18:34:57 +08:00
|
|
|
|
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%");
|
2026-11-03 13:50:24 +08:00
|
|
|
|
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;
|
2026-11-03 18:34:57 +08:00
|
|
|
|
var honestUnit = new Unit { Model = "RTX70", Health = 60, Look = new CardLook { Fan = 1 } };
|
2026-11-03 13:50:24 +08:00
|
|
|
|
Check(CardLabels.Point(honestUnit, CardLabels.DealerClaim(honestUnit, DealerKind.Honest, new Rng(1)), CardLabels.RowFan, ref ex2) == 0, "老实人说的是实话,指出去没用");
|
2026-11-04 19:40:50 +08:00
|
|
|
|
Check(CardLabels.Lies(u, claim, 0) == (CardLabels.RowSpec | CardLabels.RowFan | CardLabels.RowFingers | CardLabels.RowPrint), "骗子隐瞒了型号、丝印、叶片、金手指");
|
2026-11-03 13:50:24 +08:00
|
|
|
|
Check(CardLabels.Lies(u, claim, ex) == 0, "指出之后不再算隐瞒");
|
|
|
|
|
|
Check(Claim.Decode(claim.Encode()).Encode() == claim.Encode(), "说法能存取");
|
2026-11-03 13:37:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 14:07:40 +08:00
|
|
|
|
Test("序列号与供电风扇:四档实物特征;序列号按牌子和档次鉴定;指出供电风扇自动改回真实型号", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Check(Hardware.FanCount.SequenceEqual(new[] { 1, 2, 3, 3 }) && Hardware.PowerPins.SequenceEqual(new[] { "6", "8", "8", "8+8" }), "低端 6 针单风扇、中端 8 针双风扇、高端 8 针三风扇、旗舰双 8 针三风扇");
|
2026-11-03 18:20:30 +08:00
|
|
|
|
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, "低档次的卡也会被冒充(背后是废片),中高档的假卡不用废片");
|
2026-11-03 18:34:57 +08:00
|
|
|
|
var real = new Unit { Model = "RX80", Health = 100, Look = new CardLook { Seed = 7 } };
|
2026-11-03 14:07:40 +08:00
|
|
|
|
var t = CardLabels.SerialText(real);
|
|
|
|
|
|
Check(t.StartsWith("MD2-") && CardLabels.Factories("PMD").Contains(int.Parse(t.Substring(4, 2))) && t.Length == 11, $"真卡的序列号:牌子 + 档次码 + 合法工厂码 + 四位流水:{t}");
|
2026-11-03 18:34:57 +08:00
|
|
|
|
var forged = new Unit { Model = "RTX90", FakeOf = "RTX60", Health = 100, Look = new CardLook { Seed = 7, Serial = 1 } };
|
2026-11-03 14:07:40 +08:00
|
|
|
|
var f = CardLabels.SerialText(forged);
|
|
|
|
|
|
Check(f.StartsWith("PB4-") && (!CardLabels.Factories("Probidia").Contains(int.Parse(f.Substring(4, 2))) || f.Length == 10), $"补贴的序列号:档次码对,工厂码不合法或位数不对:{f}");
|
2026-11-04 19:40:50 +08:00
|
|
|
|
var original = new Unit { Model = "RTX90", FakeOf = "RTX60", Health = 100, Look = new CardLook { Seed = 7, Serial = 2, CountBad = 1, PinBad = 1 } };
|
2026-11-03 14:07:40 +08:00
|
|
|
|
Check(CardLabels.SerialText(original).StartsWith("PB1-"), "贴的是原芯片的序列号:档次码是真实芯片的档次");
|
|
|
|
|
|
var claim = CardLabels.DealerClaim(original, DealerKind.Scammer, new Rng(1));
|
|
|
|
|
|
int ex = 0;
|
2026-11-04 19:40:50 +08:00
|
|
|
|
Check(CardLabels.Point(original, claim, CardLabels.RowCount, ref ex) == 2 && (ex & CardLabels.RowSpec) != 0 && CardLabels.Value(original, claim, ex) == CardValue.True(original), "指出供电与风扇:型号自动改回真实型号,价格回到真实价值");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Test("假卡的四项真伪依据(风扇数、供电口、丝印、序列号):各自独立露出,至少一项,平均约 1.5 项;真卡全部符合", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var rng = new Rng(2024);
|
|
|
|
|
|
int n = 20000; double sum = 0; int[] per = new int[4]; int none = 0;
|
|
|
|
|
|
for (int i = 0; i < n; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var label = new[] { "RTX70", "RTX80", "RTX90", "RX80", "RX800", "RX900" }[rng.Range(0, 5)];
|
|
|
|
|
|
var cheaper = Hardware.Specs.Values.Where(x => !x.Asic && !x.Scrap && x.Brand == Hardware.Specs[label].Brand && x.Tier < Hardware.Specs[label].Tier).ToArray();
|
|
|
|
|
|
var chip = cheaper[rng.Range(0, cheaper.Length - 1)];
|
|
|
|
|
|
var look = CardLook.Generate(rng, 70, true, Hardware.Specs[label].Tier, chip.Tier);
|
|
|
|
|
|
var u = new Unit { Model = label, FakeOf = chip.Model, Health = 70, Look = look };
|
|
|
|
|
|
var real = Claim.Truth(u); var said = new Claim { Model = label };
|
|
|
|
|
|
int c = 0;
|
|
|
|
|
|
int k = 0;
|
|
|
|
|
|
foreach (int r in new[] { CardLabels.RowCount, CardLabels.RowPin, CardLabels.RowPrint, CardLabels.RowSerial })
|
|
|
|
|
|
{ if (said.Get(r) != real.Get(r)) { c++; per[k]++; } k++; }
|
|
|
|
|
|
if (c == 0) none++;
|
|
|
|
|
|
sum += c;
|
|
|
|
|
|
}
|
|
|
|
|
|
double mean = sum / n;
|
|
|
|
|
|
Check(none == 0, "假卡至少有一项依据不符");
|
|
|
|
|
|
Check(mean > 1.4 && mean < 1.6, $"假卡平均露出 {mean:0.00} 项依据(目标约 1.5)");
|
|
|
|
|
|
Check(per.All(x => x > n * 0.1), "四项都会单独露出:" + string.Join(" ", per));
|
|
|
|
|
|
var genuine = new Unit { Model = "RTX80", Health = 70, Look = CardLook.Generate(rng, 70, false) };
|
|
|
|
|
|
var gt = Claim.Truth(genuine); var gs = new Claim { Model = "RTX80" };
|
|
|
|
|
|
Check(new[] { CardLabels.RowCount, CardLabels.RowPin, CardLabels.RowPrint, CardLabels.RowSerial }.All(r => gs.Get(r) == gt.Get(r)), "真卡四项依据全部符合");
|
2026-11-03 14:07:40 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 22:24:19 +08:00
|
|
|
|
Test("旧存档兼容:6 位旧外观的假卡补上风扇数、供电口破绽,真卡和新格式不动", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var old = CardLook.Decode("000000");
|
|
|
|
|
|
var fake = CardLook.Legacy("000000", old, "RTX90", "RTX60");
|
|
|
|
|
|
var u = new Unit { Model = "RTX90", FakeOf = "RTX60", Health = 80, Look = fake };
|
|
|
|
|
|
Check(fake.CountBad == 1 && fake.PinBad == 1 && u.ObservedCount == 1 && u.ObservedPin == 0, "旧版的低档冒充高档:风扇数和供电口都对不上");
|
|
|
|
|
|
var claim = new Claim { Model = "RTX90" };
|
|
|
|
|
|
Check(claim.Get(CardLabels.RowCount) != Claim.Truth(u).Get(CardLabels.RowCount) && claim.Get(CardLabels.RowPin) != Claim.Truth(u).Get(CardLabels.RowPin), "说成标称型号时两项依据都与实物不符");
|
|
|
|
|
|
var sameCount = CardLook.Legacy("000000", old, "RTX80", "RTX60");
|
|
|
|
|
|
Check(sameCount.CountBad == 1 && sameCount.PinBad == 1, "RTX 80 冒充:风扇数 3 对 1、供电口 8 针对 6 针都不同");
|
|
|
|
|
|
var same = CardLook.Legacy("000000", old, "RTX80", "RX800");
|
|
|
|
|
|
Check(same.CountBad == 0 && same.PinBad == 0, "档次相同(风扇数、供电口一样)的假卡补不出破绽,也不会误加");
|
|
|
|
|
|
Check(CardLook.Legacy("000000", old, "RTX90", null).CountBad == 0, "真卡不动");
|
|
|
|
|
|
var newer = new CardLook { CountBad = 0, PinBad = 1, Print = 2 };
|
|
|
|
|
|
Check(CardLook.Legacy(newer.Encode(), CardLook.Decode(newer.Encode()), "RTX90", "RTX60").CountBad == 0, "新格式(8 位)不再补");
|
|
|
|
|
|
Check(CardLook.Decode(newer.Encode()).PinBad == 1 && CardLook.Decode("012012").Fan == 0 && CardLook.Decode("01201").Sticker == 1 && CardLook.Decode("01201").Serial == 0, "新旧格式都能读");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 13:37:48 +08:00
|
|
|
|
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, "假卡的破绽不随健康度变化");
|
2026-11-03 18:34:57 +08:00
|
|
|
|
var clean = new Unit { Model = "RTX70", Health = 100 };
|
|
|
|
|
|
var bad = new Unit { Model = "RTX70", Health = 100, Look = new CardLook { Fan = 2, Fingers = 2 } };
|
2026-11-03 13:37:48 +08:00
|
|
|
|
Check(Near(bad.Hash(ModuleBonus.None) / clean.Hash(ModuleBonus.None), 0.92 * 0.94, 1e-9), "风扇缺角 + 金手指氧化:算力 ×0.92 ×0.94");
|
2026-11-03 11:40:10 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 09:27:28 +08:00
|
|
|
|
Test("杰克逊博士:没有矿架必上一个;基础卡每次两张;进阶第 10 天起、旗舰第 30 天起;特供货第 20 天起必有旗舰", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var rng = new Rng(11);
|
|
|
|
|
|
var first = Dealer.Stock(1, false, false, rng);
|
|
|
|
|
|
Check(first.Count(k => k == Dealer.Rig) == 1 && first.Count(k => Dealer.Basic.Contains(k)) == 2 && first.Count == 3, $"第 1 天:矿架 + 两张基础卡:{string.Join(",", first)}");
|
|
|
|
|
|
int adv = 0, flag = 0, rigs = 0, n = 4000;
|
|
|
|
|
|
for (int i = 0; i < n; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var s9 = Dealer.Stock(9, true, false, rng);
|
|
|
|
|
|
Check(!s9.Any(k => Dealer.Advanced.Contains(k) || Dealer.Flagship.Contains(k)), "第 9 天没有进阶与旗舰");
|
|
|
|
|
|
var s40 = Dealer.Stock(40, true, false, rng);
|
|
|
|
|
|
if (s40.Any(k => Dealer.Advanced.Contains(k))) adv++;
|
|
|
|
|
|
if (s40.Any(k => Dealer.Flagship.Contains(k))) flag++;
|
|
|
|
|
|
if (s40.Contains(Dealer.Rig)) rigs++;
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(Near(adv / (double)n, 0.60, 0.03) && Near(flag / (double)n, 0.25, 0.03) && Near(rigs / (double)n, 0.30, 0.03),
|
|
|
|
|
|
$"第 40 天:进阶 {adv / (double)n:P0}(60%),旗舰 {flag / (double)n:P0}(25%),已有矿架时再上 {rigs / (double)n:P0}(30%)");
|
|
|
|
|
|
Check(Dealer.Stock(20, true, true, rng).Any(k => Dealer.Flagship.Contains(k)) && !Dealer.Stock(19, true, true, new Rng(1)).Any(k => Dealer.Flagship.Contains(k)),
|
|
|
|
|
|
"特供货:第 20 天起必有旗舰,第 19 天没有");
|
|
|
|
|
|
Check(Dealer.Basic.Concat(Dealer.Advanced).Concat(Dealer.Flagship).All(m => Hardware.Specs.ContainsKey(m) && !Hardware.Specs[m].Asic), "上架的型号都是登记过的显卡");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 09:11:46 +08:00
|
|
|
|
Test("存档:市场、稳定币、全网、钱包、交易所、随机数序列化后读回,之后的结果一致", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = Market.CreateDefault(3);
|
|
|
|
|
|
var st = new Stablecoins();
|
|
|
|
|
|
var net = new Network();
|
|
|
|
|
|
var w = new Wallet { Address = "hot", Taint = 12, Basis = 34 };
|
|
|
|
|
|
w.Add(Assets.PSC, 5);
|
|
|
|
|
|
var ex = new Exchange();
|
|
|
|
|
|
ex.History.Add(new KeyValuePair<int, double>(2, 100));
|
|
|
|
|
|
var rng = new Rng(77);
|
|
|
|
|
|
for (int d = 1; d <= 5; d++) { m.Night(new MarketEvent[0], rng, d, a => 1); st.Night(d, rng, 0); net.Night(d, m.Price, 10); }
|
|
|
|
|
|
var bundle = new SaveBundle { Market = m, Stable = st, Network = net, Hot = w, Exchange = ex, Rng = rng };
|
|
|
|
|
|
string json = Newtonsoft.Json.JsonConvert.SerializeObject(bundle);
|
|
|
|
|
|
var back = Newtonsoft.Json.JsonConvert.DeserializeObject<SaveBundle>(json);
|
|
|
|
|
|
Check(Near(back.Market.Price, m.Price, 1e-9) && back.Market.Pools.Count == m.Pools.Count && back.Market.Pools[1].Id == m.Pools[1].Id, "交易对与价格");
|
|
|
|
|
|
Check(back.Hot.Get(Assets.PSC) == 5 && back.Hot.Taint == 12 && back.Hot.Basis == 34, "钱包余额、污点、成本");
|
|
|
|
|
|
Check(back.Exchange.History.Count == 1 && back.Exchange.History[0].Value == 100, "出金记录");
|
|
|
|
|
|
Check(Near(back.Network.NpcHash, net.NpcHash, 1e-9) && Near(back.Stable.CraSubsidy, st.CraSubsidy, 1e-9), "全网算力、CRA 补贴池");
|
|
|
|
|
|
for (int d = 6; d <= 10; d++)
|
|
|
|
|
|
{
|
|
|
|
|
|
m.Night(new MarketEvent[0], rng, d, a => 1);
|
|
|
|
|
|
back.Market.Night(new MarketEvent[0], back.Rng, d, a => 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(Near(back.Market.Price, m.Price, 1e-9), "读回后继续推进,行情与原来完全相同");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 16:01:20 +08:00
|
|
|
|
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);
|
2026-11-03 18:11:20 +08:00
|
|
|
|
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), "污点 = 货值 × 热度%,成本 = 货值(溢价不带污点)");
|
2026-11-03 16:01:20 +08:00
|
|
|
|
Check(CoinPayment.Receive(w, Assets.CRC, 0, 1, 10) == 0, "零金额不收");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 16:39:40 +08:00
|
|
|
|
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);
|
2026-11-03 17:21:15 +08:00
|
|
|
|
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), "两轮:手续费与污点按轮数递减");
|
2026-11-03 16:39:40 +08:00
|
|
|
|
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, "制裁期间提出的币被标记");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 16:51:12 +08:00
|
|
|
|
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}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 19:35:48 +08:00
|
|
|
|
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, "声望不够不开放");
|
2026-11-03 20:06:32 +08:00
|
|
|
|
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%");
|
2026-11-03 19:35:48 +08:00
|
|
|
|
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");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 19:48:26 +08:00
|
|
|
|
Test("黑市订单", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Func<string, double> price = a => a == Assets.CRT ? 1 : 100;
|
|
|
|
|
|
Func<string, long> value = id => 200;
|
|
|
|
|
|
var bm = new BlackMarket();
|
2026-11-03 20:06:32 +08:00
|
|
|
|
bm.RefreshOrders(10, 75, new Rng(5), value);
|
2026-11-03 21:12:35 +08:00
|
|
|
|
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), "目录:商家与单价(基础价 × 商家系数)");
|
2026-11-03 20:50:58 +08:00
|
|
|
|
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%,提前放款打九折");
|
2026-11-03 19:48:26 +08:00
|
|
|
|
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);
|
2026-11-03 20:50:58 +08:00
|
|
|
|
var lg = new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 1000 };
|
|
|
|
|
|
var lb = new BmListing { Qty = 99, Seller = 1, Item = "x", Price = 1000 };
|
2026-11-03 19:48:26 +08:00
|
|
|
|
var w = new Wallet { Address = "t" };
|
|
|
|
|
|
w.Add(Assets.CRT, 10000); w.Taint = 4000; w.Basis = 5000;
|
2026-11-03 20:50:58 +08:00
|
|
|
|
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");
|
2026-11-03 19:48:26 +08:00
|
|
|
|
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), "骗子卖家 + 托管:退款,污点与成本一并还回(不能借退款洗钱)");
|
2026-11-03 20:50:58 +08:00
|
|
|
|
var o2 = bm.Place(lb, 1, PayMode.Advance, w, Assets.CRT, 20, price);
|
2026-11-03 20:06:32 +08:00
|
|
|
|
var ev2 = bm.Night(23, false, w, new Rng(2));
|
2026-11-03 19:48:26 +08:00
|
|
|
|
Check(o2.State == OrderState.Lost && Near(w.Get(Assets.CRT), 10000 - 900, 1e-6), "骗子卖家 + 提前放款:钱没了");
|
2026-11-03 20:50:58 +08:00
|
|
|
|
var o3 = bm.Place(lg, 1, PayMode.Escrow, w, Assets.CRT, 30, price);
|
2026-11-03 19:48:26 +08:00
|
|
|
|
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);
|
2026-11-03 20:50:58 +08:00
|
|
|
|
var oo = t.Place(new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 100 }, 1, PayMode.Escrow, ww, Assets.CRT, 1, price);
|
2026-11-03 19:48:26 +08:00
|
|
|
|
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);
|
2026-11-03 20:50:58 +08:00
|
|
|
|
var co = c.Place(new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 100 }, 1, PayMode.Escrow, cw, Assets.CRT, 1, price);
|
2026-11-03 19:48:26 +08:00
|
|
|
|
c.Night(co.DueDay, true, cw, new Rng(1));
|
|
|
|
|
|
Check(co.State == OrderState.Pending && co.DueDay == 5, "镇压期间到期顺延一天");
|
2026-11-03 20:13:18 +08:00
|
|
|
|
// 蜜罐期间下单会被追查;受控交付签收记罪证、拒签不记
|
|
|
|
|
|
var h = new BlackMarket(); h.Sellers.Add(good); h.HoneypotUntil = 12;
|
|
|
|
|
|
var hw = new Wallet(); hw.Add(Assets.CRT, 5000);
|
2026-11-03 20:50:58 +08:00
|
|
|
|
var ho = h.Place(new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 100 }, 1, PayMode.Escrow, hw, Assets.CRT, 10, price);
|
2026-11-03 20:13:18 +08:00
|
|
|
|
Check(h.Traps.Count == 1 && h.Traps[0].Day == 10 + Tuning.BmTrapDelay, "蜜罐期间下单:记一笔追查");
|
2026-11-03 21:44:19 +08:00
|
|
|
|
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, "追查到期:产生罪证事件");
|
2026-11-03 20:13:18 +08:00
|
|
|
|
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);
|
2026-11-03 20:50:58 +08:00
|
|
|
|
var o2b = t2.Place(new BmListing { Qty = 99, Seller = 0, Item = "x", Price = 100 }, 1, PayMode.Escrow, w2, Assets.CRT, 1, price);
|
2026-11-03 20:13:18 +08:00
|
|
|
|
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, "治安声望越高越容易被接管,有封顶");
|
2026-11-03 21:41:12 +08:00
|
|
|
|
// 没收到货的订单不会被追查:骗子退款后蜜罐追查取消
|
|
|
|
|
|
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, "骗子卖家退款后追查取消(没拿到货不记罪证)");
|
2026-11-03 19:48:26 +08:00
|
|
|
|
// 跑路:托管资金多则概率高;跑路后关闭
|
|
|
|
|
|
var r = new BlackMarket(); r.Sellers.Add(good);
|
|
|
|
|
|
var rw = new Wallet(); rw.Add(Assets.CRT, 1000000);
|
2026-11-03 20:50:58 +08:00
|
|
|
|
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);
|
2026-11-03 19:48:26 +08:00
|
|
|
|
Check(r.RugChance > BlackMarketTestRug(), "托管资金越多越容易跑路");
|
|
|
|
|
|
r.ClosedUntil = 40;
|
2026-11-03 20:50:58 +08:00
|
|
|
|
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, "关闭期间不能下单");
|
2026-11-03 19:48:26 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 20:23:21 +08:00
|
|
|
|
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), "跑路:上架与未放款全部归零");
|
2026-11-03 20:54:17 +08:00
|
|
|
|
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 件");
|
2026-11-03 21:50:26 +08:00
|
|
|
|
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, "已成交的货不能改价");
|
2026-11-03 20:23:21 +08:00
|
|
|
|
// 镇压期间不推进
|
|
|
|
|
|
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, "镇压期间平台下线,不成交");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 20:38:22 +08:00
|
|
|
|
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%");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 20:50:58 +08:00
|
|
|
|
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);
|
2026-11-03 21:12:35 +08:00
|
|
|
|
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("同类型商家重复");
|
2026-11-03 20:50:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
double avg = sum / n;
|
|
|
|
|
|
Check(avg > 8000 && avg < 40000, $"每周货架总值(游戏里实测平均约 1.4 万,这里用统一基础价 250):平均 {avg:0}");
|
2026-11-03 21:12:35 +08:00
|
|
|
|
Check(bulk > n * 0.15 && bulk < n * 0.35 && types.Count >= 20, $"大宗商家约 {Tuning.BmBulkChance:P0} 的周出现:{bulk}/{n};七种类型都出现过");
|
2026-11-03 20:50:58 +08:00
|
|
|
|
// 批量下单:折扣、库存扣减
|
|
|
|
|
|
Func<string, double> price = a => 1;
|
2026-11-03 21:02:54 +08:00
|
|
|
|
var m = new BlackMarket(); m.Sellers.Add(new BmSeller { Name = "a", Trades = 100, Type = MerchantType.Modules, Factor = 0.8 });
|
2026-11-03 20:50:58 +08:00
|
|
|
|
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 };
|
2026-11-03 21:02:54 +08:00
|
|
|
|
Check(bs.Days == Tuning.BmBulkDays && new BmSeller { Type = MerchantType.Modules }.Days == Tuning.BmDoorDays, "大宗商家送货更久");
|
2026-11-03 20:50:58 +08:00
|
|
|
|
// 出售量推高污点
|
|
|
|
|
|
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 天成交额推高污点,过期不算");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 21:12:35 +08:00
|
|
|
|
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}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 22:29:42 +08: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"), "格式不对的地址不合法");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 00:58:14 +08:00
|
|
|
|
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} 周)");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 00:26:50 +08:00
|
|
|
|
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, "基金见底,盈利空头被自动减仓");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 00:06:16 +08:00
|
|
|
|
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%");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 23:29:54 +08:00
|
|
|
|
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, "只有拥堵时的慢档会被丢");
|
2026-11-03 23:32:50 +08:00
|
|
|
|
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");
|
2026-11-03 23:29:54 +08:00
|
|
|
|
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, "记录最新在前、有上限");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 22:58:24 +08:00
|
|
|
|
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), "钱越多越快");
|
2026-11-03 23:05:13 +08:00
|
|
|
|
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)");
|
2026-11-03 22:58:24 +08:00
|
|
|
|
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}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 23:36:20 +08:00
|
|
|
|
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 不带");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 22:58:24 +08:00
|
|
|
|
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}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 07:26:27 +08:00
|
|
|
|
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, "买入成功");
|
2026-11-04 07:33:42 +08:00
|
|
|
|
var pos = bk.PosOf(m.Id, acct.Address);
|
2026-11-04 07:26:27 +08:00
|
|
|
|
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}");
|
2026-11-04 07:33:42 +08:00
|
|
|
|
Check(Math.Abs(acct.Get(Assets.CRC) - before - got) < 1e-6 && bk.PosOf(m.Id, acct.Address) == null, "回到账户");
|
2026-11-04 07:26:27 +08:00
|
|
|
|
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
|
2026-11-04 07:33:42 +08:00
|
|
|
|
bk.Night(4, new[] { "oxymoreWave" }, m => null, _ => acct, rng);
|
2026-11-04 07:26:27 +08:00
|
|
|
|
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);
|
2026-11-04 07:33:42 +08:00
|
|
|
|
bk.Night(5, new[] { "oxymoreWave", "powerShortage" }, bk.Happened, _ => acct, rng);
|
2026-11-04 07:26:27 +08:00
|
|
|
|
Check(win.Outcome < 0 && lose.Outcome < 0, "窗口没结束不结算");
|
2026-11-04 07:33:42 +08:00
|
|
|
|
var evs = bk.Night(6, new[] { "oxymoreWave", "powerShortage" }, bk.Happened, _ => acct, rng);
|
2026-11-04 07:26:27 +08:00
|
|
|
|
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);
|
2026-11-04 07:33:42 +08:00
|
|
|
|
var ev2 = bk.Night(9, new string[0], bk.Happened, _ => acct, rng);
|
2026-11-04 07:26:27 +08:00
|
|
|
|
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 _);
|
2026-11-04 07:33:42 +08:00
|
|
|
|
var e = b2.Night(5, new string[0], mm => true, _ => a2, new Rng((ulong)(i + 1)));
|
2026-11-04 07:26:27 +08:00
|
|
|
|
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 _);
|
2026-11-04 07:33:42 +08:00
|
|
|
|
Check(b3.Night(5, new string[0], mm => true, _ => a3, new Rng(1))[0].Kind == "win", "小额内幕安全");
|
2026-11-04 07:26:27 +08:00
|
|
|
|
// 被查:只退本金
|
|
|
|
|
|
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++)
|
|
|
|
|
|
{
|
2026-11-04 07:33:42 +08:00
|
|
|
|
var bb = new PredictBook(); bb.Markets.Add(m4); bb.Positions.AddRange(b4.Positions.Select(x => new PredictPos { Market = x.Market, Wallet = x.Wallet, Yes = x.Yes, Shares = x.Shares, Cost = x.Cost, Insider = x.Insider }));
|
2026-11-04 07:26:27 +08:00
|
|
|
|
var aa = new Wallet { Address = "p" };
|
|
|
|
|
|
m4.Outcome = -1;
|
2026-11-04 07:33:42 +08:00
|
|
|
|
var ee = bb.Night(5, new string[0], mm => true, _ => aa, new Rng(sd));
|
2026-11-04 08:36:17 +08:00
|
|
|
|
if (ee[0].Kind == "caught") { Check(aa.Get(Assets.CRC) < 1e-6, "被查本金和收益全部没收"); break; }
|
2026-11-04 07:26:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 07:33:42 +08:00
|
|
|
|
Test("预测市场:多钱包持仓各回各的钱包,限额与内幕按盘口合计", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Func<string, double> pr = a => 1;
|
|
|
|
|
|
var w1 = new Wallet { Address = "0xa" }; var w2 = new Wallet { Address = "0xb" };
|
|
|
|
|
|
w1.Add(Assets.CRC, 1e6); w2.Add(Assets.CRC, 1e6);
|
|
|
|
|
|
Func<string, Wallet> pick = ad => ad == "0xa" ? w1 : w2;
|
|
|
|
|
|
var bk = new PredictBook();
|
|
|
|
|
|
var m = PredictMarket.Create(++bk.Seq, PredictKind.Event, "e", "t", 0, 4, 5, 5, 0.1); bk.Markets.Add(m);
|
|
|
|
|
|
double half = Tuning.PredictInsiderShare * m.ShareCap * 0.6;
|
|
|
|
|
|
Check(bk.Buy(w1, m, true, m.BuyCost(true, half) * 1.02, 4, pr, true, out _) == PredictResult.Ok, "钱包 A 买入");
|
|
|
|
|
|
Check(bk.Buy(w2, m, false, 10, 4, pr, false, out _) == PredictResult.Opposite, "另一个钱包也不能买对面");
|
|
|
|
|
|
Check(bk.Buy(w2, m, true, m.BuyCost(true, half) * 1.02, 4, pr, true, out _) == PredictResult.Ok, "钱包 B 买入同一边");
|
|
|
|
|
|
Check(bk.PosAll(m.Id).Count == 2 && bk.PosOf(m.Id, "0xa") != null && bk.PosOf(m.Id, "0xb") != null, "每个钱包各一个持仓");
|
|
|
|
|
|
Check(bk.SellAll(w2, m, 4, out double got) == PredictResult.Ok && got > 0 && bk.PosOf(m.Id, "0xb") == null && bk.PosOf(m.Id, "0xa") != null, "卖出只动自己钱包的持仓");
|
|
|
|
|
|
Check(bk.Buy(w2, m, true, m.BuyCost(true, half) * 1.02, 4, pr, true, out _) == PredictResult.Ok, "再买");
|
|
|
|
|
|
// 两边各 0.6×查处线,合计超线:拆钱包躲不过
|
|
|
|
|
|
int caught = 0;
|
|
|
|
|
|
for (ulong sd = 1; sd <= 60; sd++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var b2 = new PredictBook(); b2.Markets.Add(new PredictMarket { Id = m.Id, Kind = m.Kind, Key = m.Key, Title = m.Title, FromDay = 5, ToDay = 5, P0 = m.P0, B = m.B, QYes = m.QYes, QNo = m.QNo });
|
|
|
|
|
|
foreach (var p in bk.Positions) b2.Positions.Add(new PredictPos { Market = p.Market, Wallet = p.Wallet, Yes = p.Yes, Shares = p.Shares, Cost = p.Cost, Insider = p.Insider });
|
|
|
|
|
|
double a0 = w1.Get(Assets.CRC), b0 = w2.Get(Assets.CRC);
|
|
|
|
|
|
var ev = b2.Night(5, new string[0], x => true, pick, new Rng(sd));
|
|
|
|
|
|
if (ev[0].Kind == "caught") caught++;
|
|
|
|
|
|
else Check(w1.Get(Assets.CRC) > a0 && w2.Get(Assets.CRC) > b0, "赢了各自的钱包都收到兑付");
|
|
|
|
|
|
w1.Balances[Assets.CRC] = a0; w2.Balances[Assets.CRC] = b0;
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(caught > 15 && caught < 45, $"分到两个钱包的内幕持仓合计后约一半被查:{caught}/60");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 07:53:12 +08:00
|
|
|
|
Test("预测市场:夜间消息反应、情报、新盘口先验", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var rng = new Rng(9);
|
|
|
|
|
|
var bk = new PredictBook();
|
|
|
|
|
|
var m = PredictMarket.Create(++bk.Seq, PredictKind.Event, "e", "t", 0, 5, 8, 9, 0.1); bk.Markets.Add(m);
|
|
|
|
|
|
double sum = 0; int n = 400;
|
|
|
|
|
|
for (int i = 0; i < n; i++) { m.QYes = m.QNo + m.B * Math.Log(0.1 / 0.9); bk.Drift(5, x => 0.9, rng); sum += m.PYes; }
|
|
|
|
|
|
Check(sum / n > 0.4 && sum / n < 0.6, $"一晚把 10% 往 90% 拉近约一半:{sum / n:0.00}");
|
|
|
|
|
|
var closing = PredictMarket.Create(++bk.Seq, PredictKind.Event, "e", "t", 0, 5, 6, 6, 0.1); bk.Markets.Add(closing);
|
|
|
|
|
|
double before = closing.PYes; bk.Drift(5, x => 0.9, rng);
|
|
|
|
|
|
Check(Math.Abs(closing.PYes - before) < 1e-12, "明天封盘的盘口不再反应");
|
2026-11-04 08:36:17 +08:00
|
|
|
|
var mv = bk.Drift(5, x => 0.9, rng);
|
|
|
|
|
|
Check(mv.Count == 1 && mv[0].Market == m && Math.Abs(mv[0].To - m.PYes) < 1e-12 && m.PrevP == mv[0].From, "反应记下前后价格");
|
2026-11-04 07:53:12 +08:00
|
|
|
|
Check(m.PYes >= Tuning.PredictPMin && m.PYes <= Tuning.PredictPMax, "价格夹在范围内");
|
|
|
|
|
|
// 反应是有噪声的:同一目标多次结果不全一样
|
|
|
|
|
|
var set = new System.Collections.Generic.HashSet<double>();
|
|
|
|
|
|
for (int i = 0; i < 20; i++) { m.QYes = m.QNo + m.B * Math.Log(0.1 / 0.9); bk.Drift(5, x => 0.5, rng); set.Add(Math.Round(m.PYes, 4)); }
|
|
|
|
|
|
Check(set.Count > 10, "噪声让每晚的价格不同");
|
|
|
|
|
|
// 情报:三档准确率和假情报
|
|
|
|
|
|
foreach (int tier in new[] { 0, 1, 2 })
|
|
|
|
|
|
{
|
|
|
|
|
|
int right = 0, trials = 4000;
|
|
|
|
|
|
for (int i = 0; i < trials; i++) if (PredictBook.MakeTip(m, true, tier, rng).Claim) right++;
|
|
|
|
|
|
double expect = (1 - Tuning.IntelFake) * Tuning.IntelTipAcc[tier] + Tuning.IntelFake * 0;
|
|
|
|
|
|
Check(Math.Abs(right / (double)trials - expect) < 0.04, $"第 {tier + 1} 档说对的比例约 {expect:0.00}:{right / (double)trials:0.00}");
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(Math.Abs(PredictBook.MakeTip(m, true, 2, rng).Paid - Tuning.IntelTipPrice[2] * m.MaxLoss) < 1e-9, "价格按盘口最大亏损的比例");
|
|
|
|
|
|
// 到期后消息一起清掉
|
|
|
|
|
|
bk.Tips.Add(new PredictTip { Market = closing.Id });
|
|
|
|
|
|
var w = new Wallet { Address = "x" };
|
|
|
|
|
|
bk.Night(6, new string[0], mm => true, _ => w, rng);
|
|
|
|
|
|
Check(bk.Tips.Count == 0, "盘口结算后情报作废");
|
2026-11-04 08:36:17 +08:00
|
|
|
|
Check(bk.Stats.TryGetValue("Event", out var stE) && stE.Count >= 1 && stE.Hits >= 1, "结算计入按类型的历史");
|
2026-11-04 07:53:12 +08:00
|
|
|
|
// gas / CRA / 时代 先验
|
|
|
|
|
|
double flat = PredictMath.GasFlat(10, 1.2);
|
|
|
|
|
|
double phase0 = PredictMath.GasAtLeast(10, 0, 1.2), sum2 = 0;
|
|
|
|
|
|
for (int ph = 0; ph < Tuning.TxGasPeriod; ph++) sum2 += PredictMath.GasAtLeast(10, ph, 1.2);
|
|
|
|
|
|
Check(Math.Abs(flat - sum2 / Tuning.TxGasPeriod) < 1e-12 && phase0 >= 0 && phase0 <= 1, "gas 先验是各相位的平均");
|
|
|
|
|
|
Check(PredictMath.GasAtLeast(10, 0, 0.1) == 1 && PredictMath.GasAtLeast(10, 0, 5) == 0, "gas 阈值越界取 0 和 1");
|
|
|
|
|
|
int hit = 0, runs = 20000; var r2 = new Rng(4);
|
|
|
|
|
|
for (int i = 0; i < runs; i++) if (TxRules.Gas(10, 3, r2) >= 1.3) hit++;
|
|
|
|
|
|
Check(Math.Abs(hit / (double)runs - PredictMath.GasAtLeast(10, 3, 1.3)) < 0.02, "gas 概率和真实抽样对得上");
|
|
|
|
|
|
double h = 1 - Math.Pow(1 - Tuning.CraSpiralWeekly, 1.0 / 7);
|
|
|
|
|
|
Check(Math.Abs(PredictMath.CraWarnProb(1e9, 100, 10, 12, 14, h) - (1 - Math.Pow(1 - h, 3)) * Math.Pow(1 - h, 2)) < 1e-9, "补贴池充足时按日概率,窗口前发出的不算");
|
|
|
|
|
|
Check(PredictMath.CraWarnProb(150, 100, 10, 11, 12, h) > 0.99, "补贴池撑不过窗口前一晚就必发");
|
|
|
|
|
|
Check(PredictMath.CraWarnProb(50, 100, 10, 10, 10, h) == 1, "归零那晚必发");
|
|
|
|
|
|
Check(PredictMath.EraProb(60, 61, 63, -1) > 0 && PredictMath.EraProb(60, 61, 63, -1) < 0.2, "没预警时在 50–90 里均匀,3 天窗口很小");
|
|
|
|
|
|
Check(Math.Abs(PredictMath.EraProb(60, 62, 63, 58) - 0.5) < 1e-9 || PredictMath.EraProb(60, 62, 63, 58) > 0, "预警后落在 4–7 天内");
|
|
|
|
|
|
Check(PredictMath.EraProb(60, 100, 105, -1) == 0, "窗口在时代区间之外为 0");
|
|
|
|
|
|
Check(PredictMath.EraProb(60, 62, 65, 58) > 0.5 && PredictMath.EraProb(60, 70, 71, 58) == 0, "预警后窗口覆盖区间就是高概率");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 08:03:44 +08:00
|
|
|
|
Test("操纵检出:被查概率随推价幅度升高,推价每晚衰减,行情盘押中按方向被判", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Check(Market.ManipChance(0.01) == 0 && Market.ManipChance(-0.01) == 0, "幅度低于免查线不查");
|
|
|
|
|
|
Check(Market.ManipChance(0.05) > Market.ManipChance(0.03) && Market.ManipChance(-0.05) == Market.ManipChance(0.05), "幅度越大越容易被查,方向无关");
|
|
|
|
|
|
Check(Market.ManipChance(5) == Tuning.ManipMax, "封顶");
|
|
|
|
|
|
var mk = Market.CreateDefault(5); double p0 = mk.Price;
|
|
|
|
|
|
mk.PlayerPush += Math.Log(1.1);
|
|
|
|
|
|
Check(Math.Abs(mk.NeutralPrice - p0 / 1.1 * 1.0) < 1e-6 || mk.NeutralPrice > 0, "没有玩家时的价格");
|
|
|
|
|
|
double before = mk.PlayerPush; mk.Night(new List<MarketEvent>(), new Rng(1), 5, a => 1);
|
|
|
|
|
|
Check(Math.Abs(mk.PlayerPush - before * (1 - Tuning.Revert)) < 1e-9, "推价随回归衰减");
|
|
|
|
|
|
// 行情盘:玩家推高后押「是」赢了,按概率被判操纵;押「否」不算;没推价不查
|
|
|
|
|
|
int manip = 0, win = 0, noPush = 0;
|
|
|
|
|
|
for (ulong sd = 1; sd <= 200; sd++)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var (yes, push, outcome) in new[] { (true, 0.06, true), (false, 0.06, false), (true, 0.0, true) })
|
|
|
|
|
|
{
|
|
|
|
|
|
var bk = new PredictBook(); var w = new Wallet { Address = "w" }; w.Add(Assets.CRC, 1e5);
|
|
|
|
|
|
var m = PredictMarket.Create(++bk.Seq, PredictKind.Price, "PSC", "PSC", 100, 4, 5, 5, 0.3); bk.Markets.Add(m);
|
|
|
|
|
|
bk.Buy(w, m, yes, 510, 4, a => 1, false, out _);
|
|
|
|
|
|
var ev = bk.Night(5, new string[0], x => outcome, _ => w, new Rng(sd), push);
|
|
|
|
|
|
if (ev[0].Kind == "manip") { if (yes && push > 0) manip++; else Check(false, "不该被判操纵"); Check(ev[0].Fraud > 0, "记罪证"); Check(w.Get(Assets.CRC) < 1e5 - 400, "本金没收"); }
|
|
|
|
|
|
else if (yes && push > 0) win++; else if (push == 0) noPush++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
double expect = Market.ManipChance(0.06) * 200;
|
|
|
|
|
|
Check(Math.Abs(manip - expect) < 40 && manip + win == 200 && noPush == 200, $"推价 6% 约 {expect:0} 次被判,被判 {manip},没被判 {win}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 08:11:03 +08:00
|
|
|
|
Test("高息理财:开张条件、每晚存入与利息、崩盘、被举报、卷走", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var acct = new Wallet { Address = "pb" }; acct.Add(Assets.CRC, 5000);
|
|
|
|
|
|
var p = new Ponzi();
|
|
|
|
|
|
Check(!p.Start(acct, 0.04, Tuning.PonziMinDay - 1), "没到开放日不能开");
|
|
|
|
|
|
Check(p.Start(acct, 0.04, Tuning.PonziMinDay) && Math.Abs(acct.Get(Assets.CRC) - (5000 - Tuning.PonziAdCost)) < 1e-9, "开张扣宣传费");
|
|
|
|
|
|
Check(!p.CanOpen(Tuning.PonziMinDay + 1), "已经开着不能再开");
|
|
|
|
|
|
// 池子增长不超过上限;利息先用新存入的付,不够才动账户
|
|
|
|
|
|
var rng = new Rng(3);
|
|
|
|
|
|
double maxPool = 0; bool ended = false;
|
|
|
|
|
|
for (int d = Tuning.PonziMinDay; d < Tuning.PonziMinDay + 40 && p.Open; d++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ev = p.Night(d, 50, acct, rng);
|
|
|
|
|
|
maxPool = Math.Max(maxPool, p.Pool);
|
|
|
|
|
|
Check(p.Pool <= Ponzi.Cap(d) + 1e-6, "池子不超过上限");
|
|
|
|
|
|
if (ev != null) { ended = true; Check(ev.Amount >= 0 && p.Done && !p.Open && p.Pool == 0, "崩盘后池子清零、产品关闭"); }
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(maxPool > 0 && ended, "池子涨起来、最后会崩盘(40 晚内)");
|
|
|
|
|
|
Check(!p.CanOpen(100) && p.Rug(acct) == 0, "崩盘后不能重开也不能卷走");
|
|
|
|
|
|
// 付不出利息就挤兑
|
|
|
|
|
|
var poor = new Wallet { Address = "x" }; poor.Add(Assets.CRC, Tuning.PonziAdCost);
|
|
|
|
|
|
var q = new Ponzi(); q.Start(poor, 0.10, 20);
|
|
|
|
|
|
q.Pool = 100000; var ev2 = q.Night(20, 50, poor, new Rng(1));
|
|
|
|
|
|
Check(ev2 != null && ev2.Kind == "collapse", "账户里没钱补利息:崩盘");
|
|
|
|
|
|
// 卷走:池子进账户、污点全额
|
|
|
|
|
|
var w = new Wallet { Address = "y" }; w.Add(Assets.CRC, 2000);
|
|
|
|
|
|
var r = new Ponzi(); r.Start(w, 0.04, 20); r.Pool = 5000;
|
|
|
|
|
|
double cash = w.Get(Assets.CRC);
|
|
|
|
|
|
Check(Math.Abs(r.Rug(w) - 5000) < 1e-9 && Math.Abs(w.Get(Assets.CRC) - cash - 5000) < 1e-9 && Math.Abs(w.Taint - 5000) < 1e-9 && r.Done, "卷走:池子进账户,污点全额");
|
|
|
|
|
|
// 被举报概率随日息和池子升高
|
|
|
|
|
|
Check(Ponzi.ReportChance(0.10, 5000, 20) > Ponzi.ReportChance(0.02, 5000, 20) && Ponzi.ReportChance(0.04, 9000, 20) > Ponzi.ReportChance(0.04, 100, 20), "日息越高、池子越大越容易被举报");
|
|
|
|
|
|
Check(Ponzi.InflowMean(20, 0.08, 50) > Ponzi.InflowMean(20, 0.04, 50) * 2, "日息高于 4% 时存入按指数放大");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 08:28:43 +08:00
|
|
|
|
Test("NPC 骗局:插针、空投与授权、第三方收益池", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
// 插针:出现频率、幅度、标记价只吃一半、当晚回去
|
|
|
|
|
|
int wicks = 0, n = 4000;
|
|
|
|
|
|
for (int i = 0; i < n; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var mk = Market.CreateDefault(20); var rng = new Rng((ulong)(i + 1));
|
|
|
|
|
|
double p0 = mk.Price;
|
|
|
|
|
|
mk.Open(new List<MarketEvent>(), rng, 20);
|
|
|
|
|
|
double open = mk.OpenPrice;
|
|
|
|
|
|
double w = mk.TryWick(rng, 20);
|
|
|
|
|
|
if (w == 0) continue;
|
|
|
|
|
|
wicks++;
|
|
|
|
|
|
Check(Math.Abs(Math.Abs(Math.Log(mk.Price / open)) - Math.Abs(w)) < 1e-9 && Math.Abs(w) >= Tuning.WickMin && Math.Abs(w) <= Tuning.WickMax, "插针幅度");
|
|
|
|
|
|
Check(Math.Abs(mk.Mark - (open + mk.Price) / 2) < 1e-9 && mk.OpenPrice == open, "标记价用开门前的价格和插针价平均");
|
|
|
|
|
|
double back = mk.WickBack, before = mk.Price;
|
|
|
|
|
|
var none = new List<MarketEvent>();
|
|
|
|
|
|
// 夜里:先回去 WickBack,再走正常回归
|
|
|
|
|
|
var copy = Market.CreateDefault(20); copy.Main.SetPrice(before * Math.Exp(-w * back), Tuning.PoolDepth(20));
|
|
|
|
|
|
mk.Night(none, new Rng(9), 20, a => 1); copy.Night(none, new Rng(9), 20, a => 1);
|
|
|
|
|
|
Check(Math.Abs(mk.Price - copy.Price) / copy.Price < 0.05 && mk.Wick == 0, "当晚按比例回去");
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(Math.Abs(wicks / (double)n - Tuning.WickChance) < 0.015, $"插针频率约 {Tuning.WickChance}:{wicks / (double)n:0.000}");
|
|
|
|
|
|
// 空投
|
|
|
|
|
|
var sc = new Scams(); var r2 = new Rng(5);
|
|
|
|
|
|
var assets = new[] { Assets.CRT, Assets.PSC, Assets.CRC };
|
|
|
|
|
|
int scam = 0, typo = 0, unlimited = 0;
|
|
|
|
|
|
for (int i = 0; i < 2000; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var a = sc.NewAirdrop(10, r2, assets);
|
|
|
|
|
|
if (a.Scam) { scam++; if (double.IsPositiveInfinity(a.Approve) || a.Approve >= 20 * a.Reward - 10) unlimited++; if (!new[] { "Poinbase", "PSC Foundation", "RatDigg", "Revolution DAO", "Gutter Labs", "Station Rewards" }.Contains(a.Name)) typo++; }
|
|
|
|
|
|
else Check(!double.IsPositiveInfinity(a.Approve) && a.Approve <= 60, "正规空投不要大额授权");
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(Math.Abs(scam / 2000.0 - Tuning.AirdropScam) < 0.04 && unlimited == scam && typo > scam * 0.4, $"钓鱼占比 {scam / 2000.0:0.00},全部要无上限或远超奖励的授权,过半名字是仿冒 {typo}");
|
|
|
|
|
|
var wl = new Wallet { Address = "0xa" }; wl.Add(Assets.CRT, 1000);
|
|
|
|
|
|
var fake = new Airdrop { Id = 1, Name = "P0inbase", Asset = Assets.CRT, Approve = double.PositiveInfinity, Reward = 200, Scam = true, Expire = 12 };
|
|
|
|
|
|
var ap = sc.Claim(fake, wl, 10, r2, out double got);
|
|
|
|
|
|
Check(ap != null && ap.Scam && got < 20 && sc.Claim(fake, wl, 10, r2, out _) == null, "钓鱼空投只给诱饵,不能重复领");
|
|
|
|
|
|
sc.Airdrops.Clear(); sc.Pools.Clear();
|
|
|
|
|
|
var ev = sc.Night(ap.DrainDay, r2, x => x == "0xa" ? wl : null, assets);
|
|
|
|
|
|
Check(ev.Any(e => e.Kind == "drain" && Math.Abs(e.Amount - 1000) < 1e-9) && wl.Get(Assets.CRT) < 1e-9, "到期那晚转走这个币");
|
|
|
|
|
|
var wl2 = new Wallet { Address = "0xb" }; wl2.Add(Assets.CRT, 1000);
|
|
|
|
|
|
var ap2 = sc.Claim(new Airdrop { Id = 2, Name = "x", Asset = Assets.CRT, Approve = double.PositiveInfinity, Scam = true, Expire = 12 }, wl2, 10, r2, out _);
|
|
|
|
|
|
Check(sc.Revoke(ap2.Id), "撤销授权");
|
|
|
|
|
|
sc.Night(ap2.DrainDay, r2, x => wl2, assets);
|
|
|
|
|
|
Check(Math.Abs(wl2.Get(Assets.CRT) - 1000) < 1e-9, "撤销后不会被转走");
|
|
|
|
|
|
var good = sc.Claim(new Airdrop { Id = 3, Name = "Poinbase", Asset = Assets.CRT, Approve = 0, Reward = 50, Expire = 12 }, wl2, 10, r2, out double g2);
|
|
|
|
|
|
Check(good == null && Math.Abs(g2 - 50) < 1e-9, "正规空投给奖励、不留授权");
|
|
|
|
|
|
// 收益池:骗局概率随日息、时间锁、审计变化
|
|
|
|
|
|
Check(Scams.ScamChance(0.03, false, false) > Scams.ScamChance(0.01, false, false) && Scams.ScamChance(0.02, true, true) < Scams.ScamChance(0.02, false, false), "日息越高越像骗局,时间锁和审计降低概率");
|
|
|
|
|
|
// 正规池:计息、储备耗尽退本金;时间锁取款延迟到账
|
|
|
|
|
|
var pool = new YieldPool { Id = 1, Name = "ok", Rate = 0.02, Reserve = 100, Cap = 1e9, Timelock = true, OpenDay = 1 };
|
|
|
|
|
|
var sc2 = new Scams(); sc2.Pools.Add(pool);
|
|
|
|
|
|
var w3 = new Wallet { Address = "0xc" }; w3.Add(Assets.CRC, 3000);
|
|
|
|
|
|
Check(sc2.Deposit(pool, w3, 1000, a => 1) && Math.Abs(w3.Get(Assets.CRC) - 2000) < 1e-9, "存入");
|
|
|
|
|
|
sc2.Night(2, new Rng(1), x => w3, assets);
|
|
|
|
|
|
Check(Math.Abs(pool.Of("0xc") - 1020) < 1e-9 && Math.Abs(pool.Reserve - 80) < 1e-9, "计息从储备出");
|
|
|
|
|
|
Check(sc2.Withdraw(pool, w3, 3) == 0 && pool.Pending.Count == 1, "时间锁:进取款队列");
|
|
|
|
|
|
sc2.Night(3 + Tuning.YieldLockDays, new Rng(1), x => w3, assets);
|
|
|
|
|
|
Check(Math.Abs(w3.Get(Assets.CRC) - 3020) < 1e-9, "到期到账");
|
|
|
|
|
|
var pool2 = new YieldPool { Id = 2, Name = "dry", Rate = 0.03, Reserve = 10, Cap = 1e9, OpenDay = 1 };
|
|
|
|
|
|
sc2.Pools.Add(pool2); sc2.Deposit(pool2, w3, 1000, a => 1);
|
|
|
|
|
|
var evd = sc2.Night(5, new Rng(1), x => w3, assets);
|
|
|
|
|
|
Check(evd.Any(e => e.Kind == "dry") && pool2.Closed && Math.Abs(w3.Get(Assets.CRC) - 3020) < 1e-9, "储备不够付利息:退本金关闭");
|
|
|
|
|
|
// 骗局池:跑路
|
|
|
|
|
|
int rugs = 0;
|
|
|
|
|
|
for (ulong sd = 1; sd <= 400; sd++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = new YieldPool { Id = 9, Name = "scam", Rate = 0.03, Reserve = 1e9, Cap = 1e9, Scam = true, OpenDay = 1 };
|
|
|
|
|
|
var s3 = new Scams(); s3.Pools.Add(p);
|
|
|
|
|
|
var w4 = new Wallet { Address = "0xd" }; w4.Add(Assets.CRC, 100);
|
|
|
|
|
|
s3.Deposit(p, w4, 100, a => 1);
|
|
|
|
|
|
var e4 = s3.Night(2, new Rng(sd), x => w4, assets);
|
|
|
|
|
|
if (e4.Any(e => e.Kind == "rug")) { rugs++; Check(p.Closed && p.Total == 0 && w4.Get(Assets.CRC) < 1e-9, "跑路后存款没了"); }
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(Math.Abs(rugs / 400.0 - Tuning.YieldRugNightly) < 0.05, $"骗局池每晚跑路概率:{rugs / 400.0:0.00}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 08:36:17 +08:00
|
|
|
|
Test("预测市场:内幕先买后卖也会被查", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
int caught = 0, n = 400;
|
|
|
|
|
|
for (ulong sd = 1; sd <= (ulong)n; sd++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var bk = new PredictBook(); var w = new Wallet { Address = "w" }; w.Add(Assets.CRC, 1e6);
|
|
|
|
|
|
var m = PredictMarket.Create(++bk.Seq, PredictKind.Event, "e", "t", 0, 4, 7, 7, 0.05); bk.Markets.Add(m);
|
|
|
|
|
|
double target = Tuning.PredictInsiderShare * m.ShareCap * 1.02;
|
|
|
|
|
|
bk.Buy(w, m, true, m.BuyCost(true, target) * (1 + Tuning.PredictFee), 4, a => 1, true, out _);
|
|
|
|
|
|
double cost = bk.PosOf(m.Id, "w").Cost;
|
|
|
|
|
|
double p1 = m.PYes, p2 = p1 + (1 - p1) / 2;
|
|
|
|
|
|
m.QYes = m.QNo + m.B * Math.Log(p2 / (1 - p2)); // 夜里价格又涨了一截
|
|
|
|
|
|
bk.SellAll(w, m, 5, new Rng(sd), out double got, out double fraud);
|
|
|
|
|
|
if (fraud > 0) { caught++; Check(got == 0 && fraud <= Tuning.PredictFraudCap, "被查本金和收益全部没收"); }
|
|
|
|
|
|
else Check(got > cost, "没被查时赚到价差");
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(Math.Abs(caught / (double)n - Tuning.PredictInsiderCatch) < 0.08, $"约一半被查:{caught}/{n}");
|
|
|
|
|
|
// 小额内幕、非内幕卖出不查
|
|
|
|
|
|
var bk2 = new PredictBook(); var w2 = new Wallet { Address = "w" }; w2.Add(Assets.CRC, 1e6);
|
|
|
|
|
|
var m2 = PredictMarket.Create(++bk2.Seq, PredictKind.Event, "e", "t", 0, 4, 7, 7, 0.05); bk2.Markets.Add(m2);
|
|
|
|
|
|
bk2.Buy(w2, m2, true, 200, 4, a => 1, true, out _);
|
|
|
|
|
|
m2.QYes = m2.QNo + m2.B * Math.Log(0.6 / 0.4);
|
|
|
|
|
|
bk2.SellAll(w2, m2, 5, new Rng(1), out _, out double f2);
|
|
|
|
|
|
Check(f2 == 0, "小额内幕卖出安全");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 08:47:34 +08:00
|
|
|
|
Test("日 K:开盘、日内高低、成交量、收盘、均线、保留天数", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var cb = new CandleBook();
|
|
|
|
|
|
cb.Open(Assets.PSC, 5, 100);
|
|
|
|
|
|
cb.Tick(Assets.PSC, 5, 120, 3000);
|
|
|
|
|
|
cb.Tick(Assets.PSC, 5, 90, 500, true);
|
|
|
|
|
|
cb.Close(Assets.PSC, 5, 105);
|
|
|
|
|
|
var k = cb.Of(Assets.PSC)[0];
|
|
|
|
|
|
Check(k.O == 100 && k.H == 120 && k.L == 90 && k.C == 105 && k.Closed, "开高低收");
|
|
|
|
|
|
Check(k.VolNpc == 3000 && k.VolPlayer == 500, "成交量分大户和玩家");
|
|
|
|
|
|
cb.Tick(Assets.PSC, 5, 200);
|
|
|
|
|
|
Check(k.H == 120 && k.C == 105, "收盘后不再变");
|
|
|
|
|
|
cb.Tick(Assets.PSC, 6, 110);
|
|
|
|
|
|
Check(cb.Of(Assets.PSC).Count == 2 && cb.Of(Assets.PSC)[1].O == 110, "读档时没有当天的蜡烛就按现价开一根");
|
|
|
|
|
|
var cb2 = new CandleBook();
|
|
|
|
|
|
cb2.Open(Assets.PSC, 5, 100); cb2.Close(Assets.PSC, 5, 105);
|
|
|
|
|
|
cb2.Open(Assets.PSC, 6, 98);
|
|
|
|
|
|
var k6 = cb2.Of(Assets.PSC)[1];
|
|
|
|
|
|
Check(k6.O == 105 && k6.H == 105 && k6.L == 98 && k6.C == 98, "开盘接昨天收盘,夜间变动算进今天这根");
|
|
|
|
|
|
for (int d = 7; d < 7 + CandleBook.Keep + 10; d++) { cb.Open(Assets.PSC, d, d); cb.Close(Assets.PSC, d, d); }
|
|
|
|
|
|
Check(cb.Of(Assets.PSC).Count == CandleBook.Keep, "只保留最近若干天");
|
|
|
|
|
|
var ma = CandleBook.Ma(new List<Candle> { new Candle { C = 1 }, new Candle { C = 3 }, new Candle { C = 5 } }, 2);
|
|
|
|
|
|
Check(ma[0] == 1 && ma[1] == 2 && ma[2] == 4, "均线");
|
|
|
|
|
|
Check(cb.Of("SPARK").Count == 0, "没有的币种是空的");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 09:04:50 +08:00
|
|
|
|
Test("剧情联动:各选择的倍率与期限、只生效一次、SPARK 推涨", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var saved = StoryMods.Current;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var sm = new StoryMods(); StoryMods.Current = sm;
|
|
|
|
|
|
Check(sm.Apply("serpico_sec", 50) && !sm.Apply("serpico_sec", 51), "只生效一次");
|
|
|
|
|
|
Check(sm.Aml(60) == Tuning.StorySecAmlMult && sm.Aml(50 + Tuning.StorySecDays) == 1, "治安部整顿期内出金审查加重,到期恢复");
|
|
|
|
|
|
Check(sm.Sanction(60) == Tuning.StorySecSanctionMult && sm.Enforce(60) == Tuning.StorySecEnforceMult, "制裁与查处倍率");
|
|
|
|
|
|
sm.Apply("lun_arrested", 55);
|
|
|
|
|
|
Check(Math.Abs(sm.Enforce(60) - Tuning.StorySecEnforceMult * Tuning.StoryLunMult) < 1e-12 && sm.Enforce(55 + Tuning.StoryLunDays) == Tuning.StorySecEnforceMult, "伦警官立威期叠加,先到期");
|
|
|
|
|
|
// 出金审查真的变严
|
|
|
|
|
|
var ex = new Exchange(); var ex2 = new Exchange();
|
|
|
|
|
|
var w = new Wallet { Address = "p" }; w.Add(Assets.CRC, 10000); w.Taint = 3000;
|
|
|
|
|
|
var w2 = new Wallet { Address = "p" }; w2.Add(Assets.CRC, 10000); w2.Taint = 3000;
|
|
|
|
|
|
double hit = ex.Cashout(w, 2000, a => 1, 60, 300, false, new Rng(1)).HitChance;
|
|
|
|
|
|
StoryMods.Current = new StoryMods();
|
|
|
|
|
|
double hit0 = ex2.Cashout(w2, 2000, a => 1, 60, 300, false, new Rng(1)).HitChance;
|
|
|
|
|
|
Check(hit > hit0 && hit <= 1, $"命中率 {hit0:0.000} → {hit:0.000}");
|
|
|
|
|
|
// CRT 脱锚减半:镇压开始时脱锚的频率
|
|
|
|
|
|
int dep0 = 0, dep1 = 0;
|
|
|
|
|
|
for (ulong sd = 1; sd <= 2000; sd++)
|
|
|
|
|
|
{
|
|
|
|
|
|
StoryMods.Current = new StoryMods(); var s0 = new Stablecoins(); s0.OnEventStart(MarketEvent.Crackdown, 10, new Rng(sd)); if (s0.Crt < 1) dep0++;
|
|
|
|
|
|
var bm = new StoryMods(); bm.Apply("serpico_bm", 1); StoryMods.Current = bm;
|
|
|
|
|
|
var s1 = new Stablecoins(); s1.OnEventStart(MarketEvent.Crackdown, 10, new Rng(sd)); if (s1.Crt < 1) dep1++;
|
|
|
|
|
|
}
|
|
|
|
|
|
Check(dep1 < dep0 * 0.65 && dep1 > dep0 * 0.35, $"CRT 脱锚约减半:{dep0} → {dep1}");
|
|
|
|
|
|
// SPARK 推涨:几晚内一共涨到设定倍数
|
|
|
|
|
|
var rv = new StoryMods(); StoryMods.Current = rv; rv.Apply("serpico_rev", 10);
|
|
|
|
|
|
Check(rv.CraSpiral(20) == Tuning.StoryCraSpiralMult, "螺旋概率减半");
|
|
|
|
|
|
double prod = 1; for (int i = 0; i < 10; i++) prod *= rv.TakeSparkPush();
|
|
|
|
|
|
Check(Math.Abs(prod - Tuning.StorySerpicoSpark) < 1e-9 && rv.SparkPushNights == 0, $"SPARK 推涨合计 {prod:0.000}");
|
|
|
|
|
|
var lk = new StoryMods(); lk.Apply("serpico_rev", 1); lk.TakeSparkPush(); lk.Apply("eleanor_leak", 2);
|
|
|
|
|
|
double p2 = 1; for (int i = 0; i < 10; i++) p2 *= lk.TakeSparkPush();
|
|
|
|
|
|
Check(Math.Abs(p2 - Math.Pow(Tuning.StorySerpicoSpark, 1 - 1.0 / Tuning.StorySparkNights) * Tuning.StoryLeakSpark) < 1e-9, "两次推涨叠加");
|
|
|
|
|
|
Check(lk.CrcHaircut == Tuning.StoryLeakHaircutMult, "泄露数据盘:CRC 减记概率翻倍");
|
|
|
|
|
|
}
|
|
|
|
|
|
finally { StoryMods.Current = saved; }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 09:12:47 +08:00
|
|
|
|
Test("Tumbler 剧情线:开始条件、上门日程、混币器倍率", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var saved = StoryMods.Current;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var t = new Tumbler();
|
|
|
|
|
|
Check(!t.TryStart(Tuning.TumblerMinDay - 1, true) && !t.TryStart(40, false), "没到日子或没解锁混币器不开始");
|
|
|
|
|
|
Check(t.TryStart(40, true) && t.StartDay == 41 && !t.TryStart(42, true), "开始一次,明天第一次上门");
|
|
|
|
|
|
Check(t.Due(40) == 0 && t.Due(41) == 1, "第 1 次在开始日");
|
|
|
|
|
|
t.Visited(41);
|
|
|
|
|
|
Check(t.Due(41) == 0 && t.Due(43) == 0 && t.Due(44) == 2, "同一天不重复,第 2 次在 +3");
|
|
|
|
|
|
t.Visited(44); Check(t.Due(46) == 3, "第 3 次在 +5"); t.Choice = Tumbler.Hide; t.Visited(46);
|
|
|
|
|
|
Check(t.Due(49) == 0 && t.Due(50) == 4, "第 4 次在 +9"); t.Visited(50);
|
|
|
|
|
|
Check(t.Done && t.Due(60) == 0, "结束");
|
|
|
|
|
|
var t2 = new Tumbler(); t2.TryStart(40, true); t2.Visited(45);
|
|
|
|
|
|
Check(t2.Due(46) == 2, "错过的上门往后补");
|
|
|
|
|
|
// 混币器倍率
|
|
|
|
|
|
var sm = new StoryMods(); StoryMods.Current = sm;
|
|
|
|
|
|
double fee0 = Mixer.Fee, rate0 = Mixer.RoundRate(1000);
|
|
|
|
|
|
sm.Apply("tumbler_kept", 50);
|
|
|
|
|
|
Check(Math.Abs(Mixer.Fee - fee0 * Tuning.TumblerKeptFee) < 1e-12 && Mixer.RoundRate(1000) > rate0, "藏好密钥盘:手续费降、匿名集变大");
|
|
|
|
|
|
var sm2 = new StoryMods(); StoryMods.Current = sm2; sm2.Apply("tumbler_sold", 50);
|
|
|
|
|
|
Check(Mixer.Fee > fee0 && sm2.MixerRug == Tuning.TumblerSoldRug, "卖给黑市:手续费涨、跑路概率上升");
|
|
|
|
|
|
var sm3 = new StoryMods(); sm3.Apply("tumbler_reported", 50);
|
|
|
|
|
|
Check(sm3.MixerClosed, "举报:混币器关闭");
|
|
|
|
|
|
}
|
|
|
|
|
|
finally { StoryMods.Current = saved; }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 14:26:21 +08:00
|
|
|
|
Test("黑市商家收币:每类商家都收 CRT 或隐私币,平台收 PSC、CRT、CRA", () =>
|
2026-11-04 11:06:38 +08:00
|
|
|
|
{
|
|
|
|
|
|
foreach (MerchantType ty in Enum.GetValues(typeof(MerchantType)))
|
|
|
|
|
|
{
|
|
|
|
|
|
var acc = Merchants.Accepts(ty);
|
|
|
|
|
|
Check(acc.Length >= 2 && acc.Distinct().Count() == acc.Length && acc.All(a => a == Assets.PSC || a == Assets.CRT || a == Assets.CRA || a == Assets.Ponero), $"{ty} 收 {string.Join("/", acc)}");
|
|
|
|
|
|
}
|
2026-11-04 14:26:21 +08:00
|
|
|
|
Check(Merchants.Accepts(MerchantType.Arms).Contains(Assets.Ponero) && !Merchants.Accepts(MerchantType.Rigs).Contains(Assets.Ponero), "违禁类收隐私币,硬件类不收");
|
2026-11-04 11:06:38 +08:00
|
|
|
|
Check(Merchants.PlatformAccepts.Contains(Assets.CRT) && Tuning.BmAutoSwapSlip > Tuning.DexFee * 10, "自动换币的滑点远高于 DEX 手续费");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 12:45:29 +08:00
|
|
|
|
Test("手册数值占位符:中英文都能从 Tuning 取到值,渲染后不留花括号", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var dir = System.IO.Path.GetFullPath(System.IO.Path.Combine(AppContext.BaseDirectory, "../../../../../src/Decentralized/ModFiles/Localization"));
|
2026-11-04 14:26:21 +08:00
|
|
|
|
foreach (var lang in new[] { "zh", "en", "de", "fr", "es", "pl", "ru", "ja" })
|
2026-11-04 12:45:29 +08:00
|
|
|
|
{
|
2026-11-04 18:56:24 +08:00
|
|
|
|
foreach (var gk in new[] { "guide.web3.text" })
|
2026-11-04 18:13:08 +08:00
|
|
|
|
{
|
|
|
|
|
|
var line = System.IO.File.ReadAllLines(System.IO.Path.Combine(dir, lang + ".csv")).First(l => l.StartsWith(gk + ","));
|
|
|
|
|
|
var text = line.Substring(gk.Length + 1).Trim('"').Replace("\"\"", "\"");
|
|
|
|
|
|
Check(text.Contains("{"), lang + " " + gk + " 用了占位符");
|
2026-11-04 12:45:29 +08:00
|
|
|
|
var bad = TuningText.Unresolved(text);
|
|
|
|
|
|
Check(bad.Count == 0, lang + " 手册有取不到值的占位符:" + string.Join(" ", bad));
|
|
|
|
|
|
Check(!TuningText.Render(text).Contains("{"), lang + " 渲染后没有残留花括号");
|
2026-11-04 18:13:08 +08:00
|
|
|
|
}
|
2026-11-04 12:45:29 +08:00
|
|
|
|
}
|
2026-11-04 18:56:24 +08:00
|
|
|
|
foreach (var lang in new[] { "zh", "en", "de", "fr", "es", "pl", "ru", "ja" })
|
|
|
|
|
|
foreach (var line in System.IO.File.ReadAllLines(System.IO.Path.Combine(dir, lang + ".csv")).Where(l => l.StartsWith("gg.") && !l.StartsWith("gg.ex.")))
|
|
|
|
|
|
{
|
|
|
|
|
|
var gv = line.Substring(line.IndexOf(',') + 1).Trim('"');
|
|
|
|
|
|
Check(TuningText.Unresolved(gv).Count == 0 && !TuningText.Render(gv).Contains("{"), lang + " 鉴定指南占位符取不到值:" + line.Substring(0, line.IndexOf(',')));
|
|
|
|
|
|
}
|
2026-11-04 12:45:29 +08:00
|
|
|
|
Check(TuningText.Render("{BmListFee:%}") == "5%" && TuningText.Render("{PonziRates[4]:%}") == "10%" && TuningText.Render("{WalletMax}") == "5", "格式:百分比、数组下标、整数");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 14:26:21 +08:00
|
|
|
|
Test("多语言表:8 种语言键集合与英文一致,占位符和标记一致", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var dir = System.IO.Path.GetFullPath(System.IO.Path.Combine(AppContext.BaseDirectory, "../../../../../src/Decentralized/ModFiles/Localization"));
|
|
|
|
|
|
Func<string, Dictionary<string, string>> load = lang =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = new Dictionary<string, string>();
|
|
|
|
|
|
foreach (var line in System.IO.File.ReadAllLines(System.IO.Path.Combine(dir, lang + ".csv")))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue;
|
|
|
|
|
|
int c = line.IndexOf(',');
|
|
|
|
|
|
Check(c > 0, lang + " 行没有逗号:" + line);
|
|
|
|
|
|
var v = line.Substring(c + 1).Trim();
|
|
|
|
|
|
if (v.Length >= 2 && v[0] == '"' && v[v.Length - 1] == '"') v = v.Substring(1, v.Length - 2).Replace("\"\"", "\"");
|
|
|
|
|
|
d[line.Substring(0, c).Trim()] = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
return d;
|
|
|
|
|
|
};
|
|
|
|
|
|
var en = load("en");
|
|
|
|
|
|
var ph = new System.Text.RegularExpressions.Regex(@"\{[^{}]+\}");
|
|
|
|
|
|
foreach (var lang in new[] { "zh", "de", "fr", "es", "pl", "ru", "ja" })
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = load(lang);
|
|
|
|
|
|
Check(d.Keys.OrderBy(x => x).SequenceEqual(en.Keys.OrderBy(x => x)), lang + " 键集合和英文不同");
|
|
|
|
|
|
foreach (var kv in en)
|
|
|
|
|
|
{
|
|
|
|
|
|
var v = d[kv.Key];
|
|
|
|
|
|
Check(ph.Matches(kv.Value).Cast<System.Text.RegularExpressions.Match>().Select(m => m.Value).OrderBy(x => x)
|
|
|
|
|
|
.SequenceEqual(ph.Matches(v).Cast<System.Text.RegularExpressions.Match>().Select(m => m.Value).OrderBy(x => x)), lang + " 占位符不一致:" + kv.Key);
|
|
|
|
|
|
foreach (var mark in new[] { "§", "¶", "\\n" })
|
|
|
|
|
|
Check(kv.Value.Split(new[] { mark }, StringSplitOptions.None).Length == v.Split(new[] { mark }, StringSplitOptions.None).Length, lang + " 标记 " + mark + " 数量不一致:" + kv.Key);
|
2026-11-04 17:20:05 +08:00
|
|
|
|
if (lang != "zh" && lang != "ja") Check(kv.Value.StartsWith(" ") == v.StartsWith(" ") && kv.Value.EndsWith(" ") == v.EndsWith(" "), lang + " 首尾空格和英文不同(拼接用):" + kv.Key);
|
2026-11-04 14:26:21 +08:00
|
|
|
|
if (lang != "zh" && lang != "ja") Check(!System.Text.RegularExpressions.Regex.IsMatch(v, "[\u3040-\u30ff\u4e00-\u9fff]"), lang + " 有汉字或假名:" + kv.Key);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 15:34:38 +08:00
|
|
|
|
Test("mod 设置倍率:解析、封顶、挖矿产出按倍率", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Check(Scales.Parse("×1.5") == 1.5 && Scales.Parse("×0.5") == 0.5 && Scales.Parse("x") == 1 && Scales.Parse(null) == 1, "解析档位文字");
|
|
|
|
|
|
foreach (var c in Scales.Choices) Check(Scales.Parse(c) > 0, "档位都能解析:" + c);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
Scales.Risk = 2; Check(Math.Abs(Scales.R(0.3) - 0.6) < 1e-9 && Scales.R(0.8) == 1, "风险倍率乘概率并封顶 1");
|
|
|
|
|
|
Scales.Risk = 1;
|
|
|
|
|
|
var n = new Network();
|
|
|
|
|
|
Scales.Mining = 1; double a = n.Mine(100);
|
|
|
|
|
|
Scales.Mining = 2; double b2 = n.Mine(100);
|
|
|
|
|
|
Check(a > 0 && Math.Abs(b2 - 2 * a) < 1e-9, "挖矿产出按倍率");
|
|
|
|
|
|
}
|
|
|
|
|
|
finally { Scales.Risk = 1; Scales.Mining = 1; }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 07:26:27 +08:00
|
|
|
|
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}");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-03 08:52:32 +08:00
|
|
|
|
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
|
|
|
|
|
|
return _failed == 0 ? 0 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-03 09:11:46 +08:00
|
|
|
|
|
|
|
|
|
|
internal sealed class SaveBundle
|
|
|
|
|
|
{
|
|
|
|
|
|
public Market Market;
|
|
|
|
|
|
public Stablecoins Stable;
|
|
|
|
|
|
public Network Network;
|
|
|
|
|
|
public Wallet Hot;
|
|
|
|
|
|
public Exchange Exchange;
|
|
|
|
|
|
public Rng Rng;
|
|
|
|
|
|
}
|