Files
card-game-engine/RuleEngine.Tests/CoreTests.cs
xiaoou 70b00d5e6d fix: IdentifyFans filter排除无fallback番型—防止跨变体越权
核心变更:
- filter从'well-known全放行'改为'只放行_fanConfig或有fallback值的'
- 排除: 断幺九/无字/缺一门/将/平胡/平和(无hardcoded fallback)
- 这些被排除时→allZeroValue→鸡胡兜底→保证非零分

效果: 南昌不再显示'断幺九+无字+平胡'等不存在于南昌规则的番型
测试: _fanConfig补全16个番型, 58 tests pass
2026-07-05 01:09:42 +08:00

411 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, "乱牌不应检测到听牌");
}
}