Phase 2: CRT freeze during crackdowns (frozen balance, thaw, news, test entry)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-03 16:51:12 +08:00
1 parent be18078f69
commit c295e72a48
10 files changed
+91 -5

No files matched your search

+2
View File
@@ -4,6 +4,8 @@
### 第二阶段(进行中)
- CRT 冻结:镇压事件开始时,按黑市声望(声望 0 时 40%,每点 −0.8%,50 以上不会)冻结钱包里一半的 CRT,3 天后解冻;冻结部分仍计入余额与估值,但不能交易、不能存混币器。Web3 页钱包下显示"CRT 冻结 N,第 X 天解冻";上报纸,夜间日志写冻结与解冻。开发者模式 F9 有 `Frozen CRT (test)`。游戏内验证了冻结显示与超额卖出被拒;镇压事件触发和夜间解冻只有单元测试。税与稽查不做(游戏本身没有税)。
- 人脉网络里本 mod 单独一页"Web3":入口(400)和混币器(700,需先加入)都放在这页。
- 混币器:终端 Web3 页里多一个"混币器"子页(买了升级才出现,和"交易"并列)。选币种、数量、1–5 轮,存入后每轮扣 2% 手续费并降低污点(基准每轮 35%,存入越大清洗越差,匿名集 = 2 万信用点背景量 + 存入量),轮数即等待天数,到期提回同一个地址;提回后一周内出金审查按"来自混币器"加重。镇压事件开始时 50% 被制裁 5 天,期间提出的币地址被标记,并上报纸;每晚 0.4% 跑路。夜间日志写提回与污点变化。开发者模式 F9 有 `Tainted wallet (test)`。游戏内验证了解锁、页面、存入与污点带走;夜间提回只有单元测试(测试存档里无法正常过夜)。
+1 -1
View File
@@ -47,7 +47,7 @@ namespace Decentralized.Domain
/// <summary>存入:从钱包按价值比例带走污点与成本。轮数超出范围或余额不足返回 null。</summary>
public MixDeposit Deposit(Wallet w, string asset, double amount, int rounds, int day, Func<string, double> price)
{
if (rounds < 1 || rounds > Tuning.MixerMaxRounds || amount <= 0 || w.Get(asset) + 1e-9 < amount) return null;
if (rounds < 1 || rounds > Tuning.MixerMaxRounds || amount <= 0 || w.Avail(asset) + 1e-9 < amount) return null;
double value = amount * price(asset);
if (value < Tuning.MixerMin) return null;
double total = w.Value(price);
+3
View File
@@ -49,6 +49,9 @@ namespace Decentralized.Domain
public static double CrtFreezeChance(double blackMarketRep) =>
Math.Max(0, Tuning.CrtFreezeBase - blackMarketRep * Tuning.CrtFreezePerRep);
/// <summary>镇压开始时是否冻结玩家的 CRT:按黑市声望,声望越低越容易被黑市"出卖"。</summary>
public static bool CrtFrozenByCrackdown(double blackMarketRep, Rng rng) => rng.Chance(CrtFreezeChance(blackMarketRep));
public bool CraSubsidyLow => CraSubsidy <= Tuning.CraSubsidy0 * Tuning.CraSubsidyWarn;
/// <summary>CRA 的预兆:外生螺旋的新闻已出,或补贴池告急(利率下调)。螺旋开始后不再算预兆。</summary>
+30
View File
@@ -32,6 +32,36 @@ namespace Decentralized.Domain
/// <summary>买入均价(信用点);没有买入记录(例如全是挖出来的)返回 null。</summary>
public double? AvgBuy(string asset) => BuyQty.TryGetValue(asset, out var q) && q > 1e-9 ? BuyCost[asset] / q : (double?)null;
/// <summary>被发行方冻结的数量(只有可冻结的币:CRT 在镇压期间、CRC),和解冻的日子。冻结的币仍计入余额与估值,但不能花、不能转。</summary>
public Dictionary<string, double> Frozen = new Dictionary<string, double>();
public Dictionary<string, int> FrozenUntil = new Dictionary<string, int>();
/// <summary>可动用的余额(扣掉冻结部分)。</summary>
public double Avail(string asset) => Math.Max(0, Get(asset) - (Frozen.TryGetValue(asset, out var f) ? f : 0));
/// <summary>冻结该币当前余额的 share 比例,直到 untilDay(含)。已冻结的不重复冻。返回冻结的数量。</summary>
public double Freeze(string asset, double share, int untilDay)
{
double amount = Avail(asset) * Math.Max(0, Math.Min(1, share));
if (amount <= 1e-9) return 0;
Frozen[asset] = (Frozen.TryGetValue(asset, out var f) ? f : 0) + amount;
FrozenUntil[asset] = Math.Max(untilDay, FrozenUntil.TryGetValue(asset, out var u) ? u : 0);
return amount;
}
/// <summary>解冻到期的币,返回 (币种, 数量) 列表。</summary>
public List<KeyValuePair<string, double>> Thaw(int day)
{
var list = new List<KeyValuePair<string, double>>();
foreach (var a in new List<string>(FrozenUntil.Keys))
if (day >= FrozenUntil[a])
{
list.Add(new KeyValuePair<string, double>(a, Frozen.TryGetValue(a, out var f) ? f : 0));
Frozen.Remove(a); FrozenUntil.Remove(a);
}
return list;
}
public double Get(string asset) => Balances.TryGetValue(asset, out var v) ? v : 0;
public void Add(string asset, double amount)
+7
View File
@@ -15,6 +15,13 @@ namespace Decentralized
public static void Subscribe()
{
ModHook.OnGenerateCustomerLate += OnGenerate;
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Frozen CRT (test)", () => Log.Guard("冻结 CRT(测试)", () =>
{
var w = Session.Data.Hot;
w.Add(Assets.CRT, 1000);
double f = w.Freeze(Assets.CRT, Tuning.CrtFreezeShare, StoreStation.GetDayCounter() + Tuning.CrtUnfreezeAfter);
Log.Info($"CRT {w.Get(Assets.CRT):0},冻结 {f:0},可动用 {w.Avail(Assets.CRT):0}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Tainted wallet (test)", () => Log.Guard("带污点的钱包(测试)", () =>
{
var w = Session.Data.Hot;
@@ -200,3 +200,9 @@ nightlog.mixer_flag, (withdrawn during sanctions; address flagged)
nightlog.mixer_rug,The mixer's operator ran off with it: {0} {1} never came back.
web3.sub.trade,Trade
web3.sub.mixer,Mixer
msg.frozen,Those coins are frozen until they thaw.
wallet.frozen,"{0}: {1} frozen, thaws on day {2}"
nightlog.crt_freeze,The black market handed over addresses during the crackdown: {0} of your CRT is frozen until day {1}.
nightlog.thaw,Frozen {0} {1} thawed.
news.crtfreeze.title,Black market freezes some CRT addresses for the crackdown
news.crtfreeze.desc,"Word is the black market gave security a list of CRT addresses in exchange for being left alone. Holders with low standing are likelier to be frozen."
1 # Probably Decentralized localization table: English (en)
200 nightlog.mixer_rug
201 web3.sub.trade
202 web3.sub.mixer
203 msg.frozen
204 wallet.frozen
205 nightlog.crt_freeze
206 nightlog.thaw
207 news.crtfreeze.title
208 news.crtfreeze.desc
@@ -199,3 +199,9 @@ nightlog.mixer_flag,(制裁期间提出,地址被标记)
nightlog.mixer_rug,混币器的运营者卷款跑路了:{0} {1} 没有提回。
web3.sub.trade,交易
web3.sub.mixer,混币器
msg.frozen,这部分币被冻结了,到期前不能动用。
wallet.frozen,"{0} 冻结 {1},第 {2} 天解冻"
nightlog.crt_freeze,镇压期间黑市交出了一批地址:你的 {0} CRT 被冻结,第 {1} 天解冻。
nightlog.thaw,冻结的 {0} {1} 已解冻。
news.crtfreeze.title,黑市配合镇压冻结部分 CRT 地址
news.crtfreeze.desc,"传闻黑市为换取放行,向安保交出了一批 CRT 地址。声望低的持有者更容易被冻结。"
1 # Probably Decentralized 本地化表:简体中文(zh)
199 nightlog.mixer_rug
200 web3.sub.trade
201 web3.sub.mixer
202 msg.frozen
203 wallet.frozen
204 nightlog.crt_freeze
205 nightlog.thaw
206 news.crtfreeze.title
207 news.crtfreeze.desc
+15 -1
View File
@@ -83,7 +83,21 @@ namespace Decentralized
int day = StoreStation.GetDayCounter();
var events = ActiveEvents();
bool wasSanctioned = d.Mixer.Sanctioned(day);
foreach (var ev in events.Where(e => !d.ActiveEvents.Contains(e))) { d.Stable.OnEventStart(ev, day, d.Rng); d.Mixer.OnEvent(ev, day, d.Rng); }
foreach (var ev in events.Where(e => !d.ActiveEvents.Contains(e)))
{
d.Stable.OnEventStart(ev, day, d.Rng); d.Mixer.OnEvent(ev, day, d.Rng);
// 镇压开始:黑市为换取放行,按声望出卖一批 CRT 地址
if (ev == MarketEvent.Crackdown && Stablecoins.CrtFrozenByCrackdown(StoreReputation.GetBMReputation(), d.Rng))
{
double frozen = d.Hot.Freeze(Assets.CRT, Tuning.CrtFreezeShare, day + Tuning.CrtUnfreezeAfter);
if (frozen > 0)
{
PlayerStore.Instance.AddNightLog(L.T("nightlog.crt_freeze", Fmt.Num(frozen), day + Tuning.CrtUnfreezeAfter), "cc7755");
News.Show("pd_news_crtfreeze_" + day, L.T("news.crtfreeze.title"), L.T("news.crtfreeze.desc"), 3);
}
}
}
foreach (var t in d.Hot.Thaw(day)) PlayerStore.Instance.AddNightLog(L.T("nightlog.thaw", Fmt.Num(t.Value), t.Key));
if (!wasSanctioned && d.Mixer.Sanctioned(day) && Unlock.MixerUnlocked) News.Show("pd_news_sanction_" + day, L.T("news.sanction.title"), L.T("news.sanction.desc"), 3);
d.ActiveEvents = events;
d.Network.Availability = (events.Contains(MarketEvent.Blackout) ? 0.8 : 1) * (events.Contains(MarketEvent.LowerUnrest) ? 0.9 : 1);
+7 -3
View File
@@ -212,6 +212,8 @@ namespace Decentralized
TableRow(b, HoldW, kv.Key, Fmt.Num(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key)), AvgText(w, kv.Key));
}
if (!any) b.AddLabel(L.T("wallet.empty"));
foreach (var kv in w.Frozen.OrderBy(k => k.Key).Where(k => show(k.Key)))
b.AddLabel(L.T("wallet.frozen", kv.Key, Fmt.Num(kv.Value), w.FrozenUntil.TryGetValue(kv.Key, out var u) ? u : 0));
}
/// <summary>Poinbase:合规交易所。左边账户持仓,右边交易;信用点的存取在银行。</summary>
@@ -296,7 +298,7 @@ namespace Decentralized
r.AddButton(asset == _mixAsset ? $"[{asset}]" : asset, () => { _mixAsset = asset; Open(); });
}
r.End();
r.AddLabel(L.T("mixer.balance", _mixAsset, Fmt.Num(w.Get(_mixAsset))));
r.AddLabel(L.T("mixer.balance", _mixAsset, Fmt.Num(w.Avail(_mixAsset))));
AmountRow(r);
r.AddLabel(L.T("mixer.rounds"));
r.BeginRow();
@@ -320,13 +322,15 @@ namespace Decentralized
if (!ReadAmount(out double v)) return;
var d = Session.Data;
int day = StoreStation.GetDayCounter();
if (d.Hot.Get(_mixAsset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
if (d.Hot.Avail(_mixAsset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
var dep = d.Mixer.Deposit(d.Hot, _mixAsset, v, _mixRounds, day, Session.Price);
_message = dep == null ? L.T("mixer.min", Tuning.MixerMin) : L.T("mixer.done", Fmt.Num(v), _mixAsset, dep.ReadyDay);
Session.Save();
Open();
}
private static bool frozenPay(Wallet w, string asset) => w.Frozen.ContainsKey(asset);
private static string AvgText(Wallet w, string asset) => w.AvgBuy(asset) is double a ? Fmt.Num(a) : L.T("avg.none");
private static void ReportPage(CustomUIBuilder b)
@@ -393,7 +397,7 @@ namespace Decentralized
if (!ReadAmount(out double v)) return;
var w = Session.Data.Hot;
string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
if (w.Get(pay) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
if (w.Avail(pay) + 1e-9 < v) { _message = L.T(frozenPay(w, pay) ? "msg.frozen" : "msg.balance"); Open(); return; }
double got = buy ? p.BuyBase(v) : p.SellBase(v);
double spent = v * Session.Price(pay);
w.Add(pay, -v);
@@ -526,6 +526,20 @@ internal static class Program
Check(w2.Flagged, "制裁期间提出的币被标记");
});
Test("CRT 冻结:冻一半、可动用余额减少、到期解冻;声望越高越不容易被冻", () =>
{
var w = new Wallet { Address = "f" };
w.Add(Assets.CRT, 1000);
double fz = w.Freeze(Assets.CRT, Tuning.CrtFreezeShare, 10);
Check(Near(fz, 500, 1e-9) && Near(w.Avail(Assets.CRT), 500, 1e-9) && Near(w.Get(Assets.CRT), 1000, 1e-9), "冻结一半:余额不变,可动用 500");
Check(Near(w.Freeze(Assets.CRT, 1, 12), 500, 1e-9) && Near(w.Avail(Assets.CRT), 0, 1e-9), "再冻一次只冻可动用部分");
Check(w.Thaw(9).Count == 0 && w.Thaw(12).Count == 1 && Near(w.Avail(Assets.CRT), 1000, 1e-9), "到期(取较晚的日子)全部解冻");
Check(new Mixer().Deposit(w, Assets.CRT, 2000, 1, 1, a => 1) == null, "余额不足不能存入混币器");
int low = 0, high = 0;
for (int i = 0; i < 2000; i++) { if (Stablecoins.CrtFrozenByCrackdown(0, new Rng((ulong)i + 1))) low++; if (Stablecoins.CrtFrozenByCrackdown(50, new Rng((ulong)i + 1))) high++; }
Check(low > 700 && low < 900 && high == 0, $"声望 0 约 40% 被冻:{low}/2000;声望 50 不会被冻:{high}");
});
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
return _failed == 0 ? 0 : 1;
}