diff --git a/src/engine/actions.py b/src/engine/actions.py index 8309d2c..887bc7a 100644 --- a/src/engine/actions.py +++ b/src/engine/actions.py @@ -37,6 +37,7 @@ def draw_tile(engine): # 返回摸到的牌和游戏是否结束的标志 return tile, False # 返回摸到的牌对象和游戏继续的标志 + def discard_tile(self, tile): """ 当前玩家打牌逻辑,记录打出的牌和当前牌河信息。 @@ -96,6 +97,7 @@ def peng(self, tile): logger.info(f"玩家 {player} 碰了一张牌: {tile}。当前明牌: {self.state.melds[player]}") + def gang(self, tile, mode): """ 当前玩家杠牌逻辑,记录杠牌类型和状态更新。 @@ -137,7 +139,6 @@ def gang(self, tile, mode): raise ValueError("无效的杠牌类型,仅支持 'ming'、'an' 或 'bu'") - def check_blood_battle(self): """ 检查游戏是否流局或血战结束,记录状态。 @@ -189,8 +190,9 @@ def set_missing_suit(player, game_state): def check_other_players(self, tile): """ - 检查其他玩家是否可以对打出的牌进行操作(如碰、杠、胡牌)。 - 如果有玩家选择碰或杠,修改游戏状态和出牌顺序。 + 检查其他玩家是否可以对打出的牌进行操作(如胡牌、杠、碰)。 + 优先级为:胡牌 > 杠牌 > 碰牌。 + 如果有玩家选择操作,修改游戏状态和出牌顺序。 """ current_player = self.state.current_player actions_taken = False @@ -199,30 +201,32 @@ def check_other_players(self, tile): if player == current_player: continue - # 检查是否可以碰 - if self.state.hands[player].tile_count[tile] >= 2: - logger.info(f"玩家 {player} 可以碰玩家 {current_player} 的牌: {tile}") - if self.handle_peng(player, tile): # 执行碰牌逻辑 - actions_taken = True - break # 碰牌后不检查其他玩家 - - # 检查是否可以杠 - if self.state.hands[player].tile_count[tile] >= 3: - logger.info(f"玩家 {player} 可以杠玩家 {current_player} 的牌: {tile}") - if self.handle_gang(player, tile, mode="ming"): # 执行明杠逻辑 - actions_taken = True - break # 杠牌后不检查其他玩家 - - # 检查是否可以胡牌 + # 优先检查胡牌 if self.can_win(self.state.hands[player], self.state.melds[player], self.state.missing_suits[player]): logger.info(f"玩家 {player} 可以胡玩家 {current_player} 的牌: {tile}") self.handle_win(player, current_player, tile) actions_taken = True break # 胡牌后结束 + # 检查是否可以杠牌 + if self.state.hands[player].tile_count[tile] >= 3: + logger.info(f"玩家 {player} 可以杠玩家 {current_player} 的牌: {tile}") + if self.handle_gang(player, tile, mode="ming"): # 执行明杠逻辑 + actions_taken = True + break # 杠牌后不检查其他玩家 + + # 检查是否可以碰牌 + if self.state.hands[player].tile_count[tile] >= 2: + logger.info(f"玩家 {player} 可以碰玩家 {current_player} 的牌: {tile}") + if self.handle_peng(player, tile): # 执行碰牌逻辑 + actions_taken = True + break # 碰牌后不检查其他玩家 + if not actions_taken: logger.info(f"玩家 {current_player} 打出的牌 {tile} 没有触发其他玩家的操作") + + def handle_peng(self, player, tile): """ 处理玩家碰牌逻辑并更新出牌顺序。 @@ -252,6 +256,7 @@ def handle_peng(self, player, tile): self.discard_tile(player, chosen_tile) return True + def get_player_discard_choice(self, player): """ 模拟获取玩家打牌的选择。 @@ -265,6 +270,7 @@ def get_player_discard_choice(self, player): logger.info(f"玩家 {player} 选择打出牌: {chosen_tile}") return chosen_tile + def handle_gang(self, player, tile, mode): """ 处理玩家杠牌逻辑并更新状态。 @@ -290,6 +296,7 @@ def handle_gang(self, player, tile, mode): self.draw_tile_for_player(player) return True + def random_discard_tile(engine): """ 当前玩家随机选择一张牌打出,优先打缺门牌。 @@ -343,4 +350,54 @@ def random_choice(hand, missing_suit): # 如果缺门牌不存在,从剩余牌中随机选择 index = random.randint(0, len(hand) - 1) - return hand[index] \ No newline at end of file + return hand[index] + + +def should_gang(ai_player, state, gang_type): + """ + 判断 AI 是否选择杠牌 + :param ai_player: 当前玩家索引 + :param state: 游戏状态对象 + :param gang_type: 杠牌类型(暗杠或明杠) + :return: True 表示杠,False 表示不杠 + """ + # 获取当前玩家分数 + current_score = state.scores[ai_player] + # 获取当前玩家手牌 + hand = state.hands[ai_player] + # 获取局势信息 + draw_counts = state.draw_counts + remaining_tiles = state.remaining_tiles + + # 基础策略:如果当前分数远低于平均分,优先杠 + average_score = sum(state.scores) / len(state.scores) + if current_score < average_score * 0.8: + return True + + # 检查听牌状态:如果杠后听牌,优先杠 + # if is_ready_to_win(hand, state.melds[ai_player], state.missing_suits[ai_player]): + # return True + + # 根据杠牌类型调整策略 + if gang_type == "暗杠": + # 暗杠通常安全,偏向杠 + return True + elif gang_type == "明杠": + # 明杠可能被针对,后期优先考虑杠 + return remaining_tiles < 30 # 剩余牌少于 30 张时偏向杠 + + # 默认不杠 + return False + + +def select_discard_tile(self, player): + """ + 选择要打出的牌(此处为简单示例,可接入 AI 策略)。 + """ + hand = self.state.hands[player] + # 优先打出缺门牌 + for tile in hand.tiles: + if tile.suit == self.state.missing_suits[player]: + return tile + # 如果没有缺门牌,随机打出一张 + return hand.tiles[0] diff --git a/src/engine/chengdu_mahjong_engine.py b/src/engine/chengdu_mahjong_engine.py index adb001e..307a334 100644 --- a/src/engine/chengdu_mahjong_engine.py +++ b/src/engine/chengdu_mahjong_engine.py @@ -2,7 +2,7 @@ import random from loguru import logger from src.engine.actions import set_missing_suit from src.engine.chengdu_mahjong_state import ChengduMahjongState -from src.engine.actions import draw_tile, discard_tile, peng, gang, check_blood_battle +from src.engine.actions import draw_tile, discard_tile, peng, gang, check_blood_battle,should_gang,random_choice class ChengduMahjongEngine: @@ -19,7 +19,7 @@ class ChengduMahjongEngine: logger.info("游戏初始化...") # 确定庄家(掷骰子) self.state.current_player = random.randint(0, 3) - logger.info(f"庄家确定为玩家 {self.state.current_player}") + logger.info(f"庄家确定为玩家: {self.state.current_player}") logger.info("游戏初始化完成,准备开始!") @@ -40,38 +40,55 @@ class ChengduMahjongEngine: logger.info("发牌结束并完成缺门设置!") def play_turn(self): - """ - 进行一回合的操作:当前玩家摸牌、出牌。 - """ - logger.info(f"轮到玩家 {self.state.current_player} 行动") + if self.state.draw_counts[self.current_player] == 0: + # 判断庄家是否天胡 + tianhu = self.state.can_win( + self.state.hands[self.current_player], + self.state.melds[self.current_player], + self.state.missing_suits[self.current_player], + ) + + if tianhu: + # 天胡结算 + self.state.winners.append(self.current_player) + self.state.print_game_state(self.current_player) + for i in range(4): + if i != self.current_player: + self.state.scores[i] -= self.state.bottom_score * 2 ** 5 + self.state.scores[self.current_player] += self.state.bottom_score * 2 ** 5 + else: + # 判断是否可以杠 + if self.state.hands[self.current_player].can_gang(): + # 获取杠牌类型 + gang_type = self.state.hands[self.current_player].get_gang_type() + + # AI 决策是否杠牌 + if should_gang(self.current_player, self.state, gang_type): + self.state.hands[self.current_player].perform_gang() # 执行杠操作 + self.state.print_game_state(self.current_player) + + # 计算杠牌得分 + if gang_type == "暗杠": + # 暗杠分数结算 + for i in range(4): + if i != self.current_player: + self.state.scores[i] -= self.state.bottom_score * 2 ** 2 # 扣分 + self.state.scores[self.current_player] += self.state.bottom_score * 2 ** 2 * 3 # 加分 + elif gang_type == "明杠": + # 明杠分数结算 + for i in range(4): + if i != self.current_player: + self.state.scores[i] -= self.state.bottom_score * 2 ** 1 # 扣分 + self.state.scores[self.current_player] += self.state.bottom_score * 2 ** 1 * 3 # 加分 + + logger.info(f"玩家 {self.current_player} 杠牌,类型: {gang_type}") + else: + random_choice(self.state.hands[self.current_player], self.state.missing_suits[self.current_player]) - # 玩家摸牌 - reward, done = draw_tile(self) - if done: - logger.info("游戏因牌堆摸空而结束!") - return - # 玩家打牌(逻辑可扩展为选择最佳牌) - tile_to_discard = self.select_discard_tile(self.state.current_player) - discard_tile(self, tile_to_discard) - # 检查游戏是否结束 - self.check_game_over() - # 切换到下一位玩家 - self.state.current_player = (self.state.current_player + 1) % 4 - def select_discard_tile(self, player): - """ - 选择要打出的牌(此处为简单示例,可接入 AI 策略)。 - """ - hand = self.state.hands[player] - # 优先打出缺门牌 - for tile in hand.tiles: - if tile.suit == self.state.missing_suits[player]: - return tile - # 如果没有缺门牌,随机打出一张 - return hand.tiles[0] def check_game_over(self): """ diff --git a/src/engine/chengdu_mahjong_state.py b/src/engine/chengdu_mahjong_state.py index 2539620..614d64f 100644 --- a/src/engine/chengdu_mahjong_state.py +++ b/src/engine/chengdu_mahjong_state.py @@ -17,12 +17,16 @@ class ChengduMahjongState: 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 # 每个玩家的缺门("条"、"筒" 或 "万") diff --git a/src/engine/hand.py b/src/engine/hand.py index ca4323d..1929232 100644 --- a/src/engine/hand.py +++ b/src/engine/hand.py @@ -37,11 +37,47 @@ class Hand: raise ValueError("必须是 MahjongTile 类型的牌") return self.tile_count[tile] == 2 # 摸一张牌后总数为 3 张,才可以碰 - def can_gang(self, tile): - """ 判断是否可以杠(即是否已经有3张相同的牌,摸一张牌后可以杠) """ - if not isinstance(tile, MahjongTile): - raise ValueError("必须是 MahjongTile 类型的牌") - return self.tile_count[tile] == 4 # 摸一张牌后总数为 4 张,才可以杠 + def can_gang(self, tile=None): + """ + 判断是否可以杠牌。 + 两种情况: + 1. 当前手牌中已经有 4 张相同的牌,可以选择杠。 + 2. 当前手牌中有 3 张相同的牌,摸到第 4 张后可以杠。 + + :param tile: 需要判断的牌(可以为空)。 + :return: True 如果可以杠,否则 False。 + """ + if tile is not None: + # 情况 1: 摸到一张牌后形成四张 + if not isinstance(tile, MahjongTile): + raise ValueError("必须是 MahjongTile 类型的牌") + return self.tile_count[tile] == 4 + else: + # 情况 2: 手牌中已有四张一样的牌 + for t, count in self.tile_count.items(): + if count == 4: + return True + return False + + def get_gang_type(self, tile=None): + """ + 判断杠牌的类型(暗杠或明杠)。 + :param tile: 如果指定了牌,检查是否可以明杠。 + :return: 杠牌类型,"暗杠" 或 "明杠",如果不能杠返回 None。 + """ + if tile is not None: + # 明杠的判断逻辑:手牌中已有3张相同的牌,摸到第4张 + if not isinstance(tile, MahjongTile): + raise ValueError("必须是 MahjongTile 类型的牌") + if self.tile_count[tile] == 4: + return "明杠" + else: + # 暗杠的判断逻辑:手牌中已有4张相同的牌 + for t, count in self.tile_count.items(): + if count == 4: + return "暗杠" + return None + def __repr__(self): """ 返回手牌的字符串表示 """