Cards: appearance follows wear, hash depends on fan and contacts, seller confrontation
Fan, contact and scratch levels are recomputed from health and a per-card seed, so a card looks older as it wears. Fan and contacts scale hashrate. Each inspection-tray detail now maps to exactly one dimension value. Quoting dealers price the card as if its condition were fine; the player points out fan, contacts, bracket, print or sticker in the tray to take the matching discount or expose a fake (a wrong accusation costs reputation like the vanilla confrontation). Unowned cards show a coarse grade and rated specs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
68265095a8
commit
f93dbc6eec
9 files changed
+279
-76
No files matched your search
@@ -22,6 +22,36 @@ namespace Decentralized.Domain
|
||||
|
||||
public const int Variants = 3;
|
||||
|
||||
/// <summary>每张卡自己的随机种子:老化类维度(风扇、金手指、挡板划痕)由它和当前健康度决定,每晚损耗后重新算,
|
||||
/// 所以越旧的卡看着越旧。为 0 时表示外观是写死的(测试用)。</summary>
|
||||
public uint Seed;
|
||||
|
||||
private static double U(uint seed, int salt)
|
||||
{
|
||||
uint x = seed * 2654435761u + (uint)salt * 40503u + 12345u;
|
||||
x ^= x >> 15; x *= 2246822519u; x ^= x >> 13; x *= 3266489917u; x ^= x >> 16;
|
||||
return (x >> 8) * (1.0 / (1 << 24));
|
||||
}
|
||||
|
||||
/// <summary>按当前健康度重新算出的外观:磨损度 = 1 − 健康度 / 100,再加上这张卡自己的偏差(±0.15)。
|
||||
/// 风扇磨损 ≥ 0.25 积灰、≥ 0.55 缺角;金手指 ≥ 0.30 发暗、≥ 0.60 氧化;挡板磨损 ≥ 0.45 出划痕(接口数不对是假卡的固定特征,不变)。</summary>
|
||||
public CardLook Appearance(double health)
|
||||
{
|
||||
if (Seed == 0) return this;
|
||||
double worn = 1 - Math.Max(0, Math.Min(100, health)) / 100.0;
|
||||
var l = this;
|
||||
double fan = worn + (U(Seed, 1) - 0.5) * 0.3, fin = worn + (U(Seed, 2) - 0.5) * 0.3, brk = worn + (U(Seed, 3) - 0.5) * 0.3;
|
||||
l.Fan = fan >= 0.55 ? 2 : fan >= 0.25 ? 1 : 0;
|
||||
l.Fingers = fin >= 0.60 ? 2 : fin >= 0.30 ? 1 : 0;
|
||||
if (Bracket != 2) l.Bracket = brk >= 0.45 ? 1 : 0;
|
||||
return l;
|
||||
}
|
||||
|
||||
private static readonly double[] FanHash = { 1.00, 0.97, 0.92 }, FingerHash = { 1.00, 0.98, 0.94 };
|
||||
|
||||
/// <summary>算力倍数:风扇积灰 ×0.97、缺角 ×0.92(降频);金手指发暗 ×0.98、氧化 ×0.94(接触不良掉速)。</summary>
|
||||
public double HashFactor => FanHash[Fan] * FingerHash[Fingers];
|
||||
|
||||
/// <summary>五位数字,存在物品标签上:风扇、金手指、挡板、丝印、贴纸。</summary>
|
||||
public string Encode() => $"{Fan}{Fingers}{Bracket}{Print}{Sticker}";
|
||||
|
||||
@@ -37,7 +67,15 @@ namespace Decentralized.Domain
|
||||
private static readonly double[] FingerValue = { 1.00, 0.96, 0.88 }, FingerDeath = { 1.00, 1.10, 1.30 };
|
||||
|
||||
/// <summary>外观对价值的折扣:风扇积灰 −4%、缺角 −12%;金手指发暗 −4%、氧化 −12%;挡板划痕 −2%;封条缺失(开过封)−6%。</summary>
|
||||
public double ValueFactor => FanValue[Fan] * FingerValue[Fingers] * (Bracket == 1 ? 0.98 : 1.0) * (Sticker == 2 ? 0.94 : 1.0);
|
||||
public double ValueFactor => ValueFactorOf(AllDims);
|
||||
|
||||
/// <summary>外观折扣的维度位:1 风扇、2 金手指、4 挡板划痕、8 封条缺失。</summary>
|
||||
public const int DimFan = 1, DimFingers = 2, DimScratch = 4, DimSeal = 8, AllDims = 15;
|
||||
|
||||
/// <summary>只计入 mask 里的维度的折扣:贩子报价时先装作这些问题都没有,玩家指出一项才扣一项。</summary>
|
||||
public double ValueFactorOf(int mask) =>
|
||||
((mask & DimFan) != 0 ? FanValue[Fan] : 1.0) * ((mask & DimFingers) != 0 ? FingerValue[Fingers] : 1.0)
|
||||
* ((mask & DimScratch) != 0 && Bracket == 1 ? 0.98 : 1.0) * ((mask & DimSeal) != 0 && Sticker == 2 ? 0.94 : 1.0);
|
||||
|
||||
/// <summary>每晚损耗倍数:风扇积灰散热差 ×1.1,缺角 ×1.3。</summary>
|
||||
public double WearFactor => FanWear[Fan];
|
||||
@@ -54,36 +92,29 @@ namespace Decentralized.Domain
|
||||
/// </summary>
|
||||
public static CardLook Generate(Rng rng, double health, bool fake)
|
||||
{
|
||||
double worn = 1 - Math.Max(0, Math.Min(100, health)) / 100.0;
|
||||
var look = new CardLook
|
||||
var look = new CardLook { Seed = rng.NextUInt() | 1, Sticker = rng.Chance(fake ? 0.35 : 0.15 + 0.30 * (1 - Math.Max(0, Math.Min(100, health)) / 100.0)) ? 2 : 0 };
|
||||
if (fake)
|
||||
{
|
||||
Fan = Pick(rng, 0.10 + 0.55 * worn, 0.35 * worn),
|
||||
Fingers = Pick(rng, 0.08 + 0.55 * worn, 0.40 * worn),
|
||||
Bracket = rng.Chance(0.10 + 0.50 * worn) ? 1 : 0,
|
||||
Sticker = rng.Chance(fake ? 0.35 : 0.15 + 0.30 * worn) ? 2 : 0,
|
||||
};
|
||||
if (!fake) return look;
|
||||
int signs = rng.Chance(0.55) ? 1 : rng.Chance(0.6) ? 2 : 3;
|
||||
var order = new[] { 0, 1, 2 };
|
||||
for (int i = order.Length - 1; i > 0; i--)
|
||||
{
|
||||
int j = rng.Range(0, i);
|
||||
(order[i], order[j]) = (order[j], order[i]);
|
||||
}
|
||||
for (int k = 0; k < signs; k++)
|
||||
{
|
||||
switch (order[k])
|
||||
int signs = rng.Chance(0.55) ? 1 : rng.Chance(0.6) ? 2 : 3;
|
||||
var order = new[] { 0, 1, 2 };
|
||||
for (int i = order.Length - 1; i > 0; i--)
|
||||
{
|
||||
case 0: look.Bracket = 2; break;
|
||||
case 1: look.Print = rng.Chance(0.6) ? 1 : 2; break;
|
||||
default: look.Sticker = 1; break;
|
||||
int j = rng.Range(0, i);
|
||||
(order[i], order[j]) = (order[j], order[i]);
|
||||
}
|
||||
for (int k = 0; k < signs; k++)
|
||||
{
|
||||
switch (order[k])
|
||||
{
|
||||
case 0: look.Bracket = 2; break;
|
||||
case 1: look.Print = rng.Chance(0.6) ? 1 : 2; break;
|
||||
default: look.Sticker = 1; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return look;
|
||||
return look.Appearance(health);
|
||||
}
|
||||
|
||||
/// <summary>p1 的概率取 1,再以 p2 的概率升到 2。</summary>
|
||||
private static int Pick(Rng rng, double p1, double p2) => rng.Chance(p1) ? (rng.Chance(p2 / Math.Max(p1, 1e-9)) ? 2 : 1) : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -101,7 +132,21 @@ namespace Decentralized.Domain
|
||||
}
|
||||
|
||||
/// <summary>真实价值(芯片型号 + 健康度 + 外观折扣)。</summary>
|
||||
public static long True(Unit u) => Math.Max(1, (long)Math.Round(Of(u.FakeOf ?? u.Model, u.Health) * u.Look.ValueFactor));
|
||||
public static long True(Unit u) => Math.Max(1, (long)Math.Round(Of(u.FakeOf ?? u.Model, u.Health) * u.Appearance.ValueFactor));
|
||||
|
||||
public const double ScamDiscount = 0.90;
|
||||
|
||||
/// <summary>
|
||||
/// 贩子的报价:装作成色没问题(外观折扣只计玩家已指出的维度),在此之上加价 markupPct%。
|
||||
/// 没拆穿的假卡按标称型号的价再打九折;拆穿之后按真实芯片算,和真卡一样。
|
||||
/// </summary>
|
||||
public static long Quote(Unit u, int markupPct, int pointedDims, bool exposed)
|
||||
{
|
||||
bool fakeHidden = u.FakeOf != null && !exposed;
|
||||
double v = Of(fakeHidden ? u.Model : (u.FakeOf ?? u.Model), u.Health) * u.Appearance.ValueFactorOf(pointedDims);
|
||||
v *= fakeHidden ? ScamDiscount : 1 + markupPct / 100.0;
|
||||
return Math.Max(1, (long)Math.Round(v));
|
||||
}
|
||||
|
||||
/// <summary>标称价:贩子报的价,按标称型号算;二手卡也折旧,但只按贩子"自称"的健康度。</summary>
|
||||
public static long Quoted(Unit u, double claimedHealth) => Of(u.Model, claimedHealth);
|
||||
|
||||
@@ -86,6 +86,8 @@ namespace Decentralized.Domain
|
||||
public bool Refurbished;
|
||||
/// <summary>外观;新卡(正规渠道)没有记录,按全新画。</summary>
|
||||
public CardLook Look;
|
||||
/// <summary>按当前健康度算出的外观。</summary>
|
||||
public CardLook Appearance => Look.Appearance(Health);
|
||||
|
||||
public HardwareSpec Label => Hardware.Specs[Model];
|
||||
public HardwareSpec Chip => Hardware.Specs[FakeOf ?? Model];
|
||||
@@ -94,7 +96,7 @@ namespace Decentralized.Domain
|
||||
public double Hash(ModuleBonus b)
|
||||
{
|
||||
if (Dead) return 0;
|
||||
return Chip.Hash * (Health >= 50 ? 1.0 : 0.6 + 0.4 * Health / 50) * b.Hash;
|
||||
return Chip.Hash * (Health >= 50 ? 1.0 : 0.6 + 0.4 * Health / 50) * b.Hash * Appearance.HashFactor;
|
||||
}
|
||||
|
||||
public double Kj(ModuleBonus b) => Dead ? 0 : Chip.Kj * b.Kj;
|
||||
@@ -103,14 +105,14 @@ namespace Decentralized.Domain
|
||||
public double DeathChance(ModuleBonus b)
|
||||
{
|
||||
double p = Tuning.DeathBase + Tuning.DeathWorn * Math.Pow(1 - Health / 100, 2);
|
||||
return p * Label.DeathFactor * (FakeOf != null ? Tuning.FakeDeathFactor : 1) * b.Death * Look.DeathFactor;
|
||||
return p * Label.DeathFactor * (FakeOf != null ? Tuning.FakeDeathFactor : 1) * b.Death * Appearance.DeathFactor;
|
||||
}
|
||||
|
||||
/// <summary>一夜的损耗与暴毙判定;返回是否在今晚暴毙。</summary>
|
||||
public bool Night(ModuleBonus b, Rng rng)
|
||||
{
|
||||
if (Dead) return false;
|
||||
Health = Math.Max(0, Health - Label.WearFactor * b.Wear * Look.WearFactor);
|
||||
Health = Math.Max(0, Health - Label.WearFactor * b.Wear * Appearance.WearFactor);
|
||||
if (rng.Chance(DeathChance(b)))
|
||||
{
|
||||
Dead = true;
|
||||
|
||||
@@ -7,18 +7,20 @@ namespace Decentralized.Domain
|
||||
/// <summary>二手显卡贩子的类型:老实人按真实价卖;中间商加价;骗子夹带假卡,按标称型号的价卖。</summary>
|
||||
public enum DealerKind { Honest, Middleman, Scammer }
|
||||
|
||||
/// <summary>贩子带来的一张卡:<see cref="MarkupPct"/> 是柜台上相对真实价值的加价百分比(骗子卖假卡时是正的,按标称型号报价再打九折)。</summary>
|
||||
/// <summary>贩子带来的一张卡。老实人不报价(按真实价值卖);其他贩子 <see cref="Quoted"/> 为真,按 <see cref="CardValue.Quote"/> 报价,
|
||||
/// <see cref="MarkupPct"/> 是加价百分比,玩家指出外观问题或假货迹象就按维度扣回去。</summary>
|
||||
public sealed class UsedCard
|
||||
{
|
||||
public string Model;
|
||||
public string FakeOf;
|
||||
public double Health;
|
||||
public int MarkupPct;
|
||||
public bool Quoted;
|
||||
}
|
||||
|
||||
public static class UsedDealers
|
||||
{
|
||||
public const double MiddlemanMarkup = 0.08, ScamDiscount = 0.90;
|
||||
public const double MiddlemanMarkup = 0.08, ScamDiscount = CardValue.ScamDiscount;
|
||||
public const double FakeShareMiddleman = 0.10, FakeShareScammer = 0.45;
|
||||
|
||||
/// <summary>今天来的贩子的类型:骗子越往后越多。</summary>
|
||||
@@ -49,13 +51,9 @@ namespace Decentralized.Domain
|
||||
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();
|
||||
if (cheaper.Length > 0 && rng.Chance(share))
|
||||
{
|
||||
card.FakeOf = cheaper[rng.Range(0, cheaper.Length - 1)];
|
||||
var u = new Unit { Model = card.Model, FakeOf = card.FakeOf, Health = card.Health };
|
||||
card.MarkupPct = (int)Math.Round((CardValue.Of(label, card.Health) * ScamDiscount / CardValue.True(u) - 1) * 100);
|
||||
}
|
||||
else if (kind == DealerKind.Middleman) card.MarkupPct = (int)Math.Round(MiddlemanMarkup * 100);
|
||||
if (cheaper.Length > 0 && rng.Chance(share)) card.FakeOf = cheaper[rng.Range(0, cheaper.Length - 1)];
|
||||
card.Quoted = kind != DealerKind.Honest;
|
||||
card.MarkupPct = kind == DealerKind.Middleman ? (int)Math.Round(MiddlemanMarkup * 100) : 0;
|
||||
cards.Add(card);
|
||||
}
|
||||
return cards;
|
||||
|
||||
Reference in new issue
Block a user