using System; using System.Collections.Generic; using System.Linq; using Decentralized.Domain; namespace Decentralized { /// /// 预测市场的接入层:按游戏当前状态给出候选盘口、公开信息下的合理概率(盘口夜里向它靠拢)、线索、情报贩子的情报, /// 夜间读游戏事件、派系行动、水纯度、gas 和本 mod 的状态来结算。 /// public static class PredictGame { public static readonly string[] Factions = { "FACTION_SECURITY", "FACTION_REVOLUTION", "FACTION_LOWER_LEVEL", "FACTION_UPPER_LEVEL", "FACTION_BLACK_MARKET" }; private static readonly double[] GutterCuts = { 8300, 8700 }, RustCuts = { 9050, 9100 }; private static readonly double[] PriceMult = { 0.9, 0.95, 1.0, 1.05, 1.1 }; private static readonly double[] GasCuts = { 0.8, 1.0, 1.2, 1.4 }; /// 夜间结算开始时记下的收盘数据:行情盘读夜间更新前的价格,gas 盘读当天的 gas(接着链上夜间事件就会改成明天的)。 public static double ClosePrice, CloseGas, ClosePush; public static bool Available(int day) => Session.Data != null && Unlock.IsUnlocked && day >= Tuning.PredictUnlockDay; /// 情报贩子的门路:Web3 黑市解锁,且声望够。 public static bool BrokerOpen => Unlock.BlackUnlocked && StoreReputation.GetBMReputation() >= Tuning.IntelRep; /// 夜间观测用的标识:生效中的事件,以及派系行动的 "F:派系"。 public static List ActiveIds() { var list = new List(); var mgr = StoreStation.Instance?.storeEventManager; if (mgr == null) return list; foreach (var e in mgr.GetActiveEvents()) { list.Add(e.identifier); if (e.isFactionOperation && !string.IsNullOrEmpty(e.sourceFaction)) list.Add("F:" + e.sourceFaction); } return list; } // ——— 游戏事件模型 ——— private static double DayChance(StoreEventManager mgr) { int c = mgr.GetEventCounter(); return c == 0 ? 0.50 : c == 1 ? 0.35 : c == 2 ? 0.25 : 0; } private static double EventProb(StoreEventManager mgr, string id, int days, bool ignoreCooldown = false) { var pool = StoreEventManager.normalEventBlueprints.Where(e => ignoreCooldown || !mgr.IsEventOnCooldown(e.identifier)).ToList(); var bp = pool.Find(e => e.identifier == id); if (bp == null) return 0; return PredictMath.EventProb(ignoreCooldown ? 0.35 : DayChance(mgr), bp.odd, pool.Sum(e => (double)e.odd), days); } /// 已经排进日程的事件 / 派系行动在窗口里的开始日(known = 只看玩家已知的)。 private static int? QueuedStart(PredictMarket m, bool knownOnly) { var mgr = StoreStation.Instance?.storeEventManager; if (mgr == null || (m.Kind != PredictKind.Event && m.Kind != PredictKind.Faction)) return null; int today = StoreStation.GetDayCounter(); for (int i = 0; i < mgr.futurEvents.Count; i++) { var e = mgr.futurEvents[i]; if (knownOnly && !e.IsKnown) continue; bool match = m.Kind == PredictKind.Event ? e.identifier == m.Key : e.isFactionOperation && e.sourceFaction == m.Key; if (i >= mgr.futurEventsDay.Count) break; int start = today + mgr.futurEventsDay[i]; if (match && start >= m.FromDay && start <= m.ToDay) return start; } return null; } private static bool FactionBusy(StoreEventManager mgr, string faction) => mgr.GetActiveEvents().Any(e => e.isFactionOperation && e.sourceFaction == faction); // ——— 候选盘口 ——— public static List Candidates(int day) { var d = Session.Data; var list = new List(); var mgr = StoreStation.Instance?.storeEventManager; if (mgr == null) return list; double dayChance = DayChance(mgr); var pool = StoreEventManager.normalEventBlueprints.Where(e => !mgr.IsEventOnCooldown(e.identifier)).ToList(); double total = pool.Sum(e => (double)e.odd); foreach (var bp in pool) { if (mgr.IsEventActive(bp.identifier)) continue; string name; try { name = bp.storeEventFunc().GetDisplayName(); } catch (Exception) { continue; } double odd = bp.odd; list.Add(new PredictCandidate { Kind = PredictKind.Event, Key = bp.identifier, Title = name, Prob = (from, to) => PredictMath.EventProb(dayChance, odd, total, to - from + 1) }); } foreach (var f in Factions) { if (FactionBusy(mgr, f)) continue; list.Add(new PredictCandidate { Kind = PredictKind.Faction, Key = f, Title = L.T("predict.faction." + f), Prob = (from, to) => PredictMath.FactionProb(to - from + 1) }); } foreach (var t in GutterCuts) { double thr = t; list.Add(new PredictCandidate { Kind = PredictKind.Purity, Key = "gutter", Title = L.T("predict.purity.gutter"), Param = thr, Tomorrow = true, Prob = (a, b) => PredictMath.PurityAtLeast(thr, 8000, 8999) }); } foreach (var t in RustCuts) { double thr = t; list.Add(new PredictCandidate { Kind = PredictKind.Purity, Key = "rust", Title = L.T("predict.purity.rust"), Param = thr, Tomorrow = true, Prob = (a, b) => PredictMath.PurityAtLeast(thr, 9000, 9150) }); } double price = d.Market.Price, anchor = d.Market.Anchor(d.ActiveEvents); foreach (var mult in PriceMult) { double thr = Math.Round(price * mult, price >= 100 ? 0 : 1); list.Add(new PredictCandidate { Kind = PredictKind.Price, Key = "PSC", Title = "PSC", Param = thr, Prob = (from, to) => PredictMath.PriceAtLeast(price, anchor, Tuning.PscVol, Tuning.Revert, to - day, thr) }); } if (d.GasPhase >= 0) foreach (var t in GasCuts) { double thr = t; list.Add(new PredictCandidate { Kind = PredictKind.Gas, Key = "gas", Title = "Gas", Param = thr, Tomorrow = true, Prob = (a, b) => GasPublic(d, day + 1, thr) }); } if (d.Stable.CraWarnDay < 0 && d.Stable.CraCrashDay < 0) list.Add(new PredictCandidate { Kind = PredictKind.Cra, Key = "cra", Title = "CRA", Prob = (from, to) => PredictMath.CraWarnProb(d.Stable.Cushion, CraDrain(d), day, from, to, CraHazard) }); if (!d.Network.EraDone && d.Network.EraDay > 0 && day + 4 >= (d.Network.EraWarned ? d.Network.EraWarnDay : Tuning.EraMin)) list.Add(new PredictCandidate { Kind = PredictKind.Era, Key = "era", Title = L.T("predict.era"), Prob = (from, to) => PredictMath.EraProb(day, from, to, d.Network.EraWarned ? d.Network.EraWarnDay : -1) }); return list; } /// gas 盘的公开价:绝大部分按真实相位、小部分按不知道相位(PredictGasBlind)——七天周期是链上公开的,盘口里的 NPC 多数看得见;数清周期的人只剩一点优势。 private static double GasPublic(SaveData d, int day, double thr) => d.GasPhase < 0 ? PredictMath.GasFlat(day, thr) : (1 - Tuning.PredictGasBlind) * PredictMath.GasAtLeast(day, d.GasPhase, thr) + Tuning.PredictGasBlind * PredictMath.GasFlat(day, thr); private static double CraHazard => 1 - Math.Pow(1 - Tuning.CraSpiralWeekly, 1.0 / 7); private static double CraDrain(SaveData d) => Stablecoins.CushionDrain(Tuning.CraNpcStake + d.Staking.Earning, d.Stable.CraRate); /// 开门后补盘。价格盘一次只留一个(阈值不同的盘口会互相对冲)。 public static void Morning(SaveData d, int day) { if (!Available(day)) return; ForceKnown(d, day); OpenTrial(d, day); var made = d.Predict.Refill(day, d.Rng, Candidates(day)); foreach (var m in made) Log.Info($"预测盘口 #{m.Id}:{m.Kind} {m.Key} {m.Title} 阈值 {m.Param} 窗口 {m.FromDay}-{m.ToDay} 先验 {m.P0:0.000}"); } /// /// 柜台线人(游戏里卖消息的顾客)、传闻纸条和派系特权会把事件排进日程且标为已知:每个这样的事件保证有一个盘口(窗口 = 开始那天), /// 开盘价按公开信息下的基础概率,不因为已经排期而抬高——知道的人能赚到价格追上来之前的差。开始日是明天的来不及,不开。 /// private static void ForceKnown(SaveData d, int day) { var mgr = StoreStation.Instance?.storeEventManager; if (mgr == null) return; var bk = d.Predict; for (int i = 0; i < mgr.futurEvents.Count; i++) { var e = mgr.futurEvents[i]; if (i >= mgr.futurEventsDay.Count) break; int start = day + mgr.futurEventsDay[i]; if (!e.IsKnown || start < day + 2 || e.eventType == StoreEvent.EventType.COSMETIC) continue; bool op = e.isFactionOperation; var kind = op ? PredictKind.Faction : PredictKind.Event; string key = op ? e.sourceFaction : e.identifier; if (!op && !StoreEventManager.normalEventBlueprints.Any(b => b.identifier == key)) continue; if (bk.Markets.Any(m => m.Outcome < 0 && m.Kind == kind && m.Key == key && m.FromDay <= start && m.ToDay >= start)) continue; if (bk.Markets.Count(m => m.Outcome < 0) >= Tuning.PredictOpenMax + 2) break; double p = op ? PredictMath.FactionProb(1) : EventProb(mgr, key, 1, true); string title = op ? L.T("predict.faction." + key) : e.GetDisplayName(); var m2 = PredictMarket.Create(++bk.Seq, kind, key, title, 0, day, start, start, p); bk.Markets.Add(m2); Log.Info($"线人情报开盘 #{m2.Id}:{kind} {key} 第 {start} 天,先验 {p:0.000}"); } } /// 举报 Tumbler 后的审判盘:开盘日起强制开(不进候选池),宣判那天结算,先验 = 定罪概率。 public static void OpenTrial(SaveData d, int day) { var a = d.Story.After; if (a.Verdict < 0 || a.TrialMarket || day < a.TrialOpenDay || day >= a.TrialDay) return; a.TrialMarket = true; var bk = d.Predict; var m = PredictMarket.Create(++bk.Seq, PredictKind.Trial, "tumbler", "Tumbler", 0, day, a.TrialDay, a.TrialDay, Tuning.TumblerTrialConvict); bk.Markets.Add(m); Log.Info($"审判盘开盘 #{m.Id}:第 {a.TrialDay} 天宣判"); } // ——— 公开信息下的合理概率(盘口夜里向它靠拢) ——— /// /// 市场里的 NPC 看得到的东西:报纸上已经排期的事件、正在进行的事件与派系行动、冷却、当前行情、gas 的周期、补贴池、矿机时代预警。 /// 看不到:没上报纸的排期(传闻、派系特权之外)、减半抬价和排期好的镇压对价格锚的未来影响。day 是今晚。 /// public static double? PublicProb(PredictMarket m, SaveData d, int day) { var mgr = StoreStation.Instance?.storeEventManager; int days = m.ToDay - m.FromDay + 1; switch (m.Kind) { case PredictKind.Event: if (mgr == null) return null; if (QueuedStart(m, true) != null) return 0.92; if (mgr.IsEventActive(m.Key) || mgr.IsEventOnCooldown(m.Key)) return 0.02; return EventProb(mgr, m.Key, days); case PredictKind.Faction: if (mgr == null) return null; if (QueuedStart(m, true) != null) return 0.92; if (FactionBusy(mgr, m.Key)) return 0.02; return PredictMath.FactionProb(days); case PredictKind.Price: return PredictMath.PriceAtLeast(d.Market.Price, d.Market.Anchor(d.ActiveEvents), Tuning.PscVol, Tuning.Revert, Math.Max(1, m.ToDay - day), m.Param); case PredictKind.Gas: return d.GasPhase < 0 ? (double?)null : GasPublic(d, m.ToDay, m.Param); case PredictKind.Cra: if (d.Stable.CraWarnDay >= 0 || d.Stable.CraCrashDay >= 0) return 0.02; return PredictMath.CraWarnProb(d.Stable.Cushion, CraDrain(d), day + 1, m.FromDay, m.ToDay, CraHazard); case PredictKind.Era: return PredictMath.EraProb(day, m.FromDay, m.ToDay, d.Network.EraWarned ? d.Network.EraWarnDay : -1); case PredictKind.Trial: return d.Story.After.TrialPublic(); } return null; } // ——— 结算 ——— /// 夜间结算的结果:事件、派系读观测;纯度读当天的数值;行情读夜间更新之前的价格;gas 读当天的指数;CRA 预警与矿机时代读 mod 状态(要在这两项夜间逻辑之后结算)。 public static bool? Outcome(PredictMarket m, SaveData d) { switch (m.Kind) { case PredictKind.Event: case PredictKind.Faction: return d.Predict.Happened(m); case PredictKind.Purity: var st = StoreStation.Instance; return st == null ? (bool?)null : (m.Key == "gutter" ? st.todayGutterflowPurity : st.todayRustwaterPurity) >= m.Param; case PredictKind.Price: return ClosePrice >= m.Param; case PredictKind.Gas: return CloseGas >= m.Param; case PredictKind.Cra: return d.Stable.CraWarnDay >= m.FromDay && d.Stable.CraWarnDay <= m.ToDay; case PredictKind.Era: return d.Network.EraDay < 0 ? (bool?)null : d.Network.EraDay >= m.FromDay && d.Network.EraDay <= m.ToDay; case PredictKind.Trial: return d.Story.After.Verdict < 0 ? (bool?)null : d.Story.After.Verdict == 1; } return null; } // ——— 情报 ——— /// 这个盘口的真相是不是已经定了(情报贩子只能卖已经定了的):派系行动看排期(包括没上报纸的),矿机时代看 mod 自己掷出的日子。 public static bool? Truth(PredictMarket m) { var d = Session.Data; if (m.Kind == PredictKind.Faction) return QueuedStart(m, false) != null; if (m.Kind == PredictKind.Era) return d.Network.EraDay > 0 ? d.Network.EraDay >= m.FromDay && d.Network.EraDay <= m.ToDay : (bool?)null; if (m.Kind == PredictKind.Trial) return d.Story.After.Verdict < 0 ? (bool?)null : d.Story.After.Verdict == 1; return null; } public static bool Tippable(PredictMarket m, int day) => m.IsOpen(day) && Truth(m) != null && (m.Kind != PredictKind.Faction || m.ToDay <= day + 4); /// 下注时玩家是否已经知道答案:这个事件(或派系行动)已经以玩家看得到的形式排进日程(传闻纸条、派系特权),或者买了情报贩子的内幕消息且说的就是这一边。 public static bool Known(PredictMarket m, bool yes) { var tip = Session.Data?.Predict.TipFor(m.Id); if (tip != null && tip.Claim == yes) return true; return yes && QueuedStart(m, true) != null; } public static string Question(PredictMarket m) { string win = m.FromDay == m.ToDay ? L.T("predict.win.one", m.FromDay) : L.T("predict.win.range", m.FromDay, m.ToDay); switch (m.Kind) { case PredictKind.Event: return L.T("predict.q.event", win, m.Title); case PredictKind.Faction: return L.T("predict.q.faction", win, m.Title); case PredictKind.Purity: return L.T("predict.q.purity", win, m.Title, Fmt.Num(m.Param)); case PredictKind.Gas: return L.T("predict.q.gas", win, Fmt.Num(m.Param)); case PredictKind.Cra: return L.T("predict.q.cra", win); case PredictKind.Era: return L.T("predict.q.era", win); case PredictKind.Trial: return L.T("predict.q.trial", win); default: return L.T("predict.q.price", win, m.Title, Fmt.Num(m.Param)); } } // ——— 线索 ——— /// 盘口面板里的"线索":玩家手里已有的、和这个盘口有关的信息。每条尽量短。 public static List Clues(PredictMarket m) { var d = Session.Data; var list = new List(); var mgr = StoreStation.Instance?.storeEventManager; int today = StoreStation.GetDayCounter(); var tip = d.Predict.TipFor(m.Id); if (tip != null) list.Add(L.T("predict.clue.tip", L.T("predict.tier." + tip.Tier), L.T(tip.Claim ? "predict.yes" : "predict.no"))); switch (m.Kind) { case PredictKind.Event: { int? s = QueuedStart(m, true); if (s != null) list.Add(L.T("predict.clue.queued", s.Value)); else if (mgr != null && (mgr.IsEventActive(m.Key) || mgr.IsEventOnCooldown(m.Key))) list.Add(L.T("predict.clue.cool")); break; } case PredictKind.Faction: { int? s = QueuedStart(m, true); if (s != null) list.Add(L.T("predict.clue.queued", s.Value)); else if (mgr != null && FactionBusy(mgr, m.Key)) list.Add(L.T("predict.clue.busy")); break; } case PredictKind.Price: { int toHalv = Tuning.HalvingDays - today % Tuning.HalvingDays; if (toHalv <= m.ToDay - today + Tuning.HalvingRampDays) list.Add(L.T("predict.clue.halving", today + toHalv)); if (mgr != null) for (int i = 0; i < mgr.futurEvents.Count; i++) { if (i >= mgr.futurEventsDay.Count) break; var e = mgr.futurEvents[i]; if (e.IsKnown && (e.identifier == "crackdown" || e.identifier == "stationWideBlackout" || e.identifier == "powerShortage")) list.Add(L.T("predict.clue.queued2", today + mgr.futurEventsDay[i], e.GetDisplayName())); } list.Add(L.T("predict.clue.anchor", Fmt.Num(d.Market.Anchor(d.ActiveEvents)))); if (Math.Abs(d.Market.PlayerPush) >= Tuning.ManipFree) list.Add(L.T("predict.clue.push", (d.Market.PlayerPush * 100).ToString("+0.0;-0.0", System.Globalization.CultureInfo.InvariantCulture))); break; } case PredictKind.Gas: if (d.GasHist.Count > 0) list.Add(L.T("predict.clue.gas", string.Join(" ", d.GasHist.Skip(Math.Max(0, d.GasHist.Count - 7)).Select(x => x.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture))))); break; case PredictKind.Cra: list.Add(L.T("predict.clue.subsidy", (d.Stable.Cushion / Stablecoins.Cushion0 * 100).ToString("0"))); list.Add(L.T("predict.clue.drain", Fmt.Num(CraDrain(d)))); break; case PredictKind.Trial: { var a = d.Story.After; if (a.Hints.Count > 0) list.Add(L.T("predict.clue.trial", a.Hints.Count(h => h), a.Hints.Count(h => !h))); break; } case PredictKind.Era: list.Add(d.Network.EraWarned ? L.T("predict.clue.era.warned", d.Network.EraWarnDay + Tuning.EraWarnMin, d.Network.EraWarnDay + Tuning.EraWarnMax) : L.T("predict.clue.era", Tuning.EraMin, Tuning.EraMax)); break; } return list; } // ——— 局势简报 ——— private static string OpName(string id) { var bp = StoreOperationManager.operationBlueprints.Find(b => b.identifier == id); try { return bp != null ? bp.storeEventFunc().GetDisplayName() : id; } catch (Exception) { return id; } } /// 按游戏的阈值列出每个派系现在能发起的行动。fake:假情报,数值是编的。 public static List MakeBrief(bool fake, int day) { var mgr = StoreStation.Instance?.storeEventManager; var om = StoreStation.Instance?.storeOperationManager; var lines = new List(); if (om == null || mgr == null) return lines; int sec = om.secPower, rev = om.revPower, bm = om.blackMarketPower, ll = om.lowerLevelUnrest, ul = om.upperLevelFriendliness; if (fake) { var rng = Session.Data.Rng; sec = rng.Range(0, 100); rev = rng.Range(0, 100); bm = rng.Range(0, 100); ll = rng.Range(0, 100); ul = rng.Range(0, 100); } lines.Add(L.T("predict.brief.levels", sec, rev, bm, ll, ul)); Func> ops = f => { var l = new List(); switch (f) { case "FACTION_SECURITY": if (sec >= 20 && bm >= 40) l.Add("crackdown"); if (sec >= 20 && (rev >= 33 || ll >= 33 || bm >= 33)) l.Add("sec_increase_patrols"); if (sec >= 15 && rev >= 40) l.Add("sec_raid_revolution"); if (sec >= 15 && bm >= 40) l.Add("sec_raid_black_market"); if (sec >= 50 && (rev >= 65 || ll >= 65 || bm >= 65)) l.Add("sec_martial_law"); if (sec <= 5) l.Add("sec_budget_increase"); break; case "FACTION_REVOLUTION": if (day > 7) { if (rev <= 25) l.Add("rev_mobilization_secret"); if (rev >= 50) l.Add("rev_mobilization"); if (rev >= 15) l.Add("rev_raid_upper_level"); if (rev >= 25) l.Add("raid_security"); } break; case "FACTION_LOWER_LEVEL": if (ll >= 85) l.Add("ll_riot"); if (ll >= 50) l.Add("ll_worker_strike"); break; case "FACTION_UPPER_LEVEL": if (ul >= 50 && ll >= 50) l.Add("ul_medical_aid"); if (ul >= 50 && ll <= 35) l.Add("ul_household_relief"); break; case "FACTION_BLACK_MARKET": if (day > 9) { if (bm >= 85) l.Add("bm_gang_war"); l.Add("bm_illegal_weapon_shipment"); } break; } return l; }; foreach (var f in Factions) { string name = L.T("predict.faction." + f); if (FactionBusy(mgr, f)) { lines.Add(L.T("predict.brief.busy", name)); continue; } if (mgr.futurEvents.Any(e => e.isFactionOperation && e.sourceFaction == f)) { lines.Add(L.T("predict.brief.queued", name)); continue; } var l = ops(f); lines.Add(l.Count == 0 ? L.T("predict.brief.none", name) : L.T("predict.brief.can", name, string.Join("、", l.Select(OpName)))); } return lines; } } }