parent
af5721992d
commit
b9a2b3bc31
22
README.md
22
README.md
|
|
@ -150,6 +150,28 @@
|
|||
|
||||
**自摸 **:是指玩家通过自己摸牌完成胡牌。自摸时,其他玩家都需要给赢家支付相应的分数。
|
||||
|
||||
## 成都麻将游戏流程
|
||||
|
||||
1.确定庄家:通常在第一局开始时通过掷骰子来决定庄家。以后每局由上一局胡牌的玩家坐庄,如果流局则庄家不变。
|
||||
|
||||
2.庄家摸牌:从掷骰子确定的位置开始,庄家先摸14张牌,其他玩家每人摸13张牌。
|
||||
|
||||
3.庄家出牌:庄家先打出一张牌,开始这一局的游戏。
|
||||
|
||||
4.顺时针出牌:接下来按照顺时针方向,每位玩家依次摸牌和出牌。
|
||||
|
||||
5.摸牌与出牌:每个玩家轮流摸一张牌,然后选择出一张牌。玩家可以进行碰、杠等操作。
|
||||
|
||||
6.打缺一门:玩家必须选择先打完定缺的花色牌,才能出其他牌。(缺一种花色(筒、条、万中的一种),即手牌中只能保留两种花色)
|
||||
|
||||
7.自摸:玩家摸到的牌使自己胡牌。
|
||||
|
||||
8.点炮:其他玩家打出的牌使自己胡牌。
|
||||
|
||||
9.计分:胡牌后根据胡牌的番数和其他规则进行计分。)
|
||||
|
||||
10.结算
|
||||
|
||||
## 成都麻将规则建模
|
||||
|
||||
麻将游戏引擎建模代码于项目根src/engine/目录下。
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
return missing_suit
|
||||
|
|
@ -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("游戏已结束")
|
||||
|
|
@ -163,3 +163,5 @@ def is_dragon_seven_pairs(hand, melds):
|
|||
return 12, -1 # 龙七对计为 12 番,并减少 1 根
|
||||
return 0, 0
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# 填充手牌、明牌和弃牌信息
|
||||
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
|
||||
|
||||
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),
|
||||
# 添加其他条件...
|
||||
return {
|
||||
"hand": hand,
|
||||
"melds": melds,
|
||||
"discard_pile": discard_pile,
|
||||
"dealer": self.state.current_player
|
||||
}
|
||||
|
||||
# 动态计算番数
|
||||
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])
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue