Files
card-game-engine/RuleEngine/Phase/PhaseMachine.cs
xiaoou b6bdf064ff feat: 吃(chi)反应支持 — HandleDiscardReactions + 人类/AI
- CanChi→CanChiCheck公开给MahjongRoom调用
- HandleDiscardReactions: offset=1(上家)且非字牌时检查chi
- 人类HUD: c(吃)选项
- ExecuteChi: 找到配对的两张牌→创建chi副露→出牌
- ExecutePungKongHuman: 抽取碰/杠人类交互逻辑
2026-07-04 17:51:58 +08:00

191 lines
7.2 KiB
C#

namespace RuleEngine.Phase;
using RuleEngine.Core;
using RuleEngine.Patterns;
public class PhaseConfig
{
public string Name { get; set; } = "";
public string Type { get; set; } = ""; // "auto", "mahjong_turn"
public string Action { get; set; } = "";
public string? Next { get; set; }
public string TurnOrder { get; set; } = "counter_clockwise";
public string? FirstPlayer { get; set; }
public List<ActionOption> SelfActions { get; set; } = new();
public List<ActionOption> OthersReactions { get; set; } = new();
public string PriorityPolicy { get; set; } = "highest_wins";
public string? OnWin { get; set; }
public List<EndCondition> EndConditions { get; set; } = new();
public bool ParallelElimination { get; set; }
public string? OnEliminate { get; set; }
public int MaxFan { get; set; } = int.MaxValue;
}
public class ActionOption
{
public string Action { get; set; } = "";
public int Priority { get; set; }
public string? Condition { get; set; }
}
public class EndCondition
{
public string Type { get; set; } = "";
public string Action { get; set; } = "";
}
public class MahjongPhaseMachine
{
private readonly List<PhaseConfig> _phases;
private readonly MeldsSolver _solver;
private readonly int _wildcardCount;
private readonly bool _require258;
public MahjongPhaseMachine(List<PhaseConfig> phases, MeldsSolver solver,
int wildcardCount = 0, bool require258 = false)
{
_phases = phases;
_solver = solver;
_wildcardCount = wildcardCount;
_require258 = require258;
}
public PhaseConfig? GetPhase(string name) => _phases.FirstOrDefault(p => p.Name == name);
public List<ActionOption> GetLegalActions(MahjongGameState state, string playerId, bool requireWinFan = false)
{
var actions = new List<ActionOption>();
// Always can discard
foreach (var t in state.Hands[playerId].Distinct())
actions.Add(new ActionOption { Action = "discard", Priority = 0 });
// Check pung/kong/win for last discard
if (state.LastDiscard.HasValue && state.LastDiscardPlayer != playerId)
{
if (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 = fullHand.Count(Core.MahjongTile.IsWildcard);
var result = _solver.CheckWin(fullHand,
wildcardCount: wildInHand, require258Pair: _require258);
if (result != null && result.IsWin)
{
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)
if (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)
actions.Add(new ActionOption { Action = "win", Priority = 4 });
}
// Chi
if (state.LastDiscard.HasValue && state.LastDiscardPlayer != playerId)
{
if (CanChiCheck(state, playerId, state.LastDiscard.Value))
actions.Add(new ActionOption { Action = "chi", Priority = 1 });
}
actions.Add(new ActionOption { Action = "pass", Priority = 0 });
return actions;
}
private int GetFanValue(string fanName)
{
// Simple lookup — full implementation would read from DSL
return fanName switch
{
"清一色" => 4,
"对对胡" => 2,
"暗七对" => 4,
"带幺九" => 2,
_ => 1
};
}
private bool CanPung(MahjongGameState state, string playerId, int tile)
{
int count = state.Hands[playerId].Count(t => t == tile);
return count >= 2;
}
private bool CanMingKong(MahjongGameState state, string playerId, int tile)
{
int count = state.Hands[playerId].Count(t => t == tile);
return count >= 3;
}
private bool CanAnKong(MahjongGameState state, string playerId)
{
// Check if player has 4 identical tiles in hand (暗杠)
var hand = state.Hands[playerId];
return hand.GroupBy(t => t).Any(g => g.Count() >= 4 && !MahjongTile.IsWildcard(g.Key));
}
private bool CanBuKong(MahjongGameState state, string playerId)
{
// Check if player has an exposed pung and drew the 4th tile
if (!state.Exposed.ContainsKey(playerId)) return false;
return state.Exposed[playerId].Any(m => m.Type == "pung" &&
state.Hands[playerId].Contains(m.Tiles[0]));
}
public bool CanChiCheck(MahjongGameState state, string playerId, int tile)
{
if (MahjongTile.IsHonor(tile)) return false;
int suit = MahjongTile.Suit(tile);
int rank = MahjongTile.Rank(tile);
var hand = state.Hands[playerId];
// Check two sequential combinations
bool hasLower = rank >= 3
&& hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 2) > 0
&& hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1) > 0;
bool hasMiddle = rank >= 2 && rank <= 8
&& hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank - 1) > 0
&& hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1) > 0;
bool hasUpper = rank <= 7
&& hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 1) > 0
&& hand.Count(t => MahjongTile.Suit(t) == suit && MahjongTile.Rank(t) == rank + 2) > 0;
return hasLower || hasMiddle || hasUpper;
}
public string GetNextPlayer(MahjongGameState state, string currentPlayer)
{
var alive = state.AlivePlayers;
int idx = alive.IndexOf(currentPlayer);
if (idx < 0) return alive[0];
int next = (idx + 1) % alive.Count;
return alive[next];
}
/// <summary>Combine hand + exposed melds into full virtual hand for CheckWin.</summary>
private static List<int> FullHand(MahjongGameState state, string player, int? extraTile = null)
{
var tiles = new List<int>(state.Hands[player]);
if (extraTile.HasValue) tiles.Add(extraTile.Value);
if (state.Exposed.TryGetValue(player, out var melds))
foreach (var m in melds)
tiles.AddRange(m.Tiles.Where(t => t != -1));
return tiles;
}
}