diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a95907..4cc5c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,4 @@ - 纯逻辑层:市场(价格锚、情绪、游戏事件冲击、NPC 大户、多交易对套利回归、当日均价),三种稳定币(CRC 脱锚与减记、CRT 脱锚与冻结概率、CRA 补贴池与死亡螺旋),合规出金与 AML(大额与累计阈值、污点比例、轻度与重度命中、只对收益计税)。 - mod 本体(第一阶段的第一部分):人脉网络"矿业渠道"(300)交付交易终端、矿架和一张 RZX 50;矿架是带电池槽和 4 个显卡槽的机器,每晚从电池扣电挖 PSCoin;显卡按型号显示算力、耗电与健康度,每晚损耗并可能烧毁;交易终端有钱包、交易(四个交易对、存入信用点、提取 CRC 过合规审查)、报告三页;夜间结算写晨间日志;PSCoin 交易所开业与减半上报纸;状态存进游戏的 mod 存档数据。中英双语。 - 物品图:art/sprites.py 生成显卡(Probidia、PMD、烧毁)、矿架、交易终端。 +- 矿架加入游戏原有的通用模块槽:性能、效率、品质三项百分比折算 1/4 后作用于算力、耗电和显卡损耗,算力最多 +20%、省电最多 25%、损耗最多 −30%;性能加成同时提高损耗与暴毙率。取代原计划的 mod 自带超频模块。 diff --git a/src/Decentralized.Domain/Mining.cs b/src/Decentralized.Domain/Mining.cs index 83816ca..ea92bf9 100644 --- a/src/Decentralized.Domain/Mining.cs +++ b/src/Decentralized.Domain/Mining.cs @@ -48,6 +48,33 @@ namespace Decentralized.Domain }; } + /// + /// 矿架上游戏模块的效果。机器汇总的三项百分比(性能、效率、品质)各除以 后生效, + /// 并有上限,避免模块把挖矿收益放大到破坏经济:算力最多 +20%,省电最多 25%,损耗最多降 30%。 + /// 性能加成同时按比例提高损耗与暴毙率。负数(例如节能模块的负性能、超频模块的负效率)照常生效,耗电增加不设上限。 + /// + public struct ModuleBonus + { + public double Hash; // 算力乘数 + public double Kj; // 耗电乘数 + public double Wear; // 每日损耗乘数 + public double Death; // 暴毙率乘数 + + public static ModuleBonus None => new ModuleBonus { Hash = 1, Kj = 1, Wear = 1, Death = 1 }; + + public static ModuleBonus From(int performance, int efficiency, int quality) + { + double r = Tuning.ModuleRatio * 100.0; + double hash = Clamp(performance / r, -0.5, Tuning.ModuleHashCap); + double save = Clamp(efficiency / r, -1.0, Tuning.ModuleKjCap); + double wearCut = Clamp(quality / r, -1.0, Tuning.ModuleWearCap); + double heat = 1 + Math.Max(0, hash); + return new ModuleBonus { Hash = 1 + hash, Kj = 1 - save, Wear = (1 - wearCut) * heat, Death = heat }; + } + + private static double Clamp(double v, double lo, double hi) => Math.Max(lo, Math.Min(hi, v)); + } + /// 一张显卡或一台矿机的实例状态(存进物品标签)。 public sealed class Unit { @@ -62,29 +89,27 @@ namespace Decentralized.Domain public HardwareSpec Chip => Hardware.Specs[FakeOf ?? Model]; /// 当前算力:健康 ≥ 50 时满算力,低于 50 线性降到健康 0 时的 60%。 - public double Hash(bool overclock) + public double Hash(ModuleBonus b) { if (Dead) return 0; - double h = Chip.Hash * (Health >= 50 ? 1.0 : 0.6 + 0.4 * Health / 50); - return overclock ? h * Tuning.OcHash : h; + return Chip.Hash * (Health >= 50 ? 1.0 : 0.6 + 0.4 * Health / 50) * b.Hash; } - public double Kj(bool overclock) => Dead ? 0 : Chip.Kj * (overclock ? Tuning.OcKj : 1); + public double Kj(ModuleBonus b) => Dead ? 0 : Chip.Kj * b.Kj; /// 今晚的暴毙概率。 - public double DeathChance(bool overclock) + public double DeathChance(ModuleBonus b) { double p = Tuning.DeathBase + Tuning.DeathWorn * Math.Pow(1 - Health / 100, 2); - p *= Label.DeathFactor * (FakeOf != null ? Tuning.FakeDeathFactor : 1) * (overclock ? Tuning.OcDeath : 1); - return p; + return p * Label.DeathFactor * (FakeOf != null ? Tuning.FakeDeathFactor : 1) * b.Death; } /// 一夜的损耗与暴毙判定;返回是否在今晚暴毙。 - public bool Night(bool overclock, Rng rng) + public bool Night(ModuleBonus b, Rng rng) { if (Dead) return false; - Health = Math.Max(0, Health - Label.WearFactor * (overclock ? Tuning.OcWear : 1)); - if (rng.Chance(DeathChance(overclock))) + Health = Math.Max(0, Health - Label.WearFactor * b.Wear); + if (rng.Chance(DeathChance(b))) { Dead = true; return true; diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs index bb858f4..0e52271 100644 --- a/src/Decentralized.Domain/Tuning.cs +++ b/src/Decentralized.Domain/Tuning.cs @@ -16,7 +16,8 @@ namespace Decentralized.Domain public const double NpcAdj = 0.05; public const double PoolFee = 0.03; public const int RigPrice = 400, RigKj = 2, RigSlots = 4; - public const double OcHash = 1.25, OcKj = 1.40, OcWear = 1.3, OcDeath = 1.2; + /// 矿架上模块百分比的折算系数(原版机器用 2–4),以及各项上限。 + public const double ModuleRatio = 4, ModuleHashCap = 0.20, ModuleKjCap = 0.25, ModuleWearCap = 0.30; public const double DeathBase = 0.001, DeathWorn = 0.03, FakeDeathFactor = 2; public const double RefurbishHealth = 30; diff --git a/src/Decentralized/Items.cs b/src/Decentralized/Items.cs index cf03473..ea13fbf 100644 --- a/src/Decentralized/Items.cs +++ b/src/Decentralized/Items.cs @@ -120,9 +120,11 @@ namespace Decentralized private static GameItem CreateRig() { var battery = new GameSlotInventory(); + var modules = new GameGridInventory(MachineHelper.DEFAULT_MODULE_BAY_CONFIG, 7); var cards = new GameGridInventory(2, 4); var window = MachineHelper.CreateMachineryWindow(true, MachineHelper.PowerPanel(battery), + MachineHelper.Panel(L.T("rig.label.modules"), modules, "module"), MachineHelper.Panel(L.T("rig.label.cards"), cards, "cards")); var rig = ItemDirectory.CreateEmptyItem(Rig).SetContentWindow(window); rig.SetSpriteAndShapeFromMod(ModInfo.Id, "rig"); @@ -132,41 +134,63 @@ namespace Decentralized rig.EnableTag("CONTAINER_TAG"); rig.EnableTag("STANDARD_MACHINE_TAG"); MachineryHelper.InitMachinery(rig, Tuning.RigKj); + // 游戏的通用模块槽(含模块相邻效果);模块变动后游戏会按原版公式改写耗电,再由 UpdateRig 按本 mod 的规则重算 + MachineHelper.SetupModuleBay(modules, rig, () => UpdateRig(rig, modules, cards)); cards.mayInventoryAddItemFunc = (item, _) => GeneralHelper.IsItemOwned(item) && IsGpu(item) && !item.IsTag(DeadTag); - cards.onSlotAddItemEndFunc = (_, _, _) => UpdateRig(rig, cards); - cards.onSlotRemoveItemEndFunc = (_, _, _) => UpdateRig(rig, cards); - rig.onLoaded = _ => UpdateRig(rig, cards); - rig.onCycleEndEarlySlotItemFunc = (_, _, _) => Log.Guard("矿架扣电", () => NightDraw(rig, battery, cards)); + cards.onSlotAddItemEndFunc = (_, _, _) => UpdateRig(rig, modules, cards); + cards.onSlotRemoveItemEndFunc = (_, _, _) => UpdateRig(rig, modules, cards); + rig.onLoaded = _ => UpdateRig(rig, modules, cards); + rig.onCycleEndEarlySlotItemFunc = (_, _, _) => Log.Guard("矿架扣电", () => NightDraw(rig, battery, modules, cards)); ModHelper.SetSource(rig, ModInfo.Id, ModInfo.Name); - UpdateRig(rig, cards); + UpdateRig(rig, modules, cards); return rig; } public static List Cards(GameInventory cards) => cards.childItems.Where(IsGpu).ToList(); - private static int RigKj(GameInventory cards) => - Tuning.RigKj + Mathf.CeilToInt((float)Cards(cards).Select(ToUnit).Sum(u => u.Kj(false))); - - private static void UpdateRig(GameItem rig, GameInventory cards) + /// 按模块槽里的模块重新汇总三项百分比(游戏自己的公式,含相邻效果),换算成本 mod 的模块效果。 + public static ModuleBonus Bonus(GameItem rig, GameInventory modules) { - int kj = RigKj(cards); - rig.ModifyTag(MachineryHelper.MACHINERY_DRAWN_POWER, s => s.Enable().SetInt(kj)); - double hash = Cards(cards).Select(ToUnit).Sum(u => u.Hash(false)); - rig.shortDescription = L.T("item.rig.desc", Fmt.Num(hash * 10), kj); + ModuleHelper.ComputeModuleEffect(modules.childItems, rig); + return ModuleBonus.From( + rig.GetTagReadonly(ModuleHelper.TOTAL_PERCENTAGE_PERFORMANCE_BONUS_INT).GetInt(), + rig.GetTagReadonly(ModuleHelper.TOTAL_PERCENTAGE_POWER_BONUS_INT).GetInt(), + rig.GetTagReadonly(ModuleHelper.TOTAL_PERCENTAGE_QUALITY_BONUS_INT).GetInt()); } - /// 夜间早段:按矿架上的显卡总耗电从电池扣电;扣电成功,登记今晚参与挖矿的显卡。 - private static void NightDraw(GameItem rig, GameSlotInventory battery, GameInventory cards) + private static int RigKj(GameInventory cards, ModuleBonus b) => + Tuning.RigKj + Mathf.CeilToInt((float)Cards(cards).Select(ToUnit).Sum(u => u.Kj(b))); + + private static void UpdateRig(GameItem rig, GameInventory modules, GameInventory cards) + { + var b = Bonus(rig, modules); + int kj = RigKj(cards, b); + rig.ModifyTag(MachineryHelper.MACHINERY_DRAWN_POWER, s => s.Enable().SetInt(kj)); + double hash = Cards(cards).Select(ToUnit).Sum(u => u.Hash(b)); + rig.shortDescription = L.T("item.rig.desc", Fmt.Num(hash * 10), kj); + if (b.Hash != 1 || b.Kj != 1 || b.Wear != 1) + rig.shortDescription += " " + L.T("item.rig.modules", Pct(b.Hash), Pct(b.Kj), Pct(b.Wear)); + } + + private static string Pct(double mult) + { + int p = (int)Math.Round((mult - 1) * 100); + return p > 0 ? "+" + p + "%" : p + "%"; + } + + /// 夜间早段:按矿架上的显卡总耗电(含模块效果)从电池扣电;扣电成功,登记今晚参与挖矿的显卡和模块效果。 + private static void NightDraw(GameItem rig, GameSlotInventory battery, GameInventory modules, GameInventory cards) { var list = Cards(cards).Where(c => !c.IsTag(DeadTag)).ToList(); Session.TonightRigs++; if (list.Count == 0) return; - int kj = RigKj(cards); + var b = Bonus(rig, modules); + int kj = RigKj(cards, b); if (battery.childItems.Count == 0 || !PowerHelper.DrawPowerSource(battery.childItem, kj)) return; Session.TonightPoweredRigs++; Session.TonightKj += kj; - Session.TonightCards.AddRange(list); - UpdateRig(rig, cards); + foreach (var c in list) Session.TonightCards.Add(new KeyValuePair(c, b)); + UpdateRig(rig, modules, cards); } /// diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv index b7d905f..ea08bd7 100644 --- a/src/Decentralized/ModFiles/Localization/en.csv +++ b/src/Decentralized/ModFiles/Localization/en.csv @@ -7,6 +7,8 @@ item.terminal.name,Trading Terminal item.terminal.desc,Wallet, exchange and the nightly mining report. item.rig.name,Mining Rig item.rig.desc,"Holds up to four graphics cards. Hashrate {0} MH/s. Draws {1} kJ per day." +rig.label.modules,Modules +item.rig.modules,"Modules: hashrate {0}, power draw {1}, wear {2}." rig.label.cards,Graphics cards item.gpu.name,{0} {1} item.gpu.desc,"Hashrate {0} MH/s. Draws {1} kJ per day. Condition {2}%." diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv index 75deaf7..b0a8d62 100644 --- a/src/Decentralized/ModFiles/Localization/zh.csv +++ b/src/Decentralized/ModFiles/Localization/zh.csv @@ -6,6 +6,8 @@ item.terminal.name,交易终端 item.terminal.desc,钱包、交易所与夜间挖矿报告。 item.rig.name,矿架 item.rig.desc,可装 4 张显卡。算力 {0} MH/s。耗电 {1} kJ/天。 +rig.label.modules,模块 +item.rig.modules,模块:算力 {0},耗电 {1},损耗 {2}。 rig.label.cards,显卡 item.gpu.name,{0} {1} item.gpu.desc,算力 {0} MH/s。耗电 {1} kJ/天。健康度 {2}%。 diff --git a/src/Decentralized/Night.cs b/src/Decentralized/Night.cs index 245e922..2fd8a0c 100644 --- a/src/Decentralized/Night.cs +++ b/src/Decentralized/Night.cs @@ -86,8 +86,8 @@ namespace Decentralized d.ActiveEvents = events; d.Network.Availability = (events.Contains(MarketEvent.Blackout) ? 0.8 : 1) * (events.Contains(MarketEvent.LowerUnrest) ? 0.9 : 1); - var units = Session.TonightCards.Select(c => (item: c, unit: Items.ToUnit(c))).ToList(); - double hash = units.Sum(x => x.unit.Hash(false)); + var units = Session.TonightCards.Select(c => (item: c.Key, unit: Items.ToUnit(c.Key), bonus: c.Value)).ToList(); + double hash = units.Sum(x => x.unit.Hash(x.bonus)); double mined = d.Network.Mine(hash); if (mined > 0) d.Hot.Add(Assets.PSC, mined); @@ -96,9 +96,9 @@ namespace Decentralized Day = day, Rigs = Session.TonightRigs, PoweredRigs = Session.TonightPoweredRigs, Kj = Session.TonightKj, Hash = hash, NetworkHash = d.Network.NpcHash * d.Network.Availability + hash, Mined = mined, }; - foreach (var (item, unit) in units) + foreach (var (item, unit, bonus) in units) { - bool died = unit.Night(false, d.Rng); + bool died = unit.Night(bonus, d.Rng); Items.Store(item, unit); if (died) report.Burnt.Add(item.name); } diff --git a/src/Decentralized/Session.cs b/src/Decentralized/Session.cs index c53a5f0..6c9e7f5 100644 --- a/src/Decentralized/Session.cs +++ b/src/Decentralized/Session.cs @@ -37,7 +37,7 @@ namespace Decentralized public static SaveData Data; /// 今晚有电的矿架上的显卡(矿架在夜间早段扣电时登记,结算时用)。 - public static readonly List TonightCards = new List(); + public static readonly List> TonightCards = new List>(); public static int TonightRigs, TonightPoweredRigs; public static double TonightKj; diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs index 3151ae4..eaa56ef 100644 --- a/tests/Decentralized.Domain.Tests/Program.cs +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -115,22 +115,36 @@ internal static class Program Check(Hardware.Specs["RZX70"].DisplayHash == 50 && Hardware.Specs["RZ6800"].DisplayHash == 40, "RZX70 50 MH/s,RZ6800 40 MH/s"); Check(Near(Hardware.Specs["M3"].DisplayHash, 3, 1e-9), "RatDigg M3 3 GH/s"); var u = new Unit { Model = "RZX70" }; - Check(u.Hash(false) == 5, "满健康 5 H"); + Check(u.Hash(ModuleBonus.None) == 5, "满健康 5 H"); u.Health = 25; - Check(Near(u.Hash(false), 4.0, 1e-9), "健康 25:5 × (0.6 + 0.4 × 0.5) = 4 H"); + Check(Near(u.Hash(ModuleBonus.None), 4.0, 1e-9), "健康 25:5 × (0.6 + 0.4 × 0.5) = 4 H"); u.Health = 0; - Check(Near(u.Hash(false), 3.0, 1e-9), "健康 0:60%"); - Check(Near(new Unit { Model = "RZX70" }.Hash(true), 6.25, 1e-9), "超频 +25%"); + Check(Near(u.Hash(ModuleBonus.None), 3.0, 1e-9), "健康 0:60%"); + Check(Near(new Unit { Model = "RZX70" }.Hash(ModuleBonus.From(36, -44, 0)), 5 * 1.09, 1e-9), "一个超频模块(+36 性能):算力 +9%"); var fake = new Unit { Model = "RZX90", FakeOf = "RZX60" }; - Check(fake.Hash(false) == 1.5 && fake.Label.Model == "RZX90", "假卡标着 RZX90,算力按 RZX60"); - Check(Near(fake.DeathChance(false), new Unit { Model = "RZX90" }.DeathChance(false) * 2, 1e-12), "假卡暴毙率 ×2"); + Check(fake.Hash(ModuleBonus.None) == 1.5 && fake.Label.Model == "RZX90", "假卡标着 RZX90,算力按 RZX60"); + Check(Near(fake.DeathChance(ModuleBonus.None), new Unit { Model = "RZX90" }.DeathChance(ModuleBonus.None) * 2, 1e-12), "假卡暴毙率 ×2"); var r = new Unit { Model = "RZX70", Health = 50 }; Check(r.Refurbish() && r.Health == 80 && !r.Refurbish(), "翻新一次 +30,不能再翻新"); }); + Test("模块:折算系数 4,算力 +20%、省电 25%、损耗 −30% 封顶;性能加成提高损耗与暴毙", () => + { + var perf = ModuleBonus.From(16, 0, 0); + Check(Near(perf.Hash, 1.04, 1e-9) && Near(perf.Wear, 1.04, 1e-9) && Near(perf.Death, 1.04, 1e-9), "一个性能模块(+16):算力 +4%,损耗和暴毙 +4%"); + var cores = ModuleBonus.From(200 * 3, -100 * 3, 100 * 3); + Check(Near(cores.Hash, 1.20, 1e-9), $"塞满神经核心,算力也只 +20%:{cores.Hash:0.00}"); + Check(Near(cores.Kj, 1.75, 1e-9), $"神经核心的负效率照常增加耗电:×{cores.Kj:0.00}"); + Check(Near(cores.Wear, 0.70 * 1.20, 1e-9), "品质封顶 −30%,再乘性能带来的 +20%"); + var eco = ModuleBonus.From(-16, 32 * 5, 0); + Check(Near(eco.Kj, 0.75, 1e-9) && Near(eco.Hash, 0.96, 1e-9), "省电封顶 25%;节能模块的负性能照常降低算力"); + var none = ModuleBonus.From(0, 0, 0); + Check(none.Hash == 1 && none.Kj == 1 && none.Wear == 1 && none.Death == 1, "没有模块时不变"); + }); + Test("显卡:累计暴毙概率与模拟器一致(第 50 / 100 / 150 天)", () => { - double[] Run(string model, bool oc, int n) + double[] Run(string model, ModuleBonus b, int n) { var rng = new Rng(2026); var deadBy = new int[3]; @@ -139,7 +153,7 @@ internal static class Program var u = new Unit { Model = model }; for (int d = 1; d <= 150; d++) { - u.Night(oc, rng); + u.Night(b, rng); if (u.Dead) { if (d <= 50) deadBy[0]++; @@ -151,10 +165,10 @@ internal static class Program } return deadBy.Select(x => (double)x / n).ToArray(); } - var p = Run("RZX70", false, 20000); + var p = Run("RZX70", ModuleBonus.None, 20000); Check(Near(p[0], 0.11, 0.02) && Near(p[1], 0.53, 0.02) && Near(p[2], 0.84, 0.02), $"Probidia 11% / 53% / 84%:{p[0]:P0} / {p[1]:P0} / {p[2]:P0}"); - var q = Run("RZ6800", false, 20000); + var q = Run("RZ6800", ModuleBonus.None, 20000); Check(Near(q[0], 0.20, 0.02) && Near(q[1], 0.76, 0.02) && Near(q[2], 0.95, 0.02), $"PMD 20% / 76% / 95%:{q[0]:P0} / {q[1]:P0} / {q[2]:P0}"); });