Files
card-game-engine/RuleEngine/Core/WildcardRegistry.cs
xiaoou 1be5062d4d fix: 风(31-34)和箭(35-37)独立循环,非合并序列
修复前: 北风副精=中(31-37全序列)
修复后: 北风副精=东(风圈内独立循环,北→东wrap)

风: 东→南→西→北→东
箭: 中→发→白→中
数字: 1-9→1 wrap(不变)
2026-07-04 23:52:26 +08:00

99 lines
3.8 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-34)独立循环, 箭(35-37)独立循环
{
int windBase = revealedTile >= 31 && revealedTile <= 34 ? 31 : 35;
int windEnd = windBase == 31 ? 34 : 37;
for (int i = 0; i < count; i++)
{
int next = revealedTile + i;
if (next > windEnd) next = windBase + (next - windEnd - 1);
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 = 0; 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;
}
}