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:
commit
ce3bed1467
14 files changed
+702
No files matched your search
@@ -0,0 +1,9 @@
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
.vs/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
.DS_Store
|
||||
tests/**/bin/
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 未发布
|
||||
|
||||
- 纯逻辑层:带种子的随机数、资产登记、地址账本(污点与成本随币走)、恒定乘积交易对、挖矿(显卡、矿机、全网算力、减半、损耗与暴毙)。
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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:挖矿、虚构币市场、稳定币、洗钱及其风险,接入游戏原有的电力、事件和执法系统。开发中,尚未发布。所有币种、交易所和事件均为虚构,不具有任何现实价值。
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user