diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36c5678..57a219c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
## 未发布
+### 第三阶段:数值调优(进行中)
+
+- 混币器每轮手续费 6% → 4%;用币付款的顾客概率提到上层 30%、黑市 50%、革命派 50%、下层 25%,并多付 12%(溢价只加在币数上,污点仍按货值算)。
+- 区块奖励 90 → 100;RZ800 耗电 12 → 14 kJ。
+- 减半对币价的传导改成每次在 0.15–0.85 的指数里随机(锚 +11% 到 +80%),历次累计存档。
+- 低档次的卡也会被冒充:新增废片芯片 RZ10(不上架,只作假卡背后的真实芯片)。
+- 型号档次与实物不符(供电口、风扇数)的卡,摆给顾客时必被看穿并扣信誉(原来 50%),堵住用标签机把低档卡改成高档型号转卖的漏洞。
+- 买家看穿率、信誉扣减封顶收进 `Tuning.cs`。
+- 单元测试 8909 项通过;游戏内只验证了模组正常载入,没有跑到新的假卡、减半随机和币付款溢价的实际画面。
+
### 第二阶段(进行中)
- 开发者模式 F9 新增 `Run night (test)`、`Morning report (test)`、`Newspaper (test)`(走游戏自己的夜间流程并进入下一天,事件按天数过期)和 `Start crackdown (test)`,用来测夜间逻辑;`Tainted wallet (test)` 现在同时给 1000 CRT。
diff --git a/src/Decentralized.Domain/Market.cs b/src/Decentralized.Domain/Market.cs
index 8714412..bc5e246 100644
--- a/src/Decentralized.Domain/Market.cs
+++ b/src/Decentralized.Domain/Market.cs
@@ -44,16 +44,22 @@ namespace Decentralized.Domain
/// PSC 的参考价(信用点)。
public double Price => Main.Price;
- /// 供给因子的目标:奖励比起始奖励每少一半,锚上移 2^传导指数;减半前 7 天内再逐日加预期。
- public static double SupplyTarget(int day, double reward)
+ /// 历次减半抽到的传导指数之和:价格锚的供给因子目标为 2^此值。
+ public double HalvingLog;
+
+ /// 减半当晚:抽本次的传导指数。
+ public void OnHalving(Rng rng) => HalvingLog += Tuning.HalvingPassMin + rng.NextDouble() * (Tuning.HalvingPassMax - Tuning.HalvingPassMin);
+
+ /// 供给因子的目标:已发生的减半各自抬高锚;减半前 7 天内再逐日加预期。
+ public double SupplyTarget(int day)
{
- double t = Math.Pow(Tuning.BlockReward / Math.Max(1e-9, reward), Tuning.HalvingPassThrough);
+ double t = Math.Pow(2, HalvingLog);
int toHalving = Tuning.HalvingDays - day % Tuning.HalvingDays; // 减半当晚 day % HalvingDays == 0,那天之后又是一个完整周期
if (toHalving <= Tuning.HalvingRampDays) t *= 1 + Tuning.HalvingRamp * (Tuning.HalvingRampDays - toHalving + 1) / Tuning.HalvingRampDays;
return t;
}
- public void UpdateSupply(int day, double reward) => Supply += (SupplyTarget(day, reward) - Supply) * Tuning.SupplyAdjust;
+ public void UpdateSupply(int day) => Supply += (SupplyTarget(day) - Supply) * Tuning.SupplyAdjust;
public double Anchor(ICollection active)
{
diff --git a/src/Decentralized.Domain/Mining.cs b/src/Decentralized.Domain/Mining.cs
index 8ce749b..b27bc58 100644
--- a/src/Decentralized.Domain/Mining.cs
+++ b/src/Decentralized.Domain/Mining.cs
@@ -15,6 +15,8 @@ namespace Decentralized.Domain
/// 暴毙概率的品牌系数。
/// 档次:0 低端、1 中端、2 高端、3 旗舰(显卡)。实物特征(供电口、风扇数)只看档次。
public int Tier;
+ /// 废片:只会作为假卡背后的真实芯片出现,不上架、不单独出现。低档次的卡也能被冒充靠它。
+ public bool Scrap;
public double DeathFactor;
/// 每天损耗的系数。
public double WearFactor;
@@ -39,7 +41,8 @@ namespace Decentralized.Domain
Gpu("RZX60", Probidia, 0, 1.0, 4, 220), Gpu("RZX70", Probidia, 1, 5.0, 12, 800),
Gpu("RZX80", Probidia, 2, 8.0, 18, 1300), Gpu("RZX90", Probidia, 3, 20.0, 30, 5000),
Gpu("RZ70", Pmd, 0, 0.8, 2.5, 170), Gpu("RZ80", Pmd, 1, 4.0, 8, 680),
- Gpu("RZ800", Pmd, 2, 8.0, 12, 1500), Gpu("RZ900", Pmd, 3, 15.0, 20, 4000),
+ Gpu("RZ800", Pmd, 2, 8.0, 14, 1500), Gpu("RZ900", Pmd, 3, 15.0, 20, 4000),
+ Scrap("RZ10", Pmd, 0, 0.3, 2, 60),
Asic("M1", 50, 60, 11000), Asic("M2", 120, 110, 27000), Asic("M3", 300, 220, 72000),
}.ToDictionary(s => s.Model);
@@ -49,6 +52,13 @@ namespace Decentralized.Domain
DeathFactor = brand == Probidia ? 0.7 : 1.0, WearFactor = brand == Probidia ? 1.0 : 1.2,
};
+ private static HardwareSpec Scrap(string model, string brand, int tier, double h, double kj, int price)
+ {
+ var s = Gpu(model, brand, tier, h, kj, price);
+ s.Scrap = true;
+ return s;
+ }
+
private static HardwareSpec Asic(string model, double h, double kj, int price) => new HardwareSpec
{
Model = model, Brand = RatDigg, Hash = h, Kj = kj, Price = price, DeathFactor = 0.5, WearFactor = 0.8, Asic = true,
diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs
index 95f684f..7c7fe10 100644
--- a/src/Decentralized.Domain/Tuning.cs
+++ b/src/Decentralized.Domain/Tuning.cs
@@ -10,10 +10,11 @@ namespace Decentralized.Domain
// 挖矿
public const double PscStartPrice = 100;
- public const double BlockReward = 90;
+ public const double BlockReward = 100;
public const int HalvingDays = 90;
- /// 减半对币价的传导:奖励减半时,价格锚上移 2^传导指数 倍(0.6 即约 +52%),市场已部分预期到,所以不是翻倍。
- public const double HalvingPassThrough = 0.6;
+ /// 减半对币价的传导:每次减半在区间内随机一个指数,价格锚上移 2^指数 倍(0.15–0.85 即约 +11% 到 +80%)。
+ /// 参考比特币:每次减半后的涨幅差别很大,市场也已部分预期到,所以既不固定也不翻倍。
+ public const double HalvingPassMin = 0.15, HalvingPassMax = 0.85;
/// 减半前一周的预期行情:锚逐日抬高,到减半当天为 +8%。
public const int HalvingRampDays = 7;
public const double HalvingRamp = 0.08;
@@ -67,6 +68,9 @@ namespace Decentralized.Domain
public const double CoinPayCrc = 0.30, CoinPayCrt = 0.50, CoinPayCra = 0.50, CoinPayPsc = 0.25;
/// 用币付款的溢价:顾客多付的比例,补偿持币的污点与兑出的手续费。
public const double CoinPayPremium = 0.12;
+ /// 顾客验货:说法里每一项隐瞒被看穿的概率;型号档次不符(供电口、风扇数一眼可见)必被看穿;一次看穿最多扣的信誉。
+ public const double CustomerSpotChance = 0.50, CustomerSpotTierMismatch = 1.0;
+ public const int RepLossMax = 25;
/// 单个金融违规事件记入的罪证上限。
public const double CrimeEventCap = 400;
diff --git a/src/Decentralized.Domain/UsedDealer.cs b/src/Decentralized.Domain/UsedDealer.cs
index fe6b5e8..c89aaff 100644
--- a/src/Decentralized.Domain/UsedDealer.cs
+++ b/src/Decentralized.Domain/UsedDealer.cs
@@ -48,7 +48,7 @@ namespace Decentralized.Domain
double share = kind == DealerKind.Scammer ? FakeShareScammer : kind == DealerKind.Middleman ? FakeShareMiddleman : 0;
var label = pool[rng.Range(0, pool.Length - 1)];
var card = new UsedCard { Model = label, Health = 25 + rng.NextDouble() * 60 };
- var cheaper = Hardware.Specs.Values.Where(s => !s.Asic && s.Price * 2 <= Hardware.Specs[label].Price).Select(s => s.Model).ToArray();
+ var cheaper = Hardware.Specs.Values.Where(s => !s.Asic && s.Price * 2 <= Hardware.Specs[label].Price && (!s.Scrap || Hardware.Specs[label].Tier == 0)).Select(s => s.Model).ToArray();
if (cheaper.Length > 0 && rng.Chance(share)) card.FakeOf = cheaper[rng.Range(0, cheaper.Length - 1)];
card.MarkupPct = kind == DealerKind.Middleman ? (int)Math.Round(MiddlemanMarkup * 100) : 0;
cards.Add(card);
diff --git a/src/Decentralized/GpuInspectView.cs b/src/Decentralized/GpuInspectView.cs
index f9280be..02743c2 100644
--- a/src/Decentralized/GpuInspectView.cs
+++ b/src/Decentralized/GpuInspectView.cs
@@ -283,7 +283,7 @@ namespace Decentralized
var cur = Items.GetClaim(item) ?? Claim.Truth(u);
if (row == CardLabels.RowSpec)
{
- var models = Hardware.Specs.Values.Where(s => !s.Asic).Select(s => s.Model).ToList();
+ var models = Hardware.Specs.Values.Where(s => !s.Asic && !s.Scrap).Select(s => s.Model).ToList();
Items.EditClaim(item, row, 0, models[(models.IndexOf(cur.Model) + 1) % models.Count]);
}
else Items.EditClaim(item, row, row == CardLabels.RowSeal ? (cur.Get(row) == 0 ? 2 : 0) : row == CardLabels.RowBracket ? (cur.Get(row) + 1) % 2 : (cur.Get(row) + 1) % 3);
diff --git a/src/Decentralized/Items.cs b/src/Decentralized/Items.cs
index 346b465..c4dbd47 100644
--- a/src/Decentralized/Items.cs
+++ b/src/Decentralized/Items.cs
@@ -38,7 +38,7 @@ namespace Decentralized
dir.Add(Terminal, CreateTerminal);
dir.Add(Rig, CreateRig);
dir.Add(Guide, CreateGuide);
- foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic))
+ foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic && !s.Scrap))
{
var s = spec;
dir.Add(GpuId(s.Model), () => CreateGpu(s));
@@ -123,14 +123,12 @@ namespace Decentralized
private static object checkedClient;
private static readonly HashSet checkedItems = new HashSet();
- public const double CustomerSpotChance = 0.50;
- public const int RepLossMax = 25;
/// 被顾客看穿时的信誉损失:骗的价差每满一周房租扣 1 点,最少 1,最多 25。
- public static int RepLoss(long gap) => (int)Math.Max(1, Math.Min(RepLossMax, Math.Round(gap / (double)Math.Max(1, PlayerStore.Instance.rentValue))));
+ public static int RepLoss(long gap) => (int)Math.Max(1, Math.Min(Tuning.RepLossMax, Math.Round(gap / (double)Math.Max(1, PlayerStore.Instance.rentValue))));
///
- /// 自己的卡摆给想买的顾客:说法里隐瞒的每一项,顾客有 50% 的概率看出来;看出来就拆穿这一项(价格回到真实情况),
+ /// 自己的卡摆给想买的顾客:说法里隐瞒的每一项,顾客有 50% 的概率看出来(型号档次不符的必看出来,供电口和风扇数一眼可见);看出来就拆穿这一项(价格回到真实情况),
/// 并且掉一点信誉。每位顾客对每张卡只看一次。说法比实际更差(把好卡说差了)顾客不会去指。
///
public static void OfferForTest(GameItem item) { checkedItems.Clear(); OnOffered(item, true); }
@@ -152,7 +150,7 @@ namespace Decentralized
var rng = Session.Data.Rng;
int found = 0;
foreach (int r in CardLabels.Rows)
- if ((lies & r) != 0 && rng.Chance(CustomerSpotChance))
+ if ((lies & r) != 0 && rng.Chance(r == CardLabels.RowSpec && claim.Value.Tier != Claim.Truth(u).Tier ? Tuning.CustomerSpotTierMismatch : Tuning.CustomerSpotChance))
{
found |= r;
if (r != CardLabels.RowSpec && CardLabels.IsEvidence(r, claim.Value, Claim.Truth(u))) found |= CardLabels.RowSpec | CardLabels.RowBuild;
diff --git a/src/Decentralized/Night.cs b/src/Decentralized/Night.cs
index ec48ec3..3c4ce8b 100644
--- a/src/Decentralized/Night.cs
+++ b/src/Decentralized/Night.cs
@@ -122,10 +122,11 @@ namespace Decentralized
double rewardBefore = d.Network.Reward;
d.Network.Night(day, d.Market.Price, hash);
+ if (d.Network.Reward < rewardBefore) d.Market.OnHalving(d.Rng);
if (d.Network.Reward < rewardBefore) News.Show("pd_news_halving_" + day, L.T("news.halving.title"), L.T("news.halving.desc"), 2);
double crcFactor = d.Stable.Night(day, d.Rng, 0);
if (crcFactor < 1) d.Hot.Balances[Assets.CRC] = d.Hot.Get(Assets.CRC) * crcFactor;
- d.Market.UpdateSupply(day, d.Network.Reward);
+ d.Market.UpdateSupply(day);
if (Tuning.HalvingDays - day % Tuning.HalvingDays == 3) News.Show("pd_news_prehalving_" + day, L.T("news.prehalving.title"), L.T("news.prehalving.desc"), 3);
d.Market.Night(events, d.Rng, day, Session.Price);
report.Price = d.Market.Price;
diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs
index e8c3b05..57fe91d 100644
--- a/tests/Decentralized.Domain.Tests/Program.cs
+++ b/tests/Decentralized.Domain.Tests/Program.cs
@@ -89,11 +89,11 @@ internal static class Program
Check(threw, "余额不足时拒绝");
});
- Test("挖矿:开局 1 H 每晚约 24.2 信用点;减半在第 90 天", () =>
+ Test("挖矿:开局 1 H 每晚约 26.9 信用点;减半在第 90 天", () =>
{
var net = new Network();
double perH = net.Mine(1) * Tuning.PscStartPrice;
- Check(Near(perH, 24.2, 0.2), $"1 H 每晚约 24.2 信用点(抽成后):{perH:0.00}");
+ Check(Near(perH, 26.9, 0.2), $"1 H 每晚约 26.9 信用点(抽成后):{perH:0.00}");
for (int d = 1; d <= 89; d++) net.Night(d, 100, 0);
Check(net.Reward == Tuning.BlockReward, "第 89 天还没减半");
net.Night(90, 100, 0);
@@ -261,19 +261,28 @@ internal static class Program
Check(w.AvgBuy("PSC") == null, "清仓后记录清除");
});
- Test("市场:减半抬高价格锚(逐日靠拢),减半前一周有预期行情", () =>
+ Test("市场:减半抬高价格锚(随机幅度,逐日靠拢),减半前一周有预期行情", () =>
{
- Check(Near(Market.SupplyTarget(10, Tuning.BlockReward), 1.0, 1e-9), "减半前很远:目标因子 1");
- double before = Market.SupplyTarget(89, Tuning.BlockReward), start = Market.SupplyTarget(83, Tuning.BlockReward);
- Check(start > 1.0 && before > start && before <= 1 + Tuning.HalvingRamp + 1e-9, $"减半前 7 天逐日抬高,最多 +8%:{start:0.000} → {before:0.000}");
- double after = Market.SupplyTarget(91, Tuning.BlockReward / 2);
- Check(Near(after, Math.Pow(2, Tuning.HalvingPassThrough), 1e-9), $"减半后目标因子 2^0.6 = {after:0.000}");
var m = new Market();
- for (int i = 0; i < 7; i++) m.UpdateSupply(91 + i, Tuning.BlockReward / 2);
- Check(m.Supply > 1.0 && m.Supply < after, "逐晚靠拢目标,不一次跳到位");
- for (int i = 0; i < 40; i++) m.UpdateSupply(100 + i % 5, Tuning.BlockReward / 2);
- Check(Near(m.Supply, after, 0.02), "几十晚后基本到位");
- Check(m.Anchor(new MarketEvent[0]) > Tuning.PscStartPrice * 1.4, "锚随之上移");
+ Check(Near(m.SupplyTarget(10), 1.0, 1e-9), "减半前很远:目标因子 1");
+ double before = m.SupplyTarget(89), start = m.SupplyTarget(83);
+ Check(start > 1.0 && before > start && before <= 1 + Tuning.HalvingRamp + 1e-9, $"减半前 7 天逐日抬高,最多 +8%:{start:0.000} → {before:0.000}");
+ double lo = 1e9, hi = 0;
+ for (int seed = 1; seed <= 200; seed++)
+ {
+ var x = new Market(); x.OnHalving(new Rng((ulong)seed));
+ lo = Math.Min(lo, x.HalvingLog); hi = Math.Max(hi, x.HalvingLog);
+ }
+ Check(lo >= Tuning.HalvingPassMin && hi <= Tuning.HalvingPassMax && hi - lo > 0.5, $"传导指数在区间内且确有变化:{lo:0.00}–{hi:0.00}");
+ m.OnHalving(new Rng(7UL));
+ double after = m.SupplyTarget(91);
+ Check(Near(after, Math.Pow(2, m.HalvingLog), 1e-9), $"减半后目标因子 2^{m.HalvingLog:0.00} = {after:0.000}");
+ var n = new Market { HalvingLog = m.HalvingLog };
+ for (int i = 0; i < 7; i++) n.UpdateSupply(91 + i);
+ Check(n.Supply > 1.0 && n.Supply < after, "逐晚靠拢目标,不一次跳到位");
+ for (int i = 0; i < 40; i++) n.UpdateSupply(100 + i % 5);
+ Check(Near(n.Supply, after, 0.02), "几十晚后基本到位");
+ Check(n.Anchor(new MarketEvent[0]) > Tuning.PscStartPrice * 1.05, "锚随之上移");
});
Test("市场:其他交易对每晚被套利拉向参考价,三角价差收敛", () =>
@@ -406,7 +415,18 @@ internal static class Program
Test("序列号与供电风扇:四档实物特征;序列号按牌子和档次鉴定;指出供电风扇自动改回真实型号", () =>
{
Check(Hardware.FanCount.SequenceEqual(new[] { 1, 2, 3, 3 }) && Hardware.PowerPins.SequenceEqual(new[] { "6", "8", "8", "8+8" }), "低端 6 针单风扇、中端 8 针双风扇、高端 8 针三风扇、旗舰双 8 针三风扇");
- Check(Hardware.Specs.Values.Where(x => !x.Asic).All(x => Hardware.Specs.Values.Count(y => !y.Asic && y.Brand == x.Brand && y.Tier == x.Tier) == 1), "每个牌子每个档次只有一个型号");
+ Check(Hardware.Specs.Values.Where(x => !x.Asic && !x.Scrap).All(x => Hardware.Specs.Values.Count(y => !y.Asic && !y.Scrap && y.Brand == x.Brand && y.Tier == x.Tier) == 1), "每个牌子每个档次只有一个型号(废片不算)");
+ var rngF = new Rng(99UL); bool lowFake = false, midScrap = false;
+ for (int i = 0; i < 400; i++)
+ {
+ foreach (var c in UsedDealers.Cards(DealerKind.Scammer, 60, rngF))
+ {
+ if (c.FakeOf == null) continue;
+ if (Hardware.Specs[c.Model].Tier == 0 && Hardware.Specs[c.FakeOf].Scrap) lowFake = true;
+ if (Hardware.Specs[c.Model].Tier > 0 && Hardware.Specs[c.FakeOf].Scrap) midScrap = true;
+ }
+ }
+ Check(lowFake && !midScrap, "低档次的卡也会被冒充(背后是废片),中高档的假卡不用废片");
var real = new Unit { Model = "RZ80", Health = 100, Look = new CardLook { Seed = 7 } };
var t = CardLabels.SerialText(real);
Check(t.StartsWith("MD2-") && CardLabels.Factories("PMD").Contains(int.Parse(t.Substring(4, 2))) && t.Length == 11, $"真卡的序列号:牌子 + 档次码 + 合法工厂码 + 四位流水:{t}");