核心变更: - filter从'well-known全放行'改为'只放行_fanConfig或有fallback值的' - 排除: 断幺九/无字/缺一门/将/平胡/平和(无hardcoded fallback) - 这些被排除时→allZeroValue→鸡胡兜底→保证非零分 效果: 南昌不再显示'断幺九+无字+平胡'等不存在于南昌规则的番型 测试: _fanConfig补全16个番型, 58 tests pass
411 lines
14 KiB
C#
411 lines
14 KiB
C#
using RuleEngine.Core;
|
||
using RuleEngine.Patterns;
|
||
|
||
namespace RuleEngine.Tests;
|
||
|
||
public class MahjongTileTests
|
||
{
|
||
[Fact]
|
||
public void Encode_万1_Returns1() => Assert.Equal(1, MahjongTile.Encode("万", 1));
|
||
[Fact]
|
||
public void Encode_条9_Returns19() => Assert.Equal(19, MahjongTile.Encode("条", 9));
|
||
[Fact]
|
||
public void Encode_筒3_Returns23() => Assert.Equal(23, MahjongTile.Encode("筒", 3));
|
||
[Fact]
|
||
public void Suit_万1_Returns0() => Assert.Equal(0, MahjongTile.Suit(1));
|
||
[Fact]
|
||
public void Suit_条19_Returns1() => Assert.Equal(1, MahjongTile.Suit(19));
|
||
[Fact]
|
||
public void Rank_万9_Returns9() => Assert.Equal(9, MahjongTile.Rank(9));
|
||
[Fact]
|
||
public void Rank_条11_Returns1() => Assert.Equal(1, MahjongTile.Rank(11));
|
||
[Fact]
|
||
public void ToString_各类型正确()
|
||
{
|
||
Assert.Equal("5万", MahjongTile.ToString(5));
|
||
Assert.Equal("8条", MahjongTile.ToString(18));
|
||
Assert.Equal("3筒", MahjongTile.ToString(23));
|
||
Assert.Equal("东", MahjongTile.ToString(31));
|
||
Assert.Equal("春", MahjongTile.ToString(41));
|
||
Assert.Equal("🃏", MahjongTile.ToString(50));
|
||
}
|
||
[Fact]
|
||
public void AllTiles_108张() => Assert.Equal(27, MahjongTile.AllTiles().Length);
|
||
[Fact]
|
||
public void AllTiles_含字牌_34种() => Assert.Equal(34, MahjongTile.AllTiles(includeHonors: true).Length);
|
||
[Fact]
|
||
public void AllTiles_含花牌_35种() => Assert.Equal(35, MahjongTile.AllTiles(includeFlowers: true).Length);
|
||
[Fact]
|
||
public void IsWildcard_50_True() => Assert.True(MahjongTile.IsWildcard(50));
|
||
[Fact]
|
||
public void IsWildcard_普通牌_False() => Assert.False(MahjongTile.IsWildcard(1));
|
||
|
||
[Fact]
|
||
public void ToString_配置wildcard_显示癞子标记()
|
||
{
|
||
var wc = new WildcardRegistry();
|
||
wc.Add(35);
|
||
Assert.Equal("🀫中", MahjongTile.ToString(35, wc));
|
||
}
|
||
}
|
||
|
||
public class DeckTests
|
||
{
|
||
[Fact]
|
||
public void 四川牌库_108张()
|
||
{
|
||
var deck = new MahjongDeck(includeHonors: false, includeFlowers: false);
|
||
Assert.Equal(108, deck.Tiles.Count);
|
||
}
|
||
[Fact]
|
||
public void 广东牌库_136张()
|
||
{
|
||
// 108 numbered + 28 honors = 136 (no flowers)
|
||
var deck = new MahjongDeck(includeHonors: true, includeFlowers: false);
|
||
Assert.Equal(136, deck.Tiles.Count);
|
||
}
|
||
[Fact]
|
||
public void 国标牌库_144张()
|
||
{
|
||
// 108 numbered + 28 honors + 8 flowers = 144
|
||
var deck = new MahjongDeck(includeHonors: true, includeFlowers: true);
|
||
Assert.Equal(144, deck.Tiles.Count);
|
||
}
|
||
[Fact]
|
||
public void 每张牌4份()
|
||
{
|
||
var deck = new MahjongDeck();
|
||
var groups = deck.Tiles.Where(t => !MahjongTile.IsFlower(t)).GroupBy(t => t);
|
||
Assert.All(groups, g => Assert.Equal(4, g.Count()));
|
||
}
|
||
[Fact]
|
||
public void 抽牌_减少计数()
|
||
{
|
||
var deck = new MahjongDeck();
|
||
int before = deck.Count;
|
||
deck.Draw();
|
||
Assert.Equal(before - 1, deck.Count);
|
||
}
|
||
}
|
||
|
||
public class MeldsSolverTests
|
||
{
|
||
private readonly MeldsSolver _solver = new(new Dictionary<string, FanConfig>
|
||
{
|
||
["清一色"] = new() { BaseFan = 24 },
|
||
["混一色"] = new() { BaseFan = 6 },
|
||
["字一色"] = new() { BaseFan = 64 },
|
||
["对对胡"] = new() { BaseFan = 2 },
|
||
["碰碰和"] = new() { BaseFan = 6 },
|
||
["暗七对"] = new() { BaseFan = 8 },
|
||
["带幺九"] = new() { BaseFan = 2 },
|
||
["缺一门"] = new() { BaseFan = 0 },
|
||
["平胡"] = new() { BaseFan = 0 },
|
||
["断幺九"] = new() { BaseFan = 2 },
|
||
["无字"] = new() { BaseFan = 0 },
|
||
["全大"] = new() { BaseFan = 12 },
|
||
["全中"] = new() { BaseFan = 12 },
|
||
["全小"] = new() { BaseFan = 12 },
|
||
["全双"] = new() { BaseFan = 24 },
|
||
["鸡胡"] = new() { BaseFan = 1 },
|
||
});
|
||
|
||
// === 标准胡牌 ===
|
||
[Fact]
|
||
public void 标准胡_4面子1对_ShouldWin()
|
||
{
|
||
// 123万 456万 789万 111条 99条
|
||
var hand = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 11, 19, 19 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
}
|
||
|
||
[Fact]
|
||
public void 标准胡_碰碰胡_ShouldWin()
|
||
{
|
||
// 111万 333万 555万 111条 99条
|
||
var hand = new List<int> { 1, 1, 1, 3, 3, 3, 5, 5, 5, 11, 11, 11, 19, 19 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
}
|
||
|
||
[Fact]
|
||
public void 不能胡_缺面子_ShouldNotWin()
|
||
{
|
||
// All sequential but wrong structure: 123 456 78? can't form 4 melds
|
||
var hand = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.False(result.IsWin);
|
||
}
|
||
|
||
[Fact]
|
||
public void 不能胡_14张全不同_ShouldNotWin()
|
||
{
|
||
var hand = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 21, 15, 19 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.False(result.IsWin);
|
||
}
|
||
|
||
// === 七对 ===
|
||
[Fact]
|
||
public void 七对_ShouldWin()
|
||
{
|
||
var hand = new List<int> { 1, 1, 3, 3, 5, 5, 7, 7, 11, 11, 13, 13, 21, 21 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
}
|
||
|
||
[Fact]
|
||
public void 七对_缺一对_ShouldNotWin()
|
||
{
|
||
var hand = new List<int> { 1, 1, 3, 3, 5, 5, 7, 7, 11, 11, 13, 13, 21, 5 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.False(result.IsWin);
|
||
}
|
||
|
||
// === 十三幺 ===
|
||
[Fact]
|
||
public void 十三幺_ShouldWin()
|
||
{
|
||
var hand = new List<int> { 1, 9, 11, 19, 21, 29, 31, 32, 33, 34, 35, 36, 37, 1 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
}
|
||
|
||
[Fact]
|
||
public void 十三幺_缺字牌_ShouldNotWin()
|
||
{
|
||
var hand = new List<int> { 1, 9, 11, 19, 21, 29, 2, 3, 4, 5, 6, 7, 8, 1 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.False(result.IsWin);
|
||
}
|
||
|
||
// === 番型识别 ===
|
||
[Fact]
|
||
public void 清一色_ShouldIdentify()
|
||
{
|
||
// 全万: 111万 234万 567万 789万 99万
|
||
var hand = new List<int> { 1, 1, 1, 2, 3, 4, 5, 6, 7, 7, 8, 9, 9, 9 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
var fans = _solver.IdentifyFans(result!);
|
||
Assert.Contains("清一色", fans);
|
||
}
|
||
|
||
[Fact]
|
||
public void 对对胡_ShouldIdentify()
|
||
{
|
||
var hand = new List<int> { 1, 1, 1, 3, 3, 3, 5, 5, 5, 11, 11, 11, 19, 19 };
|
||
var result = _solver.CheckWin(hand);
|
||
var fans = _solver.IdentifyFans(result!);
|
||
Assert.Contains("对对胡", fans);
|
||
}
|
||
|
||
// === Wildcard ===
|
||
[Fact]
|
||
public void 鬼牌_1wildcard补刻子_ShouldWin()
|
||
{
|
||
// 111万 234万 567万 (3 melds) + 条11,条11 + wildcard = 刻子 (4th meld) + 筒9,筒9 = pair
|
||
// 13 tiles + 1 wildcard = 14
|
||
var hand = new List<int> { 1, 1, 1, 2, 3, 4, 5, 6, 7, 11, 11, 9, 9 };
|
||
var result = _solver.CheckWin(hand, wildcardCount: 1);
|
||
Assert.True(result.IsWin);
|
||
}
|
||
|
||
[Fact]
|
||
public void 鬼牌_2wildcard做对_ShouldWin()
|
||
{
|
||
// 111万 234万 555万 888万 + 2 wildcards as the pair
|
||
var hand = new List<int> { 1, 1, 1, 2, 3, 4, 5, 5, 5, 8, 8, 8 };
|
||
var result = _solver.CheckWin(hand, wildcardCount: 2);
|
||
Assert.True(result.IsWin);
|
||
}
|
||
|
||
// === 258将 ===
|
||
[Fact]
|
||
public void 武汉麻将_258将_2万_ShouldWin()
|
||
{
|
||
var hand = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 11, 2, 2 };
|
||
var result = _solver.CheckWin(hand, require258Pair: true);
|
||
Assert.True(result.IsWin);
|
||
}
|
||
|
||
[Fact]
|
||
public void 武汉麻将_258将_7万将_ShouldFail()
|
||
{
|
||
var hand = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 11, 7, 7 };
|
||
var result = _solver.CheckWin(hand, require258Pair: true);
|
||
Assert.False(result.IsWin);
|
||
}
|
||
|
||
// === 全不靠 ===
|
||
// 全不靠: 14 tiles chosen from 16-position template (9 numbered gaps + 7 honors).
|
||
// Two positions may be empty per rule — no wildcards needed.
|
||
[Fact]
|
||
public void 全不靠_14牌0wildcard_ShouldWin()
|
||
{
|
||
// 7 honors + 万147 + 条258 + 筒3 = 14 tiles from 16-position pool
|
||
var hand = new List<int> { 1, 4, 7, 12, 15, 18, 23, 31, 32, 33, 34, 35, 36, 37 };
|
||
var result = _solver.CheckWin(hand, wildcardCount: 0);
|
||
Assert.True(result.IsWin, "全不靠(14牌从16位置模板选)应判定为胡牌");
|
||
}
|
||
|
||
[Fact]
|
||
public void 全不靠_3wildcard补位_ShouldWin()
|
||
{
|
||
// 5 honors + 万147 + 条258 = 11 tiles + 3 wildcards → fill 5 missing → 14 total
|
||
var hand = new List<int> { 1, 4, 7, 12, 15, 18, 31, 32, 33, 34, 35 };
|
||
var result = _solver.CheckWin(hand, wildcardCount: 3);
|
||
Assert.True(result.IsWin, "全不靠(11牌+3wildcard补位)应判定为胡牌");
|
||
}
|
||
|
||
[Fact]
|
||
public void 全不靠_0wildcard不完整_ShouldNotWin()
|
||
{
|
||
// Only 6 honors + 万147 = 9 tiles, 5 positions missing, 0 wildcard → fail
|
||
var hand = new List<int> { 1, 4, 7, 31, 32, 33, 34, 35, 36 };
|
||
var result = _solver.CheckWin(hand, wildcardCount: 0);
|
||
Assert.False(result.IsWin, "全不靠(9牌+0wildcard不完整)不应判定为胡牌");
|
||
}
|
||
|
||
// === 一色双龙会 (需 wildcard: 9 ranks × 2 = 18 positions, 14 tiles + wildcards) ===
|
||
[Fact]
|
||
public void 一色双龙会_4wildcard_ShouldWin()
|
||
{
|
||
// 5 ranks × 2 = 10 tiles + 4 wildcards for remaining 4 ranks = 14 total
|
||
var hand = new List<int> { 1, 1, 2, 2, 3, 3, 4, 4, 5, 5 };
|
||
var result = _solver.CheckWin(hand, wildcardCount: 4);
|
||
Assert.True(result.IsWin, "一色双龙会(10牌+4wildcard)应判定为胡牌");
|
||
}
|
||
|
||
[Fact]
|
||
public void 一色双龙会_不足wildcard_ShouldNotWin()
|
||
{
|
||
// Same hand, only 2 wildcards → ranks 6-9 need 2 each → 6 missing, only 2 available
|
||
var hand = new List<int> { 1, 1, 2, 2, 3, 3, 4, 4, 5, 5 };
|
||
var result = _solver.CheckWin(hand, wildcardCount: 2);
|
||
Assert.False(result.IsWin, "一色双龙会不足wildcard不应判定为胡牌");
|
||
}
|
||
|
||
// === 新番型识别测试 ===
|
||
[Fact]
|
||
public void 混一色_ShouldIdentify()
|
||
{
|
||
// 万牌 + 字牌对将: 111万 234万 567万 东东东 发发
|
||
var hand = new List<int> { 1, 1, 1, 2, 3, 4, 5, 6, 7, 31, 31, 31, 36, 36 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
var fans = _solver.IdentifyFans(result!);
|
||
Assert.Contains("混一色", fans);
|
||
}
|
||
|
||
[Fact]
|
||
public void 字一色_ShouldIdentify()
|
||
{
|
||
// All honors: 111 333 555 777 99 in encoding
|
||
var hand = new List<int> { 31, 31, 31, 33, 33, 33, 35, 35, 35, 37, 37, 37, 32, 32 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
var fans = _solver.IdentifyFans(result!);
|
||
Assert.Contains("字一色", fans);
|
||
}
|
||
|
||
[Fact]
|
||
public void 缺一门_ShouldIdentify()
|
||
{
|
||
// Only 万 and 条, no 筒: 123万 456万 789万 111条 99条
|
||
var hand = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 11, 19, 19 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
var fans = _solver.IdentifyFans(result!);
|
||
Assert.Contains("缺一门", fans);
|
||
}
|
||
|
||
[Fact]
|
||
public void 平胡_全顺子_ShouldIdentify()
|
||
{
|
||
// All shunzi: 123万 456万 789万 234条 55条
|
||
var hand = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 15 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
var fans = _solver.IdentifyFans(result!);
|
||
Assert.Contains("平胡", fans);
|
||
}
|
||
|
||
[Fact]
|
||
public void 全大_7到9_ShouldIdentify()
|
||
{
|
||
// All ranks 7-9: 777万 888万 999万 777条 99条
|
||
var hand = new List<int> { 7, 7, 7, 8, 8, 8, 9, 9, 9, 17, 17, 17, 19, 19 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
var fans = _solver.IdentifyFans(result!);
|
||
Assert.Contains("全大", fans);
|
||
}
|
||
|
||
[Fact]
|
||
public void 全小_1到3_ShouldIdentify()
|
||
{
|
||
// All ranks 1-3: 111万 222万 333万 111条 33条
|
||
var hand = new List<int> { 1, 1, 1, 2, 2, 2, 3, 3, 3, 11, 11, 11, 13, 13 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
var fans = _solver.IdentifyFans(result!);
|
||
Assert.Contains("全小", fans);
|
||
}
|
||
|
||
[Fact]
|
||
public void 全双_偶数_ShouldIdentify()
|
||
{
|
||
// All even ranks: 222万 444万 666万 222条 88条
|
||
var hand = new List<int> { 2, 2, 2, 4, 4, 4, 6, 6, 6, 12, 12, 12, 18, 18 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
var fans = _solver.IdentifyFans(result!);
|
||
Assert.Contains("全双", fans);
|
||
}
|
||
|
||
[Fact]
|
||
public void 断幺九_ShouldIdentify()
|
||
{
|
||
// No terminals: 234万 456万 678万 234条 55条 (ranks 2-8 only)
|
||
var hand = new List<int> { 2, 3, 4, 4, 5, 6, 6, 7, 8, 12, 13, 14, 15, 15 };
|
||
var result = _solver.CheckWin(hand);
|
||
Assert.True(result.IsWin);
|
||
var fans = _solver.IdentifyFans(result!);
|
||
Assert.Contains("断幺九", fans);
|
||
}
|
||
|
||
// === 听牌检查 ===
|
||
[Fact]
|
||
public void 听牌_缺一张__ShouldDetectTing()
|
||
{
|
||
// 13 tiles, needs 9万 to complete 123万 456万 789万 111条 99条
|
||
var hand = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 11, 11, 11, 19, 19 };
|
||
var allTiles = MahjongTile.AllTiles(false, false);
|
||
bool hasTing = false;
|
||
foreach (var tile in allTiles)
|
||
{
|
||
if (hand.Count(t => t == tile) >= 4) continue;
|
||
var r = _solver.CheckWin(hand, newTile: tile);
|
||
if (r != null && r.IsWin) { hasTing = true; break; }
|
||
}
|
||
Assert.True(hasTing, "13张听牌应检测到至少一张能胡的牌");
|
||
}
|
||
|
||
[Fact]
|
||
public void 听牌_乱牌__ShouldNotDetectTing()
|
||
{
|
||
var hand = new List<int> { 1, 3, 5, 7, 9, 11, 13, 15, 21, 23, 25, 31, 33 };
|
||
var allTiles = MahjongTile.AllTiles(false, false);
|
||
bool hasTing = false;
|
||
foreach (var tile in allTiles)
|
||
{
|
||
if (hand.Count(t => t == tile) >= 4) continue;
|
||
var r = _solver.CheckWin(hand, newTile: tile);
|
||
if (r != null && r.IsWin) { hasTing = true; break; }
|
||
}
|
||
Assert.False(hasTing, "乱牌不应检测到听牌");
|
||
}
|
||
}
|