fix: V-learning 出牌率 0%→81% — MakePlay Type=None导致所有候选被当pass

根因: MakePlay(Type=CardType1.None)
  → play.Type==None 对所有候选为true
  → afterHand保持原样(未出牌)
  → 所有候选V值相同 → 始终选第一个(pass)

修复:
- play.Type→play.Ids.Length判断是否真pass
- EncodeAfterPlay更新ShenYuCard(出牌后剩余-1)
- EncodeAfterPlay设MaxPlayCard为出的牌
- isPass用Ids==null而非Type

效果: Play 117 Pass 28(81%出牌) vs ISMCTS -108/10局
This commit is contained in:
2026-07-13 03:03:36 +08:00
parent f43e47b0a6
commit e5e1f4818e
19 changed files with 90 additions and 76 deletions

View File

@ -88,13 +88,30 @@ namespace PdkFriendServer.Logic
// 3. V-learning: 对每个候选,编码出牌后的状态,选 V 值最高的
float bestV = float.MinValue;
int bestIdx = 0;
for (int i = 0; i < candidates.Count; i++)
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.Type == CardType1.None
? hand
: hand.Where(c => play.Ids == null || !play.Ids.Contains(c.ID)).ToArray();
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);
@ -109,9 +126,10 @@ namespace PdkFriendServer.Logic
}
var chosen = candidates[bestIdx];
bool isPass = chosen.play.Type == CardType1.None;
Console.WriteLine($"[NeuralBot pos{view.MyPos}] {candidates.Count}c → " +
$"{(isPass ? "pass" : $"{chosen.play.Ids?.Length ?? 1}c rank={chosen.play.GameNum}")} V={bestV:F3}");
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}]" : ""));
return chosen.play;
}

View File

@ -64,12 +64,23 @@ namespace PdkFriendServer.Logic
public static float[] EncodeAfterPlay(PdkBotView view, CardTracker tracker,
TCardInfoPdkF[] afterHand, int[] playedIds)
{
// 更新剩余张数:我打出了牌
int playedCount = playedIds?.Length ?? 0;
byte[] afterShenYu = new byte[view.ShenYuCard.Length];
Array.Copy(view.ShenYuCard, afterShenYu, view.ShenYuCard.Length);
afterShenYu[view.MyPos - 1] = (byte)Math.Max(0, afterShenYu[view.MyPos - 1] - playedCount);
var afterView = new PdkBotView
{
MyHand = afterHand,
MyPos = view.MyPos,
ShenYuCard = view.ShenYuCard,
MaxPlayCard = view.MaxPlayCard,
ShenYuCard = afterShenYu,
// 出牌后如果我不是passMaxPlayCard改成我出的牌
MaxPlayCard = playedIds != null && playedIds.Length > 0
? new PlayOutCardPdkF { Pos = (byte)view.MyPos,
GameNum = view.MyHand.FirstOrDefault(c => playedIds.Contains(c.ID)).GameNum,
Type = CardType1.DanZhang, Ids = playedIds.Select(id => (byte)id).ToArray() }
: view.MaxPlayCard,
Rule = view.Rule,
};
return EncodeState(afterView, tracker, playedIds);