修复前: 北风副精=中(31-37全序列) 修复后: 北风副精=东(风圈内独立循环,北→东wrap) 风: 东→南→西→北→东 箭: 中→发→白→中 数字: 1-9→1 wrap(不变)
99 lines
3.8 KiB
C#
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;
|
|
}
|
|
}
|