From b9a2b3bc31c72ca2caa98beb96e0376bb6e2c786 Mon Sep 17 00:00:00 2001 From: wangsiyuan <2392948297@qq.com> Date: Sun, 1 Dec 2024 04:48:55 +0800 Subject: [PATCH] 1 1 --- README.md | 22 ++++ src/engine/actions.py | 31 +++-- src/engine/chengdu_mahjong_engine.py | 112 +++++++++++++----- src/engine/fan_type.py | 2 + src/environment/chengdu_majiang_env.py | 155 ++++++++----------------- 5 files changed, 175 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index ac67e58..b830276 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,28 @@ **自摸 **:是指玩家通过自己摸牌完成胡牌。自摸时,其他玩家都需要给赢家支付相应的分数。 +## 成都麻将游戏流程 + +1.确定庄家:通常在第一局开始时通过掷骰子来决定庄家。以后每局由上一局胡牌的玩家坐庄,如果流局则庄家不变。 + +2.庄家摸牌:从掷骰子确定的位置开始,庄家先摸14张牌,其他玩家每人摸13张牌。 + +3.庄家出牌:庄家先打出一张牌,开始这一局的游戏。 + +4.顺时针出牌:接下来按照顺时针方向,每位玩家依次摸牌和出牌。 + +5.摸牌与出牌:每个玩家轮流摸一张牌,然后选择出一张牌。玩家可以进行碰、杠等操作。 + +6.打缺一门:玩家必须选择先打完定缺的花色牌,才能出其他牌。(缺一种花色(筒、条、万中的一种),即手牌中只能保留两种花色) + +7.自摸:玩家摸到的牌使自己胡牌。 + +8.点炮:其他玩家打出的牌使自己胡牌。 + +9.计分:胡牌后根据胡牌的番数和其他规则进行计分。) + +10.结算 + ## 成都麻将规则建模 麻将游戏引擎建模代码于项目根src/engine/目录下。 diff --git a/src/engine/actions.py b/src/engine/actions.py index 6158ec1..bb19e86 100644 --- a/src/engine/actions.py +++ b/src/engine/actions.py @@ -97,28 +97,37 @@ def check_blood_battle(self): self.game_over = True -def set_missing_suit(player, missing_suit, game_state): +def set_missing_suit(player, game_state): """ - 玩家设置缺门的动作。 + 玩家自动根据手牌选择缺门。 参数: - player: 玩家索引(0-3)。 - - missing_suit: 玩家选择的缺门("条"、"筒" 或 "万")。 - game_state: 当前的游戏状态(`ChengduMahjongState` 实例)。 - 异常: - - ValueError: 如果缺门设置无效。 + 返回: + - str: 玩家设置的缺门花色。 """ valid_suits = ["条", "筒", "万"] - if missing_suit not in valid_suits: - logger.error(f"玩家 {player} 尝试设置无效的缺门: {missing_suit}") - raise ValueError("缺门设置无效") + hand = game_state.hands[player] # 获取玩家手牌 + # 统计每种花色的牌数量 + suit_counts = {suit: 0 for suit in valid_suits} + for tile in hand.tiles: + suit_counts[tile.suit] += 1 + + # 找到数量最少的花色 + missing_suit = min(suit_counts, key=suit_counts.get) + + # 检查是否已经设置过缺门 if game_state.missing_suits[player] is not None: - logger.error(f"玩家 {player} 已经设置了缺门,不能重复设置") + logger.warning(f"玩家 {player} 已设置过缺门,不能重复设置") raise ValueError("缺门已经设置,不能重复设置") + # 设置缺门并记录日志 game_state.missing_suits[player] = missing_suit - logger.info(f"玩家 {player} 设置缺门为: {missing_suit}") + logger.info( + f"玩家 {player} 手牌花色分布: {suit_counts}。缺门设置为: {missing_suit}" + ) - return game_state.missing_suits[player] \ No newline at end of file + return missing_suit \ No newline at end of file diff --git a/src/engine/chengdu_mahjong_engine.py b/src/engine/chengdu_mahjong_engine.py index 2eea05f..adb001e 100644 --- a/src/engine/chengdu_mahjong_engine.py +++ b/src/engine/chengdu_mahjong_engine.py @@ -1,47 +1,99 @@ import random - from loguru import logger - -from .chengdu_mahjong_state import ChengduMahjongState +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 class ChengduMahjongEngine: def __init__(self): - self.state = ChengduMahjongState() # 创建游戏状态 + self.state = ChengduMahjongState() self.game_over = False - self.game_started = False # 游戏是否已开始 - self.deal_tiles() # 发牌 + self.game_started = False + self.current_player = 0 + + def initialize_game(self): + """ + 初始化游戏,确定庄家,发牌并设置缺门。 + """ + logger.info("游戏初始化...") + # 确定庄家(掷骰子) + self.state.current_player = random.randint(0, 3) + logger.info(f"庄家确定为玩家 {self.state.current_player}") + + logger.info("游戏初始化完成,准备开始!") def deal_tiles(self): - """ 发牌,每个玩家发13张牌,并设置缺门 """ - logger.info("发牌中...") + """ 发牌:庄家摸14张,其余玩家摸13张 """ + logger.info("开始发牌...") + random.shuffle(self.state.deck) # 洗牌 - # 洗牌(随机打乱牌堆) - random.shuffle(self.state.deck) - - # 随机发牌给每个玩家 for player in range(4): - for _ in range(13): # 每个玩家13张牌 - tile = self.state.deck.pop() # 从牌堆抽取一张牌 - self.state.hands[player][tile] += 1 # 增加玩家手牌的计数 + tiles_to_draw = 14 if player == self.state.current_player else 13 + for _ in range(tiles_to_draw): + tile = self.state.deck.pop() + self.state.hands[player].add_tile(tile) - # 设置缺门:每个玩家定缺(这里假设我们让每个玩家的缺门都为“条”) - for player in range(4): - missing_suit = "条" # 这里可以通过其他方式设置缺门,比如随机选择 - self.state.set_missing_suit(player, missing_suit) + # 自动设置缺门 + set_missing_suit(player, self.state) - def start_game(self): - """ 开始游戏 """ - if not self.game_started: - self.game_started = True - logger.info("游戏开始!") - else: - logger.warning("游戏已经开始,不能重复启动!") + logger.info("发牌结束并完成缺门设置!") + + def play_turn(self): + """ + 进行一回合的操作:当前玩家摸牌、出牌。 + """ + logger.info(f"轮到玩家 {self.state.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): - """ 检查游戏是否结束 """ - # 你可以根据游戏规则检查是否有玩家胡牌或其他结束条件 - if len(self.state.deck) == 0: + """ + 检查游戏是否结束。 + """ + # 检查是否已无牌可摸 + if self.state.remaining_tiles == 0: self.game_over = True - logger.info("游戏结束!") + logger.info("游戏结束:牌堆已空") + return + # 检查是否满足血战结束条件 + check_blood_battle(self) + + def run(self): + """ + 运行游戏主循环。 + """ + self.initialize_game() + self.game_started = True + + while not self.game_over: + self.play_turn() + + logger.info("游戏已结束") \ No newline at end of file diff --git a/src/engine/fan_type.py b/src/engine/fan_type.py index 1cdb3d3..7130ddb 100644 --- a/src/engine/fan_type.py +++ b/src/engine/fan_type.py @@ -163,3 +163,5 @@ def is_dragon_seven_pairs(hand, melds): return 12, -1 # 龙七对计为 12 番,并减少 1 根 return 0, 0 + + diff --git a/src/environment/chengdu_majiang_env.py b/src/environment/chengdu_majiang_env.py index c45b3a3..6f0177f 100644 --- a/src/environment/chengdu_majiang_env.py +++ b/src/environment/chengdu_majiang_env.py @@ -1,120 +1,63 @@ import gym -import numpy as np from gym import spaces -from src.engine.actions import draw_tile, discard_tile, peng, gang, check_blood_battle -from src.engine.calculate_fan import calculate_fan, is_seven_pairs, is_cleared, is_big_pairs -from src.engine.chengdu_mahjong_engine import ChengduMahjongEngine -from src.engine.scoring import calculate_score +import numpy as np +from src.engine.chengdu_mahjong_state import ChengduMahjongState - -class MahjongEnv(gym.Env): +class ChengduMahjongEnv(gym.Env): def __init__(self): - super(MahjongEnv, self).__init__() - self.engine = ChengduMahjongEngine() - self.scores = [100, 100, 100, 100] # 四位玩家初始分数 - self.base_score = 1 # 底分 - self.max_rounds = 100 # 最大轮数,防止游戏无限进行 - self.current_round = 0 # 当前轮数 - self.action_space = spaces.Discrete(108) # 动作空间:打牌的索引 - self.observation_space = spaces.Box(low=0, high=4, shape=(108,), dtype=np.int32) + super().__init__() + self.state = ChengduMahjongState() + self.action_space = spaces.Discrete(5) # 0: 出牌, 1: 碰, 2: 杠, 3: 胡, 4: 过 + self.observation_space = spaces.Dict({ + "hand": spaces.Box(low=0, high=4, shape=(108,), dtype=np.int32), # 手牌数量 + "melds": spaces.Box(low=0, high=4, shape=(108,), dtype=np.int32), # 明牌数量 + "discard_pile": spaces.Box(low=0, high=4, shape=(108,), dtype=np.int32), # 弃牌数量 + "dealer": spaces.Discrete(4), # 当前庄家 + }) + self.reset() def reset(self): - self.engine = ChengduMahjongEngine() - self.scores = [100, 100, 100, 100] # 每局重置分数 - self.current_round = 0 - return self.engine.state.hands[self.engine.state.current_player] + """重置游戏状态""" + self.state.reset() # 初始化游戏状态 + return self._get_observation() def step(self, action): - """ - 执行玩家动作并更新游戏状态。 - - 参数: - - action: 玩家动作,0 代表摸牌,1 代表打牌,2 代表碰牌,3 代表杠牌 - - 返回: - - next_state: 当前玩家的手牌 - - reward: 奖励 - - done: 是否结束 - - info: 其他信息(如奖励历史等) - """ - done = False reward = 0 + done = False - try: - if action == 0: # 0代表摸牌 - reward, done = draw_tile(self.engine) # 调用摸牌函数 - elif action == 1: # 1代表打牌 - tile = self.engine.state.hands[self.engine.state.current_player][0] # 假设选择第一张牌 - discard_tile(self.engine, tile) # 调用打牌函数 - reward, done = -1, False - elif action == 2: # 2代表碰牌 - tile = self.engine.state.hands[self.engine.state.current_player][0] # 假设选择第一张牌 - peng(self.engine, tile) # 调用碰牌函数 - reward, done = 0, False - elif action == 3: # 3代表杠牌 - tile = self.engine.state.hands[self.engine.state.current_player][0] # 假设选择第一张牌 - gang(self.engine, tile, mode="ming") # 暂时假设为明杠 - reward, done = 0, False + if action == 0: # 出牌 + self.state.discard() + elif action == 1: # 碰 + self.state.peng() + elif action == 2: # 杠 + self.state.kong() + elif action == 3: # 胡 + reward, done = self.state.win() + elif action == 4: # 过 + self.state.pass_turn() - # 检查是否胡牌 - if self.engine.state.can_win(self.engine.state.hands[self.engine.state.current_player]): - reward, done = self.handle_win() # 胡牌时处理胜利逻辑 + # 检查游戏是否结束 + done = done or self.state.is_game_over() + return self._get_observation(), reward, done, {} - # 检查游戏结束条件 - check_blood_battle(self.engine) + def _get_observation(self): + """获取玩家当前的观察空间""" + player_index = self.state.current_player + hand = np.zeros(108, dtype=np.int32) + melds = np.zeros(108, dtype=np.int32) + discard_pile = np.zeros(108, dtype=np.int32) - if self.engine.game_over: # 检查是否游戏结束 - done = True - - except ValueError: - reward, done = -10, False # 非法操作扣分 - - # 切换到下一个玩家 - self.engine.state.current_player = (self.engine.state.current_player + 1) % 4 - self.current_round += 1 - - # 如果达到最大轮数,结束游戏 - if self.current_round >= self.max_rounds: - done = True - reward = 0 # 平局奖励或惩罚(可调整) - - return self.engine.state.hands[self.engine.state.current_player], reward, done, {} - - def handle_win(self): - """ - 处理胡牌后的分数结算和奖励。 - """ - winner = self.engine.state.current_player - hand = self.engine.state.hands[winner] - melds = self.engine.state.melds[winner] - is_self_draw = True # 假设自摸(后续可动态判断) - - conditions = { - "is_cleared": is_cleared(hand, melds), - "is_seven_pairs": is_seven_pairs(hand), - "is_big_pairs": is_big_pairs(hand), - # 添加其他条件... - } - - # 动态计算番数 - fan = calculate_fan(hand, melds, is_self_draw, is_cleared, conditions) - - # 动态计算得分 - scores = calculate_score(fan, self.base_score, is_self_draw) - self.scores[winner] += scores["winner"] - for i, score in enumerate(scores["loser"]): - self.scores[i] += score # 扣分 - - # 奖励设置为赢家得分 - reward = scores["winner"] - self.engine.state.winners.append(winner) # 添加赢家到列表 - return reward, True # 胡牌结束当前局 - - def render(self, mode="human"): - """ - 打印游戏状态信息,便于调试。 - """ - print(f"当前轮数: {self.current_round}") - print("玩家分数:", self.scores) - print("当前玩家状态:", self.engine.state.hands[self.engine.state.current_player]) + # 填充手牌、明牌和弃牌信息 + for tile, count in self.state.hands[player_index].tile_count.items(): + hand[tile.index] = count + for meld in self.state.melds[player_index]: + melds[meld.tile.index] += meld.count + for tile in self.state.discards[player_index]: + discard_pile[tile.index] += 1 + return { + "hand": hand, + "melds": melds, + "discard_pile": discard_pile, + "dealer": self.state.current_player + } \ No newline at end of file