mjAi/src/engine/mahjong/chengdu_mahjong_state.py

142 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

from collections import Counter
from src.engine.mahjong.hand import Hand
from src.engine.mahjong.mahjong_tile import MahjongTile
from src.engine.mahjong.meld import Meld
from loguru import logger
class ChengduMahjongState:
def __init__(self):
# 每个玩家的手牌
self.hands = [Hand() for _ in range(4)] # 每个玩家的手牌由 Hand 类表示
# 每个玩家的打出的牌
self.discards = [[] for _ in range(4)] # 每个玩家的弃牌列表
# 每个玩家的明牌(碰、杠)
self.melds = [[] for _ in range(4)] # 每个玩家的明牌列表,存储 Meld 对象
# 剩余的牌堆
self.deck = [MahjongTile(suit, value) for suit in ["", "", ""] for value in range(1, 10)] * 4 # 108张牌
# 当前玩家索引
self.current_player = 0
# 底分
self.bottom_score = 1
# 玩家分数
self.scores = [100, 100, 100, 100]
# 剩余牌数量
self.remaining_tiles = len(self.deck)
# 胜利玩家列表
self.winners = []
# 记录每个玩家的抓牌次数
self.draw_counts = [0] * 4
# 缺门信息
self.missing_suits = [None] * 4 # 每个玩家的缺门("条"、"筒" 或 "万"
def set_missing_suit(self, player, missing_suit):
"""
设置玩家的缺门信息。
参数:
- player: 玩家索引0-3
- missing_suit: 玩家选择的缺门("""""")。
异常:
- ValueError: 如果缺门设置无效。
"""
valid_suits = ["", "", ""]
if missing_suit not in valid_suits:
raise ValueError("缺门设置无效")
self.missing_suits[player] = missing_suit
def print_game_state(self, player_index: int):
"""
打印指定玩家的手牌、暗牌、明牌和缺门信息。
:param player_index: 要打印的玩家索引0-3
"""
hand = self.hands[player_index]
melds = self.melds[player_index]
missing_suit = self.missing_suits[player_index]
# 打印日志,所有信息在一行
logger.info(
f"玩家索引: {player_index}, 手牌: {hand}, 明牌: {melds}, 总牌数: {len(hand.tiles) + sum(meld.count for meld in melds)}, 缺门: {missing_suit}"
)
def can_win(self, hand: Hand, melds: list[Meld], missing_suit: str):
"""
判断玩家是否能胡牌。
:param hand: 玩家手牌Hand 对象)。
:param melds: 玩家已明牌的列表Meld 对象列表)。
:param missing_suit: 玩家设置的缺门花色。
:return: True 表示能胡牌False 表示不能胡牌。
"""
def is_valid_group(tiles):
"""
检查是否是合法组AAA 或 ABC
"""
if len(tiles) != 3:
return False
tiles.sort(key=lambda t: (t.suit, t.value)) # 按花色和数值排序
return (tiles[0].value == tiles[1].value == tiles[2].value and # AAA
tiles[0].suit == tiles[1].suit == tiles[2].suit) or \
(tiles[0].value + 1 == tiles[1].value and # ABC
tiles[1].value + 1 == tiles[2].value and
tiles[0].suit == tiles[1].suit == tiles[2].suit)
def try_win(remaining_tiles, pairs_found=False):
"""
尝试将剩余牌分组,必须满足 n * AAA + m * ABC + DD。
"""
# 如果没有剩余牌,检查是否已经找到对子
if not remaining_tiles:
return pairs_found # 必须存在一个对子
# 尝试找到一个对子
if not pairs_found:
tile_counter = Counter(remaining_tiles)
for tile, count in tile_counter.items():
if count >= 2: # 找到一个对子
temp_tiles = remaining_tiles[:]
temp_tiles.remove(tile)
temp_tiles.remove(tile)
if try_win(temp_tiles, pairs_found=True):
return True
# 尝试找到一个合法组AAA 或 ABC
for i in range(len(remaining_tiles)):
for j in range(i + 1, len(remaining_tiles)):
for k in range(j + 1, len(remaining_tiles)):
group = [remaining_tiles[i], remaining_tiles[j], remaining_tiles[k]]
if is_valid_group(group):
next_tiles = remaining_tiles[:i] + remaining_tiles[i + 1:j] + \
remaining_tiles[j + 1:k] + remaining_tiles[k + 1:]
if try_win(next_tiles, pairs_found):
return True
return False
# **第一步:检查花色限制**
suits = {tile.suit for tile in hand.tiles}
if len(suits) > 2:
# logger.info("花色超过两种,不能胡牌")
return False # 花色超过两种,不能胡牌
# 检查是否打完缺门的花色
if any(tile.suit == missing_suit for tile in hand.tiles):
logger.info("仍有缺门花色,不能胡牌")
return False # 仍有缺门的花色,不能胡牌
# **第二步:分离明牌(杠、碰)和暗牌**
# 提取明牌(碰和杠)的组数
groups_from_melds = 0
for meld in melds:
if meld.type == "":
groups_from_melds += 1 # 碰牌构成 1 组
elif meld.type == "":
groups_from_melds += 1 # 杠牌也构成 1 组
# 获取所有暗牌
remaining_tiles = hand.tiles[:]
logger.info(f"暗牌: {remaining_tiles}, 明牌: {melds}, 已构成的组数: {groups_from_melds}")
# **第三步:检查暗牌是否满足分组条件**
return try_win(remaining_tiles, pairs_found=False) and groups_from_melds >= 0