问题: 1. 交互模式 Run() 直接跑完整局,只渲染最终状态 2. RenderGame 手牌.OrderBy()排序,洗牌后仍显顺序 3. 牌墙剩余显示0因为游戏已跑完 修复: - 交互模式: room.Deal()→RenderDeal→逐回合StepTurn+ReadLine - 发牌输出: 显示每人张数 + 牌墙剩余,不排序手牌 - 每回合: 显示事件(摸/打/碰/杠/胡) + 当前手牌 - Run()保留用于自动模式
397 lines
13 KiB
C#
397 lines
13 KiB
C#
namespace RuleEngine;
|
|
|
|
using RuleEngine.AI;
|
|
using RuleEngine.Core;
|
|
using RuleEngine.Dsl;
|
|
using RuleEngine.Patterns;
|
|
using RuleEngine.Phase;
|
|
using RuleEngine.Scoring;
|
|
|
|
public class MahjongRoom
|
|
{
|
|
public MahjongGameState State { get; private set; } = new();
|
|
public bool IsFinished { get; private set; }
|
|
private readonly MahjongDslRoot _rules;
|
|
private readonly List<RandomMahjongAI> _ais;
|
|
private readonly MeldsSolver _solver;
|
|
private readonly MahjongPhaseMachine _phaseMachine;
|
|
private readonly MahjongScoreEngine _scoreEngine;
|
|
private readonly bool _autoMode;
|
|
private readonly Random _rng = Random.Shared;
|
|
|
|
public MahjongRoom(MahjongDslRoot rules, string[] playerNames, bool autoMode = false)
|
|
{
|
|
_rules = rules;
|
|
_autoMode = autoMode;
|
|
_ais = playerNames.Select((n, i) => new RandomMahjongAI(n, new Random(i * 7919))).ToList();
|
|
|
|
var fanConfig = BuildFanConfig();
|
|
_solver = new MeldsSolver(fanConfig);
|
|
|
|
var phases = BuildPhases();
|
|
_phaseMachine = new MahjongPhaseMachine(phases, _solver);
|
|
|
|
_scoreEngine = new MahjongScoreEngine(new ScoringConfig
|
|
{
|
|
Mode = _rules.Scoring.Mode,
|
|
MaxCap = _rules.Scoring.MaxCap
|
|
}, _solver);
|
|
|
|
// Init state
|
|
State.PlayerOrder = playerNames.ToList();
|
|
State.AlivePlayers = playerNames.ToList();
|
|
State.Dealer = playerNames[0];
|
|
State.CurrentPlayer = playerNames[0];
|
|
State.Phase = "deal";
|
|
|
|
foreach (var p in playerNames)
|
|
{
|
|
State.Hands[p] = new List<int>();
|
|
State.Exposed[p] = new List<Meld>();
|
|
State.Scores[p] = 0;
|
|
}
|
|
}
|
|
|
|
private Dictionary<string, FanConfig> BuildFanConfig()
|
|
{
|
|
var config = new Dictionary<string, FanConfig>();
|
|
foreach (var f in _rules.FanTypes)
|
|
{
|
|
config[f.Name] = new FanConfig
|
|
{
|
|
Name = f.Name,
|
|
BaseFan = f.BaseFan,
|
|
Level = f.Level,
|
|
Excludes = f.Excludes ?? new(),
|
|
Conflicts = f.Conflicts ?? new(),
|
|
Condition = f.Condition
|
|
};
|
|
}
|
|
return config;
|
|
}
|
|
|
|
private List<PhaseConfig> BuildPhases()
|
|
{
|
|
return _rules.Phases.Select(p => new PhaseConfig
|
|
{
|
|
Name = p.Name,
|
|
Type = p.Type,
|
|
Action = p.Action ?? "",
|
|
Next = p.Next,
|
|
TurnOrder = "counter_clockwise",
|
|
ParallelElimination = p.ParallelElimination,
|
|
SelfActions = p.SubPhases?.SelfAction?.Options?.Select(o => new ActionOption
|
|
{
|
|
Action = o.Action, Priority = o.Priority, Condition = o.Condition
|
|
}).ToList() ?? new(),
|
|
OthersReactions = p.SubPhases?.OthersReaction?.Options?.Select(o => new ActionOption
|
|
{
|
|
Action = o.Action, Priority = o.Priority, Condition = o.Condition
|
|
}).ToList() ?? new(),
|
|
PriorityPolicy = p.SubPhases?.OthersReaction?.PriorityPolicy ?? "highest_wins",
|
|
OnWin = p.SubPhases?.OthersReaction?.OnWin
|
|
}).ToList();
|
|
}
|
|
|
|
public void Run()
|
|
{
|
|
Deal();
|
|
IsFinished = false;
|
|
|
|
while (!IsFinished)
|
|
{
|
|
if (State.Phase == "deal") { State.Phase = "play"; continue; }
|
|
if (State.Phase == "settle" || IsFinished) break;
|
|
|
|
StepTurn();
|
|
}
|
|
|
|
if (!IsFinished)
|
|
Settle();
|
|
}
|
|
|
|
public List<GameEvent> StepTurn()
|
|
{
|
|
State.RecentEvents.Clear();
|
|
|
|
// Draw card
|
|
if (State.Deck.Count > 0)
|
|
{
|
|
int drawn = State.Deck[^1];
|
|
State.Deck.RemoveAt(State.Deck.Count - 1);
|
|
State.Hands[State.CurrentPlayer].Add(drawn);
|
|
State.AddEvent("draw", State.CurrentPlayer, drawn,
|
|
$"摸牌: {MahjongTile.ToString(drawn)}");
|
|
|
|
// Check flower
|
|
if (MahjongTile.IsFlower(drawn))
|
|
{
|
|
State.FlowerReplaced++;
|
|
State.AddEvent("flower_drawn", State.CurrentPlayer, drawn,
|
|
$"花牌 {MahjongTile.ToString(drawn)} → 补牌");
|
|
// Flower into flower pool and draw again
|
|
if (!State.FlowerPool.ContainsKey(State.CurrentPlayer))
|
|
State.FlowerPool[State.CurrentPlayer] = new List<int>();
|
|
State.FlowerPool[State.CurrentPlayer].Add(drawn);
|
|
State.Hands[State.CurrentPlayer].Remove(drawn);
|
|
if (State.Deck.Count > 0)
|
|
{
|
|
int extra = State.Deck[^1];
|
|
State.Deck.RemoveAt(State.Deck.Count - 1);
|
|
State.Hands[State.CurrentPlayer].Add(extra);
|
|
State.AddEvent("draw", State.CurrentPlayer, extra,
|
|
$"补牌: {MahjongTile.ToString(extra)}");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
State.IsDeckExhausted = true;
|
|
State.AddEvent("deck_exhausted", "", null, "牌墙耗尽");
|
|
|
|
// Blood war: check ting and hua zhu
|
|
if (_rules.Phases.Any(p => p.ParallelElimination))
|
|
{
|
|
CheckHuaZhu();
|
|
CheckTing();
|
|
}
|
|
Settle();
|
|
return State.RecentEvents;
|
|
}
|
|
|
|
// Get legal actions for current player
|
|
var player = State.CurrentPlayer;
|
|
bool requireWinFan = _rules.WinMinFan > 0;
|
|
var legalActions = _phaseMachine.GetLegalActions(State, player, requireWinFan);
|
|
|
|
// AI decides
|
|
var (action, tile) = _ais.First(a => a.Name == player).Decide(legalActions, State);
|
|
|
|
if (action == "win")
|
|
{
|
|
var result = _solver.CheckWin(State.Hands[player]);
|
|
State.HuPlayers.Add(player);
|
|
State.AlivePlayers.Remove(player);
|
|
State.AddEvent("win", player, null,
|
|
$"自摸!番型: {string.Join(" + ", result?.Fans ?? new())}");
|
|
|
|
if (_rules.Phases.Any(p => p.ParallelElimination))
|
|
{
|
|
// Blood war: continue without the winner
|
|
if (State.AlivePlayers.Count <= 1)
|
|
{
|
|
Settle();
|
|
return State.RecentEvents;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_scoreEngine.Settle(State, player, result!, isSelfDraw: true);
|
|
Settle();
|
|
return State.RecentEvents;
|
|
}
|
|
}
|
|
else if (action == "discard" && tile.HasValue)
|
|
{
|
|
State.Hands[player].Remove(tile.Value);
|
|
State.LastDiscard = tile.Value;
|
|
State.LastDiscardPlayer = player;
|
|
State.DiscardPool.Add(tile.Value);
|
|
State.AddEvent("discard", player, tile.Value,
|
|
$"出牌: {MahjongTile.ToString(tile.Value)}");
|
|
|
|
// Check others' reactions (pung/kong/win)
|
|
bool reactionTaken = CheckReactions(tile.Value);
|
|
if (reactionTaken)
|
|
{
|
|
AdvancePlayer();
|
|
return State.RecentEvents;
|
|
}
|
|
}
|
|
else if (action == "pung")
|
|
{
|
|
// Handle pung
|
|
if (State.LastDiscard.HasValue)
|
|
{
|
|
int t = State.LastDiscard.Value;
|
|
State.Hands[player].RemoveAll(x => x == t);
|
|
State.Hands[player].RemoveAll(x => x == t);
|
|
State.LastDiscard = null;
|
|
State.Exposed[player].Add(new Meld
|
|
{
|
|
Type = "pung",
|
|
Tiles = new List<int> { t, t, t },
|
|
SourcePlayer = State.LastDiscardPlayer ?? ""
|
|
});
|
|
State.AddEvent("pung", player, t, $"碰!{MahjongTile.ToString(t)}");
|
|
}
|
|
}
|
|
|
|
AdvancePlayer();
|
|
return State.RecentEvents;
|
|
}
|
|
|
|
private bool CheckReactions(int discardTile)
|
|
{
|
|
// Collect all claims with their priorities
|
|
var claims = new List<(string player, int priority, string action)>();
|
|
|
|
foreach (var p in State.AlivePlayers)
|
|
{
|
|
if (p == State.CurrentPlayer) continue;
|
|
|
|
var handWithTile = new List<int>(State.Hands[p]) { discardTile };
|
|
var winResult = _solver.CheckWin(handWithTile);
|
|
|
|
if (winResult != null && winResult.IsWin)
|
|
claims.Add((p, 4, "win"));
|
|
|
|
int sameCount = State.Hands[p].Count(t => t == discardTile);
|
|
if (sameCount >= 3)
|
|
claims.Add((p, 3, "ming_kong"));
|
|
else if (sameCount >= 2)
|
|
claims.Add((p, 2, "pung"));
|
|
}
|
|
|
|
if (claims.Count == 0) return false;
|
|
|
|
// Highest-priority claim wins
|
|
var best = claims.OrderByDescending(c => c.priority).First();
|
|
|
|
if (best.action == "win")
|
|
{
|
|
var winResult = _solver.CheckWin(
|
|
new List<int>(State.Hands[best.player]) { discardTile });
|
|
|
|
State.HuPlayers.Add(best.player);
|
|
State.AlivePlayers.Remove(best.player);
|
|
State.AddEvent("win", best.player, discardTile,
|
|
$"胡!{MahjongTile.ToString(discardTile)}");
|
|
|
|
if (!_rules.Phases.Any(ph => ph.ParallelElimination))
|
|
{
|
|
_scoreEngine.Settle(State, best.player, winResult!, isSelfDraw: false);
|
|
IsFinished = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (best.action == "pung" || best.action == "ming_kong")
|
|
{
|
|
State.Hands[best.player].RemoveAll(t => t == discardTile);
|
|
State.Hands[best.player].RemoveAll(t => t == discardTile);
|
|
if (best.action == "ming_kong")
|
|
State.Hands[best.player].RemoveAll(t => t == discardTile);
|
|
|
|
State.Exposed[best.player].Add(new Meld
|
|
{
|
|
Type = best.action,
|
|
Tiles = new List<int> { discardTile, discardTile, discardTile,
|
|
discardTile }, // 4th for kong
|
|
SourcePlayer = State.CurrentPlayer
|
|
});
|
|
State.CurrentPlayer = best.player;
|
|
State.LastDiscard = null;
|
|
State.AddEvent(best.action, best.player, discardTile, best.action == "pung" ? "碰!" : "明杠!");
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void AdvancePlayer()
|
|
{
|
|
int idx = State.PlayerOrder.IndexOf(State.CurrentPlayer);
|
|
for (int i = 0; i < State.PlayerOrder.Count; i++)
|
|
{
|
|
int next = (idx + 1 + i) % State.PlayerOrder.Count;
|
|
if (State.AlivePlayers.Contains(State.PlayerOrder[next]))
|
|
{
|
|
State.CurrentPlayer = State.PlayerOrder[next];
|
|
State.RoundNumber++;
|
|
return;
|
|
}
|
|
}
|
|
// No alive players
|
|
IsFinished = true;
|
|
}
|
|
|
|
public void Deal()
|
|
{
|
|
int perPlayer = _rules.Deal.CardsPerPlayer;
|
|
bool includeFlowers = _rules.Deck.IncludeFlowers;
|
|
bool includeHonors = _rules.Deck.IncludeHonors;
|
|
int wildcardCount = _rules.WildcardRules != null ? _rules.Deck.WildcardCount : 0;
|
|
|
|
var deck = new MahjongDeck(includeHonors, includeFlowers, wildcardCount);
|
|
deck.Shuffle(_rng);
|
|
|
|
State.Deck = deck.Tiles;
|
|
State.AddEvent("deal_start", "", null,
|
|
$"发牌 — {deck.Count}张牌({(includeHonors ? "+字" : "")}{(includeFlowers ? "+花" : "")}{(wildcardCount > 0 ? "+癞子" : "")})");
|
|
|
|
foreach (var p in State.PlayerOrder)
|
|
{
|
|
int count = p == State.Dealer ? perPlayer + _rules.Deal.DealerExtra : perPlayer;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
int tile = State.Deck[^1];
|
|
State.Deck.RemoveAt(State.Deck.Count - 1);
|
|
State.Hands[p].Add(tile);
|
|
}
|
|
}
|
|
|
|
State.AddEvent("deal_done", "", null, "发牌完成");
|
|
}
|
|
|
|
private void CheckHuaZhu()
|
|
{
|
|
foreach (var p in State.AlivePlayers)
|
|
{
|
|
var suits = State.Hands[p]
|
|
.Where(t => MahjongTile.IsNumbered(t))
|
|
.Select(MahjongTile.Suit)
|
|
.Distinct()
|
|
.Count();
|
|
if (suits == 3)
|
|
State.AddEvent("hua_zhu", p, null, "花猪!三色齐全");
|
|
else
|
|
State.AddEvent("hua_zhu_ok", p, null, $"✓ {suits}种花色");
|
|
}
|
|
}
|
|
|
|
private void CheckTing()
|
|
{
|
|
bool hasWildcard = _rules.WildcardRules != null;
|
|
int wildcardCount = hasWildcard ? _rules.Deck.WildcardCount : 0;
|
|
bool require258 = _rules.WinCondition?.PairMustBe258 ?? false;
|
|
|
|
foreach (var p in State.AlivePlayers)
|
|
{
|
|
bool ting = false;
|
|
var hand = State.Hands[p];
|
|
for (int i = 0; i < hand.Count; i++)
|
|
{
|
|
var testHand = new List<int>(hand);
|
|
testHand.RemoveAt(i);
|
|
var r = _solver.CheckWin(testHand, wildcardCount: wildcardCount, require258Pair: require258);
|
|
if (r != null && r.IsWin) { ting = true; break; }
|
|
}
|
|
if (ting)
|
|
State.AddEvent("ting_checked", p, null, "✓ 听牌");
|
|
else
|
|
{
|
|
State.AddEvent("ting_failed", p, null, "❌ 未听牌");
|
|
State.AddEvent("pair_validated_258", p, null, "258将检查");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Settle()
|
|
{
|
|
IsFinished = true;
|
|
State.Phase = "settle";
|
|
State.AddEvent("settle", "", null, "结算");
|
|
}
|
|
}
|