feat: 四归一/圈风/门风/单钓将识别器 + GameState状态跟踪

新增4个番型识别器:
- 四归一: 4张同牌跨面子使用(不在单个kezi/kong) — 纯结构检测
- 圈风: 当前风圈对应的风刻(31=东/32=南/33=西/34=北)
- 门风: 玩家座位风位对应的风刻(庄=东)
- 单钓将: 自摸时摸到的牌完成了将牌的单吊对子

GameState扩展:
- WindRound(当前风圈) + SeatWinds(玩家→风向) + IsSingleWait flag
- Deal()分配座位风位(庄家=东,按逆时针)
- StepTurn自摸时检测单钓将(摸牌←→对子匹配)

效果: 国标--check 6→0 (圈风/门风/单钓将/四归一全部消除)
所有玩法合规检查通过(南昌/血战仅base_fan=0标记警告)
This commit is contained in:
xiaoou
2026-07-04 23:31:31 +08:00
parent 816000a286
commit 3f91d7c3f8
4 changed files with 53 additions and 3 deletions

View File

@ -239,7 +239,23 @@ public class MahjongRoom
var result = _solver.CheckWin(fullHand,
wildcardCount: wildInHand, require258Pair: r258);
if (result != null)
{
// 单钓将: drawn tile completed a pair from a singleton
if (State.LastDrawnTile.HasValue && result.PairTiles.Count == 2)
{
int drawn = State.LastDrawnTile.Value;
if (result.PairTiles.Contains(drawn))
{
int other = result.PairTiles[0] == drawn ? result.PairTiles[1] : result.PairTiles[0];
// Before drawing the winning tile, hand had 1 of the other pair tile
var handBefore = new List<int>(fullHand);
handBefore.Remove(drawn);
if (handBefore.Count(t => t == other) == 1)
State.IsSingleWait = true;
}
}
result.Fans = _solver.IdentifyFans(result, State); // event-based fans
}
State.HuPlayers.Add(player);
State.AlivePlayers.Remove(player);
State.AddEvent("win", player, null,
@ -758,6 +774,15 @@ public class MahjongRoom
State.AddEvent("deal_done", "", null, "发牌完成");
// 圈风/门风: assign seat winds based on player order relative to dealer
int[] winds = { 31, 32, 33, 34 }; // 东南西北
int dealerIdx = State.PlayerOrder.IndexOf(State.Dealer);
for (int i = 0; i < State.PlayerOrder.Count; i++)
{
int windIdx = (i - dealerIdx + 4) % 4;
State.SeatWinds[State.PlayerOrder[i]] = winds[windIdx];
}
// Random wildcard (骰子翻牌定精): flip one tile from remaining deck to determine 精
if (_rules.WildcardRules?.Type == "random" && State.Deck.Count > 0)
{