fix: GetLegalActions从硬编码改为读DSL+条件求值+wildcard计数修正
子代理审查MED级bug修复: 1. GetLegalActions硬编码忽略DSL action列表 →从play phase的SelfActions/OthersReactions读取允许的action →DSL可禁用an_kong/bu_kong/pung等操作 2. action condition字符串完全未求值 →CheckCondition()支持:can_win/can_chi/has_two_same等10种条件 →国标can_win_tumo_and_fan_ge_8条件被评估 3. wildcard bonus仅计缺口填充 →改为统计赢家手牌+副露中全部癞子(per_wildcard_in_win语义) 4. PhaseMachine两处wildcard计数从静态IsWildcard改为_wildcards实例 6个文件:PhaseMachine+ScoreEngine(核心)+MahjongRoom(WildcardRegistry引用)
This commit is contained in:
@ -69,50 +69,102 @@ public class MahjongPhaseMachine
|
|||||||
{
|
{
|
||||||
var actions = new List<ActionOption>();
|
var actions = new List<ActionOption>();
|
||||||
|
|
||||||
// Always can discard
|
// Read action lists from DSL play phase (instead of hardcoding)
|
||||||
foreach (var t in state.Hands[playerId].Distinct())
|
var playPhase = _phases.FirstOrDefault(p => p.Name == "play");
|
||||||
actions.Add(new ActionOption { Action = "discard", Priority = 0 });
|
var selfActs = playPhase?.SelfActions ?? new();
|
||||||
|
var reactActs = playPhase?.OthersReactions ?? new();
|
||||||
|
bool allowDiscard = selfActs.Any(a => a.Action == "discard");
|
||||||
|
bool allowAnKong = selfActs.Any(a => a.Action == "an_kong");
|
||||||
|
bool allowBuKong = selfActs.Any(a => a.Action == "bu_kong");
|
||||||
|
bool allowSelfDrawWin = selfActs.Any(a => a.Action == "win");
|
||||||
|
bool allowPung = reactActs.Any(a => a.Action == "pung");
|
||||||
|
bool allowMingKong = reactActs.Any(a => a.Action == "ming_kong");
|
||||||
|
bool allowReactionWin = reactActs.Any(a => a.Action == "win");
|
||||||
|
bool chiAllowed = _chiAllowed;
|
||||||
|
|
||||||
// Check pung/kong/win for last discard
|
// Helper: evaluate DSL condition string
|
||||||
|
bool CheckCondition(ActionOption opt, bool canPung, bool canMingKong, bool canSelfWin, int fanSum)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(opt.Condition)) return true;
|
||||||
|
return opt.Condition switch
|
||||||
|
{
|
||||||
|
"has_punged_pair" => CanBuKong(state, playerId),
|
||||||
|
"has_two_same" => canPung,
|
||||||
|
"has_three_same" => canMingKong,
|
||||||
|
"can_chi" => chiAllowed,
|
||||||
|
"can_win" => canSelfWin,
|
||||||
|
"can_win_tumo" => canSelfWin,
|
||||||
|
"can_win_tumo_and_fan_ge_8" => canSelfWin && fanSum >= 8,
|
||||||
|
"can_win_and_fan_ge_8" => canSelfWin && fanSum >= 8,
|
||||||
|
_ => true // unknown condition → allow (fail-open for safety)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always can discard (every variant supports this)
|
||||||
|
if (allowDiscard)
|
||||||
|
{
|
||||||
|
foreach (var t in state.Hands[playerId].Distinct())
|
||||||
|
actions.Add(new ActionOption { Action = "discard", Priority = 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check pung/kong/win for last discard (only if DSL allows them)
|
||||||
if (state.LastDiscard.HasValue && state.LastDiscardPlayer != playerId)
|
if (state.LastDiscard.HasValue && state.LastDiscardPlayer != playerId)
|
||||||
{
|
{
|
||||||
if (CanPung(state, playerId, state.LastDiscard.Value))
|
if (allowPung && CanPung(state, playerId, state.LastDiscard.Value))
|
||||||
actions.Add(new ActionOption { Action = "pung", Priority = 2 });
|
|
||||||
|
|
||||||
if (CanMingKong(state, playerId, state.LastDiscard.Value))
|
|
||||||
actions.Add(new ActionOption { Action = "ming_kong", Priority = 3 });
|
|
||||||
|
|
||||||
var fullHand = FullHand(state, playerId, extraTile: state.LastDiscard.Value);
|
|
||||||
int wildInHand = _wildcards.CountWildcards(fullHand);
|
|
||||||
var result = _solver.CheckWin(fullHand,
|
|
||||||
wildcardCount: wildInHand, require258Pair: _require258);
|
|
||||||
if (result != null && result.IsWin)
|
|
||||||
{
|
{
|
||||||
// 鸡胡只能自摸 — block discard win if 鸡胡 is the only/lowest fan
|
if (CheckCondition(reactActs.First(a => a.Action == "pung"), true, false, false, 0))
|
||||||
if (_jiHuSelfDrawOnly && result.Fans.Count > 0
|
actions.Add(new ActionOption { Action = "pung", Priority = 2 });
|
||||||
&& (result.Fans.Contains("鸡胡") || result.Fans.All(f => GetFanValue(f) <= 1)))
|
}
|
||||||
|
|
||||||
|
if (allowMingKong && CanMingKong(state, playerId, state.LastDiscard.Value))
|
||||||
|
{
|
||||||
|
if (CheckCondition(reactActs.First(a => a.Action == "ming_kong"), false, true, false, 0))
|
||||||
|
actions.Add(new ActionOption { Action = "ming_kong", Priority = 3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowReactionWin)
|
||||||
|
{
|
||||||
|
var fullHand = FullHand(state, playerId, extraTile: state.LastDiscard.Value);
|
||||||
|
int wildInHand = _wildcards.CountWildcards(fullHand);
|
||||||
|
var result = _solver.CheckWin(fullHand,
|
||||||
|
wildcardCount: wildInHand, require258Pair: _require258);
|
||||||
|
if (result != null && result.IsWin)
|
||||||
{
|
{
|
||||||
// Don't add win action; 鸡胡 can only self-draw
|
// 鸡胡只能自摸 — block discard win if 鸡胡 is the only/lowest fan
|
||||||
|
if (_jiHuSelfDrawOnly && result.Fans.Count > 0
|
||||||
|
&& (result.Fans.Contains("鸡胡") || result.Fans.All(f => GetFanValue(f) <= 1)))
|
||||||
|
{
|
||||||
|
// Don't add win action; 鸡胡 can only self-draw
|
||||||
|
}
|
||||||
|
else if (!requireWinFan || result.Fans.Sum(f => GetFanValue(f)) >= 8)
|
||||||
|
actions.Add(new ActionOption { Action = "win", Priority = 4 });
|
||||||
}
|
}
|
||||||
else if (!requireWinFan || result.Fans.Sum(f => GetFanValue(f)) >= 8)
|
|
||||||
actions.Add(new ActionOption { Action = "win", Priority = 4 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Self actions: an_kong, bu_kong, win (tumo)
|
// Self actions: only if DSL allows them
|
||||||
if (CanAnKong(state, playerId))
|
if (allowAnKong && CanAnKong(state, playerId))
|
||||||
actions.Add(new ActionOption { Action = "an_kong", Priority = 1 });
|
|
||||||
if (CanBuKong(state, playerId))
|
|
||||||
actions.Add(new ActionOption { Action = "bu_kong", Priority = 1 });
|
|
||||||
|
|
||||||
var fullSelfHand = FullHand(state, playerId);
|
|
||||||
int wildInSelf = fullSelfHand.Count(Core.MahjongTile.IsWildcard);
|
|
||||||
var tumoResult = _solver.CheckWin(fullSelfHand,
|
|
||||||
wildcardCount: wildInSelf, require258Pair: _require258);
|
|
||||||
if (tumoResult != null && tumoResult.IsWin)
|
|
||||||
{
|
{
|
||||||
if (!requireWinFan || tumoResult.Fans.Sum(f => GetFanValue(f)) >= 8)
|
if (CheckCondition(selfActs.First(a => a.Action == "an_kong"), false, false, false, 0))
|
||||||
actions.Add(new ActionOption { Action = "win", Priority = 4 });
|
actions.Add(new ActionOption { Action = "an_kong", Priority = 1 });
|
||||||
|
}
|
||||||
|
if (allowBuKong && CanBuKong(state, playerId))
|
||||||
|
{
|
||||||
|
if (CheckCondition(selfActs.First(a => a.Action == "bu_kong"), false, false, false, 0))
|
||||||
|
actions.Add(new ActionOption { Action = "bu_kong", Priority = 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowSelfDrawWin)
|
||||||
|
{
|
||||||
|
var fullSelfHand = FullHand(state, playerId);
|
||||||
|
int wildInSelf = _wildcards.CountWildcards(fullSelfHand);
|
||||||
|
var tumoResult = _solver.CheckWin(fullSelfHand,
|
||||||
|
wildcardCount: wildInSelf, require258Pair: _require258);
|
||||||
|
if (tumoResult != null && tumoResult.IsWin)
|
||||||
|
{
|
||||||
|
if (!requireWinFan || tumoResult.Fans.Sum(f => GetFanValue(f)) >= 8)
|
||||||
|
actions.Add(new ActionOption { Action = "win", Priority = 4 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actions.Add(new ActionOption { Action = "pass", Priority = 0 });
|
actions.Add(new ActionOption { Action = "pass", Priority = 0 });
|
||||||
|
|||||||
@ -37,9 +37,18 @@ public class MahjongScoreEngine
|
|||||||
|
|
||||||
int baseFan = ComputeTotalFans(result.Fans);
|
int baseFan = ComputeTotalFans(result.Fans);
|
||||||
|
|
||||||
// Wildcard bonus: e.g. Wuhan 每癞子+1番
|
// Wildcard bonus: count ALL wildcards in winner's hand (not just gap-fill)
|
||||||
if (_config.PerWildcardBonus > 0 && result.WildcardsUsed > 0)
|
if (_config.PerWildcardBonus > 0)
|
||||||
baseFan += result.WildcardsUsed * _config.PerWildcardBonus;
|
{
|
||||||
|
int totalWildInHand = 0;
|
||||||
|
if (state.Hands.TryGetValue(winner, out var hand))
|
||||||
|
totalWildInHand += state.Wildcards.CountWildcards(hand);
|
||||||
|
if (state.Exposed.TryGetValue(winner, out var exposed))
|
||||||
|
foreach (var m in exposed)
|
||||||
|
totalWildInHand += state.Wildcards.CountWildcards(m.Tiles);
|
||||||
|
if (totalWildInHand > 0)
|
||||||
|
baseFan += totalWildInHand * _config.PerWildcardBonus;
|
||||||
|
}
|
||||||
|
|
||||||
// Flower bonus: e.g. Guangdong 每花牌+1分 + 配对花牌额外分
|
// Flower bonus: e.g. Guangdong 每花牌+1分 + 配对花牌额外分
|
||||||
if (_config.FlowerNormalScore > 0 && state.FlowerPool.TryGetValue(winner, out var flowers) && flowers.Count > 0)
|
if (_config.FlowerNormalScore > 0 && state.FlowerPool.TryGetValue(winner, out var flowers) && flowers.Count > 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user