diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fef2a5..7e085cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ - 收尾核算(`research/sim/bm_econ.py`,按 Tuning 常量算期望):Ponero 来回 15.4%,与混币器洗掉 80% 污点的 15–18% 相当,但当天完成、清得彻底、不受制裁,所以手续费 8% → 10%(来回 19%),日额度 1000 → 600(日额度按钱包污点占比估,约够清掉稳态的污点流入,后期会成为瓶颈);出售的最优倍率取决于玩家的资金机会成本:每天 1% 时 ×1.8,2–3% 时 ×1.5,倍率不是越高越好;订单:托管期望净 +0.02、提前放款 +0.07(现货 −0.30),所以订单是便宜进货,不是赚钱路线;订单进货再挂高价出售的套利每件约 +0.13–0.18(20 天一轮,资金占用),量小,暂不限制上架数。 - 订单改成商家货架(用户意见):黑市页"订单"改名"商家",每周从 6 种类型里抽 5 家(全新矿卡经销商、二手矿卡贩子、模组高科技、军火、证件芯片、药物),另有 25% 的周出现一家大宗商家(库存 10–25 件、价格系数 0.55–0.75、送货 6 天)。每家有自己的价格系数(订单价 = 基础价 × 系数,一般 0.70–0.95,全新卡 0.85–1.00,二手卡 0.65–0.90),每家 4 种货、库存 3–6 件。一次订 5 件以上 95 折、10 件以上 9 折。界面:件数按钮 ×1 / ×5 / ×10,商家按钮,所选商家的货架。药物(药片、梦尘、管制化学品)只在药物商家和大宗商家出现,也能转手出售。每周货架总值在游戏里抽样 60 周平均 13.2k(目标 1.4 万)。 - 商家类型扩到 18 种 + 大宗(用户意见,对照游戏的物品目录和玩家职业):全新矿卡经销商、二手矿卡贩子、矿架与能源、模组、军火(枪、手雷、炸药、刀具、电击枪)、枪匠配件(枪械配件、3D 打印芯片与模组、枪械零件)、证件芯片、药物(药片、注射器、血袋、手术工具)、化学品(化学用品、助熔剂、化学设备模组、量筒试剂瓶)、酿酒(酵母、荧光莓、啤酒、酒瓶)、水培农场(种子、水培模组、植物分析仪)、养殖(鱼卵、鱼子酱、循环水养殖系统、血清、屠宰机)、矿工(矿石、金属锭、电子元件、拾荒扫描仪)、水务(滤水、净水、储水、检测)、仓储(走私暗格、储藏区扩建、军用背包)、工具(涡轮增压器、等离子切割机、X 射线扫描仪、心智重编程器)、奢侈品(香水、精华液、香烟)。每周 7 家:2 家从矿卡 / 矿架 / 模组 4 类里抽(保证矿工路线总有货),其余 5 家从其余类型里抽;每家 3 种货,库存 2–5 件,单件基础价超过 4000 的不上。游戏里抽样 60 周货架总值平均 13.2k(目标 1.4 万)。药物改成并入药物商家,现货柜台的货不变。所有货都能转手出售。 +- 尖端店铺与个人卖家(用户意见):贵货(单件基础价 ≥ 2000:超科技涡轮增压器、超科技储藏区、心智重编程器、顶级显卡等)不再被一刀切掉,改由"尖端店铺"卖:每周只有 15% 的概率出现,2 种货、库存 1–2 件、价格系数 0.85–1.00、送货 5 天;顶级全新显卡也在这里。个人卖家:每周固定 2 位,什么都卖(可能是药、模组、显卡、仍有 10% 混进贵货),每件价格系数各自随机 0.50–1.20(捡漏或被宰),库存 1–2 件,是骗子的概率 25%,都是新号所以只接受托管。常规商家仍只卖基础价 ≤ 4000 的货。 +- 每周货架总值放宽到 2 万左右(用户意见):库存范围 2–5 → 2–6,游戏里抽样 60 周平均 22.2k,尖端店铺出现概率保持 15% 不变,不靠它拉高周转量。 - 同时上架最多 10 件(用户意见),出售页显示"上架 n/10",超出时提示。 - 出售:货款污点随近 7 天成交额加重(成交额达 1.4 万时概率与比例各加 30%);出售页加"全部上架"按钮。套利核算(`bm_econ.py`,商家系数均值 0.825 再加批量折扣):普通商家提前放款进货 + ×1.5 出售每件约 +21%,大宗商家约 +33%,一周可买的货约 1.3 万,带污点的货款约够吃满 Ponero 日额度。 - 调整(用户意见):Ponero 手续费 10% → 6%(一进一出约 12%),日额度 600 → 1000;混币器每轮手续费 4% → 3%。黑市货架上的显卡有 15% 是全新卡(成色 100,封条完好,不会是假卡),标签显示"全新"。全新卡只有单元测试,没有在游戏里见过。 diff --git a/src/Decentralized.Domain/BmOrders.cs b/src/Decentralized.Domain/BmOrders.cs index 16197ac..902f9b8 100644 --- a/src/Decentralized.Domain/BmOrders.cs +++ b/src/Decentralized.Domain/BmOrders.cs @@ -5,8 +5,8 @@ using System.Linq; namespace Decentralized.Domain { public enum PayMode { Escrow, Advance } - /// 商家类型:矿卡(全新 / 二手)、矿架能源、模组、军火、枪匠配件、证件芯片、药物、化学、酿酒、水培、养殖、矿工、水务、仓储、工具、奢侈品;大宗商家随机出现。 - public enum MerchantType { GpuNew, GpuUsed, Rigs, Modules, Arms, GunMods, Docs, Pharma, Chem, Brewer, Farm, Husbandry, Miner, Water, Storage, Tools, Luxury, Bulk } + /// 商家类型:矿卡(全新 / 二手)、矿架能源、模组、军火、枪匠配件、证件芯片、药物、化学、酿酒、水培、养殖、矿工、水务、仓储、工具、奢侈品;大宗商家、尖端店铺(只卖贵货,概率低)、个人卖家(什么都卖,价格随心所欲)随机出现。 + public enum MerchantType { GpuNew, GpuUsed, Rigs, Modules, Arms, GunMods, Docs, Pharma, Chem, Brewer, Farm, Husbandry, Miner, Water, Storage, Tools, Luxury, Bulk, Elite, Personal } public enum OrderState { Pending, Ready, Done, Refunded, Lost } /// 黑市上的卖家:成交数与好评率公开;是不是骗子是隐藏的。 @@ -19,7 +19,7 @@ namespace Decentralized.Domain public MerchantType Type; /// 商家的价格系数(订单价 = 基础价 × 系数,各家不同)。 public double Factor; - public int Days => Type == MerchantType.Bulk ? Tuning.BmBulkDays : Tuning.BmDoorDays; + public int Days => Type == MerchantType.Bulk ? Tuning.BmBulkDays : Type == MerchantType.Elite ? Tuning.BmEliteDays : Tuning.BmDoorDays; /// 只有成交数够多的卖家才接受提前放款。 public bool Advance => Trades >= Tuning.BmAdvanceMinTrades; } @@ -111,36 +111,53 @@ namespace Decentralized.Domain private static double F(Rng rng, double lo, double hi) => lo + rng.NextDouble() * (hi - lo); - /// 商家类型对应的货(含货种、库存),违禁品按声望放开等级。 + private static List AllEntries() => Merchants.Table.Values.SelectMany(x => x).GroupBy(x => x.Id).Select(g => g.First()).ToList(); + + private static string[] HighGpus => Hardware.Specs.Values.Where(x => !x.Asic && !x.Scrap && x.Tier >= 3).Select(x => x.Model).ToArray(); + + /// 商家类型对应的货(含货种、库存),违禁品按声望放开等级。个人卖家什么都卖、每件各自定价;尖端店铺只卖贵货。 private List MerchantGoods(int idx, BmSeller s, int day, int rep, Rng rng, Func value) { var list = new List(); - int n = s.Type == MerchantType.Bulk ? 2 : Tuning.BmMerchantListings; bool gpuKind = s.Type == MerchantType.GpuNew || s.Type == MerchantType.GpuUsed; - var entries = s.Type == MerchantType.Bulk ? Merchants.Table.Values.SelectMany(x => x).GroupBy(x => x.Id).Select(g => g.First()).ToList() - : gpuKind ? new List() : Merchants.Table[s.Type].ToList(); + int n = s.Type == MerchantType.Bulk ? 2 : s.Type == MerchantType.Elite ? Tuning.BmEliteListings : s.Type == MerchantType.Personal ? Tuning.BmPersonalListings : Tuning.BmMerchantListings; + List entries = gpuKind ? new List() : s.Type == MerchantType.Bulk || s.Type == MerchantType.Personal || s.Type == MerchantType.Elite ? AllEntries() : Merchants.Table[s.Type].ToList(); entries = entries.Where(e => e.Kind != GoodKind.Contraband || e.Level <= MaxLevel(rep)).ToList(); var gpus = UsedDealers.Pool(day); for (int i = 0; i < n; i++) { - int qty = s.Type == MerchantType.Bulk ? rng.Range(Tuning.BmBulkQtyMin, Tuning.BmBulkQtyMax) : s.Type == MerchantType.GpuNew ? rng.Range(Math.Max(1, Tuning.BmQtyMin - 1), Tuning.BmQtyMax - 2) : rng.Range(Tuning.BmQtyMin, Tuning.BmQtyMax); - double jitter = F(rng, 0.97, 1.03); - BmListing l; - if (gpuKind) + int qty = s.Type == MerchantType.Bulk ? rng.Range(Tuning.BmBulkQtyMin, Tuning.BmBulkQtyMax) + : s.Type == MerchantType.Elite ? rng.Range(1, Tuning.BmEliteQtyMax) + : s.Type == MerchantType.Personal ? rng.Range(1, Tuning.BmPersonalQtyMax) + : s.Type == MerchantType.GpuNew ? rng.Range(Math.Max(1, Tuning.BmQtyMin - 1), Tuning.BmQtyMax - 2) : rng.Range(Tuning.BmQtyMin, Tuning.BmQtyMax); + double factor = s.Type == MerchantType.Personal ? F(rng, Tuning.BmPersonalFactorMin, Tuning.BmPersonalFactorMax) : s.Factor * F(rng, 0.97, 1.03); + BmListing l = null; + bool eliteGpu = s.Type == MerchantType.Elite && rng.Chance(0.4); + bool personalGpu = s.Type == MerchantType.Personal && rng.Chance(0.15); + if (gpuKind || eliteGpu || personalGpu) { - bool fresh = s.Type == MerchantType.GpuNew; - var c = UsedDealers.Card(gpus, fresh ? 0 : Tuning.BmGpuFake, 0, rng); + bool fresh = s.Type == MerchantType.GpuNew || eliteGpu || (personalGpu && rng.Chance(0.3)); + var pool = eliteGpu ? HighGpus : gpus; + var c = UsedDealers.Card(pool, fresh ? 0 : Tuning.BmGpuFake, 0, rng); if (fresh) c.Health = 100; - l = new BmListing { Item = "gpu:" + c.Model, Kind = GoodKind.Gpu, Level = 2, Model = c.Model, FakeOf = c.FakeOf, Health = c.Health, Price = (int)Math.Max(1, Math.Round(CardValue.Of(c.Model, c.Health) * s.Factor * jitter)) }; + l = new BmListing { Item = "gpu:" + c.Model, Kind = GoodKind.Gpu, Level = 2, Model = c.Model, FakeOf = c.FakeOf, Health = c.Health, Price = (int)Math.Max(1, Math.Round(CardValue.Of(c.Model, c.Health) * factor)) }; } else { - if (entries.Count == 0) break; - int k = rng.Range(0, entries.Count - 1); - var e = entries[k]; entries.RemoveAt(k); - long v = value(e.Id); - if (v <= 0 || v > Tuning.BmMaxUnitValue) { i--; continue; } - l = new BmListing { Item = e.Id, Kind = e.Kind, Level = e.Level, Price = (int)Math.Max(1, Math.Round(v * s.Factor * jitter)) }; + bool wantElite = s.Type == MerchantType.Elite || (s.Type == MerchantType.Personal && rng.Chance(Tuning.BmPersonalEliteShare)); + BmEntry pick = default; long v = 0; bool found = false; + for (int tries = 0; tries < 40 && entries.Count > 0 && !found; tries++) + { + int k = rng.Range(0, entries.Count - 1); + var e = entries[k]; + long val = value(e.Id); + if (val <= 0) { entries.RemoveAt(k); continue; } + bool ok = s.Type == MerchantType.Elite ? val >= Tuning.BmEliteMinValue : wantElite ? val > Tuning.BmMaxUnitValue : val <= Tuning.BmMaxUnitValue; + if (!ok) continue; + pick = e; v = val; found = true; entries.RemoveAt(k); + } + if (!found) continue; + l = new BmListing { Item = pick.Id, Kind = pick.Kind, Level = pick.Level, Price = (int)Math.Max(1, Math.Round(v * factor)) }; } l.Seller = idx; l.Qty = qty; list.Add(l); @@ -155,18 +172,21 @@ namespace Decentralized.Domain var types = Core.OrderBy(_ => rng.NextDouble()).Take(Tuning.BmCoreMerchants).ToList(); types.AddRange(Others.OrderBy(_ => rng.NextDouble()).Take(Tuning.BmMerchantCount - Tuning.BmCoreMerchants)); if (rng.Chance(Tuning.BmBulkChance)) types.Add(MerchantType.Bulk); + if (rng.Chance(Tuning.BmEliteChance)) types.Add(MerchantType.Elite); + for (int i = 0; i < Tuning.BmPersonalCount; i++) types.Add(MerchantType.Personal); foreach (var t in types) { - bool scam = rng.Chance(Tuning.BmScamSellerShare); - // 骗子多半是新号,也有刷过好评的老号 - bool fresh = scam ? rng.Chance(0.6) : rng.Chance(0.25); - double lo = t == MerchantType.GpuNew ? Tuning.BmFactorNewMin : t == MerchantType.GpuUsed ? Tuning.BmFactorUsedMin : t == MerchantType.Bulk ? Tuning.BmFactorBulkMin : Tuning.BmFactorMin; - double hi = t == MerchantType.GpuNew ? Tuning.BmFactorNewMax : t == MerchantType.GpuUsed ? Tuning.BmFactorUsedMax : t == MerchantType.Bulk ? Tuning.BmFactorBulkMax : Tuning.BmFactorMax; + bool personal = t == MerchantType.Personal; + bool scam = rng.Chance(personal ? Tuning.BmPersonalScam : Tuning.BmScamSellerShare); + // 骗子多半是新号,也有刷过好评的老号;个人卖家都是新号 + bool fresh = personal || (scam ? rng.Chance(0.6) : rng.Chance(0.25)); + double lo = t == MerchantType.Elite ? Tuning.BmFactorEliteMin : t == MerchantType.GpuNew ? Tuning.BmFactorNewMin : t == MerchantType.GpuUsed ? Tuning.BmFactorUsedMin : t == MerchantType.Bulk ? Tuning.BmFactorBulkMin : Tuning.BmFactorMin; + double hi = t == MerchantType.Elite ? Tuning.BmFactorEliteMax : t == MerchantType.GpuNew ? Tuning.BmFactorNewMax : t == MerchantType.GpuUsed ? Tuning.BmFactorUsedMax : t == MerchantType.Bulk ? Tuning.BmFactorBulkMax : Tuning.BmFactorMax; var s = new BmSeller { Name = NameA[rng.Range(0, NameA.Length - 1)] + "_" + NameB[rng.Range(0, NameB.Length - 1)] + rng.Range(1, 99), - Scam = scam, Type = t, Factor = F(rng, lo, hi), - Trades = fresh ? rng.Range(1, 12) : rng.Range(25, 220), + Scam = scam, Type = t, Factor = personal ? 1.0 : F(rng, lo, hi), + Trades = fresh ? rng.Range(0, 12) : rng.Range(25, 220), Rating = scam && !fresh ? rng.Range(94, 99) : fresh ? rng.Range(70, 100) : rng.Range(90, 99), }; Sellers.Add(s); diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs index 1ebef79..d9edb71 100644 --- a/src/Decentralized.Domain/Tuning.cs +++ b/src/Decentralized.Domain/Tuning.cs @@ -108,10 +108,17 @@ namespace Decentralized.Domain public const int BmSaleHold = 3, BmRatingStart = 90, BmRatingFakeLoss = 10, BmMaxListings = 10; /// 商家货架:每周的商家数(其中 2 家从矿卡 / 矿架 / 模组 4 类里抽,其余从其他类型里抽)、每家的货种数、每种货的库存范围;大宗商家出现概率、库存范围、送货天数; /// 各类商家的价格系数范围(订单价 = 基础价 × 系数);批量折扣档(件数、折扣)。 - public const int BmMerchantCount = 7, BmCoreMerchants = 2, BmMerchantListings = 3, BmQtyMin = 2, BmQtyMax = 5, BmBulkQtyMin = 10, BmBulkQtyMax = 25, BmBulkDays = 6; + public const int BmMerchantCount = 7, BmCoreMerchants = 2, BmMerchantListings = 3, BmQtyMin = 2, BmQtyMax = 6, BmBulkQtyMin = 10, BmBulkQtyMax = 25, BmBulkDays = 6; public const double BmBulkChance = 0.25, BmFactorMin = 0.70, BmFactorMax = 0.95, BmFactorNewMin = 0.85, BmFactorNewMax = 1.00, BmFactorUsedMin = 0.65, BmFactorUsedMax = 0.90, BmFactorBulkMin = 0.55, BmFactorBulkMax = 0.75; /// 商家货架上单件基础价的上限(过贵的货不上,免得一件把周转量撑歪)。 public const long BmMaxUnitValue = 4000; + /// 尖端店铺:每周出现概率、货种数、库存范围、单件基础价下限、价格系数范围、送货天数。 + public const double BmEliteChance = 0.15, BmFactorEliteMin = 0.85, BmFactorEliteMax = 1.00; + public const int BmEliteListings = 2, BmEliteQtyMax = 2, BmEliteDays = 5; + public const long BmEliteMinValue = 2000; + /// 个人卖家:每周家数、货种数、库存上限、每件的价格系数范围(各件独立随机)、是骗子的概率、混进尖端货的概率。 + public const int BmPersonalCount = 2, BmPersonalListings = 3, BmPersonalQtyMax = 2; + public const double BmPersonalFactorMin = 0.50, BmPersonalFactorMax = 1.20, BmPersonalScam = 0.25, BmPersonalEliteShare = 0.10; public const int BmBulk1Count = 5, BmBulk2Count = 10; public const double BmBulk1Discount = 0.05, BmBulk2Discount = 0.10; /// 出售量越大货款越容易带污点:近 7 天成交额达到参考量时,污点概率与比例各加最多 BmVolumeTaintMax。 diff --git a/src/Decentralized/BlackDelivery.cs b/src/Decentralized/BlackDelivery.cs index 20a0e2a..8368a5a 100644 --- a/src/Decentralized/BlackDelivery.cs +++ b/src/Decentralized/BlackDelivery.cs @@ -20,12 +20,15 @@ namespace Decentralized ModHook.OnGenerateCustomerNormal -= OnGenerate; } + private static readonly System.Collections.Generic.Dictionary ValueCache = new System.Collections.Generic.Dictionary(); + public static long Value(string id) { + if (ValueCache.TryGetValue(id, out var cached)) return cached; var it = ModHelper.SpawnItem(id); - if (it == null) return 0; - long v = it.GetBaseValue(); - it.Destroy(); + long v = 0; + if (it != null) { v = it.GetBaseValue(); it.Destroy(); } + ValueCache[id] = v; return v; } diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv index 20e1c3a..170b65e 100644 --- a/src/Decentralized/ModFiles/Localization/en.csv +++ b/src/Decentralized/ModFiles/Localization/en.csv @@ -332,3 +332,5 @@ merchant.Storage,Storage merchant.Tools,Tools merchant.Luxury,Luxury black.kind.Goods,Goods +merchant.Elite,Elite shop +merchant.Personal,Private seller diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv index 3ef9ee3..9fb7e5e 100644 --- a/src/Decentralized/ModFiles/Localization/zh.csv +++ b/src/Decentralized/ModFiles/Localization/zh.csv @@ -331,3 +331,5 @@ merchant.Storage,仓储 merchant.Tools,工具 merchant.Luxury,奢侈品 black.kind.Goods,商品 +merchant.Elite,尖端店铺 +merchant.Personal,个人卖家 diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs index 94cbd87..7e30973 100644 --- a/tests/Decentralized.Domain.Tests/Program.cs +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -591,7 +591,7 @@ internal static class Program Func value = id => 200; var bm = new BlackMarket(); bm.RefreshOrders(10, 75, new Rng(5), value); - Check(bm.Sellers.Count >= Tuning.BmMerchantCount && bm.Catalog.Count > 0 && bm.Catalog.All(l => l.Qty > 0 && l.Price >= 1) && bm.Catalog.Where(l => l.Kind != GoodKind.Gpu).All(l => l.Price >= 200 * bm.Sellers[l.Seller].Factor * 0.97 - 1 && l.Price <= 200 * bm.Sellers[l.Seller].Factor * 1.03 + 1), "目录:商家与单价(基础价 × 商家系数)"); + Check(bm.Sellers.Count >= Tuning.BmMerchantCount && bm.Catalog.Count > 0 && bm.Catalog.All(l => l.Qty > 0 && l.Price >= 1) && bm.Catalog.Where(l => l.Kind != GoodKind.Gpu && bm.Sellers[l.Seller].Type != MerchantType.Personal).All(l => l.Price >= 200 * bm.Sellers[l.Seller].Factor * 0.97 - 1 && l.Price <= 200 * bm.Sellers[l.Seller].Factor * 1.03 + 1), "目录:商家与单价(基础价 × 商家系数)"); Check(BlackMarket.Pay(170, PayMode.Escrow) == 170 + 9 && BlackMarket.Pay(170, PayMode.Advance) == 153 && BlackMarket.OrderTotal(100, 5) == 475 && BlackMarket.OrderTotal(100, 10) == 900 && BlackMarket.OrderTotal(100, 4) == 400, "托管多付 5%,提前放款打九折"); var good = new BmSeller { Name = "g", Trades = 100, Rating = 95 }; var bad = new BmSeller { Name = "b", Trades = 100, Rating = 95, Scam = true }; @@ -733,11 +733,11 @@ internal static class Program sum += bm.CatalogValue; if (bm.Sellers.Any(x => x.Type == MerchantType.Bulk)) bulk++; foreach (var s in bm.Sellers) types.Add(s.Type); - if (bm.Sellers.Where(x => x.Type != MerchantType.Bulk).Select(x => x.Type).Distinct().Count() != bm.Sellers.Count(x => x.Type != MerchantType.Bulk)) throw new Exception("同类型商家重复"); + if (bm.Sellers.Where(x => x.Type != MerchantType.Bulk && x.Type != MerchantType.Personal).Select(x => x.Type).Distinct().Count() != bm.Sellers.Count(x => x.Type != MerchantType.Bulk && x.Type != MerchantType.Personal)) throw new Exception("同类型商家重复"); } double avg = sum / n; Check(avg > 8000 && avg < 40000, $"每周货架总值(游戏里实测平均约 1.4 万,这里用统一基础价 250):平均 {avg:0}"); - Check(bulk > n * 0.15 && bulk < n * 0.35 && types.Count >= 18, $"大宗商家约 {Tuning.BmBulkChance:P0} 的周出现:{bulk}/{n};七种类型都出现过"); + Check(bulk > n * 0.15 && bulk < n * 0.35 && types.Count >= 20, $"大宗商家约 {Tuning.BmBulkChance:P0} 的周出现:{bulk}/{n};七种类型都出现过"); // 批量下单:折扣、库存扣减 Func price = a => 1; var m = new BlackMarket(); m.Sellers.Add(new BmSeller { Name = "a", Trades = 100, Type = MerchantType.Modules, Factor = 0.8 }); @@ -756,6 +756,24 @@ internal static class Program Check(Near(v0, 0, 1e-9) && Near(b2.VolumeTaint(10), Tuning.BmVolumeTaintMax, 1e-9) && b2.VolumeTaint(30) == 0, "近 7 天成交额推高污点,过期不算"); }); + Test("尖端店铺与个人卖家", () => + { + int elite = 0, n = 400, personalListings = 0; double lo = 9, hi = 0; + for (int i = 0; i < n; i++) + { + var bm = new BlackMarket(); + bm.RefreshOrders(20, 75, new Rng((ulong)i + 77), id => id == "hypertech_bay" || id == "turbo_booster_hypertech" ? 10000 : id == "mind_reprogrammer" ? 5000 : 200); + if (bm.Sellers.Any(x => x.Type == MerchantType.Elite)) elite++; + if (bm.Sellers.Count(x => x.Type == MerchantType.Personal) != Tuning.BmPersonalCount) throw new Exception("个人卖家数不对"); + foreach (var l in bm.Catalog.Where(x => bm.Sellers[x.Seller].Type == MerchantType.Elite)) + if (l.Kind != GoodKind.Gpu && l.Price < Tuning.BmEliteMinValue * Tuning.BmFactorEliteMin * 0.97 - 1) throw new Exception("尖端店铺卖了便宜货:" + l.Item + " " + l.Price); + foreach (var l in bm.Catalog.Where(x => bm.Sellers[x.Seller].Type == MerchantType.Personal && x.Kind == GoodKind.Contraband)) { personalListings++; double f = l.Price / 200.0; lo = Math.Min(lo, f); hi = Math.Max(hi, f); } + foreach (var s in bm.Sellers.Where(x => x.Type == MerchantType.Personal)) if (s.Advance) throw new Exception("个人卖家不该接受提前放款"); + } + Check(elite > n * 0.08 && elite < n * 0.25, $"尖端店铺约 {Tuning.BmEliteChance:P0} 的周出现:{elite}/{n}"); + Check(personalListings > 100 && lo < 0.65 && hi > 1.05, $"个人卖家每件价格系数分散:{lo:0.00}–{hi:0.00}"); + }); + Console.WriteLine($"\n{_passed} 通过,{_failed} 失败"); return _failed == 0 ? 0 : 1; }