fix: 癞子=136张内的牌(红中),不是额外+4张 + 宝牌HUD显示

- MahjongTile.ConfigureWildcards: 支持固定wildcard(如红中35)
- IsWildcard: 同时检查encoding 50+和配置的wildcard
- ToString: wildcard牌显示🀫中/🀫发等
- Deck: 固定wildcard作为普通牌加入(红中在136内)
- Deal: fixed类型wildcard不额外加牌
- HUD: 显示 '🀫 宝牌(癞子): 🀫中'
- Wuhan DSL: 移除wildcardCount:4
- 测试: Wuhan牌库136张
This commit is contained in:
xiaoou
2026-07-04 17:37:20 +08:00
parent 21bf35c453
commit 7c9d5b4637
6 changed files with 55 additions and 13 deletions

View File

@ -55,8 +55,8 @@ public class MahjongRoomIntegrationTests
room.Deal();
int totalTiles = room.State.Hands.Sum(h => h.Value.Count) + room.State.Deck.Count;
// 136 tiles + 4 wildcards = 140
Assert.Equal(140, totalTiles);
// 136 tiles total (108 numbered + 28 honors, 红中 is wildcard but part of the 136)
Assert.Equal(136, totalTiles);
}
// === Turn flow ===

View File

@ -33,6 +33,11 @@ public class HumanMahjongPlayer
Console.WriteLine($"=== {Name} 的回合 ===");
Console.WriteLine($" 牌墙: {state.Deck.Count}张 | 弃牌堆: {state.DiscardPool.Count}张");
// Show wildcard info
var wcTiles = MahjongTile.ConfiguredWildcards;
if (wcTiles.Count > 0)
Console.WriteLine($" 🀫 宝牌(癞子): {string.Join(" ", wcTiles.Select(MahjongTile.ToString))}");
// Show exposed melds
if (state.Exposed.TryGetValue(Name, out var exposed) && exposed.Count > 0)
{

View File

@ -10,12 +10,13 @@ public class MahjongDeck
foreach (var t in allTiles)
{
int count = MahjongTile.IsFlower(t) ? 1 : 4;
// Wildcards are handled by AllTiles already
if (MahjongTile.IsWildcard(t)) continue;
// Skip extra encoding-50+ wildcards (they're added separately below)
// But include fixed wildcards (e.g. 红中) as normal tiles
if (t >= MahjongTile.WildcardBase) continue;
for (int i = 0; i < count; i++)
Tiles.Add(t);
}
// Add wildcards separately
// Add extra wildcard tiles
for (int i = 0; i < wildcardCount; i++)
Tiles.Add(MahjongTile.WildcardBase + i);
}

View File

@ -6,7 +6,19 @@ public static class MahjongTile
// Encoding: 万1-9=1-9, 条1-9=11-19, 筒1-9=21-29
// 字: 东=31,南=32,西=33,北=34,中=35,发=36,白=37
// 花: 春=41..菊=48
// 宝牌: 50-59
// 宝牌: 50-59 (extra wildcards added to deck)
private static readonly HashSet<int> _wildcards = new();
/// <summary>Configure which normal tiles (e.g. 红中=35) also act as wildcards.</summary>
public static void ConfigureWildcards(IEnumerable<int> tiles)
{
_wildcards.Clear();
foreach (var t in tiles) _wildcards.Add(t);
}
/// <summary>Get the set of normal tiles configured as wildcards.</summary>
public static IReadOnlySet<int> ConfiguredWildcards => _wildcards;
public static int Encode(string suit, int rank)
{
@ -24,6 +36,7 @@ public static class MahjongTile
if (tile >= 1 && tile <= 9) return $"{tile}万";
if (tile >= 11 && tile <= 19) return $"{tile - 10}条";
if (tile >= 21 && tile <= 29) return $"{tile - 20}筒";
if (_wildcards.Contains(tile)) return $"🀫{TileName(tile)}";
return tile switch
{
31 => "东", 32 => "南", 33 => "西", 34 => "北",
@ -35,6 +48,13 @@ public static class MahjongTile
};
}
private static string TileName(int tile) => tile switch
{
31 => "东", 32 => "南", 33 => "西", 34 => "北",
35 => "中", 36 => "发", 37 => "白",
_ => $"?{tile}"
};
/// 0=万, 1=条, 2=筒, 3=字, 4=花, 5=宝牌
public static int Suit(int tile)
{
@ -64,7 +84,7 @@ public static class MahjongTile
public static bool IsHonor(int tile) => tile >= 31 && tile <= 37;
public static bool IsFlower(int tile) => tile >= 41 && tile <= 48;
public static bool IsWildcard(int tile) => tile >= 50 && tile <= 59;
public static bool IsWildcard(int tile) => tile >= 50 && tile <= 59 || _wildcards.Contains(tile);
public static bool IsNumbered(int tile) => tile >= 1 && tile <= 29;
public static bool IsTerminal(int tile)
{

View File

@ -438,7 +438,25 @@ public class MahjongRoom
int perPlayer = _rules.Deal.CardsPerPlayer;
bool includeFlowers = _rules.Deck.IncludeFlowers;
bool includeHonors = _rules.Deck.IncludeHonors;
int wildcardCount = _rules.WildcardRules != null ? _rules.Deck.WildcardCount : 0;
// Configure fixed wildcards (e.g. Wuhan 红中)
int wildcardCount = 0;
if (_rules.WildcardRules != null)
{
if (_rules.WildcardRules.Type == "fixed" && _rules.WildcardRules.Tiles != null)
{
// Fixed wildcards: specified tiles ARE the wildcards, not extra tiles
var wcTiles = _rules.WildcardRules.Tiles
.Select(t => t switch { "红中" => 35, "发财" => 36, "白板" => 37, _ => -1 })
.Where(t => t > 0).ToArray();
MahjongTile.ConfigureWildcards(wcTiles);
wildcardCount = 0; // No extra tiles
}
else
{
wildcardCount = _rules.Deck.WildcardCount;
}
}
var deck = new MahjongDeck(includeHonors, includeFlowers, wildcardCount);
deck.Shuffle(_rng);
@ -479,8 +497,6 @@ public class MahjongRoom
private void CheckTing()
{
bool hasWildcard = _rules.WildcardRules != null;
int wildcardCount = hasWildcard ? _rules.Deck.WildcardCount : 0;
bool require258 = _rules.WinCondition?.PairMustBe258 ?? false;
// All possible tiles that could be drawn (numbered 1-9 across 3 suits + 7 honors)
@ -500,8 +516,10 @@ public class MahjongRoom
int alreadyHave = hand.Count(t => t == tile);
if (alreadyHave >= 4) continue; // can't draw more of this tile
var handWithTile = new List<int>(hand) { tile };
int wc = CountWildcardsInHand(handWithTile);
var r = _solver.CheckWin(hand, newTile: tile,
wildcardCount: wildcardCount, require258Pair: require258);
wildcardCount: wc, require258Pair: require258);
if (r != null && r.IsWin) { ting = true; break; }
}

View File

@ -17,8 +17,6 @@ deck:
generator: mahjong
include_honors: true
include_flowers: false
wildcard_tile: 红中
wildcard_count: 4
total: 136
deal: