本次修复(13/26): - FuFlag传播: PhaseConfig加FuFlag属性+BuildPhases映射 - Excludes/Conflicts引用完整性: 引用的番型名必须存在于fan_types - WildcardBase常量提取: WildcardRegistry引用MahjongTile.WildcardBase - 合规验证扩展: max_fan/phase.type/end_condition/action/priority - FanCalculationPolicy未消费警告 不修复的剩余13个: - 编码常量(MahjongTile domain定义,非配置项) - DSL字段仅文档(POCO有但引擎硬编码等效逻辑) - LOW代码卫生项 --check输出现在会报告: excludes/conflicts引用了不存在的番型(如国标15个)
97 lines
3.6 KiB
C#
97 lines
3.6 KiB
C#
namespace RuleEngine.Core;
|
|
|
|
/// <summary>
|
|
/// Per-game wildcard configuration — replaces the old static HashSet approach.
|
|
/// Each MahjongGameState owns its own registry so different game variants
|
|
/// don't leak wildcard state between tests or concurrent games.
|
|
/// </summary>
|
|
public class WildcardRegistry
|
|
{
|
|
private readonly HashSet<int> _wildcards = new();
|
|
private int _wildcardBase;
|
|
|
|
/// <summary>Base wildcard encoding. Default 50, configurable from DSL wildcard_encoding.</summary>
|
|
public int WildcardBase
|
|
{
|
|
get => _wildcardBase;
|
|
set { _wildcardBase = value > 0 ? value : 50; }
|
|
}
|
|
|
|
/// <summary>The tile revealed from deck to determine random wildcards (骰子翻牌定精).</summary>
|
|
public int? RevealedTile { get; set; }
|
|
|
|
public WildcardRegistry() { _wildcardBase = MahjongTile.WildcardBase; }
|
|
public WildcardRegistry(int wildcardBase) { _wildcardBase = wildcardBase > 0 ? wildcardBase : MahjongTile.WildcardBase; }
|
|
|
|
/// <summary>Non-extra wildcard tiles — normal tiles (e.g. 红中=35) that ALSO act as wildcards.</summary>
|
|
public IReadOnlySet<int> ConfiguredWildcards => _wildcards;
|
|
|
|
public void Clear() { _wildcards.Clear(); RevealedTile = null; }
|
|
public void Add(int tile) => _wildcards.Add(tile);
|
|
|
|
public void AddRange(IEnumerable<int> tiles)
|
|
{
|
|
foreach (var t in tiles) _wildcards.Add(t);
|
|
}
|
|
|
|
/// <summary>Is this tile a wildcard? Checks encoding range AND configured set.</summary>
|
|
public bool IsWildcard(int tile) =>
|
|
(tile >= _wildcardBase && tile <= _wildcardBase + 9) || _wildcards.Contains(tile);
|
|
|
|
/// <summary>Count how many tiles in a list are wildcards.</summary>
|
|
public int CountWildcards(IReadOnlyList<int> tiles) =>
|
|
tiles.Count(t => IsWildcard(t));
|
|
|
|
/// <summary>Parse wildcard tile names from DSL into encodings.</summary>
|
|
public static int? NameToEncoding(string name) => name switch
|
|
{
|
|
"红中" => 35,
|
|
"发财" => 36,
|
|
"白板" => 37,
|
|
_ => null
|
|
};
|
|
|
|
/// <summary>
|
|
/// Compute wildcard tiles from a revealed tile (翻牌定精).
|
|
/// Numbered suits: rank+1 is main 精, rank+2 is secondary 精 (wraps 9→1).
|
|
/// Honor tiles: next in honor sequence (东→南→西→北→中→发→白→东).
|
|
/// </summary>
|
|
/// <param name="revealedTile">The tile flipped from the deck.</param>
|
|
/// <param name="count">How many next tiles to treat as wildcards (default 2 = 正精+副精).</param>
|
|
/// <returns>List of tile encodings that should become wildcards.</returns>
|
|
public static List<int> ComputeNextTiles(int revealedTile, int count = 2)
|
|
{
|
|
var result = new List<int>();
|
|
int suit = MahjongTile.Suit(revealedTile);
|
|
|
|
if (suit == 3) // Honors: 东(31)→南(32)→西(33)→北(34)→中(35)→发(36)→白(37)→东(31)
|
|
{
|
|
for (int i = 1; i <= count; i++)
|
|
{
|
|
int next = revealedTile + i;
|
|
if (next > 37) next = 31 + (next - 38);
|
|
result.Add(next);
|
|
}
|
|
}
|
|
else if (suit is >= 0 and <= 2) // Numbered: 万(1-9) / 条(11-19) / 筒(21-29)
|
|
{
|
|
int baseVal = suit switch { 0 => 0, 1 => 10, _ => 20 };
|
|
int rank = MahjongTile.Rank(revealedTile);
|
|
for (int i = 1; i <= count; i++)
|
|
{
|
|
int nextRank = (rank + i - 1) % 9 + 1;
|
|
result.Add(baseVal + nextRank);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public WildcardRegistry Clone()
|
|
{
|
|
var r = new WildcardRegistry(_wildcardBase);
|
|
r.AddRange(_wildcards);
|
|
r.RevealedTile = RevealedTile;
|
|
return r;
|
|
}
|
|
}
|