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:
1 parent
be18078f69
commit
c295e72a48
10 files changed
+91
-5
No files matched your search
@@ -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)`。游戏内验证了解锁、页面、存入与污点带走;夜间提回只有单元测试(测试存档里无法正常过夜)。
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
|
@@ -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 地址。声望低的持有者更容易被冻结。"
|
||||
|
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user