Tumbler 扩展:治安部探员与搜查、密钥泄露还原出款污点、审判预测盘、黑市跑路、报复、长期联系人、混币器换皮重开

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Opus 5.5 committed 2026-11-08 18:30:28 +08:00
1 parent 0f26d88c03
commit a37dd4ca7c
19 files changed
+904 -20

No files matched your search

@@ -1928,6 +1928,97 @@ internal static class Program
finally { StoryMods.Current = saved; }
});
Test("Tumbler 之后:探员与搜查卡住上门日程、泄露还原出款污点、审判、黑市跑路、换皮重开", () =>
{
var saved = StoryMods.Current;
try
{
StoryMods.Current = new StoryMods();
// 探员:第 2 次之后的第二天起来,来过的第二天起才排第 3 次
var t = new Tumbler(); var a = new TumblerAfter();
t.TryStart(40, true); t.Visited(41); t.Visited(44);
Check(!a.AgentDue(t, 44) && a.AgentDue(t, 45), "探员在第 2 次上门的第二天起来");
Check(t.Due(46, a) == 0 && t.Due(46) == 3, "探员没来过,第 3 次不排(不传进度时照旧)");
a.Agent = TumblerAfter.Deny; a.AgentDay = 46;
Check(t.Due(46, a) == 0 && t.Due(47, a) == 3 && !a.AgentDue(t, 47), "探员来过的第二天起排第 3 次,只来一次");
t.Choice = Tumbler.Hide; t.Visited(47);
Check(!a.NeedSearch(t) && t.Due(51, a) == 4, "否认过:不搜查,第 4 次照常");
// 配合过又藏了:第 4 次前一天搜查
var t2 = new Tumbler(); var a2 = new TumblerAfter { Agent = TumblerAfter.Cooperate, AgentDay = 45 };
t2.TryStart(40, true); t2.Visited(41); t2.Visited(44); t2.Choice = Tumbler.Hide; t2.Visited(46);
Check(!a2.SearchDue(t2, 48) && a2.SearchDue(t2, 49) && t2.Due(50, a2) == 0, "配合 + 藏:第 4 次前一天搜查,搜查前不排第 4 次");
a2.Searched = true; a2.SearchDay = 50;
Check(t2.Due(50, a2) == 0 && t2.Due(51, a2) == 4 && !a2.SearchDue(t2, 51), "搜查的第二天起排第 4 次");
var t3 = new Tumbler { Choice = Tumbler.Report }; t3.TryStart(40, true); t3.Visited(41); t3.Visited(44); t3.Visited(46);
Check(!a2.NeedSearch(t3), "举报的不搜查");
// 出款记录与泄露
Func<string, double> px = x => 1;
var w = new Wallet { Address = "w" }; w.Add(Assets.CRC, 5000); w.Taint = 4000;
var m = new Mixer();
m.Deposit(w, Assets.CRC, 1000, 3, 1, px);
m.Night(w, 4, px, new Rng(1));
var r0 = m.Returns.Single();
Check(r0.Day == 4 && r0.Wallet == "w" && r0.Cleaned > 100 && Near(r0.Cleaned, 800 - (w.Taint - 3200), 1e-6), $"出款记下洗掉的污点:{r0.Cleaned:0}");
double before = w.Taint;
var lk = m.Leak(4 + Tuning.TumblerLeakDays, x => x == "w" ? w : null, px);
Check(lk.Count == 1 && Near(w.Taint - before, r0.Cleaned, 1e-6) && lk.Over == 0 && m.Returns.Count == 0, "泄露:洗掉的污点全部加回,记录清空");
var m2 = new Mixer();
m2.Returns.Add(new MixReturn { Day = 1, Wallet = "v", Amount = 10, Cleaned = 500 });
m2.Returns.Add(new MixReturn { Day = 40, Wallet = "v", Amount = 10, Cleaned = 300 });
m2.Returns.Add(new MixReturn { Day = 40, Wallet = "gone", Amount = 10, Cleaned = 300 });
var v = new Wallet { Address = "v" }; v.Add(Assets.CRC, 100); v.Taint = 0;
var lk2 = m2.Leak(45, x => x == "v" ? v : null, px);
Check(lk2.Count == 1 && Near(v.Taint, 100, 1e-9) && Near(lk2.Over, 200, 1e-9), "只还原 30 天内的;钱包不在的跳过;超出钱包价值的部分返回");
a.LeakTonight(50); a.LeakTonight(60);
Check(a.LeakDay == 50, "泄露只安排一次");
// 审判
int conv = 0;
for (int i = 0; i < 2000; i++) { var x = new TumblerAfter(); x.StartTrial(10, new Rng((ulong)i + 1)); if (x.Verdict == 1) conv++; }
Check(Near(conv / 2000.0, Tuning.TumblerTrialConvict, 0.03), $"定罪约 {Tuning.TumblerTrialConvict:P0}:{conv / 2000.0:P1}");
var tr = new TumblerAfter(); tr.StartTrial(10, new Rng(7));
Check(tr.TrialOpenDay == 10 + Tuning.TumblerTrialOpen && tr.TrialDay == 10 + Tuning.TumblerTrialDays && tr.ClosedDay == 10, "开盘、宣判、关停的日子");
var rng = new Rng(9);
Check(tr.Hint(tr.TrialOpenDay - 2, rng) == null && tr.Hint(tr.TrialOpenDay - 1, rng) != null && tr.Hint(tr.TrialOpenDay - 1, rng) == null, "开盘前一晚起每晚一条");
for (int dd = tr.TrialOpenDay; dd < tr.TrialDay + 3; dd++) tr.Hint(dd, rng);
Check(tr.Hints.Count == tr.TrialDay - tr.TrialOpenDay + 1, $"宣判前一晚为止:{tr.Hints.Count} 条");
Check(Near(new TumblerAfter().TrialPublic(), Tuning.TumblerTrialConvict, 1e-9), "没有新闻时公开概率 = 先验");
double sumC = 0, sumA = 0; int nC = 0, nA = 0;
for (int i = 0; i < 1000; i++)
{
var x = new TumblerAfter(); var rr = new Rng((ulong)i + 11); x.StartTrial(0, rr);
for (int dd = 0; dd < 10; dd++) x.Hint(dd, rr);
if (x.Verdict == 1) { sumC += x.TrialPublic(); nC++; } else { sumA += x.TrialPublic(); nA++; }
}
Check(sumC / nC > 0.85 && sumA / nA < 0.5, $"新闻把公开概率推向真实判决:定罪 {sumC / nC:0.00},无罪 {sumA / nA:0.00}");
// 黑市跑路、换皮重开
var sold = new TumblerAfter(); sold.StartSold(20, new Rng(3));
Check(sold.BmRugDay >= 20 + Tuning.TumblerBmRugMin && sold.BmRugDay <= 20 + Tuning.TumblerBmRugMax, $"黑市跑路在 {sold.BmRugDay}");
var mr = new Mixer(); var w3 = new Wallet { Address = "z" }; w3.Add(Assets.CRC, 1000);
mr.Deposit(w3, Assets.CRC, 400, 2, 1, px); mr.Deposit(w3, Assets.CRC, 300, 3, 1, px);
var rug = mr.RugAll();
Check(rug.Count == 2 && rug.All(x => x.Rugged) && mr.Pending.Count == 0, "黑市跑路:在途的全部丢失");
var rb = new TumblerAfter { ClosedDay = 30 };
Check(!rb.RebrandDue(30 + Tuning.TumblerRebrandDays - 1) && rb.RebrandDue(30 + Tuning.TumblerRebrandDays) && !new TumblerAfter().RebrandDue(100), "关停满 15 天换皮,没关停不换");
var sm = new StoryMods(); StoryMods.Current = sm;
sm.Apply("tumbler_sold", 1); sm.Apply("tumbler_bm_rug", 2);
Check(sm.MixerClosed, "黑市跑路后混币器关停");
sm.Rebrand(false);
Check(!sm.MixerClosed && sm.MixerRebranded && Near(Mixer.Fee, Tuning.MixerFee, 1e-12) && sm.MixerRug == 1, "换皮重开:重新开放,费率和跑路倍率回到 1");
var w4 = new Wallet { Address = "h" }; w4.Add(Assets.CRC, 500);
var mh = new Mixer(); mh.Deposit(w4, Assets.CRC, 500, 1, 1, px); mh.Night(w4, 2, px, new Rng(5));
Check(!w4.Flagged, "正常的新混币器不标记");
sm.Rebrand(true);
Check(Near(Mixer.Fee, Tuning.MixerFee * Tuning.TumblerHoneypotFee, 1e-12), "钓鱼混币器费率打折");
var w5 = new Wallet { Address = "k" }; w5.Add(Assets.CRC, 500);
var mk = new Mixer(); mk.Deposit(w5, Assets.CRC, 500, 1, 1, px); mk.Night(w5, 2, px, new Rng(5));
Check(w5.Flagged, "从钓鱼混币器提回的币被标记");
}
finally { StoryMods.Current = saved; }
});
Test("黑市商家收币:每类商家都收 CRT 或隐私币,平台收 PSC、CRT、CRA", () =>
{
foreach (MerchantType ty in Enum.GetValues(typeof(MerchantType)))