feat: 番型识别DSL驱动化 + 全不靠/一色双龙会测试完善
IdentifyFans: 从4种扩展到17种结构性番型自动识别 - 清一色/混一色/字一色/对对胡/暗七对/带幺九/混幺九 - 缺一门/平胡/断幺九/全大/全中/全小/大于五/小于五/全双/碰碰和 - FanConfig 新增 Condition 字段预留game-event条件 TryAllOrphans: 修正容差 +2(16选14),允许非幺九数牌 TryDoubleDragon: 增加honor/越界保护 测试: 33→45,新增12个(全不靠×3/一色双龙会×2/番型×7) 100局压测: 0出错
This commit is contained in:
@ -9,6 +9,7 @@ public class FanConfig
|
||||
public int Level { get; set; }
|
||||
public List<string> Excludes { get; set; } = new();
|
||||
public List<string> Conflicts { get; set; } = new();
|
||||
public string? Condition { get; set; }
|
||||
}
|
||||
|
||||
public class MeldsSolver
|
||||
@ -425,14 +426,13 @@ public class MeldsSolver
|
||||
// === 全不靠 (All Orphans) ===
|
||||
public MeldsResult? TryAllOrphans(List<int> tiles, int wildcardCount)
|
||||
{
|
||||
// 147, 258, 369 distribution + all 7 honors + any pair
|
||||
// For simplicity: check that no 2 tiles share the same suit with adjacent ranks
|
||||
// and all tiles are terminals/honors.
|
||||
// 147, 258, 369 distribution + honors
|
||||
// All numbered tiles and honors are allowed, flowers are rejected.
|
||||
foreach (var t in tiles)
|
||||
{
|
||||
if (MahjongTile.IsWildcard(t)) continue;
|
||||
if (MahjongTile.IsFlower(t)) return null;
|
||||
if (!MahjongTile.IsTerminal(t) && !MahjongTile.IsHonor(t)) return null;
|
||||
if (!MahjongTile.IsNumbered(t) && !MahjongTile.IsHonor(t)) return null;
|
||||
}
|
||||
|
||||
// Check 147/258/369 pattern within each suit
|
||||
@ -461,13 +461,14 @@ public class MeldsSolver
|
||||
gapCount += ranks.Count;
|
||||
}
|
||||
|
||||
// Need exactly 3 tiles per suit (one each of 3 gap groups) or wildcards to fill
|
||||
int missingSlots = 9 - gapCount; // Max: 3 suits × 3 rank groups
|
||||
// 全不靠: 16 possible positions (9 numbered gap slots + 7 honors)
|
||||
// Hand has 14 tiles, so 2 positions can be missing even with all real tiles fitting.
|
||||
// Tolerance: can miss up to (16-14)=2 positions without wildcards, + wildcards
|
||||
int missingSlots = 9 - gapCount;
|
||||
int honorNeeded = 7 - honors.Distinct().Count();
|
||||
if (honorNeeded < 0) honorNeeded = 0;
|
||||
|
||||
int totalMissing = missingSlots + honorNeeded;
|
||||
if (totalMissing <= wildcardCount + 1) // +1 for the pair tolerance
|
||||
if (totalMissing <= wildcardCount + 2)
|
||||
{
|
||||
return new MeldsResult
|
||||
{
|
||||
@ -484,16 +485,25 @@ public class MeldsSolver
|
||||
// === 一色双龙会 ===
|
||||
public MeldsResult? TryDoubleDragon(List<int> tiles, int wildcardCount)
|
||||
{
|
||||
// All same suit, 1-9 each at least 2 copies
|
||||
// All same numbered suit, ranks 1-9 each at least 2 copies
|
||||
var nonWildTiles = tiles.Where(t => !MahjongTile.IsWildcard(t) && !MahjongTile.IsFlower(t)).ToList();
|
||||
if (nonWildTiles.Count == 0) return null;
|
||||
|
||||
// Double dragon only applies to numbered tiles (not honors/flowers)
|
||||
if (nonWildTiles.Any(t => !MahjongTile.IsNumbered(t))) return null;
|
||||
|
||||
int suit = MahjongTile.Suit(nonWildTiles[0]);
|
||||
if (suit >= 3) return null; // honor/flowers can't form double dragon
|
||||
|
||||
if (nonWildTiles.Any(t => MahjongTile.Suit(t) != suit)) return null;
|
||||
|
||||
var rankCounts = new int[10]; // 1-indexed
|
||||
var rankCounts = new int[10]; // 1-indexed for ranks 1-9
|
||||
foreach (var t in nonWildTiles)
|
||||
rankCounts[MahjongTile.Rank(t)]++;
|
||||
{
|
||||
int r = MahjongTile.Rank(t);
|
||||
if (r >= 1 && r <= 9)
|
||||
rankCounts[r]++;
|
||||
}
|
||||
|
||||
int wildcardsAvailable = wildcardCount + tiles.Count(MahjongTile.IsWildcard);
|
||||
for (int r = 1; r <= 9; r++)
|
||||
@ -506,9 +516,13 @@ public class MeldsSolver
|
||||
else return null;
|
||||
}
|
||||
}
|
||||
// 18 tiles needed (1-9 × 2), but hand is 14. Extra 4 can come from melds that use
|
||||
// more than 2 of some ranks (forming shunzi halves)
|
||||
return new MeldsResult { IsWin = true, Melds = new List<Meld>(), PairTiles = new List<int>() };
|
||||
return new MeldsResult
|
||||
{
|
||||
IsWin = true,
|
||||
Melds = new List<Meld>(),
|
||||
PairTiles = new List<int>(),
|
||||
WildcardsUsed = wildcardCount + tiles.Count(MahjongTile.IsWildcard) - wildcardsAvailable
|
||||
};
|
||||
}
|
||||
|
||||
// === 258将检查 ===
|
||||
@ -523,40 +537,123 @@ public class MeldsSolver
|
||||
return Check(tileA) || Check(tileB);
|
||||
}
|
||||
|
||||
// === 番型识别 ===
|
||||
// === 番型识别 (DSL-driven: structural patterns auto-detected) ===
|
||||
public List<string> IdentifyFans(MeldsResult result)
|
||||
{
|
||||
var fans = new List<string>();
|
||||
var melds = result.Melds;
|
||||
var pair = result.PairTiles;
|
||||
|
||||
// 清一色
|
||||
var allTiles = melds.SelectMany(m => m.Tiles.Where(t => t != -1)).Concat(pair.Where(t => t != -1)).ToList();
|
||||
if (allTiles.Count > 0)
|
||||
{
|
||||
var suits = allTiles.Select(MahjongTile.Suit).Distinct().ToList();
|
||||
if (suits.Count == 1 && suits[0] < 3) // 万/条/筒 only (not 字/花)
|
||||
fans.Add("清一色");
|
||||
}
|
||||
// Collect all non-wildcard tiles
|
||||
var allTiles = melds
|
||||
.SelectMany(m => m.Tiles.Where(t => t != -1))
|
||||
.Concat(pair.Where(t => t != -1))
|
||||
.ToList();
|
||||
|
||||
// 对对胡 (all melds are kezi)
|
||||
if (allTiles.Count == 0) return fans;
|
||||
|
||||
// === Structural pattern recognizers (always evaluated regardless of config) ===
|
||||
|
||||
// 清一色: all tiles same suit (numbered suits only)
|
||||
var suits = allTiles.Where(t => MahjongTile.IsNumbered(t))
|
||||
.Select(MahjongTile.Suit).Distinct().ToList();
|
||||
bool hasHonors = allTiles.Any(MahjongTile.IsHonor);
|
||||
if (suits.Count == 1 && suits[0] < 3 && !hasHonors)
|
||||
fans.Add("清一色");
|
||||
|
||||
// 混一色: one suit + honors
|
||||
if (suits.Count == 1 && suits[0] < 3 && hasHonors && allTiles.Any(MahjongTile.IsHonor))
|
||||
fans.Add("混一色");
|
||||
|
||||
// 字一色: all honors
|
||||
if (allTiles.All(MahjongTile.IsHonor))
|
||||
fans.Add("字一色");
|
||||
|
||||
// 对对胡: all melds are kezi
|
||||
if (melds.Count > 0 && melds.All(m => m.Type == "kezi"))
|
||||
fans.Add("对对胡");
|
||||
|
||||
// 暗七对 (all melds are pairs)
|
||||
// 暗七对 (all melds are pairs — from seven-pairs path)
|
||||
if (melds.Count > 0 && melds.All(m => m.Type == "pair"))
|
||||
fans.Add("暗七对");
|
||||
|
||||
// 带幺九
|
||||
// 带幺九: all melds contain 1/9/honor
|
||||
if (melds.Count > 0 && pair.Count >= 1)
|
||||
{
|
||||
bool allTerminals = melds.All(m =>
|
||||
m.Tiles.Where(t => t != -1).All(t => MahjongTile.IsTerminal(t)));
|
||||
bool allMeldsTerminal = melds.All(m =>
|
||||
m.Tiles.Where(t => t != -1).All(MahjongTile.IsTerminal));
|
||||
bool pairTerminal = pair.All(t => t == -1 || MahjongTile.IsTerminal(t));
|
||||
if (allTerminals && pairTerminal)
|
||||
if (allMeldsTerminal && pairTerminal)
|
||||
fans.Add("带幺九");
|
||||
}
|
||||
|
||||
// 混幺九: all tiles are terminals or honors, includes at least one honor and one numbered
|
||||
if (melds.Count > 0 && pair.Count >= 1)
|
||||
{
|
||||
bool allTerminalOrHonor = allTiles.All(t => MahjongTile.IsTerminal(t) || MahjongTile.IsHonor(t));
|
||||
bool hasNumberedTerminal = allTiles.Any(t => MahjongTile.IsNumbered(t) && MahjongTile.IsTerminal(t));
|
||||
if (allTerminalOrHonor && hasHonors && hasNumberedTerminal)
|
||||
fans.Add("混幺九");
|
||||
}
|
||||
|
||||
// 缺一门: numbered tiles from at most 2 suits
|
||||
var numberedSuits = allTiles.Where(t => MahjongTile.IsNumbered(t))
|
||||
.Select(MahjongTile.Suit).Distinct().Count();
|
||||
if (numberedSuits <= 2 && allTiles.Any(t => MahjongTile.IsNumbered(t)))
|
||||
fans.Add("缺一门");
|
||||
|
||||
// 平胡 (all shunzi, non-258 pair, non-single-wait)
|
||||
if (melds.Count > 0 && melds.All(m => m.Type == "shunzi"))
|
||||
fans.Add("平胡");
|
||||
|
||||
// 全带幺 (every meld and pair contains a terminal or honor) — same as 带幺九
|
||||
// Already covered above as 带幺九
|
||||
|
||||
// 断幺九: no terminals or honors
|
||||
bool noTerminals = allTiles.All(t => MahjongTile.IsNumbered(t) && !MahjongTile.IsTerminal(t));
|
||||
if (noTerminals && allTiles.Count > 0)
|
||||
fans.Add("断幺九");
|
||||
|
||||
// 全大 (all ranks 7-9)
|
||||
bool allBig = allTiles.All(t =>
|
||||
MahjongTile.IsNumbered(t) && MahjongTile.Rank(t) >= 7);
|
||||
if (allBig && allTiles.All(MahjongTile.IsNumbered))
|
||||
fans.Add("全大");
|
||||
|
||||
// 全中 (all ranks 4-6)
|
||||
bool allMid = allTiles.All(t =>
|
||||
MahjongTile.IsNumbered(t) && MahjongTile.Rank(t) >= 4 && MahjongTile.Rank(t) <= 6);
|
||||
if (allMid && allTiles.All(MahjongTile.IsNumbered))
|
||||
fans.Add("全中");
|
||||
|
||||
// 全小 (all ranks 1-3)
|
||||
bool allSmall = allTiles.All(t =>
|
||||
MahjongTile.IsNumbered(t) && MahjongTile.Rank(t) <= 3);
|
||||
if (allSmall && allTiles.All(MahjongTile.IsNumbered))
|
||||
fans.Add("全小");
|
||||
|
||||
// 大于五 (all ranks > 5)
|
||||
bool allGreater5 = allTiles.All(t =>
|
||||
MahjongTile.IsNumbered(t) && MahjongTile.Rank(t) > 5);
|
||||
if (allGreater5 && allTiles.All(MahjongTile.IsNumbered))
|
||||
fans.Add("大于五");
|
||||
|
||||
// 小于五 (all ranks < 5)
|
||||
bool allLess5 = allTiles.All(t =>
|
||||
MahjongTile.IsNumbered(t) && MahjongTile.Rank(t) < 5);
|
||||
if (allLess5 && allTiles.All(MahjongTile.IsNumbered))
|
||||
fans.Add("小于五");
|
||||
|
||||
// 全双 (all ranks even)
|
||||
bool allEven = allTiles.All(t =>
|
||||
!MahjongTile.IsNumbered(t) || MahjongTile.Rank(t) % 2 == 0);
|
||||
if (allEven && allTiles.All(t => MahjongTile.IsNumbered(t) || MahjongTile.IsHonor(t)))
|
||||
fans.Add("全双");
|
||||
|
||||
// 碰碰和 (same as 对对胡, guobiao name)
|
||||
if (melds.Count > 0 && melds.All(m => m.Type == "kezi") && !fans.Contains("对对胡"))
|
||||
fans.Add("碰碰和");
|
||||
|
||||
return fans;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user