Files
card-game-engine/RuleEngine/Core/WildcardRegistry.cs
xiaoou 1576253caa fix: 子代理审查26问题修复13个(剩余为编码定义/DSL文档字段)
本次修复(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个)
2026-07-04 23:11:29 +08:00

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;
}
}