第十一阶段 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

+50 -19
View File
@@ -1357,32 +1357,63 @@ internal static class Program
Check(ps > 12 && ps < 20, $"夜里向 SPARK 参考价靠拢:{ps:0.00}");
});
Test("收黑币:事件对应币种、报价、收下后的污点", () =>
Test("换币客户:卖币的事件币种、污点比例与折扣、收下后的污点;买币的报价档;成交统计", () =>
{
Func<string, double> pr = a => a == Assets.Ponero ? 50 : 1;
var none = new List<MarketEvent>();
var rng = new Rng(3);
int crt = 0, psc = 0, pon = 0;
int ev = 0, sides = 0;
for (int i = 0; i < 4000; i++)
{
string a1 = HotCoins.Pick(new[] { MarketEvent.GangWar }, false, rng);
if (a1 == Assets.CRT) crt++; else if (a1 != null) psc++;
if (HotCoins.Pick(new MarketEvent[0], true, rng) == Assets.Ponero) pon++;
var o = Otc.MakeSell(new List<MarketEvent> { MarketEvent.GangWar }, false, pr, 500, rng);
if (o.Asset == Assets.CRT && o.TaintRatio == 1) ev++;
if (Otc.PickSide(rng) == OtcSide.Sell) sides++;
}
Check(psc == 0 && Math.Abs(crt / 4000.0 - Tuning.HotEventChance) < 0.03, $"帮派火并期间只有 CRT:{crt}/4000");
Check(Math.Abs(pon / 4000.0 - Tuning.HotPoneroChance) < 0.015, $"兑换处开放后的 Ponero:{pon}/4000");
Check(HotCoins.Pick(new[] { MarketEvent.RevRaidUpper }, false, new Rng(1)) is string sa ? sa == Assets.PSC : true, "革命军袭击上层是 PSC");
Check(HotCoins.Pick(new MarketEvent[0], false, rng) == null, "没有事件、兑换处没开:没人上门");
var o = HotCoins.Make(Assets.CRT, 1, 500, false, new Rng(9));
Check(o.Value >= 500 * Tuning.HotValueMinRents && o.Value <= 500 * Tuning.HotValueMaxRents && o.Cash >= o.Value * Tuning.HotCutMin - 1 && o.Cash <= o.Value * Tuning.HotCutMax + 1, $"报价:市值 {o.Value:0},现金 {o.Cash}");
Check(Math.Abs(ev / 4000.0 - Tuning.OtcEventChance) < 0.03, $"帮派火并期间多是 CRT、全额污点:{ev}/4000");
Check(Math.Abs(sides / 4000.0 - Tuning.OtcSellShare) < 0.03, $"卖币、买币各占一半:{sides}/4000");
Check(Otc.EventAsset(new List<MarketEvent> { MarketEvent.RevRaidUpper }, new Rng(1)) == Assets.PSC && Otc.EventAsset(none, new Rng(1)) == null, "革命军袭击上层是 PSC;没有事件时没有事件币");
// 平时:PSC/CRT/CRA 随机,污点比例在区间内,越脏越便宜
for (int i = 0; i < 200; i++)
{
var o = Otc.MakeSell(none, false, pr, 500, rng);
Check(Array.IndexOf(Otc.SellAssets, o.Asset) >= 0 && o.TaintRatio >= Tuning.OtcTaintMin && o.TaintRatio <= Tuning.OtcTaintMax, "平时的币种与污点比例");
Check(o.Value >= 500 * Tuning.OtcValueMinRents && o.Value <= 500 * Tuning.OtcValueMaxRents && Math.Abs(o.Cash - o.Value * Otc.CutFor(o.TaintRatio)) <= 1, $"市值与现金价:{o.Value:0} / {o.Cash}");
}
Check(Otc.CutFor(0) == Tuning.OtcCutClean && Otc.CutFor(1) == Tuning.OtcCutDirty && Otc.CutFor(0.3) > Otc.CutFor(0.7), "折扣按污点比例线性降低");
// 收下:币进钱包,成本是付的现金,污点 = 市值 × 比例(带上限)
var oc = new OtcOffer { Side = OtcSide.Sell, Asset = Assets.CRT, Qty = 1000, Value = 1000, TaintRatio = 0.4, Cash = 700 };
var w = new Wallet();
HotCoins.Receive(w, o);
Check(Near(w.Get(Assets.CRT), o.Qty, 1e-9) && Near(w.Taint, o.Value, 1e-9) && Near(w.Basis, o.Cash, 1e-9), "CRT 全额带污点,成本是付的现金");
var op = HotCoins.Make(Assets.Ponero, 50, 500, false, new Rng(9));
var wp = new Wallet();
HotCoins.Receive(wp, op);
Check(wp.Taint == 0 && op.Cash >= op.Value * Tuning.HotPoneroCutMin - 1, "Ponero 不带污点、折扣小");
Check(Otc.Receive(w, oc, pr) == 0 && Near(w.Get(Assets.CRT), 1000, 1e-9) && Near(w.Taint, 400, 1e-9) && Near(w.Basis, 700, 1e-9), "污点按比例,成本是付的现金");
var wf = new Wallet(); wf.Add(Assets.CRC, 100); wf.Taint = 1000;
Check(Near(Otc.Receive(wf, oc, pr), 300, 1e-9) && Near(wf.Taint, 1100, 1e-9), "原有污点多时超出的部分返回");
// Ponero:兑换处开放后偶尔有,不带污点
int pon = 0;
for (int i = 0; i < 4000; i++) { var o = Otc.MakeSell(none, true, pr, 500, rng); if (o.Asset == Assets.Ponero) { pon++; Check(o.TaintRatio == 0 && o.Cash >= o.Value * Tuning.OtcPoneroCutMin - 1, "Ponero 不带污点"); } }
Check(Math.Abs(pon / 4000.0 - Tuning.OtcPoneroChance) < 0.02, $"兑换处开放后的 Ponero:{pon}/4000");
int stings = 0, stingsCd = 0;
for (int i = 0; i < 4000; i++) { if (HotCoins.Make(Assets.CRT, 1, 500, false, rng).Sting) stings++; if (HotCoins.Make(Assets.CRT, 1, 500, true, rng).Sting) stingsCd++; }
Check(stingsCd > stings * 2, $"镇压期间钓鱼执法更多:{stings} → {stingsCd}");
for (int i = 0; i < 4000; i++)
{
if (Otc.MakeSell(none, false, pr, 500, rng).Sting) stings++;
if (Otc.MakeSell(new List<MarketEvent> { MarketEvent.Crackdown }, false, pr, 500, rng).Sting) stingsCd++;
}
Check(stingsCd > stings * 2, $"镇压期间便衣更多:{stings} → {stingsCd}");
// 买币:市值按房租,三档溢价,越高越难成交
var ob = Otc.MakeBuy(pr, 500, new Rng(7));
Check(Array.IndexOf(Otc.BuyAssets, ob.Asset) >= 0 && ob.Value >= 500 * Tuning.OtcBuyMinRents && ob.Value <= 500 * Tuning.OtcBuyMaxRents && Near(ob.Qty * pr(ob.Asset), ob.Value, 1e-6), "买币的币种与数量");
Check(Otc.Quote(ob, 0) < Otc.Quote(ob, 1) && Otc.Quote(ob, 1) < Otc.Quote(ob, 2) && Otc.Quote(ob, 2) == (int)Math.Round(ob.Value * (1 + Tuning.OtcPremium[2])), "三档报价");
for (int t = 0; t < 3; t++)
{
int ok = 0; var r = new Rng((ulong)(t + 11));
for (int i = 0; i < 4000; i++) if (Otc.Accepts(t, r)) ok++;
Check(Math.Abs(ok / 4000.0 - Tuning.OtcAccept[t]) < 0.03, $"第 {t + 1} 档成交概率:{ok}/4000");
}
// 来历:脏的来源按污点比例出现,数量占比合计为 1
var hist = Otc.History(new OtcOffer { TaintRatio = 1 }, new[] { "dirty" }, new[] { "clean" }, new Rng(2));
Check(hist.Count >= 1 && hist.Count <= Tuning.OtcHistoryMax && hist.TrueForAll(kv => kv.Key == "dirty") && Near(hist.Sum(kv => kv.Value), 1, 1e-9), "全脏的币来历都是脏来源");
var histC = Otc.History(new OtcOffer { TaintRatio = 0 }, new[] { "dirty" }, new[] { "clean" }, new Rng(2));
Check(histC.TrueForAll(kv => kv.Key == "clean"), "干净的币来历都是干净来源");
var desk = new OtcDesk(); desk.Record(700); desk.Record(300);
Check(desk.Volume == 1000 && desk.Deals == 2, "成交统计");
});
Test("预测市场:LMSR 定价、最大亏损、买卖", () =>