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 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Opus 5.5 committed 2026-11-03 08:52:32 +08:00
commit ce3bed1467
14 files changed
+702

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
bin/
obj/
*.user
.vs/
.idea/
.vscode/
*.swp
.DS_Store
tests/**/bin/
+5
View File
@@ -0,0 +1,5 @@
# Changelog
## 未发布
- 纯逻辑层:带种子的随机数、资产登记、地址账本(污点与成本随币走)、恒定乘积交易对、挖矿(显卡、矿机、全网算力、减半、损耗与暴毙)。
+21
View File
@@ -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.
+36
View File
@@ -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
+20
View File
@@ -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:挖矿、虚构币市场、稳定币、洗钱及其风险,接入游戏原有的电力、事件和执法系统。开发中,尚未发布。所有币种、交易所和事件均为虚构,不具有任何现实价值。
+84
View File
@@ -0,0 +1,84 @@
using System;
namespace Decentralized.Domain
{
public enum Venue { Exchange, Dex, Darknet }
/// <summary>
/// 一个交易对 = 一个恒定乘积池(x·y = k)。Base 是被交易的币,Quote 是计价币;价格 = Quote 储备 / Base 储备。
/// 每个交易对各有深度;夜间由 NPC 套利者把价格按比例拉向目标价。
/// </summary>
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 };
/// <summary>预估:卖出 baseIn 个 Base 能得到多少 Quote(不改池子)。</summary>
public double QuoteOut(double baseIn)
{
double x = BaseReserve + baseIn * (1 - Fee);
return QuoteReserve - BaseReserve * QuoteReserve / x;
}
/// <summary>预估:花 quoteIn 个 Quote 能买到多少 Base(不改池子)。</summary>
public double BaseOut(double quoteIn)
{
double y = QuoteReserve + quoteIn * (1 - Fee);
return BaseReserve - BaseReserve * QuoteReserve / y;
}
/// <summary>卖出 Base,返回得到的 Quote。手续费留在池里。</summary>
public double SellBase(double baseIn)
{
if (baseIn <= 0) return 0;
double out_ = QuoteOut(baseIn);
BaseReserve += baseIn;
QuoteReserve -= out_;
return out_;
}
/// <summary>用 Quote 买入 Base,返回得到的 Base。</summary>
public double BuyBase(double quoteIn)
{
if (quoteIn <= 0) return 0;
double out_ = BaseOut(quoteIn);
QuoteReserve += quoteIn;
BaseReserve -= out_;
return out_;
}
/// <summary>一笔交易的价格冲击(交易后价格相对交易前的变化比例)。</summary>
public double ImpactOfBuy(double quoteIn)
{
double before = Price;
double y = QuoteReserve + quoteIn * (1 - Fee);
double x = BaseReserve * QuoteReserve / y;
return (y / x) / before - 1;
}
/// <summary>
/// 夜间:价格在对数空间里向 target 靠拢 rate 的比例,同时把 Quote 一侧的深度重设为 quoteDepth。
/// </summary>
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;
}
}
}
+51
View File
@@ -0,0 +1,51 @@
using System.Collections.Generic;
namespace Decentralized.Domain
{
/// <summary>资产的性质。新增币种只是往 <see cref="Assets"/> 里加一条。</summary>
public sealed class AssetSpec
{
public string Id;
/// <summary>挂钩 1 信用点的稳定币。</summary>
public bool Stable;
/// <summary>发行方可以冻结地址(CRC;CRT 只在镇压期间)。</summary>
public bool Freezable;
/// <summary>链上可追踪:污点随币传播。Ponero 为 false,换入时污点停止传播。</summary>
public bool Traceable = true;
/// <summary>合规交易所可以上架。</summary>
public bool Compliant = true;
/// <summary>程序生成或玩家发行的代币。</summary>
public bool Issued;
}
/// <summary>资产登记。信用点(游戏现金)不在链上,不登记。</summary>
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<string, AssetSpec> Table = new Dictionary<string, AssetSpec>();
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<AssetSpec> All => Table.Values;
}
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- 纯逻辑:资产与账本、AMM 交易对、市场、挖矿。不引用 Unity / 游戏程序集。
目标 netstandard2.1:游戏是 Unity Mono。JSON 用游戏自带的 Newtonsoft.Json 13(编译时引用 NuGet 包,运行时用游戏的那份,不随 mod 发布)。 -->
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>ProbablyDecentralized.Domain</AssemblyName>
<RootNamespace>Decentralized.Domain</RootNamespace>
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>
</Project>
+134
View File
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>显卡或矿机的型号。算力内部单位 H(1 H = 10 MH/s),耗电单位 kJ/天。</summary>
public sealed class HardwareSpec
{
public string Model;
public string Brand;
public double Hash;
public double Kj;
public int Price;
/// <summary>暴毙概率的品牌系数。</summary>
public double DeathFactor;
/// <summary>每天损耗的系数。</summary>
public double WearFactor;
/// <summary>矿机(ASIC):独立机器;false 为插在矿架上的显卡。</summary>
public bool Asic;
/// <summary>显示用算力:显卡 MH/s,矿机 GH/s。</summary>
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<string, HardwareSpec> 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,
};
}
/// <summary>一张显卡或一台矿机的实例状态(存进物品标签)。</summary>
public sealed class Unit
{
public string Model;
/// <summary>假卡:标着 Model,芯片实为 FakeOf。</summary>
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];
/// <summary>当前算力:健康 ≥ 50 时满算力,低于 50 线性降到健康 0 时的 60%。</summary>
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);
/// <summary>今晚的暴毙概率。</summary>
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;
}
/// <summary>一夜的损耗与暴毙判定;返回是否在今晚暴毙。</summary>
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;
}
/// <summary>翻新:每张卡一次,健康 +30。</summary>
public bool Refurbish()
{
if (Dead || Refurbished) return false;
Refurbished = true;
Health = Math.Min(100, Health + Tuning.RefurbishHealth);
return true;
}
}
/// <summary>全网:NPC 背景算力、出块奖励与减半。</summary>
public sealed class Network
{
public double NpcHash = Tuning.NpcHash0;
public double Reward = Tuning.BlockReward;
/// <summary>停电、动荡等事件对 NPC 算力的可用比例。</summary>
public double Availability = 1.0;
/// <summary>按算力分得今晚的出块(已扣矿池抽成)。</summary>
public double Mine(double playerHash)
{
if (playerHash <= 0) return 0;
double total = NpcHash * Availability + playerHash;
return Reward * playerHash / total * (1 - Tuning.PoolFee);
}
/// <summary>
/// 夜间:第 HalvingDays 天的倍数时减半;NPC 矿工按"每 H 收益 / 电费"与目标比的偏离扩张或关机,每晚最多 ±5%。
/// </summary>
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;
}
}
}
+61
View File
@@ -0,0 +1,61 @@
using System;
namespace Decentralized.Domain
{
/// <summary>
/// 带种子、状态可存档的随机数(xoshiro128**)。游戏自己的 RNG 不带种子;mod 的事件、行情、暴毙都用这个,
/// 状态随存档保存,读档后结果不变,测试也能复现。
/// </summary>
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;
}
/// <summary>[0, 1)</summary>
public double NextDouble() => (NextUInt() >> 8) * (1.0 / (1 << 24));
public bool Chance(double p) => NextDouble() < p;
/// <summary>[min, max],含两端。</summary>
public int Range(int min, int max) => min + (int)(NextDouble() * (max - min + 1));
public double Range(double min, double max) => min + NextDouble() * (max - min);
/// <summary>标准正态(Box–Muller)。</summary>
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);
}
}
}
+34
View File
@@ -0,0 +1,34 @@
namespace Decentralized.Domain
{
/// <summary>
/// 数值(普通难度基准)。改数值只改这里。
/// </summary>
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;
/// <summary>主交易对(信用点一侧)的深度随天数增长。</summary>
public static double PoolDepth(int day) => PoolDepth0 * (1 + day / 60.0);
}
}
+54
View File
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>
/// 一个链上地址:各币种余额,加上整个地址的污点额与成本(信用点等值)。
/// 污点和成本按转出的价值比例随币走。
/// </summary>
public sealed class Wallet
{
public string Address;
public Dictionary<string, double> Balances = new Dictionary<string, double>();
/// <summary>污点额(信用点等值)。</summary>
public double Taint;
/// <summary>成本(信用点):出金时只对超出成本的部分征税。</summary>
public double Basis;
/// <summary>被执法标记。</summary>
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;
}
/// <summary>按价格表估值(信用点)。</summary>
public double Value(Func<string, double> price) => Balances.Sum(kv => kv.Value * price(kv.Key));
/// <summary>
/// 转出:taint 和 basis 按转出价值占地址总价值的比例带走。Ponero 等不可追踪资产转入时,污点不随之进入目标地址。
/// </summary>
public static void Transfer(Wallet from, Wallet to, string asset, double amount, Func<string, double> 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;
}
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- 纯逻辑单元测试:不需要游戏和测试框架。运行方法见 README。 -->
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../src/Decentralized.Domain/Decentralized.Domain.csproj" />
</ItemGroup>
</Project>
+164
View File
@@ -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<string, double> px = a => a == Assets.PSC ? 100 : 1;
var a = new Wallet { Address = "A", Taint = 300, Basis = 1000 };
a.Add(Assets.CRT, 1000);
var b = new Wallet { Address = "B" };
Wallet.Transfer(a, b, Assets.CRT, 250, px);
Check(Near(b.Taint, 75, 1e-9) && Near(a.Taint, 225, 1e-9), $"转出 1/4,污点带走 1/4:{b.Taint}");
Check(Near(b.Basis, 250, 1e-9), "成本带走 1/4");
var c = new Wallet { Address = "C" };
b.Add(Assets.Ponero, 10);
Wallet.Transfer(b, c, Assets.Ponero, 10, px);
Check(c.Taint == 0, "Ponero 转入的地址不带污点");
bool threw = false;
try { a.Add(Assets.CRT, -10000); } catch (InvalidOperationException) { threw = true; }
Check(threw, "余额不足时拒绝");
});
Test("挖矿:开局 1 H 每晚约 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;
}
}