第十一阶段 stage 2:场外换币台(暗灯招换币客户)

- 新人脉升级“场外换币台”(OtcUnlockPrice 500),附赠一盏暗灯
- 包装原版暗灯工厂:切换顺序末尾加目标 pd_otc(只接管进出这一步,其余交给原切换函数);顾客工厂写进公开的 StoreClientListDict.storeClientDict,顾客面板与概率走原版逻辑
- 换色:底图换成原版熄灭的暗灯,叠加层画黄绿色光晕(otc_lamp_on),不进存档
- 换币客户(OtcClient)一半卖币、一半买币:卖币并入原收黑币(Domain/Otc.cs 取代 HotCoins.cs,HotCoinSeller 删除),污点比例随机、越脏越便宜、地址来历写进账本;买币三档报价,PLEDGER 转账确认后现金到账,当晚未付作废
- 只在生成顾客的回调窗口里生成报价、写账本(原版取名字也会调用工厂)
- 存档数据版本 4:旧存档出块奖励、NPC 算力按 MiningScale 放大
- 8 种语言文字、手册“换币客户”一节;单元测试

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Opus 5.5 committed 2026-11-08 14:29:49 +08:00
1 parent bc23ae056b
commit ec578a39d9
25 files changed
+833 -321

No files matched your search

