commit ce3bed1467181b26506eefed2515c51b1282b340 Author: ZHU MINCHAO <48154958+MinchaoZhu@users.noreply.github.com> Date: Tue Nov 3 08:52:32 2026 +0800 Domain core: seeded RNG, asset registry, wallets, AMM pairs, mining Pure logic layer with no game references: xoshiro128** RNG with savable state, asset registry (PSC, CRC, CRT, CRA, SPARK, Ponero), wallets whose taint and cost basis move with transfers, constant-product pools with nightly reversion, and mining (Probidia/PMD GPUs, RatDigg ASICs, network hash response, halving, wear and sudden death). 35 checks pass. Co-Authored-By: Claude Opus 5.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf610d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +bin/ +obj/ +*.user +.vs/ +.idea/ +.vscode/ +*.swp +.DS_Store +tests/**/bin/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cb6db3b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 未发布 + +- 纯逻辑层:带种子的随机数、资产登记、地址账本(污点与成本随币走)、恒定乘积交易对、挖矿(显卡、矿机、全网算力、减半、损耗与暴毙)。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b2c5b99 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 ZHU MINCHAO + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ProbablyDecentralized.sln b/ProbablyDecentralized.sln new file mode 100644 index 0000000..0e82993 --- /dev/null +++ b/ProbablyDecentralized.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{65D2EE76-8B79-48B0-BFF3-BF1B7FE5C096}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Decentralized.Domain", "src\Decentralized.Domain\Decentralized.Domain.csproj", "{9F515839-7902-4AB8-86D6-33755009DDDF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BC589259-3F05-4F25-81C9-183268862282}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Decentralized.Domain.Tests", "tests\Decentralized.Domain.Tests\Decentralized.Domain.Tests.csproj", "{FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9F515839-7902-4AB8-86D6-33755009DDDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F515839-7902-4AB8-86D6-33755009DDDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F515839-7902-4AB8-86D6-33755009DDDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F515839-7902-4AB8-86D6-33755009DDDF}.Release|Any CPU.Build.0 = Release|Any CPU + {FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {9F515839-7902-4AB8-86D6-33755009DDDF} = {65D2EE76-8B79-48B0-BFF3-BF1B7FE5C096} + {FCBED7E8-41D2-4474-99F3-C8EC5FF6BEF5} = {BC589259-3F05-4F25-81C9-183268862282} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc707b9 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Probably Decentralized + +A mod for *Probably Stolen*: crypto mining, a fictional coin market, stablecoins, laundering and its risks, built on the game's own power, events and law systems. Work in progress; nothing is released yet. + +All coins, exchanges and events are fictional and have no real-world value. + +## Layout + +| Path | Content | +|---|---| +| `src/Decentralized.Domain` | Pure logic (assets and ledger, AMM pairs, market, mining). No game or Unity references. | +| `tests/Decentralized.Domain.Tests` | Console test runner: `dotnet run --project tests/Decentralized.Domain.Tests` | + +## License + +MIT, see `LICENSE`. + +--- + +《Probably Stolen》的 mod:挖矿、虚构币市场、稳定币、洗钱及其风险,接入游戏原有的电力、事件和执法系统。开发中,尚未发布。所有币种、交易所和事件均为虚构,不具有任何现实价值。 diff --git a/src/Decentralized.Domain/Amm.cs b/src/Decentralized.Domain/Amm.cs new file mode 100644 index 0000000..d27ebc0 --- /dev/null +++ b/src/Decentralized.Domain/Amm.cs @@ -0,0 +1,84 @@ +using System; + +namespace Decentralized.Domain +{ + public enum Venue { Exchange, Dex, Darknet } + + /// + /// 一个交易对 = 一个恒定乘积池(x·y = k)。Base 是被交易的币,Quote 是计价币;价格 = Quote 储备 / Base 储备。 + /// 每个交易对各有深度;夜间由 NPC 套利者把价格按比例拉向目标价。 + /// + public sealed class Pool + { + public string Base, Quote; + public Venue Venue; + public double BaseReserve, QuoteReserve; + public double Fee; + + public string Id => $"{Venue}:{Base}/{Quote}"; + + public double Price => QuoteReserve / BaseReserve; + + public static Pool Create(Venue venue, string baseAsset, string quote, double price, double quoteDepth, double fee) => + new Pool { Venue = venue, Base = baseAsset, Quote = quote, QuoteReserve = quoteDepth, BaseReserve = quoteDepth / price, Fee = fee }; + + /// 预估:卖出 baseIn 个 Base 能得到多少 Quote(不改池子)。 + public double QuoteOut(double baseIn) + { + double x = BaseReserve + baseIn * (1 - Fee); + return QuoteReserve - BaseReserve * QuoteReserve / x; + } + + /// 预估:花 quoteIn 个 Quote 能买到多少 Base(不改池子)。 + public double BaseOut(double quoteIn) + { + double y = QuoteReserve + quoteIn * (1 - Fee); + return BaseReserve - BaseReserve * QuoteReserve / y; + } + + /// 卖出 Base,返回得到的 Quote。手续费留在池里。 + public double SellBase(double baseIn) + { + if (baseIn <= 0) return 0; + double out_ = QuoteOut(baseIn); + BaseReserve += baseIn; + QuoteReserve -= out_; + return out_; + } + + /// 用 Quote 买入 Base,返回得到的 Base。 + public double BuyBase(double quoteIn) + { + if (quoteIn <= 0) return 0; + double out_ = BaseOut(quoteIn); + QuoteReserve += quoteIn; + BaseReserve -= out_; + return out_; + } + + /// 一笔交易的价格冲击(交易后价格相对交易前的变化比例)。 + public double ImpactOfBuy(double quoteIn) + { + double before = Price; + double y = QuoteReserve + quoteIn * (1 - Fee); + double x = BaseReserve * QuoteReserve / y; + return (y / x) / before - 1; + } + + /// + /// 夜间:价格在对数空间里向 target 靠拢 rate 的比例,同时把 Quote 一侧的深度重设为 quoteDepth。 + /// + public void Revert(double target, double rate, double quoteDepth) + { + double lp = Math.Log(Price); + lp += rate * (Math.Log(target) - lp); + SetPrice(Math.Exp(lp), quoteDepth); + } + + public void SetPrice(double price, double quoteDepth) + { + QuoteReserve = quoteDepth; + BaseReserve = quoteDepth / price; + } + } +} diff --git a/src/Decentralized.Domain/Assets.cs b/src/Decentralized.Domain/Assets.cs new file mode 100644 index 0000000..43657aa --- /dev/null +++ b/src/Decentralized.Domain/Assets.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; + +namespace Decentralized.Domain +{ + /// 资产的性质。新增币种只是往 里加一条。 + public sealed class AssetSpec + { + public string Id; + /// 挂钩 1 信用点的稳定币。 + public bool Stable; + /// 发行方可以冻结地址(CRC;CRT 只在镇压期间)。 + public bool Freezable; + /// 链上可追踪:污点随币传播。Ponero 为 false,换入时污点停止传播。 + public bool Traceable = true; + /// 合规交易所可以上架。 + public bool Compliant = true; + /// 程序生成或玩家发行的代币。 + public bool Issued; + } + + /// 资产登记。信用点(游戏现金)不在链上,不登记。 + public static class Assets + { + public const string PSC = "PSC"; + public const string CRC = "CRC"; + public const string CRT = "CRT"; + public const string CRA = "CRA"; + public const string SPARK = "SPARK"; + public const string Ponero = "PONERO"; + + private static readonly Dictionary Table = new Dictionary(); + + static Assets() + { + Register(new AssetSpec { Id = PSC }); + Register(new AssetSpec { Id = CRC, Stable = true, Freezable = true }); + Register(new AssetSpec { Id = CRT, Stable = true, Freezable = true }); + Register(new AssetSpec { Id = CRA, Stable = true }); + Register(new AssetSpec { Id = SPARK }); + Register(new AssetSpec { Id = Ponero, Traceable = false, Compliant = false }); + } + + public static void Register(AssetSpec spec) => Table[spec.Id] = spec; + + public static bool TryGet(string id, out AssetSpec spec) => Table.TryGetValue(id, out spec); + + public static AssetSpec Get(string id) => Table.TryGetValue(id, out var s) ? s : null; + + public static IEnumerable All => Table.Values; + } +} diff --git a/src/Decentralized.Domain/Decentralized.Domain.csproj b/src/Decentralized.Domain/Decentralized.Domain.csproj new file mode 100644 index 0000000..9921c14 --- /dev/null +++ b/src/Decentralized.Domain/Decentralized.Domain.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.1 + ProbablyDecentralized.Domain + Decentralized.Domain + latest + disable + disable + false + + + + + diff --git a/src/Decentralized.Domain/Mining.cs b/src/Decentralized.Domain/Mining.cs new file mode 100644 index 0000000..83816ca --- /dev/null +++ b/src/Decentralized.Domain/Mining.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Decentralized.Domain +{ + /// 显卡或矿机的型号。算力内部单位 H(1 H = 10 MH/s),耗电单位 kJ/天。 + public sealed class HardwareSpec + { + public string Model; + public string Brand; + public double Hash; + public double Kj; + public int Price; + /// 暴毙概率的品牌系数。 + public double DeathFactor; + /// 每天损耗的系数。 + public double WearFactor; + /// 矿机(ASIC):独立机器;false 为插在矿架上的显卡。 + public bool Asic; + + /// 显示用算力:显卡 MH/s,矿机 GH/s。 + public double DisplayHash => Asic ? Hash * 10 / 1000 : Hash * 10; + } + + public static class Hardware + { + public const string Probidia = "Probidia", Pmd = "PMD", RatDigg = "RatDigg"; + + public static readonly IReadOnlyDictionary Specs = new[] + { + Gpu("RZX50", Probidia, 1.0, 4, 120), Gpu("RZX60", Probidia, 1.5, 5, 180), Gpu("RZX70", Probidia, 5.0, 12, 450), + Gpu("RZX80Ti", Probidia, 8.0, 18, 750), Gpu("RZX90", Probidia, 20.0, 30, 2800), + Gpu("RZ470", Pmd, 0.8, 2.5, 90), Gpu("RZ580", Pmd, 1.2, 3, 140), Gpu("RZ6800", Pmd, 4.0, 8, 380), + Gpu("RZ7900", Pmd, 15.0, 20, 2200), + Asic("M1", 50, 60, 6000), Asic("M2", 120, 110, 15000), Asic("M3", 300, 220, 40000), + }.ToDictionary(s => s.Model); + + private static HardwareSpec Gpu(string model, string brand, double h, double kj, int price) => new HardwareSpec + { + Model = model, Brand = brand, Hash = h, Kj = kj, Price = price, + DeathFactor = brand == Probidia ? 0.7 : 1.0, WearFactor = brand == Probidia ? 1.0 : 1.2, + }; + + private static HardwareSpec Asic(string model, double h, double kj, int price) => new HardwareSpec + { + Model = model, Brand = RatDigg, Hash = h, Kj = kj, Price = price, DeathFactor = 0.5, WearFactor = 0.8, Asic = true, + }; + } + + /// 一张显卡或一台矿机的实例状态(存进物品标签)。 + public sealed class Unit + { + public string Model; + /// 假卡:标着 Model,芯片实为 FakeOf。 + public string FakeOf; + public double Health = 100; + public bool Dead; + public bool Refurbished; + + public HardwareSpec Label => Hardware.Specs[Model]; + public HardwareSpec Chip => Hardware.Specs[FakeOf ?? Model]; + + /// 当前算力:健康 ≥ 50 时满算力,低于 50 线性降到健康 0 时的 60%。 + public double Hash(bool overclock) + { + if (Dead) return 0; + double h = Chip.Hash * (Health >= 50 ? 1.0 : 0.6 + 0.4 * Health / 50); + return overclock ? h * Tuning.OcHash : h; + } + + public double Kj(bool overclock) => Dead ? 0 : Chip.Kj * (overclock ? Tuning.OcKj : 1); + + /// 今晚的暴毙概率。 + public double DeathChance(bool overclock) + { + double p = Tuning.DeathBase + Tuning.DeathWorn * Math.Pow(1 - Health / 100, 2); + p *= Label.DeathFactor * (FakeOf != null ? Tuning.FakeDeathFactor : 1) * (overclock ? Tuning.OcDeath : 1); + return p; + } + + /// 一夜的损耗与暴毙判定;返回是否在今晚暴毙。 + public bool Night(bool overclock, Rng rng) + { + if (Dead) return false; + Health = Math.Max(0, Health - Label.WearFactor * (overclock ? Tuning.OcWear : 1)); + if (rng.Chance(DeathChance(overclock))) + { + Dead = true; + return true; + } + return false; + } + + /// 翻新:每张卡一次,健康 +30。 + public bool Refurbish() + { + if (Dead || Refurbished) return false; + Refurbished = true; + Health = Math.Min(100, Health + Tuning.RefurbishHealth); + return true; + } + } + + /// 全网:NPC 背景算力、出块奖励与减半。 + public sealed class Network + { + public double NpcHash = Tuning.NpcHash0; + public double Reward = Tuning.BlockReward; + /// 停电、动荡等事件对 NPC 算力的可用比例。 + public double Availability = 1.0; + + /// 按算力分得今晚的出块(已扣矿池抽成)。 + public double Mine(double playerHash) + { + if (playerHash <= 0) return 0; + double total = NpcHash * Availability + playerHash; + return Reward * playerHash / total * (1 - Tuning.PoolFee); + } + + /// + /// 夜间:第 HalvingDays 天的倍数时减半;NPC 矿工按"每 H 收益 / 电费"与目标比的偏离扩张或关机,每晚最多 ±5%。 + /// + public void Night(int day, double price, double playerHash) + { + if (day > 0 && day % Tuning.HalvingDays == 0) Reward /= 2; + double total = NpcHash * Availability + playerHash; + double perH = Reward * price / total; + double ratio = perH / (Tuning.NpcKjPerH * Tuning.ElecPrice); + double adj = Math.Max(-Tuning.NpcAdj, Math.Min(Tuning.NpcAdj, Tuning.NpcAdj * (ratio / Tuning.NpcTargetRatio - 1))); + NpcHash *= 1 + adj; + } + } +} diff --git a/src/Decentralized.Domain/Rng.cs b/src/Decentralized.Domain/Rng.cs new file mode 100644 index 0000000..d753508 --- /dev/null +++ b/src/Decentralized.Domain/Rng.cs @@ -0,0 +1,61 @@ +using System; + +namespace Decentralized.Domain +{ + /// + /// 带种子、状态可存档的随机数(xoshiro128**)。游戏自己的 RNG 不带种子;mod 的事件、行情、暴毙都用这个, + /// 状态随存档保存,读档后结果不变,测试也能复现。 + /// + public sealed class Rng + { + public uint S0, S1, S2, S3; + + public Rng() : this(1) { } + + public Rng(ulong seed) + { + // SplitMix64 展开种子 + ulong z = seed; + ulong Next() + { + z += 0x9E3779B97F4A7C15UL; + ulong x = z; + x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9UL; + x = (x ^ (x >> 27)) * 0x94D049BB133111EBUL; + return x ^ (x >> 31); + } + ulong a = Next(), b = Next(); + S0 = (uint)a; S1 = (uint)(a >> 32); S2 = (uint)b; S3 = (uint)(b >> 32); + if ((S0 | S1 | S2 | S3) == 0) S0 = 1; + } + + private static uint Rotl(uint x, int k) => (x << k) | (x >> (32 - k)); + + public uint NextUInt() + { + uint result = Rotl(S1 * 5, 7) * 9; + uint t = S1 << 9; + S2 ^= S0; S3 ^= S1; S1 ^= S2; S0 ^= S3; + S2 ^= t; + S3 = Rotl(S3, 11); + return result; + } + + /// [0, 1) + public double NextDouble() => (NextUInt() >> 8) * (1.0 / (1 << 24)); + + public bool Chance(double p) => NextDouble() < p; + + /// [min, max],含两端。 + public int Range(int min, int max) => min + (int)(NextDouble() * (max - min + 1)); + + public double Range(double min, double max) => min + NextDouble() * (max - min); + + /// 标准正态(Box–Muller)。 + public double Gauss() + { + double u1 = 1.0 - NextDouble(), u2 = NextDouble(); + return Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Cos(2.0 * Math.PI * u2); + } + } +} diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs new file mode 100644 index 0000000..fc3a849 --- /dev/null +++ b/src/Decentralized.Domain/Tuning.cs @@ -0,0 +1,34 @@ +namespace Decentralized.Domain +{ + /// + /// 数值(普通难度基准)。改数值只改这里。 + /// + public static class Tuning + { + // 挖矿 + public const double PscStartPrice = 100; + public const double BlockReward = 50; + public const int HalvingDays = 90; + public const double NpcHash0 = 360; + public const double NpcKjPerH = 2.4; + public const double ElecPrice = 1.5; + public const double NpcTargetRatio = 3.5; + public const double NpcAdj = 0.05; + public const double PoolFee = 0.03; + public const int RigPrice = 400, RigKj = 2, RigSlots = 4; + public const double OcHash = 1.25, OcKj = 1.40, OcWear = 1.3, OcDeath = 1.2; + public const double DeathBase = 0.001, DeathWorn = 0.03, FakeDeathFactor = 2; + public const double RefurbishHealth = 30; + + // 市场 + public const double PoolDepth0 = 50000; + public const double DexFee = 0.003, ExchangeFee = 0.002; + public const double Revert = 0.15; + public const double PscVol = 0.04; + public const double SentimentKappa = 0.02, SentimentVol = 0.02; + public const double PriceFloor = 0.3, PriceCeiling = 3.0; + + /// 主交易对(信用点一侧)的深度随天数增长。 + public static double PoolDepth(int day) => PoolDepth0 * (1 + day / 60.0); + } +} diff --git a/src/Decentralized.Domain/Wallet.cs b/src/Decentralized.Domain/Wallet.cs new file mode 100644 index 0000000..0212fb3 --- /dev/null +++ b/src/Decentralized.Domain/Wallet.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Decentralized.Domain +{ + /// + /// 一个链上地址:各币种余额,加上整个地址的污点额与成本(信用点等值)。 + /// 污点和成本按转出的价值比例随币走。 + /// + public sealed class Wallet + { + public string Address; + public Dictionary Balances = new Dictionary(); + /// 污点额(信用点等值)。 + public double Taint; + /// 成本(信用点):出金时只对超出成本的部分征税。 + public double Basis; + /// 被执法标记。 + public bool Flagged; + + public double Get(string asset) => Balances.TryGetValue(asset, out var v) ? v : 0; + + public void Add(string asset, double amount) + { + if (amount == 0) return; + double v = Get(asset) + amount; + if (v < -1e-9) throw new InvalidOperationException($"{Address}: {asset} 余额不足"); + if (Math.Abs(v) < 1e-12) Balances.Remove(asset); + else Balances[asset] = v; + } + + /// 按价格表估值(信用点)。 + public double Value(Func price) => Balances.Sum(kv => kv.Value * price(kv.Key)); + + /// + /// 转出:taint 和 basis 按转出价值占地址总价值的比例带走。Ponero 等不可追踪资产转入时,污点不随之进入目标地址。 + /// + public static void Transfer(Wallet from, Wallet to, string asset, double amount, Func price) + { + if (amount <= 0) return; + double total = from.Value(price); + double share = total > 0 ? Math.Min(1.0, amount * price(asset) / total) : 0; + double taint = from.Taint * share, basis = from.Basis * share; + from.Add(asset, -amount); + from.Taint -= taint; + from.Basis -= basis; + to.Add(asset, amount); + var spec = Assets.Get(asset); + if (spec == null || spec.Traceable) to.Taint += taint; + to.Basis += basis; + } + } +} diff --git a/tests/Decentralized.Domain.Tests/Decentralized.Domain.Tests.csproj b/tests/Decentralized.Domain.Tests/Decentralized.Domain.Tests.csproj new file mode 100644 index 0000000..4910422 --- /dev/null +++ b/tests/Decentralized.Domain.Tests/Decentralized.Domain.Tests.csproj @@ -0,0 +1,13 @@ + + + + Exe + net8.0 + disable + latest + disable + + + + + diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs new file mode 100644 index 0000000..504cb1c --- /dev/null +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -0,0 +1,164 @@ +using System; +using System.Linq; +using Decentralized.Domain; + +// 纯逻辑单元测试(不需要游戏)。每个 Test 是一个场景,失败时打印原因,最后返回非零退出码。 +// 涉及数值的检查以离线经济模拟的结果为对照。 +internal static class Program +{ + private static int _failed, _passed; + + private static void Check(bool ok, string what) + { + if (ok) _passed++; + else { _failed++; Console.WriteLine(" ✗ " + what); } + } + + private static void Test(string name, Action body) + { + Console.WriteLine("• " + name); + try { body(); } + catch (Exception ex) { _failed++; Console.WriteLine(" ✗ 异常: " + ex); } + } + + private static bool Near(double a, double b, double tol) => Math.Abs(a - b) <= tol; + + private static int Main() + { + Test("随机数:同种子同序列;状态可复制后继续", () => + { + var a = new Rng(42); + var b = new Rng(42); + Check(Enumerable.Range(0, 100).All(_ => a.NextUInt() == b.NextUInt()), "同种子序列相同"); + var c = new Rng { S0 = a.S0, S1 = a.S1, S2 = a.S2, S3 = a.S3 }; + Check(Enumerable.Range(0, 100).All(_ => a.NextDouble() == c.NextDouble()), "复制状态后序列相同(存档读档)"); + var r = new Rng(7); + double mean = Enumerable.Range(0, 20000).Select(_ => r.NextDouble()).Average(); + Check(Near(mean, 0.5, 0.01), $"均匀分布均值约 0.5:{mean:0.000}"); + double g = Enumerable.Range(0, 20000).Select(_ => r.Gauss()).Select(x => x * x).Average(); + Check(Near(g, 1.0, 0.05), $"正态分布方差约 1:{g:0.000}"); + }); + + Test("资产登记:Ponero 不可追踪、不上合规交易所;稳定币挂钩", () => + { + Check(!Assets.Get(Assets.Ponero).Traceable && !Assets.Get(Assets.Ponero).Compliant, "Ponero"); + Check(Assets.Get(Assets.CRC).Stable && Assets.Get(Assets.CRC).Freezable, "CRC 可冻结"); + Check(Assets.Get(Assets.CRA).Stable && !Assets.Get(Assets.CRA).Freezable, "CRA 不可冻结"); + Assets.Register(new AssetSpec { Id = "NPC_TEST", Issued = true }); + Check(Assets.Get("NPC_TEST")?.Issued == true, "新增币种只是登记一条"); + }); + + Test("交易对:恒定乘积、手续费、500 信用点约 2% 价格冲击", () => + { + var pool = Pool.Create(Venue.Dex, Assets.PSC, Assets.CRC, 100, 50000, Tuning.DexFee); + Check(Near(pool.Price, 100, 1e-9), "初始价格 100"); + double k = pool.BaseReserve * pool.QuoteReserve; + Check(Near(pool.ImpactOfBuy(500), 0.02, 0.001), $"500 信用点买入的冲击约 2%:{pool.ImpactOfBuy(500):P2}"); + double got = pool.BuyBase(500); + Check(got > 4.9 && got < 4.97, $"500 信用点买到约 4.93 PSC:{got:0.000}"); + Check(pool.BaseReserve * pool.QuoteReserve >= k, "手续费留在池里,k 不减少"); + double back = pool.SellBase(got); + Check(back < 500 && back > 496, $"马上卖回只损失手续费:{back:0.00}"); + }); + + Test("交易对:夜间向目标价回归 15%(对数空间)并重设深度", () => + { + var pool = Pool.Create(Venue.Dex, Assets.PSC, Assets.CRC, 80, 50000, Tuning.DexFee); + pool.Revert(100, Tuning.Revert, 60000); + double expect = Math.Exp(Math.Log(80) + 0.15 * (Math.Log(100) - Math.Log(80))); + Check(Near(pool.Price, expect, 1e-9), $"回归后价格 {pool.Price:0.00},期望 {expect:0.00}"); + Check(Near(pool.QuoteReserve, 60000, 1e-9), "深度重设"); + }); + + Test("地址:污点和成本按价值比例随币走;Ponero 换入时污点停止传播", () => + { + Func px = a => a == Assets.PSC ? 100 : 1; + var a = new Wallet { Address = "A", Taint = 300, Basis = 1000 }; + a.Add(Assets.CRT, 1000); + var b = new Wallet { Address = "B" }; + Wallet.Transfer(a, b, Assets.CRT, 250, px); + Check(Near(b.Taint, 75, 1e-9) && Near(a.Taint, 225, 1e-9), $"转出 1/4,污点带走 1/4:{b.Taint}"); + Check(Near(b.Basis, 250, 1e-9), "成本带走 1/4"); + var c = new Wallet { Address = "C" }; + b.Add(Assets.Ponero, 10); + Wallet.Transfer(b, c, Assets.Ponero, 10, px); + Check(c.Taint == 0, "Ponero 转入的地址不带污点"); + bool threw = false; + try { a.Add(Assets.CRT, -10000); } catch (InvalidOperationException) { threw = true; } + Check(threw, "余额不足时拒绝"); + }); + + Test("挖矿:开局 1 H 每晚约 13.4 信用点;减半在第 90 天", () => + { + var net = new Network(); + double perH = net.Mine(1) * Tuning.PscStartPrice; + Check(Near(perH, 13.4, 0.2), $"1 H 每晚约 13.4 信用点(抽成后):{perH:0.00}"); + for (int d = 1; d <= 89; d++) net.Night(d, 100, 0); + Check(net.Reward == Tuning.BlockReward, "第 89 天还没减半"); + net.Night(90, 100, 0); + Check(net.Reward == Tuning.BlockReward / 2, "第 90 天减半"); + }); + + Test("挖矿:NPC 算力在有利可图时扩张,币价低时关机", () => + { + var up = new Network(); + up.Night(1, 100, 0); + Check(up.NpcHash > Tuning.NpcHash0, $"币价 100:扩张到 {up.NpcHash:0.0}"); + var down = new Network(); + down.Night(1, 30, 0); + Check(down.NpcHash < Tuning.NpcHash0 && down.NpcHash >= Tuning.NpcHash0 * 0.95, $"币价 30:关机,最多 −5%:{down.NpcHash:0.0}"); + }); + + Test("显卡:型号、显示单位、健康度与算力衰减", () => + { + Check(Hardware.Specs["RZX70"].DisplayHash == 50 && Hardware.Specs["RZ6800"].DisplayHash == 40, "RZX70 50 MH/s,RZ6800 40 MH/s"); + Check(Near(Hardware.Specs["M3"].DisplayHash, 3, 1e-9), "RatDigg M3 3 GH/s"); + var u = new Unit { Model = "RZX70" }; + Check(u.Hash(false) == 5, "满健康 5 H"); + u.Health = 25; + Check(Near(u.Hash(false), 4.0, 1e-9), "健康 25:5 × (0.6 + 0.4 × 0.5) = 4 H"); + u.Health = 0; + Check(Near(u.Hash(false), 3.0, 1e-9), "健康 0:60%"); + Check(Near(new Unit { Model = "RZX70" }.Hash(true), 6.25, 1e-9), "超频 +25%"); + var fake = new Unit { Model = "RZX90", FakeOf = "RZX60" }; + Check(fake.Hash(false) == 1.5 && fake.Label.Model == "RZX90", "假卡标着 RZX90,算力按 RZX60"); + Check(Near(fake.DeathChance(false), new Unit { Model = "RZX90" }.DeathChance(false) * 2, 1e-12), "假卡暴毙率 ×2"); + var r = new Unit { Model = "RZX70", Health = 50 }; + Check(r.Refurbish() && r.Health == 80 && !r.Refurbish(), "翻新一次 +30,不能再翻新"); + }); + + Test("显卡:累计暴毙概率与模拟器一致(第 50 / 100 / 150 天)", () => + { + double[] Run(string model, bool oc, int n) + { + var rng = new Rng(2026); + var deadBy = new int[3]; + for (int i = 0; i < n; i++) + { + var u = new Unit { Model = model }; + for (int d = 1; d <= 150; d++) + { + u.Night(oc, rng); + if (u.Dead) + { + if (d <= 50) deadBy[0]++; + if (d <= 100) deadBy[1]++; + deadBy[2]++; + break; + } + } + } + return deadBy.Select(x => (double)x / n).ToArray(); + } + var p = Run("RZX70", false, 20000); + Check(Near(p[0], 0.11, 0.02) && Near(p[1], 0.53, 0.02) && Near(p[2], 0.84, 0.02), + $"Probidia 11% / 53% / 84%:{p[0]:P0} / {p[1]:P0} / {p[2]:P0}"); + var q = Run("RZ6800", false, 20000); + Check(Near(q[0], 0.20, 0.02) && Near(q[1], 0.76, 0.02) && Near(q[2], 0.95, 0.02), + $"PMD 20% / 76% / 95%:{q[0]:P0} / {q[1]:P0} / {q[2]:P0}"); + }); + + Console.WriteLine($"\n{_passed} 通过,{_failed} 失败"); + return _failed == 0 ? 0 : 1; + } +}