diff --git a/README.md b/README.md index b830276..7a6744d 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ - **缺一门**:玩家必须选择缺少一种花色(条、筒、万中的任意一种),即只能用两种花色来胡牌。如果手中只有单一花色,则为清一色。 - **定缺**:游戏开始时,每位玩家需要扣下一张牌作为自己缺的那门,并且不能更改。如果本身就是两门牌,则可以报“天缺”而不扣牌。 - **起牌与打牌**:庄家通过掷骰子决定起牌位置,然后按顺序抓牌。庄家先出牌,之后每家依次摸牌打牌。 -- **碰、杠**:允许碰牌和杠牌,但不允许吃牌。杠牌分为明杠和暗杠,明杠是其他玩家打出的牌被你碰后又摸到相同的牌;暗杠则是你自己摸到四张相同的牌。 +- **碰、杠**:允许碰牌和杠牌,但不允许吃牌。杠牌分为明杠和暗杠,明杠是其他玩家打出的牌刚好与你手里有三张的牌相同;暗杠则是你自己摸到四张相同的牌。 - **胡牌**:胡牌的基本条件是拥有一个对子加上四个顺子或刻子(三个相同牌)。自摸为三家给分,点炮则由放炮者给分。n*AAA+m*ABC+DD ,mn可以等于0。 - **血战到底**:一家胡牌后,其他未胡牌的玩家继续游戏,直到只剩下最后一位玩家或者黄庄(所有牌都被摸完)为止。 diff --git a/src/engine/actions.py b/src/engine/actions.py index 887bc7a..df679eb 100644 --- a/src/engine/actions.py +++ b/src/engine/actions.py @@ -2,6 +2,7 @@ from random import random from loguru import logger from src.engine.mahjong_tile import MahjongTile +from src.engine.calculate_fan import calculate_fan def draw_tile(engine): @@ -204,21 +205,21 @@ def check_other_players(self, tile): # 优先检查胡牌 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) + 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"): # 执行明杠逻辑 + if 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): # 执行碰牌逻辑 + if handle_peng(player, tile): # 执行碰牌逻辑 actions_taken = True break # 碰牌后不检查其他玩家 @@ -241,22 +242,10 @@ def handle_peng(self, player, tile): logger.info(f"玩家 {player} 碰了牌: {tile}。当前明牌: {self.state.melds[player]}") - # 设置出牌顺序为碰牌的玩家 - self.state.current_player = player - - # 提供玩家手牌信息,等待玩家选择打出的牌 - chosen_tile = self.get_player_discard_choice(player) - - # 验证玩家选择的牌是否合法 - if chosen_tile not in self.state.hands[player].tiles: - logger.error(f"玩家 {player} 选择了不合法的牌: {chosen_tile}") - raise ValueError("打出的牌必须存在于玩家的手牌中") - - # 玩家选择打出这张牌 - self.discard_tile(player, chosen_tile) return True + def get_player_discard_choice(self, player): """ 模拟获取玩家打牌的选择。 @@ -273,28 +262,53 @@ def get_player_discard_choice(self, player): def handle_gang(self, player, tile, mode): """ - 处理玩家杠牌逻辑并更新状态。 + 处理玩家杠牌逻辑、计算分数并更新状态。 + :param player: 杠牌玩家索引 + :param tile: 杠牌的那张牌 + :param mode: 杠牌的类型 ("ming" 或 "an") + :return: True 如果操作成功,否则 False """ - if mode == "ming" and self.state.hands[player].tile_count[tile] < 3: - logger.error(f"玩家 {player} 无法明杠: {tile}") - return False + base_score = self.state.bottom_score # 底分 - # 减少牌数量并更新明牌 - if mode == "ming": + if mode == "ming": # 明杠逻辑 + if self.state.hands[player].tile_count[tile] < 3: + logger.error(f"玩家 {player} 无法明杠: {tile}") + return None + + # 减少牌数量并更新明牌 self.state.hands[player].tile_count[tile] -= 3 - self.state.melds[player].append(("ming_gang", tile)) + self.state.melds[player].append(("杠", tile)) logger.info(f"玩家 {player} 明杠了牌: {tile}") - elif mode == "an": + + # 明杠分数计算 + gang_score = base_score * 2 # 明杠的分数倍率 + for i in range(4): + if i != player: + self.state.scores[i] -= gang_score # 每个其他玩家扣分 + self.state.scores[player] += gang_score * 3 # 杠牌玩家得总分 + + logger.info(f"玩家 {player} 明杠,总分变化: +{gang_score * 3},其他玩家每人扣分: -{gang_score}") + return True + + elif mode == "an": # 暗杠逻辑 if self.state.hands[player].tile_count[tile] < 4: logger.error(f"玩家 {player} 无法暗杠: {tile}") return False + + # 减少牌数量并更新暗牌 self.state.hands[player].tile_count[tile] -= 4 self.state.melds[player].append(("an_gang", tile)) logger.info(f"玩家 {player} 暗杠了牌: {tile}") - # 杠牌后玩家补摸一张牌 - self.draw_tile_for_player(player) - return True + # 暗杠分数计算 + gang_score = base_score * 4 # 暗杠的分数倍率 + for i in range(4): + if i != player: + self.state.scores[i] -= gang_score # 每个其他玩家扣分 + self.state.scores[player] += gang_score * 3 # 杠牌玩家得总分 + + logger.info(f"玩家 {player} 暗杠,总分变化: +{gang_score * 3},其他玩家每人扣分: -{gang_score}") + return True def random_discard_tile(engine): @@ -401,3 +415,40 @@ def select_discard_tile(self, player): return tile # 如果没有缺门牌,随机打出一张 return hand.tiles[0] + + + +def handle_win(self, player, current_player, tile): + """ + 处理胡牌逻辑,包括判断地胡,计算番数和分数。 + :param player: 胡牌玩家索引 + :param current_player: 打出牌的玩家索引 + :param tile: 胡牌的那张牌 + """ + logger.info(f"玩家 {player} 胡牌!胡的牌是: {tile}") + + # 判断是否地胡 + is_dihu = self.state.draw_counts[player] == 0 and player != self.state.current_player + if is_dihu: + logger.info(f"玩家 {player} 地胡!") + else: + logger.info(f"玩家 {player} 不是地胡") + + # 计算分数 + fan_count = 5 # 按你逻辑固定番数为5 + base_score = self.state.bottom_score # 底分 + win_score = base_score * (2 ** fan_count) + + self.state.scores[player] += win_score * 3 # 胡牌玩家得总分 + self.state.scores[current_player] -= win_score # 点炮玩家扣除分数 + + # 更新赢家状态 + self.state.winners.append(player) + self.state.print_game_state(player) + + # 输出日志 + logger.info(f"玩家 {player} 胡牌类型: {'地胡' if is_dihu else '普通胡牌'}") + logger.info(f"玩家 {player} 总番数: {fan_count}") + logger.info(f"玩家 {current_player} 点炮,扣分: {win_score}") + logger.info(f"玩家 {player} 加分: {win_score * 3}") + logger.info(f"当前分数: {self.state.scores}") diff --git a/src/engine/chengdu_mahjong_engine.py b/src/engine/chengdu_mahjong_engine.py index 307a334..393678e 100644 --- a/src/engine/chengdu_mahjong_engine.py +++ b/src/engine/chengdu_mahjong_engine.py @@ -1,6 +1,6 @@ import random from loguru import logger -from src.engine.actions import set_missing_suit +from src.engine.actions import set_missing_suit, check_other_players from src.engine.chengdu_mahjong_state import ChengduMahjongState from src.engine.actions import draw_tile, discard_tile, peng, gang, check_blood_battle,should_gang,random_choice @@ -84,7 +84,7 @@ class ChengduMahjongEngine: logger.info(f"玩家 {self.current_player} 杠牌,类型: {gang_type}") else: random_choice(self.state.hands[self.current_player], self.state.missing_suits[self.current_player]) - + check_other_players(self.state.hands[self.current_player], self.state.missing_suits[self.current_player]) diff --git a/tests/test_chengdu_mahjong_engine.py b/tests/test_chengdu_mahjong_engine.py index 5586d33..5c68fb7 100644 --- a/tests/test_chengdu_mahjong_engine.py +++ b/tests/test_chengdu_mahjong_engine.py @@ -1,96 +1,39 @@ -def test_draw_tile(): - from src.engine.chengdu_mahjong_engine import ChengduMahjongEngine +from src.engine.chengdu_mahjong_engine import ChengduMahjongEngine + +def test_mahjong_engine(): + """ + 测试成都麻将引擎,包括初始化、发牌、轮次逻辑等。 + """ + # 初始化麻将引擎 engine = ChengduMahjongEngine() - initial_remaining = engine.state.remaining_tiles - tile = engine.draw_tile() - # 验证牌堆数量减少 - assert engine.state.remaining_tiles == initial_remaining - 1, "牌堆数量未正确减少" - # 验证牌已加入当前玩家手牌 - assert engine.state.hands[engine.state.current_player][tile] > 0, "摸牌未加入玩家手牌" - print(f"test_draw_tile passed: 摸到了 {tile}") + # 初始化游戏 + engine.initialize_game() + # 发牌 + engine.deal_tiles() -def test_discard_tile(): - from src.engine.chengdu_mahjong_engine import ChengduMahjongEngine + # 检查发牌后的状态 + print(f"庄家: 玩家 {engine.state.current_player}") + for player in range(4): + hand = engine.state.hands[player] + print(f"玩家 {player} 的手牌: {hand}") + print(f"玩家 {player} 的缺门: {engine.state.missing_suits[player]}") - engine = ChengduMahjongEngine() - tile = engine.draw_tile() # 玩家先摸牌 - engine.discard_tile(tile) # 打出摸到的牌 + # 模拟游戏主循环 + try: + engine.run() + except Exception as e: + print(f"运行时出错: {e}") - # 验证手牌数量减少 - assert engine.state.hands[engine.state.current_player][tile] == 0, "手牌未正确移除" - # 验证牌加入了牌河 - assert tile in engine.state.discards[engine.state.current_player], "牌未正确加入牌河" - print(f"test_discard_tile passed: 打出了 {tile}") + # 打印游戏结束后的状态 + print("\n游戏结束!") + for player in range(4): + print(f"玩家 {player} 的分数: {engine.state.scores[player]}") + print(f"玩家 {player} 的明牌: {engine.state.melds[player]}") + print(f"赢家: {engine.state.winners}") - -def test_set_missing_suit(): - from src.engine.chengdu_mahjong_state import ChengduMahjongState - - state = ChengduMahjongState() - player = 0 - missing_suit = "筒" - - state.set_missing_suit(player, missing_suit) - - # 验证缺门是否正确设置 - assert state.missing_suits[player] == missing_suit, "缺门设置错误" - print(f"test_set_missing_suit passed: 缺门设置为 {missing_suit}") - - -def test_can_win(): - from src.engine.chengdu_mahjong_state import ChengduMahjongState - - state = ChengduMahjongState() - hand = [0] * 108 - hand[0] = 2 # 两张1条(对子) - hand[3] = 1 # 2条 - hand[4] = 1 # 3条 - hand[5] = 1 # 4条 - hand[10] = 1 # 5条 - hand[11] = 1 # 6条 - hand[12] = 1 # 7条 - hand[20] = 1 # 8条 - hand[21] = 1 # 9条 - hand[22] = 1 # 1筒 - hand[30] = 1 # 2筒 - hand[31] = 1 # 3筒 - hand[32] = 1 # 4筒 - - result = state.can_win(hand) - - assert result is True, "胡牌判断失败" - print(f"test_can_win passed: 胡牌条件正确") - - - - -def test_peng(): - from src.engine.chengdu_mahjong_engine import ChengduMahjongEngine - - engine = ChengduMahjongEngine() - tile = 5 # 模拟手牌中有3张牌 - engine.state.hands[engine.state.current_player][tile] = 3 - engine.peng(tile) - - # 验证手牌减少 - assert engine.state.hands[engine.state.current_player][tile] == 1, "碰牌后手牌数量错误" - # 验证明牌记录 - assert ("peng", tile) in engine.state.melds[engine.state.current_player], "碰牌未正确记录" - print(f"test_peng passed: 碰牌成功") - -def test_gang(): - from src.engine.chengdu_mahjong_engine import ChengduMahjongEngine - - engine = ChengduMahjongEngine() - tile = 10 # 模拟手牌中有4张牌 - engine.state.hands[engine.state.current_player][tile] = 4 - engine.gang(tile, mode="an") - - # 验证手牌减少 - assert engine.state.hands[engine.state.current_player][tile] == 0, "杠牌后手牌数量错误" - # 验证明牌记录 - assert ("an_gang", tile) in engine.state.melds[engine.state.current_player], "杠牌未正确记录" - print(f"test_gang passed: 杠牌成功") +# 运行测试 +if __name__ == "__main__": + test_mahjong_engine()