Files
probably-decentralized/src/Decentralized.Domain/BmSales.cs
T

154 lines
9.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
public enum SaleState { Listed, Sold, Paid, Lost, Delisted }
/// <summary>玩家在黑市上架的一件货。货交给平台(从储藏区拿走),成交后货款托管几天再到钱包,可能带污点。</summary>
public sealed class BmSale
{
public string Item;
public string Model, FakeOf;
public double Health;
/// <summary>标价倍率与标价(信用点)。</summary>
public double Mult;
/// <summary>参考价:改价时按它重算标价。</summary>
public long Ref;
public int ListPrice;
/// <summary>上架时付费的钱包地址:回款进这里。</summary>
public string Wallet;
public int ListedDay, PayDay;
public SaleState State = SaleState.Listed;
/// <summary>成交后扣掉抽成的货款(信用点)与其中带的污点。</summary>
public int Proceeds;
public double Taint;
/// <summary>终态原因:fake(买家看穿假卡退货)/ rug / delist。</summary>
public string Why;
}
public sealed partial class BlackMarket
{
public List<BmSale> Sales = new List<BmSale>();
/// <summary>玩家作为卖家的好评(%):影响成交速度;卖假卡被看穿会下降,每笔成交回升一点。</summary>
public int Rating = Tuning.BmRatingStart;
/// <summary>期望成交天数:倍率 ≤0.9 一天,1.0 两天,1.8 十四天,之间插值(0.9–1.0 线性,1.0 以上指数)。</summary>
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)));
/// <summary>货款带污点的概率与(带的时候)污点占货款的比例:都随标价倍率上升。</summary>
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);
/// <summary>每晚成交的概率:1 / 期望天数,乘好评系数(好评 90 为 1,封顶 1.1),最多 1。</summary>
public double SaleChance(double m) => Math.Min(1, 1 / ExpectedDays(m) * Math.Min(1.1, Rating / (double)Tuning.BmRatingStart));
/// <summary>近 7 天成交额推高的污点加成:出售量越大,货款越容易带污点、比例越高。</summary>
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);
/// <summary>上架:交付一件货,收上架费(付给 feeAsset 币,污点与成本按比例带走)。倍率超出范围、平台关闭或余额不足返回 null。</summary>
public BmSale List(string item, string model, string fakeOf, double health, long refValue, double m, Wallet w, string feeAsset, int day, Func<string, double> 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);
/// <summary>改价:上架中的货换一个倍率,按参考价重算标价,收改价费;不改变上架日。</summary>
public bool Reprice(BmSale s, double m, Wallet w, string feeAsset, int day, Func<string, double> 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"; }
/// <summary>未到账的货款(信用点):跑路时一并归零,也推高跑路概率。</summary>
public double HeldSales => Sales.Where(x => x.State == SaleState.Sold).Sum(x => (double)x.Proceeds);
/// <summary>每晚推进出售:上架的货按概率成交(假卡可能被看穿退货),成交的货款托管到期后进钱包。镇压期间平台下线,不推进。</summary>
private void SalesNight(int day, bool crackdown, Func<string, Wallet> wallet, Func<string, double> price, Rng rng, List<BmEvent> 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);
}
}
}