using System; using System.Collections.Generic; using System.Linq; namespace Decentralized.Domain { public enum SaleState { Listed, Sold, Paid, Lost, Delisted } /// 玩家在黑市上架的一件货。货交给平台(从储藏区拿走),成交后货款托管几天再到钱包,可能带污点。 public sealed class BmSale { public string Item; public string Model, FakeOf; public double Health; /// 标价倍率与标价(信用点)。 public double Mult; /// 参考价:改价时按它重算标价。 public long Ref; public int ListPrice; /// 上架时付费的钱包地址:回款进这里。 public string Wallet; public int ListedDay, PayDay; public SaleState State = SaleState.Listed; /// 成交后扣掉抽成的货款(信用点)与其中带的污点。 public int Proceeds; public double Taint; /// 终态原因:fake(买家看穿假卡退货)/ rug / delist。 public string Why; } public sealed partial class BlackMarket { public List Sales = new List(); /// 玩家作为卖家的好评(%):影响成交速度;卖假卡被看穿会下降,每笔成交回升一点。 public int Rating = Tuning.BmRatingStart; /// 期望成交天数:倍率 ≤0.9 一天,1.0 两天,1.8 十四天,之间插值(0.9–1.0 线性,1.0 以上指数)。 public static double ExpectedDays(double m) { if (m <= 0.9) return Tuning.BmSellDaysFast; if (m < 1.0) return Tuning.BmSellDaysFast + (m - 0.9) / 0.1 * (Tuning.BmSellDays1 - Tuning.BmSellDaysFast); return Tuning.BmSellDays1 * Math.Pow(Tuning.BmSellDaysMax / Tuning.BmSellDays1, (m - 1.0) / (Tuning.BmSellMax - 1.0)); } private static double T(double m) => Math.Max(0, Math.Min(1, (m - Tuning.BmSellMin) / (Tuning.BmSellMax - Tuning.BmSellMin))); /// 货款带污点的概率与(带的时候)污点占货款的比例:都随标价倍率上升。 public static double TaintChance(double m) => Tuning.BmSaleTaintChanceMin + T(m) * (Tuning.BmSaleTaintChanceMax - Tuning.BmSaleTaintChanceMin); public static double TaintRatio(double m) => Tuning.BmSaleTaintRatioMin + T(m) * (Tuning.BmSaleTaintRatioMax - Tuning.BmSaleTaintRatioMin); /// 每晚成交的概率:1 / 期望天数,乘好评系数(好评 90 为 1,封顶 1.1),最多 1。 public double SaleChance(double m) => Math.Min(1, 1 / ExpectedDays(m) * Math.Min(1.1, Rating / (double)Tuning.BmRatingStart)); /// 近 7 天成交额推高的污点加成:出售量越大,货款越容易带污点、比例越高。 public double VolumeTaint(int day) { double v = Sales.Where(x => (x.State == SaleState.Sold || x.State == SaleState.Paid) && day - (x.PayDay - Tuning.BmSaleHold) <= 7).Sum(x => (double)x.Proceeds); return Math.Min(Tuning.BmVolumeTaintMax, v / Tuning.BmVolumeRef * Tuning.BmVolumeTaintMax); } public int ActiveListings => Sales.Count(x => x.State == SaleState.Listed); public static int ListPriceOf(long refValue, double m) => (int)Math.Max(1, Math.Round(refValue * m)); public static int ListFee(int listPrice) => (int)Math.Ceiling(listPrice * Tuning.BmListFee); /// 上架:交付一件货,收上架费(付给 feeAsset 币,污点与成本按比例带走)。倍率超出范围、平台关闭或余额不足返回 null。 public BmSale List(string item, string model, string fakeOf, double health, long refValue, double m, Wallet w, string feeAsset, int day, Func price) { if (m < Tuning.BmSellMin - 1e-9 || m > Tuning.BmSellMax + 1e-9 || refValue <= 0 || Closed(day) || feeAsset == Assets.CRC || ActiveListings >= Tuning.BmMaxListings) return null; int listPrice = ListPriceOf(refValue, m); double p = price(feeAsset); if (p <= 0) return null; double qty = ListFee(listPrice) / p; if (w.Avail(feeAsset) + 1e-9 < qty) return null; double total = w.Value(price); double share = total > 0 ? Math.Min(1, ListFee(listPrice) / total) : 0; w.Taint -= w.Taint * share; w.Basis -= w.Basis * share; w.Add(feeAsset, -qty); var s = new BmSale { Item = item, Model = model, FakeOf = fakeOf, Health = health, Ref = refValue, Mult = m, ListPrice = listPrice, ListedDay = day, Wallet = w.Address }; Sales.Add(s); return s; } public static int RepriceFee(int newPrice) => (int)Math.Ceiling(newPrice * Tuning.BmRepriceFee); /// 改价:上架中的货换一个倍率,按参考价重算标价,收改价费;不改变上架日。 public bool Reprice(BmSale s, double m, Wallet w, string feeAsset, int day, Func price) { if (s == null || s.State != SaleState.Listed || s.Ref <= 0 || Math.Abs(m - s.Mult) < 1e-9 || m < Tuning.BmSellMin - 1e-9 || m > Tuning.BmSellMax + 1e-9 || Closed(day) || feeAsset == Assets.CRC) return false; int listPrice = ListPriceOf(s.Ref, m); double p = price(feeAsset); if (p <= 0) return false; int fee = RepriceFee(listPrice); double qty = fee / p; if (w.Avail(feeAsset) + 1e-9 < qty) return false; double total = w.Value(price); double share = total > 0 ? Math.Min(1, fee / total) : 0; w.Taint -= w.Taint * share; w.Basis -= w.Basis * share; w.Add(feeAsset, -qty); s.Mult = m; s.ListPrice = listPrice; return true; } public void Delist(BmSale s) { s.State = SaleState.Delisted; s.Why = "delist"; } /// 未到账的货款(信用点):跑路时一并归零,也推高跑路概率。 public double HeldSales => Sales.Where(x => x.State == SaleState.Sold).Sum(x => (double)x.Proceeds); /// 每晚推进出售:上架的货按概率成交(假卡可能被看穿退货),成交的货款托管到期后进钱包。镇压期间平台下线,不推进。 private void SalesNight(int day, bool crackdown, Func wallet, Func price, Rng rng, List ev) { if (crackdown) return; foreach (var s in Sales.Where(x => x.State == SaleState.Listed && x.ListedDay < day).ToList()) { if (!rng.Chance(SaleChance(s.Mult))) continue; if (s.FakeOf != null && rng.Chance(Tuning.BmFakeDetect)) { s.State = SaleState.Delisted; s.Why = "fake"; Rating = Math.Max(0, Rating - Tuning.BmRatingFakeLoss); ev.Add(new BmEvent { Kind = "sale_fake", Sale = s }); continue; } s.State = SaleState.Sold; s.Proceeds = (int)Math.Round(s.ListPrice * (1 - Tuning.BmCommission)); double vb = VolumeTaint(day); s.Taint = rng.Chance(Math.Min(1, TaintChance(s.Mult) + vb)) ? s.Proceeds * Math.Min(1, TaintRatio(s.Mult) + vb) : 0; s.PayDay = day + Tuning.BmSaleHold; Rating = Math.Min(100, Rating + 1); if (Honeypot(day)) Traps.Add(new BmTrap { Day = day + Tuning.BmTrapDelay, Amount = s.Proceeds, Why = "honeypot", Item = s.Item, Group = HoneypotStart }); ev.Add(new BmEvent { Kind = "sale_sold", Sale = s, Amount = s.Proceeds }); } foreach (var s in Sales.Where(x => x.State == SaleState.Sold && day >= x.PayDay).ToList()) { double p = price(Assets.CRT); if (p <= 0) continue; var w = wallet(s.Wallet); w.Add(Assets.CRT, s.Proceeds / p); w.Taint += s.Taint; w.Basis += s.Proceeds; s.State = SaleState.Paid; ev.Add(new BmEvent { Kind = "sale_paid", Sale = s, Amount = s.Proceeds }); } // 买家落网:近期到账的出售记录会连累卖家(玩家) var recent = Sales.Where(x => x.State == SaleState.Paid && day - x.PayDay <= 14).ToList(); if (recent.Count > 0 && rng.Chance(Tuning.BmSellerBust)) { foreach (var s in recent) Traps.Add(new BmTrap { Day = day + Tuning.BmTrapDelay, Amount = s.Proceeds, Why = "seller", Item = s.Item, Group = -day }); Forum.Add("busted_buyer"); } Sales.RemoveAll(x => (x.State == SaleState.Paid || x.State == SaleState.Delisted || x.State == SaleState.Lost) && day - Math.Max(x.ListedDay, x.PayDay) > 14); } } }