-55
View File
@@ -1,55 +0,0 @@
using System;
using System.Collections.Generic;
namespace Decentralized.Domain
{
/// <summary>一笔黑币报价:卖家手里的币、按现价的市值和他要的现金。Sting 是钓鱼执法(收下就记罪证,币不会到)。</summary>
public sealed class HotOffer
{
public string Asset;
public double Qty, Value;
public int Cash;
public bool Sting;
}
/// <summary>
/// 收黑币:游戏里出了事,手里有来路不明的币的人上门,折价换现金。
/// 帮派火并、镇压期间黑市的人出手 CRT;革命军袭击上层、下层动荡期间有人出手抢来的 PSC;黑市声望够了以后偶尔有人出手勒索得来的 Ponero。
/// PSC、CRT 链上可追,收下的币按市值全额带污点;Ponero 不可追、不带污点,折扣也小,出口只有 Ponero 兑换处。
/// </summary>
public static class HotCoins
{
/// <summary>今天哪种黑币可能有人上门(按游戏当前的事件),没有返回 null。poneroOpen:Ponero 兑换处已开放。</summary>
public static string Pick(ICollection<MarketEvent> active, bool poneroOpen, Rng rng)
{
var c = new List<string>();
if (active.Contains(MarketEvent.GangWar) || active.Contains(MarketEvent.Crackdown)) c.Add(Assets.CRT);
if (active.Contains(MarketEvent.RevRaidUpper) || active.Contains(MarketEvent.LowerUnrest)) c.Add(Assets.PSC);
if (c.Count > 0 && rng.Chance(Tuning.HotEventChance)) return c[rng.Range(0, c.Count - 1)];
if (poneroOpen && rng.Chance(Tuning.HotPoneroChance)) return Assets.Ponero;
return null;
}
/// <summary>开价:市值 = 房租 × 区间,现金要价 = 市值 × 折扣(Ponero 折扣小)。crackdown:镇压期间钓鱼执法更多。</summary>
public static HotOffer Make(string asset, double coinPrice, double rent, bool crackdown, Rng rng)
{
if (coinPrice <= 0 || rent <= 0) return null;
double value = rent * rng.Range(Tuning.HotValueMinRents, Tuning.HotValueMaxRents);
bool ponero = asset == Assets.Ponero;
double cut = ponero ? rng.Range(Tuning.HotPoneroCutMin, Tuning.HotPoneroCutMax) : rng.Range(Tuning.HotCutMin, Tuning.HotCutMax);
return new HotOffer
{
Asset = asset, Qty = value / coinPrice, Value = value, Cash = (int)Math.Round(value * cut),
Sting = rng.Chance(Tuning.HotSting + (crackdown ? Tuning.HotStingCrackdown : 0)),
};
}
/// <summary>收下:币进钱包,成本是付出的现金;PSC、CRT 按市值记污点,Ponero 不记。钓鱼执法不在这里处理。</summary>
public static void Receive(Wallet w, HotOffer o)
{
w.Add(o.Asset, o.Qty);
w.Basis += o.Cash;
if (o.Asset != Assets.Ponero) w.Taint += o.Value;
}
}
}
+129
View File
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
namespace Decentralized.Domain
{
/// <summary>换币客户的来意:卖币给玩家(玩家付现金收币),或者向玩家买币(玩家转币收现金)。</summary>
public enum OtcSide { Sell, Buy }
/// <summary>
/// 一位换币客户的报价。Sell:客户手里的币(Qty、按现价的市值 Value、污点比例 TaintRatio)和他要的现金 Cash;Sting 是便衣(收下就记罪证,币不会到)。
/// Buy:客户要买的币和数量,Value 是按现价的市值,成交价按玩家选的报价档加溢价。
/// </summary>
public sealed class OtcOffer
{
public OtcSide Side;
public string Asset;
public double Qty, Value, TaintRatio;
public int Cash;
public bool Sting;
}
/// <summary>换币台的累计统计(存档):成交额(信用点)、笔数、客户地址编号;混币器提议是否已经发过(第三阶段用)。</summary>
public sealed class OtcDesk
{
public double Volume;
public int Deals, Seq;
public bool MixerOffered;
public void Record(double cash) { Volume += Math.Max(0, cash); Deals++; }
}
/// <summary>
/// 换币客户(场外换币台):小偷灯切到"换币客户"后招来,一半来卖币、一半来买币。
/// 卖币沿用原来的收黑币:帮派火并、镇压期间多是 CRT,革命军袭击上层、下层动荡期间多是 PSC,这类币全额带污点;
/// 其他时候在 PSC、CRT、CRA 里随机,污点比例随机,现金价按污点比例打折;Ponero 兑换处开放后偶尔有人出手 Ponero(不带污点)。
/// 买币:什么币都可能要,玩家选报价档,溢价越高越难成交;卖出的币不追查污点。
/// </summary>
public static class Otc
{
/// <summary>客户卖币时可能出手的币(没有相关事件时随机)。</summary>
public static readonly string[] SellAssets = { Assets.PSC, Assets.CRT, Assets.CRA };
/// <summary>客户买币时可能要的币。</summary>
public static readonly string[] BuyAssets = { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA, Assets.SPARK };
public static OtcSide PickSide(Rng rng) => rng.Chance(Tuning.OtcSellShare) ? OtcSide.Sell : OtcSide.Buy;
/// <summary>和当前事件有关的币(帮派火并、镇压 → CRT;袭击上层、下层动荡 → PSC),没有返回 null。</summary>
public static string EventAsset(ICollection<MarketEvent> active, Rng rng)
{
var c = new List<string>();
if (active.Contains(MarketEvent.GangWar) || active.Contains(MarketEvent.Crackdown)) c.Add(Assets.CRT);
if (active.Contains(MarketEvent.RevRaidUpper) || active.Contains(MarketEvent.LowerUnrest)) c.Add(Assets.PSC);
return c.Count == 0 ? null : c[rng.Range(0, c.Count - 1)];
}
/// <summary>
/// 卖币客户的报价。有相关事件时按 OtcEventChance 出手事件里来的币(全额污点);Ponero 兑换处开放时按 OtcPoneroChance 出手 Ponero;
/// 否则在 PSC、CRT、CRA 里随机,污点比例在 OtcTaintMin–OtcTaintMax 之间。市值 = 房租 × 区间;现金价 = 市值 × 折扣,折扣按污点比例从 OtcCutClean 线性降到 OtcCutDirty。
/// crackdown:镇压期间便衣更多。
/// </summary>
public static OtcOffer MakeSell(ICollection<MarketEvent> active, bool poneroOpen, Func<string, double> price, double rent, Rng rng)
{
if (rent <= 0) return null;
string asset = null; double ratio;
string ev = EventAsset(active, rng);
if (ev != null && rng.Chance(Tuning.OtcEventChance)) { asset = ev; ratio = 1; }
else if (poneroOpen && rng.Chance(Tuning.OtcPoneroChance)) { asset = Assets.Ponero; ratio = 0; }
else { asset = SellAssets[rng.Range(0, SellAssets.Length - 1)]; ratio = rng.Range(Tuning.OtcTaintMin, Tuning.OtcTaintMax); }
double p = price(asset);
if (p <= 0) return null;
double value = rent * rng.Range(Tuning.OtcValueMinRents, Tuning.OtcValueMaxRents);
double cut = asset == Assets.Ponero ? rng.Range(Tuning.OtcPoneroCutMin, Tuning.OtcPoneroCutMax) : CutFor(ratio);
bool crackdown = active.Contains(MarketEvent.Crackdown);
return new OtcOffer
{
Side = OtcSide.Sell, Asset = asset, Qty = value / p, Value = value, TaintRatio = ratio, Cash = (int)Math.Round(value * cut),
Sting = rng.Chance(Tuning.OtcSting + (crackdown ? Tuning.OtcStingCrackdown : 0)),
};
}
/// <summary>现金价占市值的比例:污点比例 0 时 OtcCutClean,1 时 OtcCutDirty,之间线性。</summary>
public static double CutFor(double taintRatio) => Tuning.OtcCutClean + (Tuning.OtcCutDirty - Tuning.OtcCutClean) * Math.Max(0, Math.Min(1, taintRatio));
/// <summary>买币客户的来意:随机一种币,市值 = 房租 × OtcBuyMinRents–OtcBuyMaxRents。</summary>
public static OtcOffer MakeBuy(Func<string, double> price, double rent, Rng rng)
{
if (rent <= 0) return null;
string asset = BuyAssets[rng.Range(0, BuyAssets.Length - 1)];
double p = price(asset);
if (p <= 0) return null;
double value = rent * rng.Range(Tuning.OtcBuyMinRents, Tuning.OtcBuyMaxRents);
return new OtcOffer { Side = OtcSide.Buy, Asset = asset, Qty = value / p, Value = value };
}
/// <summary>买币客户在第 tier 档(0–2)的成交价(信用点):市值 × (1 + 溢价)。</summary>
public static int Quote(OtcOffer o, int tier) => (int)Math.Round(o.Value * (1 + Tuning.OtcPremium[tier]));
/// <summary>客户接不接受第 tier 档的报价。</summary>
public static bool Accepts(int tier, Rng rng) => rng.Chance(Tuning.OtcAccept[tier]);
/// <summary>收下卖币客户的币:币进钱包,成本是付出的现金;污点 = 市值 × 污点比例(带上限)。返回超出钱包价值、没加进去的污点。</summary>
public static double Receive(Wallet w, OtcOffer o, Func<string, double> price)
{
w.Add(o.Asset, o.Qty);
w.Basis += o.Cash;
return o.TaintRatio > 0 ? w.AddTaintCapped(o.Value * o.TaintRatio, price) : 0;
}
/// <summary>
/// 客户地址的来历(写进链上账本,玩家在浏览器里能查到):1–OtcHistoryMax 笔转入,每一笔按污点比例的概率来自"脏"的来源(混币器、黑市平台、骗局地址),否则来自 Poinbase 或 DEX。
/// 返回 (来源地址, 数量占比) 列表;dirty 和 clean 由接入层给出地址。
/// </summary>
public static List<KeyValuePair<string, double>> History(OtcOffer o, IList<string> dirty, IList<string> clean, Rng rng)
{
var list = new List<KeyValuePair<string, double>>();
int n = rng.Range(1, Tuning.OtcHistoryMax);
double left = 1;
for (int i = 0; i < n; i++)
{
bool isDirty = dirty.Count > 0 && rng.Chance(o.TaintRatio);
var src = isDirty ? dirty[rng.Range(0, dirty.Count - 1)] : clean[rng.Range(0, clean.Count - 1)];
double share = i == n - 1 ? left : left * rng.Range(0.3, 0.7);
left -= share;
list.Add(new KeyValuePair<string, double>(src, share));
}
return list;
}
}
}
+14 -4
View File
@@ -10,6 +10,8 @@ namespace Decentralized.Domain
// 挖矿
public const double PscStartPrice = 100;
/// <summary>第十一阶段把出块奖励和 NPC 初始算力同乘的倍数(100 → 1000、360 → 3600);读旧存档时按它放大全网。</summary>
public const double MiningScale = 10;
public const double BlockReward = 1000;
public const int HalvingDays = 90;
/// <summary>减半对币价的传导:每次减半在区间内随机一个指数,价格锚上移 2^指数 倍(0.15–0.85 即约 +11% 到 +80%)。
@@ -289,10 +291,18 @@ namespace Decentralized.Domain
public const double YieldPoolChance = 0.25, YieldRugNightly = 0.18, YieldCap0 = 4000, YieldCapPerDay = 120, YieldReserveDays = 25;
public static readonly double[] YieldRates = { 0.01, 0.02, 0.03 }, YieldScamByRate = { 0.15, 0.40, 0.75 };
public const double YieldScamTimelock = 0.25, YieldScamAudit = 0.20;
/// <summary>收黑币:相关事件期间每天有人上门的概率(PSC、CRT),Ponero 兑换处开放后每天的概率;币的市值(房租的倍数);
/// 现金要价占市值的比例(PSC、CRT 与 Ponero 分开);钓鱼执法的概率与镇压期间的加成。</summary>
public const double HotEventChance = 0.35, HotPoneroChance = 0.05, HotValueMinRents = 4, HotValueMaxRents = 15;
public const double HotCutMin = 0.45, HotCutMax = 0.60, HotPoneroCutMin = 0.70, HotPoneroCutMax = 0.80, HotSting = 0.05, HotStingCrackdown = 0.10;
/// <summary>换币客户(场外换币台):人脉升级价格;来卖币的比例(其余来买币)。</summary>
public const int OtcUnlockPrice = 500;
public const double OtcSellShare = 0.5;
/// <summary>卖币客户:有相关事件时出手事件里来的币(全额污点)的概率,Ponero 兑换处开放后出手 Ponero 的概率;币的市值(房租的倍数);
/// 其他币的污点比例范围;现金价占市值的比例(污点比例 0 时、1 时,之间线性;Ponero 单独一档);便衣的概率与镇压期间的加成;客户地址来历的最多笔数。</summary>
public const double OtcEventChance = 0.7, OtcPoneroChance = 0.15, OtcValueMinRents = 4, OtcValueMaxRents = 15;
public const double OtcTaintMin = 0.2, OtcTaintMax = 0.9, OtcCutClean = 0.80, OtcCutDirty = 0.45, OtcPoneroCutMin = 0.70, OtcPoneroCutMax = 0.80;
public const double OtcSting = 0.05, OtcStingCrackdown = 0.10;
public const int OtcHistoryMax = 3;
/// <summary>买币客户:要买的币的市值(房租的倍数);三档报价的溢价与客户接受的概率。</summary>
public const double OtcBuyMinRents = 2, OtcBuyMaxRents = 8;
public static readonly double[] OtcPremium = { 0.05, 0.15, 0.30 }, OtcAccept = { 0.95, 0.65, 0.30 };
/// <summary>假客服:每天上门的概率;伪造转账:用币付款的顾客里是骗子的比例;两者都从第几天起。</summary>
public const double FakeSupportChance = 0.04, FakeTransferShare = 0.08;
public const int ScamMinDay = 10;
+20 -10
View File
@@ -249,16 +249,6 @@ namespace Decentralized.Domain
["HalvingPassMin"] = Tuning.HalvingPassMin,
["HalvingRamp"] = Tuning.HalvingRamp,
["HalvingRampDays"] = Tuning.HalvingRampDays,
["HotCutMax"] = Tuning.HotCutMax,
["HotCutMin"] = Tuning.HotCutMin,
["HotEventChance"] = Tuning.HotEventChance,
["HotPoneroChance"] = Tuning.HotPoneroChance,
["HotPoneroCutMax"] = Tuning.HotPoneroCutMax,
["HotPoneroCutMin"] = Tuning.HotPoneroCutMin,
["HotSting"] = Tuning.HotSting,
["HotStingCrackdown"] = Tuning.HotStingCrackdown,
["HotValueMaxRents"] = Tuning.HotValueMaxRents,
["HotValueMinRents"] = Tuning.HotValueMinRents,
["IntelBriefPrice"] = Tuning.IntelBriefPrice,
["IntelFake"] = Tuning.IntelFake,
["IntelRep"] = Tuning.IntelRep,
@@ -268,6 +258,7 @@ namespace Decentralized.Domain
["ManipFree"] = Tuning.ManipFree,
["ManipMax"] = Tuning.ManipMax,
["ManipSlope"] = Tuning.ManipSlope,
["MiningScale"] = Tuning.MiningScale,
["MixerCleanRate"] = Tuning.MixerCleanRate,
["MixerFee"] = Tuning.MixerFee,
["MixerMaxRounds"] = Tuning.MixerMaxRounds,
@@ -285,6 +276,23 @@ namespace Decentralized.Domain
["NpcHash0"] = Tuning.NpcHash0,
["NpcKjPerH"] = Tuning.NpcKjPerH,
["NpcTargetRatio"] = Tuning.NpcTargetRatio,
["OtcBuyMaxRents"] = Tuning.OtcBuyMaxRents,
["OtcBuyMinRents"] = Tuning.OtcBuyMinRents,
["OtcCutClean"] = Tuning.OtcCutClean,
["OtcCutDirty"] = Tuning.OtcCutDirty,
["OtcEventChance"] = Tuning.OtcEventChance,
["OtcHistoryMax"] = Tuning.OtcHistoryMax,
["OtcPoneroChance"] = Tuning.OtcPoneroChance,
["OtcPoneroCutMax"] = Tuning.OtcPoneroCutMax,
["OtcPoneroCutMin"] = Tuning.OtcPoneroCutMin,
["OtcSellShare"] = Tuning.OtcSellShare,
["OtcSting"] = Tuning.OtcSting,
["OtcStingCrackdown"] = Tuning.OtcStingCrackdown,
["OtcTaintMax"] = Tuning.OtcTaintMax,
["OtcTaintMin"] = Tuning.OtcTaintMin,
["OtcUnlockPrice"] = Tuning.OtcUnlockPrice,
["OtcValueMaxRents"] = Tuning.OtcValueMaxRents,
["OtcValueMinRents"] = Tuning.OtcValueMinRents,
["PerpAdlHaircut"] = Tuning.PerpAdlHaircut,
["PerpBanDays"] = Tuning.PerpBanDays,
["PerpDebtDays"] = Tuning.PerpDebtDays,
@@ -480,6 +488,8 @@ namespace Decentralized.Domain
["CraSpiralPath"] = Array.ConvertAll(Tuning.CraSpiralPath, v => (double)v),
["IntelTipAcc"] = Array.ConvertAll(Tuning.IntelTipAcc, v => (double)v),
["IntelTipPrice"] = Array.ConvertAll(Tuning.IntelTipPrice, v => (double)v),
["OtcAccept"] = Array.ConvertAll(Tuning.OtcAccept, v => (double)v),
["OtcPremium"] = Array.ConvertAll(Tuning.OtcPremium, v => (double)v),
["PonziRates"] = Array.ConvertAll(Tuning.PonziRates, v => (double)v),
["SparkSpiralPath"] = Array.ConvertAll(Tuning.SparkSpiralPath, v => (double)v),
["UsedHealthMax"] = Array.ConvertAll(Tuning.UsedHealthMax, v => (double)v),