using System; using System.Collections.Generic; using System.Linq; using GameFix.PaoDeKuaiF; using GameFix.Poker; using GameMessage; using GameMessage.PaoDeKuaiF; using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; namespace PdkFriendServer.Logic { /// /// NeuralBot v1 — 加载 ONNX 模型做决策的 Bot。 /// 状态编码与 Python trainer 完全一致:15 rank × 4 suit × 7 channel → 420 float。 /// /// 用法:PdkGameMain.InitBots() 中替换部分 bot 为 NeuralBot。 /// 训练:Python trainer → 导出 ONNX → 放 model.onnx → C# 加载。 /// public class NeuralBot : IPdkBot { private readonly InferenceSession _session; private readonly Random _rng; private readonly CardTracker _tracker = new CardTracker(); private readonly float _epsilon; // 探索率 // 状态编码常量 private static readonly int[] Ranks = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16 }; private const int NumRanks = 13; private const int NumSuits = 4; private const int NumChannels = 7; private const int InputDim = TrainingCollector.InputDim; // 364 private const int OutputDim = 1; // V(state) = P(win) public NeuralBot(string modelPath = "model.onnx", float epsilon = 0.05f) { var options = new SessionOptions(); options.LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR; _session = new InferenceSession(modelPath, options); _rng = new Random(); _epsilon = epsilon; } public bool DecideBaoZhuang(PdkBotView view) { // Neural model 暂不参与包庄决策,沿用 ISMCTS 逻辑 _tracker.Update(view); int minPlays = HandOptimizer.MinPlays(view.MyHand); if (minPlays >= 7) return false; return minPlays <= 3; // 极简:手牌极好才包 } public PlayOutCardPdkF DecidePlay(PdkBotView view) { _tracker.Update(view); var hand = view.MyHand; if (hand.Length == 0) return Pass(view); // 1. 获取合法出牌 bool isFirstPlay = view.MaxPlayCard.GameNum <= 0; var tips = PdkCardAlgorithm.GetTipCard(view.MaxPlayCard, hand, null, view.Rule.AAAIsZhaDan); var candidates = new List<(PlayOutCardPdkF play, int actionIdx)>(); if (tips != null) { foreach (var tip in tips) { int rank = tip[0].GameNum; int idx = RankToIdx(rank); candidates.Add((MakePlay(tip, view.MyPos), idx)); } } // 新轮次补多牌方案 if (isFirstPlay) AddMultiCardCandidates(hand, view.MyPos, candidates); if (!isFirstPlay) candidates.Add((Pass(view), OutputDim - 1)); // pass = last index if (candidates.Count == 0) return Pass(view); if (candidates.Count == 1) return candidates[0].play; // 2. ε-greedy 探索 if (_rng.NextDouble() < _epsilon) return candidates[_rng.Next(candidates.Count)].play; // 3. V-learning: 对每个候选,编码出牌后的状态,选 V 值最高的 float bestV = float.MinValue; int bestIdx = 0; var debugVals = new System.Text.StringBuilder(); for (int i = 0; i < Math.Min(candidates.Count, 8); i++) { var play = candidates[i].play; var afterHand = play.Ids != null && play.Ids.Length > 0 ? hand.Where(c => !play.Ids.Contains(c.ID)).ToArray() : hand; var afterState = EncodeAfterPlay(view, afterHand, play); var v = RunInference(afterState); float val = v.Length > 0 ? v[0] : 0; string label = play.Ids != null && play.Ids.Length > 0 ? $"{play.Ids.Length}c r{play.GameNum}" : "pass"; debugVals.Append($" {label}={val:F2}"); if (val > bestV) { bestV = val; bestIdx = i; } } // 继续评估剩余候选(不输出日志) for (int i = 8; i < candidates.Count; i++) { var play = candidates[i].play; var afterHand = play.Ids != null && play.Ids.Length > 0 ? hand.Where(c => !play.Ids.Contains(c.ID)).ToArray() : hand; // 编码出牌后状态 var afterState = EncodeAfterPlay(view, afterHand, play); var v = RunInference(afterState); float val = v.Length > 0 ? v[0] : 0; if (val > bestV) { bestV = val; bestIdx = i; } } var chosen = candidates[bestIdx]; bool isPass = chosen.play.Ids == null || chosen.play.Ids.Length == 0; Console.WriteLine($"[NeuralBot pos{view.MyPos}] {candidates.Count}c → " + $"{(isPass ? "pass" : $"{chosen.play.Ids?.Length ?? 1}c r{chosen.play.GameNum}")} V={bestV:F3}" + (debugVals.Length > 0 ? $" [{debugVals}]" : "")); TrainingCollector.Record(view, _tracker, isPass ? 13 : TrainingCollector.RankToIdx(chosen.play.GameNum), view.MyPos - 1); return chosen.play; } // 编码出牌后状态:模拟出牌后的 PdkBotView private float[] EncodeAfterPlay(PdkBotView view, TCardInfoPdkF[] afterHand, PlayOutCardPdkF play) { var playedIds = play.Type != CardType1.None && play.Ids != null ? play.Ids.Select(id => (int)id).ToArray() : null; return TrainingCollector.EncodeAfterPlay(view, _tracker, afterHand, playedIds); } // ---- 状态编码(与 Python encode_game_state 一致)---- private float[] EncodeState(PdkBotView view) { var state = new float[InputDim]; // 7 × 13 × 4 = 364 // ch0: 我的手牌 foreach (var c in view.MyHand) { if (c.GameState != 1) continue; int ri = RankToIdx(c.GameNum); int si = SuitToIdx(c.ID); if (ri >= 0 && si >= 0) state[ChOffset(0) + ri * NumSuits + si] = 1.0f; } // ch1: 已出牌 — 通过 card tracker 的 seen cards var seenIds = GetSeenCardIds(view); foreach (var id in seenIds) { int ri = RankToIdx(IdToGameNum(id)); int si = SuitToIdx(id); if (ri >= 0 && si >= 0) state[ChOffset(1) + ri * NumSuits + si] = 1.0f; } // ch2: 我的手牌数量 int myCount = view.MyHand.Count(c => c.GameState == 1); int c2Slot = myCount % NumRanks; state[ChOffset(2) + c2Slot * NumSuits + 0] = (float)myCount / 16.0f; // ch3: 下家剩余 int opp1 = view.MyPos % 3; int c1 = view.ShenYuCard[opp1]; state[ChOffset(3) + (c1 % NumRanks) * NumSuits + 0] = (float)c1 / 16.0f; // ch4: 上家剩余 int opp2 = (view.MyPos + 1) % 3; int c2 = view.ShenYuCard[opp2]; state[ChOffset(4) + (c2 % NumRanks) * NumSuits + 0] = (float)c2 / 16.0f; // ch5: 当前位置 state[ChOffset(5) + (view.MyPos % NumRanks) * NumSuits + 0] = 1.0f; // ch6: last_play rank if (view.MaxPlayCard.GameNum > 0 && view.MaxPlayCard.Pos != view.MyPos) { int lr = RankToIdx(view.MaxPlayCard.GameNum); if (lr >= 0) state[ChOffset(6) + lr * NumSuits + 0] = 1.0f; } return state; } private float[] RunInference(float[] input) { var tensor = new DenseTensor(input, new[] { 1, InputDim }); var inputs = new List { NamedOnnxValue.CreateFromTensor("state", tensor) }; using var results = _session.Run(inputs); var output = results.First().AsTensor(); return output.ToArray(); } // ---- 辅助 ---- private static int RankToIdx(int gameNum) => gameNum switch { 3 => 0, 4 => 1, 5 => 2, 6 => 3, 7 => 4, 8 => 5, 9 => 6, 10 => 7, 11 => 8, 12 => 9, 13 => 10, 14 => 11, 16 => 12, _ => -1 }; private static int SuitToIdx(int id) { // ID = (suit-1)*13 + n, suit 1-4 int suit = (id - 1) / 13; // 0-3 return suit >= 0 && suit < 4 ? suit : -1; } private static int IdToGameNum(int id) { int n = (id - 1) % 13 + 1; // 1-13 return n == 1 ? 14 : n == 2 ? 16 : n; } private HashSet GetSeenCardIds(PdkBotView view) { var seen = new HashSet(); // Track played cards through view: all hands that have appeared on table if (view.MaxPlayCard.Ids != null) foreach (var id in view.MaxPlayCard.Ids) seen.Add(id); // Also track from our own played cards (cards not in my hand anymore) return seen; } private static int ChOffset(int ch) => ch * NumRanks * NumSuits; // ---- 多牌候选(精简版,复用 IsmctsBot 逻辑)---- private void AddMultiCardCandidates(TCardInfoPdkF[] hand, int pos, List<(PlayOutCardPdkF play, int actionIdx)> candidates) { // 对子 foreach (var g in hand.Where(c => c.GameState == 1).GroupBy(c => c.GameNum).Where(g => g.Count() >= 2)) { var pair = g.Take(2).ToList(); candidates.Add((MakePlay(pair, pos), RankToIdx(pair[0].GameNum))); } // 三张 foreach (var g in hand.Where(c => c.GameState == 1).GroupBy(c => c.GameNum).Where(g => g.Count() >= 3)) { var trip = g.Take(3).ToList(); var rem = hand.Where(c => c.GameState == 1 && c.GameNum != g.Key).ToList(); if (rem.Count >= 2) { // 优先带对子 var pair = rem.GroupBy(c => c.GameNum).Where(x => x.Count() >= 2).OrderBy(x => x.Key).FirstOrDefault(); var attach = pair != null ? pair.Take(2).ToList() : rem.OrderBy(c => c.GameNum).Take(2).ToList(); candidates.Add((MakePlay(trip.Concat(attach).ToList(), pos), RankToIdx(trip[0].GameNum))); } } // 顺子 var straights = PokerLogic.GetShunZi(hand); if (straights != null) foreach (var s in straights) { var cards = s.ToList(); candidates.Add((MakePlay(cards, pos), RankToIdx(cards[0].GameNum))); } // 炸弹 if (PokerLogic.GetPlayZhaDans(hand, out var bombs) && bombs != null) foreach (var b in bombs) { var cards = b.ToList(); candidates.Add((MakePlay(cards, pos), RankToIdx(cards[0].GameNum))); } } private static PlayOutCardPdkF MakePlay(List cards, int pos) => new PlayOutCardPdkF { Pos = (byte)pos, GameNum = cards[0].GameNum, Ids = cards.Select(c => c.ID).ToArray(), Type = CardType1.None }; private static PlayOutCardPdkF Pass(PdkBotView v) => new PlayOutCardPdkF { Pos = (byte)v.MyPos, Type = CardType1.None }; public void Dispose() => _session?.Dispose(); } }