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,19 +69,61 @@ public class MahjongPhaseMachine
|
||||
{
|
||||
var actions = new List<ActionOption>();
|
||||
|
||||
// Always can discard
|
||||
// Read action lists from DSL play phase (instead of hardcoding)
|
||||
var playPhase = _phases.FirstOrDefault(p => p.Name == "play");
|
||||
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;
|
||||
|
||||
// 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
|
||||
// Check pung/kong/win for last discard (only if DSL allows them)
|
||||
if (state.LastDiscard.HasValue && state.LastDiscardPlayer != playerId)
|
||||
{
|
||||
if (CanPung(state, playerId, state.LastDiscard.Value))
|
||||
if (allowPung && CanPung(state, playerId, state.LastDiscard.Value))
|
||||
{
|
||||
if (CheckCondition(reactActs.First(a => a.Action == "pung"), true, false, false, 0))
|
||||
actions.Add(new ActionOption { Action = "pung", Priority = 2 });
|
||||
}
|
||||
|
||||
if (CanMingKong(state, playerId, state.LastDiscard.Value))
|
||||
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,
|
||||
@ -98,15 +140,24 @@ public class MahjongPhaseMachine
|
||||
actions.Add(new ActionOption { Action = "win", Priority = 4 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Self actions: an_kong, bu_kong, win (tumo)
|
||||
if (CanAnKong(state, playerId))
|
||||
// Self actions: only if DSL allows them
|
||||
if (allowAnKong && CanAnKong(state, playerId))
|
||||
{
|
||||
if (CheckCondition(selfActs.First(a => a.Action == "an_kong"), false, false, false, 0))
|
||||
actions.Add(new ActionOption { Action = "an_kong", Priority = 1 });
|
||||
if (CanBuKong(state, playerId))
|
||||
}
|
||||
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 = fullSelfHand.Count(Core.MahjongTile.IsWildcard);
|
||||
int wildInSelf = _wildcards.CountWildcards(fullSelfHand);
|
||||
var tumoResult = _solver.CheckWin(fullSelfHand,
|
||||
wildcardCount: wildInSelf, require258Pair: _require258);
|
||||
if (tumoResult != null && tumoResult.IsWin)
|
||||
@ -114,6 +165,7 @@ public class MahjongPhaseMachine
|
||||
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 });
|
||||
return actions;
|
||||
|
||||
@ -37,9 +37,18 @@ public class MahjongScoreEngine
|
||||
|
||||
int baseFan = ComputeTotalFans(result.Fans);
|
||||
|
||||
// Wildcard bonus: e.g. Wuhan 每癞子+1番
|
||||
if (_config.PerWildcardBonus > 0 && result.WildcardsUsed > 0)
|
||||
baseFan += result.WildcardsUsed * _config.PerWildcardBonus;
|
||||
// Wildcard bonus: count ALL wildcards in winner's hand (not just gap-fill)
|
||||
if (_config.PerWildcardBonus > 0)
|
||||
{
|
||||
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分 + 配对花牌额外分
|
||||
if (_config.FlowerNormalScore > 0 && state.FlowerPool.TryGetValue(winner, out var flowers) && flowers.Count > 0)
|
||||
|
||||
Reference in New Issue
Block a user