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:
@ -41,6 +41,11 @@ public class MahjongGameState
|
||||
public bool IsRobbedKong { get; set; } // 抢杠 — set when someone adds to pung (bu_kong)
|
||||
public bool IsFirstTurn { get; set; } // 第一轮 — set for dealer after deal, non-dealer after first draw
|
||||
public string? DealerOnFirstTurn { get; set; } // 庄家首轮(用于天胡判断)
|
||||
public bool IsSingleWait { get; set; } // 单钓将: 自摸时摸到的牌完成了将牌的对子
|
||||
|
||||
// 圈风/门风: 用于圈风刻/门风刻番型
|
||||
public int WindRound { get; set; } = 31; // 当前风圈: 31=东/32=南/33=西/34=北
|
||||
public Dictionary<string, int> SeatWinds { get; set; } = new(); // 玩家→座位风位
|
||||
|
||||
public MahjongGameState Clone()
|
||||
{
|
||||
@ -71,7 +76,10 @@ public class MahjongGameState
|
||||
IsLastTile = IsLastTile,
|
||||
IsRobbedKong = IsRobbedKong,
|
||||
IsFirstTurn = IsFirstTurn,
|
||||
DealerOnFirstTurn = DealerOnFirstTurn
|
||||
DealerOnFirstTurn = DealerOnFirstTurn,
|
||||
IsSingleWait = IsSingleWait,
|
||||
WindRound = WindRound,
|
||||
SeatWinds = new Dictionary<string, int>(SeatWinds)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -68,7 +68,7 @@ public class CapabilityRegistry
|
||||
"将一色","大四喜","大三元","小四喜","小三元","三风","双箭刻",
|
||||
"一色四同顺","一色三同顺","一般高","五门齐","全求人","门前清",
|
||||
"鸡胡","癞子胡",
|
||||
"杠上开花","海底捞月","抢杠胡","天胡","地胡",
|
||||
"圈风","门风","单钓将","四归一","杠上开花","海底捞月","抢杠胡","天胡","地胡",
|
||||
"将","无字" // base_fan:0/1 markers
|
||||
};
|
||||
foreach (var f in rules.FanTypes)
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -774,8 +774,25 @@ public class MeldsSolver
|
||||
fans.Add("天胡");
|
||||
else if (state.IsFirstTurn)
|
||||
fans.Add("地胡");
|
||||
|
||||
// 圈风刻: kezi matching current wind round (e.g. 东风圈→有东刻子)
|
||||
if (keziTiles.Any(t => t == state.WindRound))
|
||||
fans.Add("圈风");
|
||||
// 门风刻: kezi matching player's seat wind (applies per-hand, identified by state owner)
|
||||
foreach (var (player, seatWind) in state.SeatWinds)
|
||||
if (keziTiles.Any(t => t == seatWind))
|
||||
fans.Add("门风");
|
||||
|
||||
// 单钓将: detected at self-draw time (single tile→pair completion)
|
||||
if (state.IsSingleWait) fans.Add("单钓将");
|
||||
}
|
||||
|
||||
// 四归一: 4 identical numbered tiles used across 2+ melds (not in a single kong)
|
||||
var tileCounts = allTiles.Where(t => MahjongTile.IsNumbered(t)).GroupBy(t => t);
|
||||
bool hasSiGuiYi = tileCounts.Any(g =>
|
||||
g.Count() >= 4 && !melds.Any(m => m.Type == "kezi" && m.Tiles.Contains(g.Key) && m.Tiles.All(x => x == g.Key)));
|
||||
if (hasSiGuiYi) fans.Add("四归一");
|
||||
|
||||
// 鸡胡: base-level fan when no other fan with positive value exists.
|
||||
// A fan has positive value if it's in _fanConfig with base_fan>0,
|
||||
// or in the hardcoded fallback list (清一色/混一色/对对胡/碰碰和/暗七对/七对/带幺九/十三幺/鸡胡).
|
||||
@ -797,7 +814,7 @@ public class MeldsSolver
|
||||
or "将一色" or "大四喜" or "大三元" or "小四喜" or "小三元" or "三风" or "双箭刻"
|
||||
or "一色四同顺" or "一色三同顺" or "一般高" or "五门齐" or "全求人" or "门前清"
|
||||
or "鸡胡" or "癞子胡" or "平和"
|
||||
or "杠上开花" or "海底捞月" or "抢杠胡" or "天胡" or "地胡").ToList();
|
||||
or "圈风" or "门风" or "单钓将" or "四归一" or "杠上开花" or "海底捞月" or "抢杠胡" or "天胡" or "地胡").ToList();
|
||||
}
|
||||
|
||||
// === 番型互斥应用 ===
|
||||
|
||||
Reference in New Issue
Block a user